summaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/handler/auth.go149
-rw-r--r--web/handler/deploy.go2
-rw-r--r--web/handler/index.go4
-rw-r--r--web/handler/instance.go6
-rw-r--r--web/middleware/auth.go40
-rw-r--r--web/views/auth.html21
-rw-r--r--web/views/f_auth_error.html3
-rw-r--r--web/views/index.html2
-rw-r--r--web/views/problem.html36
-rw-r--r--web/web.go10
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>
diff --git a/web/web.go b/web/web.go
index eaf03e8..dc9b5e0 100644
--- a/web/web.go
+++ b/web/web.go
@@ -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)))