diff options
Diffstat (limited to 'web')
| -rw-r--r-- | web/handler/auth.go | 149 | ||||
| -rw-r--r-- | web/handler/deploy.go | 2 | ||||
| -rw-r--r-- | web/handler/index.go | 4 | ||||
| -rw-r--r-- | web/handler/instance.go | 6 | ||||
| -rw-r--r-- | web/middleware/auth.go | 40 | ||||
| -rw-r--r-- | web/views/auth.html | 21 | ||||
| -rw-r--r-- | web/views/f_auth_error.html | 3 | ||||
| -rw-r--r-- | web/views/index.html | 2 | ||||
| -rw-r--r-- | web/views/problem.html | 36 | ||||
| -rw-r--r-- | web/web.go | 10 |
10 files changed, 222 insertions, 51 deletions
diff --git a/web/handler/auth.go b/web/handler/auth.go index 38b87b6..29bd47c 100644 --- a/web/handler/auth.go +++ b/web/handler/auth.go @@ -1,81 +1,164 @@ package handler import ( + "errors" "html/template" "log/slog" "net/http" - "strconv" "time" + "git.leonardobishop.net/instancer/pkg/auth" "git.leonardobishop.net/instancer/pkg/session" ) -func GetAuth(tmpl *template.Template) http.HandlerFunc { +func GetAuth(tmpl *template.Template, authProvider *auth.OIDCAuthProvider) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - tmpl.ExecuteTemplate(w, "auth.html", nil) + w.Header().Add("HX-Redirect", "/auth") + tmpl.ExecuteTemplate(w, "auth.html", struct { + Error string + OidcIdPName string + }{ + Error: "", + OidcIdPName: authProvider.Name, + }) } } -func PostAuth(tmpl *template.Template, session *session.MemoryStore) http.HandlerFunc { +func PostAuth(tmpl *template.Template, session *session.MemoryStore, authProvider *auth.OIDCAuthProvider) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - if err := r.ParseForm(); err != nil { - tmpl.ExecuteTemplate(w, "f_auth_error.html", struct { - Message string + url, err := authProvider.StartJourney(r.RemoteAddr, r.UserAgent()) + if err != nil { + slog.Error("oidc start journey failed", "cause", err) + tmpl.ExecuteTemplate(w, "auth.html", struct { + Error string + OidcIdPName string }{ - Message: "Invalid form data", + Error: "Failed to start OIDC journey", + OidcIdPName: authProvider.Name, }) return } - team := r.FormValue("team") - if team == "" { - tmpl.ExecuteTemplate(w, "f_auth_error.html", struct { - Message string - }{ - Message: "No team entered", - }) - return + http.Redirect(w, r, url, http.StatusFound) + } +} + +func GetAuthCallback(tmpl *template.Template, session *session.MemoryStore, authProvider *auth.OIDCAuthProvider) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + type pageParams struct { + Error string + OidcIdPName string } + params := pageParams{OidcIdPName: authProvider.Name} + query := r.URL.Query() - if _, err := strconv.Atoi(team); err != nil { - tmpl.ExecuteTemplate(w, "f_auth_error.html", struct { - Message string - }{ - Message: "Team ID must be number", - }) + s, err := session.Create() + if err != nil { + params.Error = "Failed to create session" + slog.Error("session creation failed", "cause", err) + w.Header().Add("HX-Push-Url", "/auth") + tmpl.ExecuteTemplate(w, "auth.html", params) return } - session, err := session.Create(team) + err = authProvider.CompleteJourney(r.Context(), query.Get("code"), query.Get("state"), r.RemoteAddr, r.UserAgent(), s) + if err != nil { - slog.Error("could not create session", "cause", err) - tmpl.ExecuteTemplate(w, "f_auth_error.html", struct { - Message string - }{ - Message: "Could not create session", - }) + if errors.Is(err, auth.ErrNotAuthorised) { + params.Error = "You are not authorised to use this service" + goto done + } else if errors.Is(err, auth.ErrInvalidState) { + params.Error = "Invalid state" + goto done + } else if errors.Is(err, auth.ErrStateVerificationFailed) { + params.Error = "State verification failed" + goto done + } else if errors.Is(err, auth.ErrInvalidIDToken) { + params.Error = "Invalid ID token" + goto done + } + params.Error = "Failed to complete OIDC journey" + slog.Error("oidc complete journey failed", "cause", err) + done: + w.Header().Add("HX-Push-Url", "/auth") + tmpl.ExecuteTemplate(w, "auth.html", params) + session.Destroy(s.Token) return } http.SetCookie(w, &http.Cookie{ - Name: "session", - Value: session.Token, + Name: "instancer-session", + Value: s.Token, Path: "/", Secure: true, SameSite: http.SameSiteStrictMode, HttpOnly: true, }) - w.Header().Add("HX-Redirect", "/") + http.Redirect(w, r, "/", http.StatusFound) } } +//func PostAuth(tmpl *template.Template, session *session.MemoryStore) http.HandlerFunc { +// return func(w http.ResponseWriter, r *http.Request) { +// if err := r.ParseForm(); err != nil { +// tmpl.ExecuteTemplate(w, "f_auth_error.html", struct { +// Message string +// }{ +// Message: "Invalid form data", +// }) +// return +// } +// +// team := r.FormValue("team") +// if team == "" { +// tmpl.ExecuteTemplate(w, "f_auth_error.html", struct { +// Message string +// }{ +// Message: "No team entered", +// }) +// return +// } +// +// if _, err := strconv.Atoi(team); err != nil { +// tmpl.ExecuteTemplate(w, "f_auth_error.html", struct { +// Message string +// }{ +// Message: "Team ID must be number", +// }) +// return +// } +// +// session, err := session.Create(team) +// if err != nil { +// slog.Error("could not create session", "cause", err) +// tmpl.ExecuteTemplate(w, "f_auth_error.html", struct { +// Message string +// }{ +// Message: "Could not create session", +// }) +// return +// } +// +// http.SetCookie(w, &http.Cookie{ +// Name: "session", +// Value: session.Token, +// +// Path: "/", +// Secure: true, +// SameSite: http.SameSiteStrictMode, +// HttpOnly: true, +// }) +// w.Header().Add("HX-Redirect", "/") +// } +//} + func GetLogout(session *session.MemoryStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { //TODO expire session here http.SetCookie(w, &http.Cookie{ - Name: "session", + Name: "instancer-session", Value: "", Expires: time.Unix(0, 0), diff --git a/web/handler/deploy.go b/web/handler/deploy.go index 24e0f95..f0c40fc 100644 --- a/web/handler/deploy.go +++ b/web/handler/deploy.go @@ -24,7 +24,7 @@ func PostDeploy(tmpl *template.Template, dockerDeployer *deployer.DockerDeployer } session := r.Context().Value("session").(*session.UserSession) - deployKey := dockerDeployer.StartDeploy(challenge, session.Team) + deployKey := dockerDeployer.StartDeploy(challenge, session.TeamID) tmpl.ExecuteTemplate(w, "f_deploy.html", struct { DeployKey string diff --git a/web/handler/index.go b/web/handler/index.go index 5cf44cf..9279151 100644 --- a/web/handler/index.go +++ b/web/handler/index.go @@ -22,10 +22,12 @@ func GetIndex(tmpl *template.Template, registryClient *registry.RegistryClient) if err := tmpl.ExecuteTemplate(w, "index.html", struct { Challenges []string + Name string Team string }{ Challenges: challenges, - Team: session.Team, + Name: session.Name, + Team: session.TeamName, }); err != nil { http.Error(w, "Internal server error", http.StatusInternalServerError) return diff --git a/web/handler/instance.go b/web/handler/instance.go index 0222a42..e5e4c53 100644 --- a/web/handler/instance.go +++ b/web/handler/instance.go @@ -15,9 +15,9 @@ func GetInstances(tmpl *template.Template, dockerDeployer *deployer.DockerDeploy return func(w http.ResponseWriter, r *http.Request) { session := r.Context().Value("session").(*session.UserSession) - instances, err := dockerDeployer.GetTeamInstances(r.Context(), session.Team) + instances, err := dockerDeployer.GetTeamInstances(r.Context(), session.TeamID) if err != nil { - slog.Error("could not get team instances", "team", session.Team, "error", err) + slog.Error("could not get team instances", "team", session.TeamID, "error", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } @@ -70,7 +70,7 @@ func PostStopInstance(tmpl *template.Template, dockerDeployer *deployer.DockerDe session := r.Context().Value("session").(*session.UserSession) deployKey := r.PathValue("deployKey") - err := dockerDeployer.StopInstance(r.Context(), deployKey, session.Team) + err := dockerDeployer.StopInstance(r.Context(), deployKey, session.TeamID) if err != nil { tmpl.ExecuteTemplate(w, "f_instance_result.html", struct { State string diff --git a/web/middleware/auth.go b/web/middleware/auth.go index fcba3b7..c0257e2 100644 --- a/web/middleware/auth.go +++ b/web/middleware/auth.go @@ -2,28 +2,60 @@ package middleware import ( "context" + "errors" + "html/template" + "log/slog" "net/http" + "git.leonardobishop.net/instancer/pkg/auth" "git.leonardobishop.net/instancer/pkg/session" ) -func MustAuthenticate(store *session.MemoryStore) func(http.HandlerFunc) http.HandlerFunc { +func MustAuthenticate(tmpl *template.Template, store *session.MemoryStore, authProvider *auth.OIDCAuthProvider) func(http.HandlerFunc) http.HandlerFunc { return func(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - sessionCookie, err := r.Cookie("session") + sessionCookie, err := r.Cookie("instancer-session") if err != nil { - w.Header().Add("HX-Redirect", "/auth") http.Redirect(w, r, "/auth", http.StatusFound) return } s := store.GetByToken(sessionCookie.Value) if s == nil { - w.Header().Add("HX-Redirect", "/auth") http.Redirect(w, r, "/auth", http.StatusFound) return } + err = authProvider.UpdateUserInfo(r.Context(), s) + if err != nil { + if errors.Is(err, auth.ErrInvalidToken) { + http.Redirect(w, r, "/auth", http.StatusFound) + return + } + slog.Error("error updating user info", "cause", err) + w.Header().Add("HX-Redirect", "/problem") + tmpl.ExecuteTemplate(w, "problem.html", struct { + Error string + ShowLogout bool + }{ + Error: "There was a problem fetching your user info. Try again later.", + ShowLogout: true, + }) + return + } + + if s.TeamID == "" || s.TeamName == "" { + w.Header().Add("HX-Redirect", "/problem") + tmpl.ExecuteTemplate(w, "problem.html", struct { + Error string + ShowLogout bool + }{ + Error: "You are not part of a team. Please join a team and then refresh this page.", + ShowLogout: true, + }) + return + } + ctx := context.WithValue(r.Context(), "session", s) next(w, r.WithContext(ctx)) diff --git a/web/views/auth.html b/web/views/auth.html index 995815a..17ca83d 100644 --- a/web/views/auth.html +++ b/web/views/auth.html @@ -18,6 +18,25 @@ <div class="col-12 col-md-6 col-lg-4"> <div class="card mx-auto"> <div class="card-body"> + <h4 class="card-title mb-3">Welcome</h4> + + <p>Please authenticate to spawn challenge instances.</p> + + {{if .Error}} + <div class="alert alert-danger" role="alert"> + {{.Error}} + </div> + {{end}} + + <form method="POST"> + <button type="submit" class="btn btn-primary w-100"> + Login with {{ .OidcIdPName }} + </button> + </form> + </div> + </div> +<!-- <div class="card mx-auto"> + <div class="card-body"> <h4 class="card-title mb-3">Enter a team ID</h4> <div id="auth-response"></div> @@ -33,7 +52,7 @@ </div> </form> </div> - </div> + </div> --> <div class="card mt-4 mx-auto"> <div class="card-body"> diff --git a/web/views/f_auth_error.html b/web/views/f_auth_error.html deleted file mode 100644 index 4ebdac3..0000000 --- a/web/views/f_auth_error.html +++ /dev/null @@ -1,3 +0,0 @@ -<div class="alert alert-danger" role="alert"> - {{.Message}} -</div> diff --git a/web/views/index.html b/web/views/index.html index 97edc88..81a144d 100644 --- a/web/views/index.html +++ b/web/views/index.html @@ -86,7 +86,7 @@ </div> <div class="mt-2 px-3"> <small class="text-muted"> - Logged in as <b>{{.Team}}</b>. + Logged in as <b>{{ .Name }}</b> ({{ .Team }}). <a href="/logout">Not you</a>? </small> </div> diff --git a/web/views/problem.html b/web/views/problem.html new file mode 100644 index 0000000..202f651 --- /dev/null +++ b/web/views/problem.html @@ -0,0 +1,36 @@ + +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Challenge Instancer</title> + + <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> + <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous"></script> + <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js" integrity="sha384-cVKIPhGWiC2Al4u+LWgxfKTRIcfu0JTxR+EQDz/bgldoEyl4H0zUF0QKbrJ0EcQF" crossorigin="anonymous"></script> +</head> +<body class="bg-light"> + +<div class="container py-5"> + <div class="row justify-content-center"> + <div class="col-12 col-md-6 col-lg-4"> + <div class="card mx-auto"> + <div class="card-body"> + <p class="card-text"> + {{.Error}} + </p> + </div> + </div> + {{if .ShowLogout}} + <div class="mt-2 px-3"> + <small class="text-muted"> + Alternatively, <a href="/logout">log out</a>. + </small> + </div> + {{end}} + </div> + </div> +</div> + +</body> +</html> @@ -5,6 +5,7 @@ import ( "html/template" "net/http" + "git.leonardobishop.net/instancer/pkg/auth" "git.leonardobishop.net/instancer/pkg/deployer" "git.leonardobishop.net/instancer/pkg/registry" "git.leonardobishop.net/instancer/pkg/session" @@ -15,7 +16,7 @@ import ( //go:embed views var views embed.FS -func NewMux(registryClient *registry.RegistryClient, dockerDeployer *deployer.DockerDeployer) *http.ServeMux { +func NewMux(registryClient *registry.RegistryClient, dockerDeployer *deployer.DockerDeployer, oidcAuthProvider *auth.OIDCAuthProvider) *http.ServeMux { tmpl, err := template.ParseFS(views, "views/*.html") if err != nil { panic(err) @@ -23,10 +24,11 @@ func NewMux(registryClient *registry.RegistryClient, dockerDeployer *deployer.Do mux := http.NewServeMux() store := session.NewMemoryStore() - mustAuthenticate := middleware.MustAuthenticate(store) + mustAuthenticate := middleware.MustAuthenticate(tmpl, store, oidcAuthProvider) - mux.HandleFunc("GET /auth", handler.GetAuth(tmpl)) - mux.HandleFunc("POST /auth", handler.PostAuth(tmpl, store)) + mux.HandleFunc("GET /auth", handler.GetAuth(tmpl, oidcAuthProvider)) + mux.HandleFunc("POST /auth", handler.PostAuth(tmpl, store, oidcAuthProvider)) + mux.HandleFunc("GET /auth/callback", handler.GetAuthCallback(tmpl, store, oidcAuthProvider)) mux.HandleFunc("GET /logout", handler.GetLogout(store)) mux.HandleFunc("GET /", mustAuthenticate(handler.GetIndex(tmpl, registryClient))) |
