package oidc import ( "context" "fmt" "net/http" "net/url" "forge.cadoles.com/cadoles/bouncer/internal/proxy/director" "forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn" "forge.cadoles.com/cadoles/bouncer/internal/store" "github.com/coreos/go-oidc/v3/oidc" "github.com/gorilla/sessions" "github.com/pkg/errors" ) type Authenticator struct{} func (a *Authenticator) PreAuthentication(w http.ResponseWriter, r *http.Request, layer *store.Layer, sess *sessions.Session) error { ctx := r.Context() originalURL, err := director.OriginalURL(ctx) if err != nil { return errors.WithStack(err) } options, err := fromStoreOptions(layer.Options) if err != nil { return errors.WithStack(err) } redirectURL := a.getRedirectURL(layer.Name, originalURL, options) logoutURL := a.getLogoutURL(layer.Name, originalURL, options) client, err := a.getClient(options, redirectURL.String()) if err != nil { return errors.WithStack(err) } switch r.URL.Path { case redirectURL.Path: if err := client.HandleCallback(w, r, sess); err != nil { return errors.WithStack(err) } case logoutURL.Path: if err := client.HandleLogout(w, r, sess, options.OIDC.PostLogoutRedirectURL); err != nil { return errors.WithStack(err) } } return nil } // Authenticate implements authn.Authenticator. func (a *Authenticator) Authenticate(w http.ResponseWriter, r *http.Request, layer *store.Layer, sess *sessions.Session) (*authn.User, error) { ctx := r.Context() originalURL, err := director.OriginalURL(ctx) if err != nil { return nil, errors.WithStack(err) } options, err := fromStoreOptions(layer.Options) if err != nil { return nil, errors.WithStack(err) } redirectURL := a.getRedirectURL(layer.Name, originalURL, options) client, err := a.getClient(options, redirectURL.String()) if err != nil { return nil, errors.WithStack(err) } idToken, err := client.Authenticate(w, r, sess) if err != nil { if errors.Is(err, ErrLoginRequired) { return nil, errors.WithStack(authn.ErrSkipRequest) } return nil, errors.WithStack(err) } user, err := a.toUser(idToken, layer.Name, originalURL, options, sess) if err != nil { return nil, errors.WithStack(err) } return user, nil } type claims struct { Issuer string `json:"iss"` Subject string `json:"sub"` Audience string `json:"aud"` Expiration int64 `json:"exp"` IssuedAt int64 `json:"iat"` AuthTime int64 `json:"auth_time"` Nonce string `json:"nonce"` ACR string `json:"acr"` AMR string `json:"amr"` AZP string `json:"amp"` Others map[string]any `json:"-"` } func (c claims) AsAttrs() map[string]any { attrs := make(map[string]any) for key, val := range c.Others { attrs["claim_"+key] = val } attrs["claim_issuer"] = c.Issuer delete(attrs, "claim_iss") attrs["claim_subject"] = c.Subject delete(attrs, "claim_sub") attrs["claim_audience"] = c.Audience delete(attrs, "claim_aud") attrs["claim_expiration"] = c.Expiration delete(attrs, "claim_exp") attrs["claim_issued_at"] = c.IssuedAt delete(attrs, "claim_iat") if c.AuthTime != 0 { attrs["claim_auth_time"] = c.AuthTime } if c.Nonce != "" { attrs["claim_nonce"] = c.Nonce } if c.ACR != "" { attrs["claim_arc"] = c.ACR } if c.AMR != "" { attrs["claim_amr"] = c.AMR } if c.AZP != "" { attrs["claim_azp"] = c.AZP } return attrs } func (a *Authenticator) toUser(idToken *oidc.IDToken, layerName store.LayerName, originalURL *url.URL, options *LayerOptions, sess *sessions.Session) (*authn.User, error) { var claims claims if err := idToken.Claims(&claims); err != nil { return nil, errors.WithStack(err) } if err := idToken.Claims(&claims.Others); err != nil { return nil, errors.WithStack(err) } attrs := claims.AsAttrs() logoutURL := a.getLogoutURL(layerName, originalURL, options) attrs["logout_url"] = logoutURL.String() if accessToken, exists := sess.Values[sessionKeyAccessToken]; exists && accessToken != nil { attrs["access_token"] = accessToken } if refreshToken, exists := sess.Values[sessionKeyRefreshToken]; exists && refreshToken != nil { attrs["refresh_token"] = refreshToken } if tokenExpiry, exists := sess.Values[sessionKeyTokenExpiry]; exists && tokenExpiry != nil { attrs["token_expiry"] = tokenExpiry } user := authn.NewUser(idToken.Subject, attrs) return user, nil } func (a *Authenticator) getRedirectURL(layerName store.LayerName, u *url.URL, options *LayerOptions) *url.URL { return &url.URL{ Scheme: u.Scheme, Host: u.Host, Path: fmt.Sprintf(options.OIDC.LoginCallbackPath, layerName), } } func (a *Authenticator) getLogoutURL(layerName store.LayerName, u *url.URL, options *LayerOptions) *url.URL { return &url.URL{ Scheme: u.Scheme, Host: u.Host, Path: fmt.Sprintf(options.OIDC.LogoutPath, layerName), } } func (a *Authenticator) getClient(options *LayerOptions, redirectURL string) (*Client, error) { ctx := context.Background() if options.OIDC.SkipIssuerVerification { ctx = oidc.InsecureIssuerURLContext(ctx, options.OIDC.IssuerURL) } provider, err := oidc.NewProvider(ctx, options.OIDC.IssuerURL) if err != nil { return nil, errors.Wrap(err, "could not create oidc provider") } client := NewClient( WithCredentials(options.OIDC.ClientID, options.OIDC.ClientSecret), WithProvider(provider), WithRedirectURL(redirectURL), WithScopes(options.OIDC.Scopes...), WithAuthParams(options.OIDC.AuthParams), ) return client, nil } var ( _ authn.PreAuthentication = &Authenticator{} _ authn.Authenticator = &Authenticator{} )