feat(auth): pass id_token_hint on Keycloak logout
Carries the raw ID token in the session cookie so logout can hand it back to Keycloak as id_token_hint, letting it skip its own logout-confirmation prompt -- otherwise a user could cancel out of it and land back in the app with a Keycloak SSO session but no geniusrun profile (e.g. right after deleting their account).
This commit is contained in:
@@ -1,9 +1,15 @@
|
||||
// Package auth implements geniusrun's own login gate: an OIDC Authorization
|
||||
// Code flow against an existing Keycloak realm (see oidc.go), backed by a
|
||||
// signed session cookie geniusrun mints itself (this file) and a chi
|
||||
// middleware that checks it (middleware.go). Keycloak's own tokens are never
|
||||
// stored or refreshed -- once HandleCallback verifies the ID token and role,
|
||||
// only this package's own cookie matters for subsequent requests.
|
||||
// middleware that checks it (middleware.go). Keycloak's own access/refresh
|
||||
// tokens are never stored or refreshed -- once HandleCallback verifies the
|
||||
// ID token and role, only this package's own cookie matters for subsequent
|
||||
// requests. The one exception is the raw ID token itself, carried opaquely
|
||||
// inside the signed session cookie (Claims.IDToken) solely so a later
|
||||
// logout can pass it back to Keycloak as id_token_hint -- letting Keycloak
|
||||
// skip its own logout-confirmation prompt for a session it can positively
|
||||
// identify, rather than leaving the user a chance to cancel out of it after
|
||||
// their geniusrun account (and profile) is already gone.
|
||||
package auth
|
||||
|
||||
import (
|
||||
@@ -26,17 +32,21 @@ const (
|
||||
)
|
||||
|
||||
// Claims identifies the authenticated user, carried in the signed session
|
||||
// cookie.
|
||||
// cookie. IDToken is the raw Keycloak ID token JWT from the OIDC callback,
|
||||
// carried opaquely so a later logout can pass it back to Keycloak as
|
||||
// id_token_hint (see EndSessionURL) -- geniusrun never inspects it itself.
|
||||
type Claims struct {
|
||||
Sub string
|
||||
Name string
|
||||
Email string
|
||||
Sub string
|
||||
Name string
|
||||
Email string
|
||||
IDToken string
|
||||
}
|
||||
|
||||
type sessionClaims struct {
|
||||
Sub string `json:"sub"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Sub string `json:"sub"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
IDToken string `json:"id_token,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
@@ -58,9 +68,10 @@ type txnClaims struct {
|
||||
func MintSessionCookie(claims Claims, secret []byte, duration time.Duration, secure bool) (*http.Cookie, error) {
|
||||
now := time.Now()
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, sessionClaims{
|
||||
Sub: claims.Sub,
|
||||
Name: claims.Name,
|
||||
Email: claims.Email,
|
||||
Sub: claims.Sub,
|
||||
Name: claims.Name,
|
||||
Email: claims.Email,
|
||||
IDToken: claims.IDToken,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(duration)),
|
||||
@@ -91,7 +102,7 @@ func ParseSessionCookie(cookie *http.Cookie, secret []byte) (Claims, error) {
|
||||
if _, err := jwt.ParseWithClaims(cookie.Value, &sc, keyfunc(secret), jwt.WithValidMethods([]string{"HS256"})); err != nil {
|
||||
return Claims{}, fmt.Errorf("parse session token: %w", err)
|
||||
}
|
||||
return Claims{Sub: sc.Sub, Name: sc.Name, Email: sc.Email}, nil
|
||||
return Claims{Sub: sc.Sub, Name: sc.Name, Email: sc.Email, IDToken: sc.IDToken}, nil
|
||||
}
|
||||
|
||||
// MintTxnCookie signs an OIDC login transaction into a short-lived cookie.
|
||||
|
||||
Reference in New Issue
Block a user