summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--go.mod3
-rw-r--r--go.sum6
-rw-r--r--main.go80
-rw-r--r--pkg/auth/oidc.go162
-rw-r--r--pkg/session/session.go14
-rw-r--r--startup.txt50
-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
16 files changed, 472 insertions, 116 deletions
diff --git a/go.mod b/go.mod
index 9e150f0..9f9f12d 100644
--- a/go.mod
+++ b/go.mod
@@ -7,10 +7,12 @@ require (
github.com/caarlos0/env/v11 v11.3.1 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
+ github.com/coreos/go-oidc/v3 v3.17.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
@@ -25,6 +27,7 @@ require (
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
+ golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sys v0.38.0 // indirect
tailscale.com v1.92.5 // indirect
)
diff --git a/go.sum b/go.sum
index ae75dfc..70785b4 100644
--- a/go.sum
+++ b/go.sum
@@ -6,6 +6,8 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
+github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
+github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
@@ -14,6 +16,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
+github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -45,6 +49,8 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
+golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
+golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
diff --git a/main.go b/main.go
index 7b6362d..6852463 100644
--- a/main.go
+++ b/main.go
@@ -2,9 +2,11 @@ package main
import (
"context"
+ _ "embed"
"log/slog"
"net/http"
+ "git.leonardobishop.net/instancer/pkg/auth"
"git.leonardobishop.net/instancer/pkg/deployer"
"git.leonardobishop.net/instancer/pkg/janitor"
"git.leonardobishop.net/instancer/pkg/registry"
@@ -21,71 +23,18 @@ type Config struct {
ImagePrefix string `env:"IMAGE_PREFIX"`
ProxyContainerName string `env:"PROXY_CONTAINER_NAME"`
-}
-
-const startupMessage = `
- , .
- , . . .
- . . . .
- what's the worst that i can say?...
- . .
- . . .
- . .
- . ...things are better if i stay...
- . . .
- . ,
- . , . . ,
- . . .
- ! ,
- ! .
- , . ^
- / \ .
- . /___\ ,
- . |= =| ,
- . | |
- | | ,
-, | |
- | | .
- | |
- . . | | ,
- , | | .
- | |
- | | .
- /|##!##|\
- . / |##!##| \
- / |##!##| \ ,
- | / ^ | ^ \ |
- . | / ( | ) \ | ,
- , . |/ ( | ) \|
- (( ))
- (( : )) .
- (( : ))
- , (( )) .
- . (( )) ,
- ( )
- .
- . . .
- , . ,
-
- _ __
- ___| |_ / _|
- / __| __| |_
- _ | (__| |_| _|
- (_)_ __ ___| \___|\__|_|_ ___ ___ _ __
- | | '_ \/ __| __/ _` + "`" + ` | '_ \ / __/ _ \ '__|
- | | | | \__ \ || (_| | | | | (_| __/ |
- |_|_| |_|___/\__\__,_|_| |_|\___\___|_|
-____^/\___^--____/\____O______________/\/\--
- /\^ ^ ^ ^ ^^ ^ '\
- -- - -- -
- -- __ ___-- ^ ^
+ OidcClientId string `env:"OIDC_CLIENT_ID"`
+ OidcClientSecret string `env:"OIDC_CLIENT_SECRET"`
+ OidcDiscoveryEndpoint string `env:"OIDC_DISCOVERY_ENDPOINT"`
+ OidcIdPName string `env:"OIDC_IDP_NAME" envDefault:"OIDC"`
+ OidcCallbackProtocol string `env:"OIDC_CALLBACK_PROTOCOL" envDefault:"https"`
+}
-`
+//go:embed startup.txt
+var startupMessage string
func main() {
- slog.Info(startupMessage)
-
var config Config
if err := env.Parse(&config); err != nil {
@@ -110,11 +59,18 @@ func main() {
panic(err)
}
+ oidcAuthProvider, err := auth.NewOIDCAuthProvider(config.OidcIdPName, config.OidcClientId, config.OidcClientSecret, config.OidcDiscoveryEndpoint, config.OidcCallbackProtocol+"://"+config.InstancerDomain+"/auth/callback")
+ if err != nil {
+ panic(err)
+ }
+
+ slog.Info(startupMessage)
+
slog.Info("staring janitor job")
go janitor.StartJanitor(context.Background(), &dockerDeployer)
slog.Info("starting http server")
- err = http.ListenAndServe(":8080", web.NewMux(&registryClient, &dockerDeployer))
+ err = http.ListenAndServe(":8080", web.NewMux(&registryClient, &dockerDeployer, &oidcAuthProvider))
slog.Error("http server closing", "reason", err.Error())
slog.Info("so long and goodnight; so long and goodnight...")
}
diff --git a/pkg/auth/oidc.go b/pkg/auth/oidc.go
new file mode 100644
index 0000000..674332e
--- /dev/null
+++ b/pkg/auth/oidc.go
@@ -0,0 +1,162 @@
+// Adapted from
+// https://git.leonardobishop.net/confplanner/plain/pkg/auth/oauth.go
+
+package auth
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/base64"
+ "errors"
+ "sync"
+ "time"
+
+ "git.leonardobishop.net/instancer/pkg/session"
+ "github.com/coreos/go-oidc/v3/oidc"
+ "golang.org/x/oauth2"
+)
+
+type OIDCAuthProvider struct {
+ Name string
+
+ oauthConfig *oauth2.Config
+ oidcProvider *oidc.Provider
+ oidcVerifier *oidc.IDTokenVerifier
+
+ states map[string]*oidcState
+ lock sync.RWMutex
+}
+
+type oidcState struct {
+ expiry time.Time
+ ip string
+ userAgent string
+}
+
+var (
+ ErrStateVerificationFailed = errors.New("state verification failed")
+ ErrInvalidState = errors.New("invalid state")
+ ErrMissingIDToken = errors.New("missing ID token")
+ ErrInvalidIDToken = errors.New("invalid ID token")
+ ErrNotAuthorised = errors.New("not authorised")
+ ErrUserSyncFailed = errors.New("user sync failed")
+
+ ErrInvalidToken = errors.New("invalid token")
+)
+
+func NewOIDCAuthProvider(name, clientID, clientSecret, endpoint, callbackURL string) (OIDCAuthProvider, error) {
+ provider, err := oidc.NewProvider(context.Background(), endpoint)
+ if err != nil {
+ return OIDCAuthProvider{}, err
+ }
+
+ return OIDCAuthProvider{
+ Name: name,
+ oauthConfig: &oauth2.Config{
+ ClientID: clientID,
+ ClientSecret: clientSecret,
+ Endpoint: provider.Endpoint(),
+ RedirectURL: callbackURL,
+ Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
+ },
+ oidcProvider: provider,
+ oidcVerifier: provider.Verifier(&oidc.Config{ClientID: clientID}),
+ states: make(map[string]*oidcState),
+ }, nil
+}
+
+func (p *OIDCAuthProvider) StartJourney(ip string, userAgent string) (string, error) {
+ b := make([]byte, 50)
+ if _, err := rand.Read(b); err != nil {
+ return "", err
+ }
+
+ state := base64.URLEncoding.EncodeToString(b)
+
+ p.lock.Lock()
+ defer p.lock.Unlock()
+
+ p.states[state] = &oidcState{
+ expiry: time.Now().Add(time.Minute * 5),
+ ip: ip,
+ userAgent: userAgent,
+ }
+
+ return p.oauthConfig.AuthCodeURL(state), nil
+}
+
+func (p *OIDCAuthProvider) CompleteJourney(ctx context.Context, authCode string, state string, ip string, userAgent string, session *session.UserSession) error {
+ var s *oidcState
+
+ p.lock.Lock()
+ s = p.states[state]
+ delete(p.states, state)
+ p.lock.Unlock()
+
+ if s == nil {
+ return ErrInvalidState
+ }
+
+ //if time.Now().After(s.expiry) || s.ip != ip || s.userAgent != userAgent {
+ // return nil, ErrStateVerificationFailed
+ //}
+ if time.Now().After(s.expiry) || s.userAgent != userAgent {
+ return ErrStateVerificationFailed
+ }
+
+ oauth2Token, err := p.oauthConfig.Exchange(ctx, authCode)
+ if err != nil {
+ return err
+ }
+
+ rawIDToken, ok := oauth2Token.Extra("id_token").(string)
+ if !ok {
+ return ErrMissingIDToken
+ }
+
+ idToken, err := p.oidcVerifier.Verify(ctx, rawIDToken)
+ if err != nil {
+ return ErrInvalidIDToken
+ }
+
+ var claims struct {
+ Subject string `json:"sub"`
+ Email string `json:"email"`
+ Name string `json:"name"`
+ }
+ if err := idToken.Claims(&claims); err != nil {
+ return ErrInvalidIDToken
+ }
+
+ session.OAuthTokenSource = p.oauthConfig.TokenSource(context.Background(), oauth2Token)
+ session.Subject = claims.Subject
+ session.Email = claims.Email
+ session.Name = claims.Name
+
+ return nil
+}
+
+func (p *OIDCAuthProvider) UpdateUserInfo(ctx context.Context, session *session.UserSession) error {
+ userInfo, err := p.oidcProvider.UserInfo(ctx, session.OAuthTokenSource)
+ if err != nil {
+ return ErrInvalidToken
+ }
+
+ var claims struct {
+ Name string `json:"name"`
+ TeamID string `json:"team_id"`
+ TeamName string `json:"team_name"`
+ }
+ err = userInfo.Claims(&claims)
+ if err != nil {
+ return err
+ }
+
+ session.Subject = userInfo.Subject
+ session.Email = userInfo.Email
+ session.Name = claims.Name
+ session.TeamID = claims.TeamID
+ session.TeamName = claims.TeamName
+
+ return nil
+}
diff --git a/pkg/session/session.go b/pkg/session/session.go
index 87306ac..b60fef6 100644
--- a/pkg/session/session.go
+++ b/pkg/session/session.go
@@ -5,11 +5,20 @@ import (
"encoding/base64"
"fmt"
"sync"
+
+ "golang.org/x/oauth2"
)
type UserSession struct {
Token string
- Team string
+
+ OAuthTokenSource oauth2.TokenSource
+ Subject string
+ Name string
+ Email string
+
+ TeamID string
+ TeamName string
}
// implemtation adapted from
@@ -38,7 +47,7 @@ func (s *MemoryStore) GetByToken(token string) *UserSession {
return s.sessions[token]
}
-func (s *MemoryStore) Create(team string) (*UserSession, error) {
+func (s *MemoryStore) Create() (*UserSession, error) {
s.lock.Lock()
defer s.lock.Unlock()
@@ -56,7 +65,6 @@ func (s *MemoryStore) Create(team string) (*UserSession, error) {
session := &UserSession{
Token: token,
- Team: team,
}
s.sessions[token] = session
diff --git a/startup.txt b/startup.txt
new file mode 100644
index 0000000..9d9ef6c
--- /dev/null
+++ b/startup.txt
@@ -0,0 +1,50 @@
+
+
+ . . .
+ . ,
+ . , . . ,
+ . . .
+ ! ,
+ ! .
+ , . ^
+ / \ .
+ . /___\ ,
+ . |= =| ,
+ . | |
+ | | ,
+, | |
+ | | .
+ | |
+ . . | | ,
+ , | | .
+ | |
+ | | .
+ /|##!##|\
+ . / |##!##| \
+ / |##!##| \ ,
+ | / ^ | ^ \ |
+ . | / ( | ) \ | ,
+ , . |/ ( | ) \|
+ (( ))
+ (( : )) .
+ (( : ))
+ , (( )) .
+ . (( )) ,
+ ( )
+ .
+ . . .
+ , . ,
+
+ _ __
+ ___| |_ / _|
+ / __| __| |_
+ _ | (__| |_| _|
+ (_)_ __ ___| \___|\__|_|_ ___ ___ _ __
+ | | '_ \/ __| __/ _` | '_ \ / __/ _ \ '__|
+ | | | | \__ \ || (_| | | | | (_| __/ |
+ |_|_| |_|___/\__\__,_|_| |_|\___\___|_|
+
+____^/\___^--____/\____O______________/\/\--
+ /\^ ^ ^ ^ ^^ ^ '\
+ -- - -- -
+ -- __ ___-- ^ ^
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)))