feat: migrate auth0mock from Node.js to Go
Refactor the application to a Go-based architecture for improved performance and maintainability. Replace the Dockerfile to utilize a multi-stage build process, enhancing image efficiency. Implement comprehensive session store tests to ensure reliability and create new OAuth handlers for managing authentication efficiently. Update documentation to reflect these structural changes.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"gitlab.com/unboundsoftware/auth0mock/auth"
|
||||
)
|
||||
|
||||
// DiscoveryHandler handles OIDC discovery endpoints
|
||||
type DiscoveryHandler struct {
|
||||
jwtService *auth.JWTService
|
||||
}
|
||||
|
||||
// NewDiscoveryHandler creates a new discovery handler
|
||||
func NewDiscoveryHandler(jwtService *auth.JWTService) *DiscoveryHandler {
|
||||
return &DiscoveryHandler{
|
||||
jwtService: jwtService,
|
||||
}
|
||||
}
|
||||
|
||||
// OpenIDConfiguration returns the OIDC discovery document
|
||||
func (h *DiscoveryHandler) OpenIDConfiguration(w http.ResponseWriter, r *http.Request) {
|
||||
issuer := h.jwtService.Issuer()
|
||||
|
||||
config := map[string]interface{}{
|
||||
"issuer": issuer,
|
||||
"authorization_endpoint": issuer + "authorize",
|
||||
"token_endpoint": issuer + "oauth/token",
|
||||
"token_endpoint_auth_methods_supported": []string{"client_secret_basic", "private_key_jwt"},
|
||||
"token_endpoint_auth_signing_alg_values_supported": []string{"RS256"},
|
||||
"userinfo_endpoint": issuer + "userinfo",
|
||||
"check_session_iframe": issuer + "check_session",
|
||||
"end_session_endpoint": issuer + "end_session",
|
||||
"jwks_uri": issuer + ".well-known/jwks.json",
|
||||
"registration_endpoint": issuer + "register",
|
||||
"scopes_supported": []string{"openid", "profile", "email", "address", "phone", "offline_access"},
|
||||
"response_types_supported": []string{"code", "code id_token", "id_token", "id_token token"},
|
||||
"acr_values_supported": []string{},
|
||||
"subject_types_supported": []string{"public", "pairwise"},
|
||||
"userinfo_signing_alg_values_supported": []string{"RS256", "ES256", "HS256"},
|
||||
"userinfo_encryption_alg_values_supported": []string{"RSA-OAEP-256", "A128KW"},
|
||||
"userinfo_encryption_enc_values_supported": []string{"A128CBC-HS256", "A128GCM"},
|
||||
"id_token_signing_alg_values_supported": []string{"RS256", "ES256", "HS256"},
|
||||
"id_token_encryption_alg_values_supported": []string{"RSA-OAEP-256", "A128KW"},
|
||||
"id_token_encryption_enc_values_supported": []string{"A128CBC-HS256", "A128GCM"},
|
||||
"request_object_signing_alg_values_supported": []string{"none", "RS256", "ES256"},
|
||||
"display_values_supported": []string{"page", "popup"},
|
||||
"claim_types_supported": []string{"normal", "distributed"},
|
||||
"claims_supported": []string{
|
||||
"sub", "iss", "auth_time", "acr",
|
||||
"name", "given_name", "family_name", "nickname",
|
||||
"profile", "picture", "website",
|
||||
"email", "email_verified", "locale", "zoneinfo",
|
||||
h.jwtService.EmailClaim(), h.jwtService.AdminClaim(),
|
||||
},
|
||||
"claims_parameter_supported": true,
|
||||
"service_documentation": "http://auth0/",
|
||||
"ui_locales_supported": []string{"en-US"},
|
||||
"code_challenge_methods_supported": []string{"plain", "S256"},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(config)
|
||||
}
|
||||
|
||||
// JWKS returns the JSON Web Key Set
|
||||
func (h *DiscoveryHandler) JWKS(w http.ResponseWriter, r *http.Request) {
|
||||
jwks, err := h.jwtService.GetJWKS()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get JWKS", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(jwks)
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gitlab.com/unboundsoftware/auth0mock/store"
|
||||
)
|
||||
|
||||
// ManagementHandler handles Auth0 Management API endpoints
|
||||
type ManagementHandler struct {
|
||||
userStore *store.UserStore
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// NewManagementHandler creates a new management handler
|
||||
func NewManagementHandler(userStore *store.UserStore, logger *slog.Logger) *ManagementHandler {
|
||||
return &ManagementHandler{
|
||||
userStore: userStore,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// UserResponse represents the user response format
|
||||
type UserResponse struct {
|
||||
Email string `json:"email,omitempty"`
|
||||
GivenName string `json:"given_name,omitempty"`
|
||||
FamilyName string `json:"family_name,omitempty"`
|
||||
UserID string `json:"user_id"`
|
||||
Picture string `json:"picture,omitempty"`
|
||||
}
|
||||
|
||||
// GetUsersByEmail handles GET /api/v2/users-by-email
|
||||
func (h *ManagementHandler) GetUsersByEmail(w http.ResponseWriter, r *http.Request) {
|
||||
email := r.URL.Query().Get("email")
|
||||
|
||||
h.logger.Debug("getting user by email", "email", email)
|
||||
|
||||
user, ok := h.userStore.GetByEmail(email)
|
||||
if !ok {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode([]interface{}{})
|
||||
return
|
||||
}
|
||||
|
||||
response := []UserResponse{
|
||||
{
|
||||
Email: user.Email,
|
||||
GivenName: user.GivenName,
|
||||
FamilyName: user.FamilyName,
|
||||
UserID: fmt.Sprintf("auth0|%s", user.UserID),
|
||||
Picture: user.Picture,
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// CreateUser handles POST /api/v2/users
|
||||
func (h *ManagementHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Email string `json:"email"`
|
||||
GivenName string `json:"given_name"`
|
||||
FamilyName string `json:"family_name"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Email == "" {
|
||||
http.Error(w, "Email is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user := &store.User{
|
||||
Email: req.Email,
|
||||
GivenName: req.GivenName,
|
||||
FamilyName: req.FamilyName,
|
||||
UserID: req.Email,
|
||||
}
|
||||
|
||||
// Set defaults if not provided
|
||||
if user.GivenName == "" {
|
||||
user.GivenName = "Given"
|
||||
}
|
||||
if user.FamilyName == "" {
|
||||
user.FamilyName = "Last"
|
||||
}
|
||||
|
||||
h.userStore.Create(req.Email, user)
|
||||
|
||||
h.logger.Info("created user", "email", req.Email)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"user_id": fmt.Sprintf("auth0|%s", req.Email),
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateUser handles PATCH /api/v2/users/{userid}
|
||||
func (h *ManagementHandler) UpdateUser(w http.ResponseWriter, r *http.Request) {
|
||||
// Extract user ID from path - format: /api/v2/users/auth0|email@example.com
|
||||
path := r.URL.Path
|
||||
userID := strings.TrimPrefix(path, "/api/v2/users/")
|
||||
|
||||
// Strip "auth0|" prefix to get email
|
||||
email := strings.TrimPrefix(userID, "auth0|")
|
||||
|
||||
h.logger.Debug("patching user", "userid", userID, "email", email)
|
||||
|
||||
var req struct {
|
||||
GivenName string `json:"given_name"`
|
||||
FamilyName string `json:"family_name"`
|
||||
Picture string `json:"picture"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
updates := &store.User{
|
||||
GivenName: req.GivenName,
|
||||
FamilyName: req.FamilyName,
|
||||
Picture: req.Picture,
|
||||
}
|
||||
|
||||
_, ok := h.userStore.Update(email, updates)
|
||||
if !ok {
|
||||
http.Error(w, "User not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("updated user", "email", email)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"user_id": fmt.Sprintf("auth0|%s", email),
|
||||
})
|
||||
}
|
||||
|
||||
// PasswordChangeTicket handles POST /api/v2/tickets/password-change
|
||||
func (h *ManagementHandler) PasswordChangeTicket(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"ticket": "https://some-url",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"gitlab.com/unboundsoftware/auth0mock/auth"
|
||||
"gitlab.com/unboundsoftware/auth0mock/store"
|
||||
)
|
||||
|
||||
//go:embed templates/login.html
|
||||
var templateFS embed.FS
|
||||
|
||||
// OAuthHandler handles OAuth/OIDC endpoints
|
||||
type OAuthHandler struct {
|
||||
jwtService *auth.JWTService
|
||||
sessionStore *store.SessionStore
|
||||
loginTemplate *template.Template
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// NewOAuthHandler creates a new OAuth handler
|
||||
func NewOAuthHandler(jwtService *auth.JWTService, sessionStore *store.SessionStore, logger *slog.Logger) (*OAuthHandler, error) {
|
||||
tmpl, err := template.ParseFS(templateFS, "templates/login.html")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse login template: %w", err)
|
||||
}
|
||||
|
||||
return &OAuthHandler{
|
||||
jwtService: jwtService,
|
||||
sessionStore: sessionStore,
|
||||
loginTemplate: tmpl,
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TokenRequest represents the token endpoint request body
|
||||
type TokenRequest struct {
|
||||
GrantType string `json:"grant_type"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
Code string `json:"code"`
|
||||
CodeVerifier string `json:"code_verifier"`
|
||||
RedirectURI string `json:"redirect_uri"`
|
||||
}
|
||||
|
||||
// TokenResponse represents the token endpoint response
|
||||
type TokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
IDToken string `json:"id_token"`
|
||||
Scope string `json:"scope"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
TokenType string `json:"token_type"`
|
||||
}
|
||||
|
||||
// Token handles the POST /oauth/token endpoint
|
||||
func (h *OAuthHandler) Token(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
grantType := r.FormValue("grant_type")
|
||||
clientID := r.FormValue("client_id")
|
||||
code := r.FormValue("code")
|
||||
codeVerifier := r.FormValue("code_verifier")
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
switch grantType {
|
||||
case "client_credentials":
|
||||
h.handleClientCredentials(w, clientID)
|
||||
case "authorization_code", "":
|
||||
if code != "" {
|
||||
h.handleAuthorizationCode(w, code, codeVerifier, clientID)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "Missing code"})
|
||||
}
|
||||
default:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "Unsupported grant type"})
|
||||
}
|
||||
}
|
||||
|
||||
func (h *OAuthHandler) handleClientCredentials(w http.ResponseWriter, clientID string) {
|
||||
if clientID == "" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "Missing client_id"})
|
||||
return
|
||||
}
|
||||
|
||||
adminClaim := map[string]interface{}{
|
||||
h.jwtService.AdminClaim(): true,
|
||||
}
|
||||
|
||||
accessToken, err := h.jwtService.SignAccessToken(
|
||||
"auth0|management",
|
||||
clientID,
|
||||
"management@example.org",
|
||||
[]map[string]interface{}{adminClaim},
|
||||
)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to sign access token", "error", err)
|
||||
http.Error(w, "Failed to generate token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
idToken, err := h.jwtService.SignIDToken(
|
||||
"auth0|management",
|
||||
clientID,
|
||||
"",
|
||||
"management@example.org",
|
||||
"Management API",
|
||||
"Management",
|
||||
"API",
|
||||
"",
|
||||
[]map[string]interface{}{adminClaim},
|
||||
)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to sign ID token", "error", err)
|
||||
http.Error(w, "Failed to generate token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("signed token for management API")
|
||||
|
||||
json.NewEncoder(w).Encode(TokenResponse{
|
||||
AccessToken: accessToken,
|
||||
IDToken: idToken,
|
||||
Scope: "openid%20profile%20email",
|
||||
ExpiresIn: 7200,
|
||||
TokenType: "Bearer",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *OAuthHandler) handleAuthorizationCode(w http.ResponseWriter, code, codeVerifier, clientID string) {
|
||||
session, ok := h.sessionStore.Get(code)
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid code"})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify PKCE if code_verifier is provided
|
||||
if codeVerifier != "" && session.CodeChallenge != "" {
|
||||
// Determine method - default to S256 if challenge looks like a hash
|
||||
method := auth.PKCEMethodS256
|
||||
if len(session.CodeChallenge) < 43 {
|
||||
method = auth.PKCEMethodPlain
|
||||
}
|
||||
|
||||
if !auth.VerifyPKCE(codeVerifier, session.CodeChallenge, method) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid code_verifier"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
accessToken, err := h.jwtService.SignAccessToken(
|
||||
"auth0|"+session.Email,
|
||||
session.ClientID,
|
||||
session.Email,
|
||||
session.CustomClaims,
|
||||
)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to sign access token", "error", err)
|
||||
http.Error(w, "Failed to generate token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
idToken, err := h.jwtService.SignIDToken(
|
||||
"auth0|"+session.Email,
|
||||
session.ClientID,
|
||||
session.Nonce,
|
||||
session.Email,
|
||||
"Example Person",
|
||||
"Example",
|
||||
"Person",
|
||||
"https://cdn.playbuzz.com/cdn/5458360f-32ea-460e-a707-1a2d26760558/70bda687-cb84-4756-8a44-8cf735ed87b3.jpg",
|
||||
session.CustomClaims,
|
||||
)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to sign ID token", "error", err)
|
||||
http.Error(w, "Failed to generate token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("signed token", "email", session.Email)
|
||||
|
||||
// Clean up session after successful token exchange
|
||||
h.sessionStore.Delete(code)
|
||||
|
||||
json.NewEncoder(w).Encode(TokenResponse{
|
||||
AccessToken: accessToken,
|
||||
IDToken: idToken,
|
||||
Scope: "openid%20profile%20email",
|
||||
ExpiresIn: 7200,
|
||||
TokenType: "Bearer",
|
||||
})
|
||||
}
|
||||
|
||||
// Code handles the POST /code endpoint (form submission from login page)
|
||||
func (h *OAuthHandler) Code(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
email := r.FormValue("email")
|
||||
password := r.FormValue("password")
|
||||
codeChallenge := r.FormValue("codeChallenge")
|
||||
redirect := r.FormValue("redirect")
|
||||
state := r.FormValue("state")
|
||||
nonce := r.FormValue("nonce")
|
||||
clientID := r.FormValue("clientId")
|
||||
admin := r.FormValue("admin") == "true"
|
||||
|
||||
if email == "" || password == "" || codeChallenge == "" {
|
||||
h.logger.Debug("invalid code request", "email", email, "hasPassword", password != "", "hasChallenge", codeChallenge != "")
|
||||
http.Error(w, "Email, password, or code challenge is missing", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
adminClaim := map[string]interface{}{
|
||||
h.jwtService.AdminClaim(): admin,
|
||||
}
|
||||
|
||||
session := &store.Session{
|
||||
Email: email,
|
||||
Password: password,
|
||||
State: state,
|
||||
Nonce: nonce,
|
||||
ClientID: clientID,
|
||||
CodeChallenge: codeChallenge,
|
||||
CustomClaims: []map[string]interface{}{adminClaim},
|
||||
}
|
||||
|
||||
h.sessionStore.Create(codeChallenge, session)
|
||||
|
||||
redirectURL := fmt.Sprintf("%s?code=%s&state=%s", redirect, codeChallenge, url.QueryEscape(state))
|
||||
http.Redirect(w, r, redirectURL, http.StatusFound)
|
||||
}
|
||||
|
||||
// Authorize handles the GET /authorize endpoint
|
||||
func (h *OAuthHandler) Authorize(w http.ResponseWriter, r *http.Request) {
|
||||
redirect := r.URL.Query().Get("redirect_uri")
|
||||
state := r.URL.Query().Get("state")
|
||||
nonce := r.URL.Query().Get("nonce")
|
||||
clientID := r.URL.Query().Get("client_id")
|
||||
codeChallenge := r.URL.Query().Get("code_challenge")
|
||||
prompt := r.URL.Query().Get("prompt")
|
||||
responseMode := r.URL.Query().Get("response_mode")
|
||||
|
||||
// Try to get existing session from cookie
|
||||
cookie, err := r.Cookie("auth0")
|
||||
var existingCode string
|
||||
if err == nil {
|
||||
existingCode = cookie.Value
|
||||
}
|
||||
|
||||
// Handle response_mode=query with existing session
|
||||
if responseMode == "query" && existingCode != "" {
|
||||
if h.sessionStore.Update(existingCode, codeChallenge, func(s *store.Session) {
|
||||
s.Nonce = nonce
|
||||
s.State = state
|
||||
s.CodeChallenge = codeChallenge
|
||||
}) {
|
||||
redirectURL := fmt.Sprintf("%s?code=%s&state=%s", redirect, codeChallenge, state)
|
||||
http.Redirect(w, r, redirectURL, http.StatusFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Handle prompt=none with response_mode=web_message (silent auth)
|
||||
if prompt == "none" && responseMode == "web_message" && existingCode != "" {
|
||||
session, ok := h.sessionStore.Get(existingCode)
|
||||
if ok {
|
||||
h.sessionStore.Update(existingCode, existingCode, func(s *store.Session) {
|
||||
s.Nonce = nonce
|
||||
s.State = state
|
||||
s.CodeChallenge = codeChallenge
|
||||
})
|
||||
|
||||
// Send postMessage response
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprintf(w, `<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
(() => {
|
||||
const msg = {
|
||||
type: 'authorization_response',
|
||||
response: {
|
||||
code: '%s',
|
||||
state: '%s'
|
||||
}
|
||||
}
|
||||
parent.postMessage(msg, "*")
|
||||
})()
|
||||
</script>
|
||||
</body>
|
||||
</html>`, existingCode, session.State)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Set cookie for session tracking
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "auth0",
|
||||
Value: codeChallenge,
|
||||
Path: "/",
|
||||
SameSite: http.SameSiteNoneMode,
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
})
|
||||
|
||||
// Render login form
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
data := map[string]string{
|
||||
"Redirect": redirect,
|
||||
"State": state,
|
||||
"Nonce": nonce,
|
||||
"ClientID": clientID,
|
||||
"CodeChallenge": codeChallenge,
|
||||
}
|
||||
|
||||
if err := h.loginTemplate.Execute(w, data); err != nil {
|
||||
h.logger.Error("failed to render login template", "error", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"gitlab.com/unboundsoftware/auth0mock/auth"
|
||||
"gitlab.com/unboundsoftware/auth0mock/store"
|
||||
)
|
||||
|
||||
// SessionHandler handles session-related endpoints
|
||||
type SessionHandler struct {
|
||||
jwtService *auth.JWTService
|
||||
sessionStore *store.SessionStore
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// NewSessionHandler creates a new session handler
|
||||
func NewSessionHandler(jwtService *auth.JWTService, sessionStore *store.SessionStore, logger *slog.Logger) *SessionHandler {
|
||||
return &SessionHandler{
|
||||
jwtService: jwtService,
|
||||
sessionStore: sessionStore,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// UserInfo handles GET /userinfo
|
||||
func (h *SessionHandler) UserInfo(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"picture": "https://cdn.playbuzz.com/cdn/5458360f-32ea-460e-a707-1a2d26760558/70bda687-cb84-4756-8a44-8cf735ed87b3.jpg",
|
||||
})
|
||||
}
|
||||
|
||||
// TokenInfo handles POST /tokeninfo
|
||||
func (h *SessionHandler) TokenInfo(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
IDToken string `json:"id_token"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.IDToken == "" {
|
||||
h.logger.Debug("no token given in body")
|
||||
http.Error(w, "missing id_token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := h.jwtService.DecodeToken(req.IDToken)
|
||||
if err != nil {
|
||||
h.logger.Debug("failed to decode token", "error", err)
|
||||
http.Error(w, "invalid id_token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if userID, ok := claims["sub"].(string); ok {
|
||||
h.logger.Debug("returning token data", "user_id", userID)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(claims)
|
||||
}
|
||||
|
||||
// Logout handles GET /v2/logout
|
||||
func (h *SessionHandler) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
returnTo := r.URL.Query().Get("returnTo")
|
||||
|
||||
// Try to get session from cookie
|
||||
cookie, err := r.Cookie("auth0")
|
||||
if err == nil && cookie.Value != "" {
|
||||
h.sessionStore.Delete(cookie.Value)
|
||||
h.logger.Debug("deleted session", "code", cookie.Value)
|
||||
}
|
||||
|
||||
// Clear the cookie
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "auth0",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
SameSite: http.SameSiteNoneMode,
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
})
|
||||
|
||||
if returnTo != "" {
|
||||
http.Redirect(w, r, returnTo, http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("Logged out"))
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Auth</title>
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<form method="post" action="/code">
|
||||
<div class="card" style="width: 18rem; margin-top: 2rem;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Login</h5>
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="text" name="email" id="email" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" name="password" id="password" class="form-control">
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="admin" value="true" id="admin">
|
||||
<label class="form-check-label" for="admin">
|
||||
Admin
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mt-3">Login</button>
|
||||
<input type="hidden" value="{{.Redirect}}" name="redirect">
|
||||
<input type="hidden" value="{{.State}}" name="state">
|
||||
<input type="hidden" value="{{.Nonce}}" name="nonce">
|
||||
<input type="hidden" value="{{.ClientID}}" name="clientId">
|
||||
<input type="hidden" value="{{.CodeChallenge}}" name="codeChallenge">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user