feat(auth): pkg/auth skeleton for OpenID Connect (ADR-0028 Phase B prep) (#69)
All checks were successful
CI/CD Pipeline / Build Docker Cache (push) Successful in 9s
CI/CD Pipeline / CI Pipeline (push) Successful in 4m4s
CI/CD Pipeline / Trigger Docker Push (push) Successful in 5s

Co-authored-by: Gabriel Radureau <arcodange@gmail.com>
Co-committed-by: Gabriel Radureau <arcodange@gmail.com>
This commit was merged in pull request #69.
This commit is contained in:
2026-05-05 19:24:41 +02:00
committed by arcodange
parent 001172e5b3
commit fbf00a3cd0
2 changed files with 117 additions and 0 deletions

104
pkg/auth/oidc.go Normal file
View File

@@ -0,0 +1,104 @@
// Package auth provides OpenID Connect client primitives for the
// dance-lessons-coach passwordless-auth migration (ADR-0028 Phase B).
//
// This file defines the client surface only. HTTP handlers wire-up
// happens in pkg/user/api/oidc_handler.go (separate phase B.3).
package auth
import (
"context"
"crypto/rsa"
"net/http"
"sync"
"time"
)
// OIDCClient is a per-provider OIDC client.
// Holds the discovery document + JWKS cache + OAuth code-exchange config.
type OIDCClient struct {
issuerURL string
clientID string
clientSecret string
httpClient *http.Client
// discovery document, lazy-fetched on first use
discoveryMu sync.RWMutex
discovery *Discovery
// JWKS cache (id_token signature verification keys), refreshed periodically
jwksMu sync.RWMutex
jwks map[string]*rsa.PublicKey
jwksFetched time.Time
}
// Discovery is the subset of the .well-known/openid-configuration document we use.
type Discovery struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
JWKSUri string `json:"jwks_uri"`
UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"`
}
// TokenResponse is the response from the token endpoint after code exchange.
type TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token,omitempty"`
IDToken string `json:"id_token"`
Scope string `json:"scope,omitempty"`
}
// IDTokenClaims represents the parsed claims from an ID token.
type IDTokenClaims struct {
Issuer string `json:"iss"`
Subject string `json:"sub"`
Audience string `json:"aud"`
ExpirationTime int64 `json:"exp"`
IssuedAt int64 `json:"iat"`
Nonce string `json:"nonce,omitempty"`
Email string `json:"email,omitempty"`
EmailVerified bool `json:"email_verified,omitempty"`
}
// NewOIDCClient constructs a client. Discovery + JWKS are NOT fetched eagerly;
// they are lazy-loaded on first use to avoid blocking server startup if the
// provider is temporarily down.
func NewOIDCClient(issuerURL, clientID, clientSecret string) *OIDCClient {
return &OIDCClient{
issuerURL: issuerURL,
clientID: clientID,
clientSecret: clientSecret,
httpClient: &http.Client{Timeout: 10 * time.Second},
jwks: make(map[string]*rsa.PublicKey),
}
}
// Discover fetches and caches the .well-known document. Idempotent.
// First call: HTTP fetch + cache. Subsequent calls: cached value.
func (c *OIDCClient) Discover(ctx context.Context) (*Discovery, error) {
// TODO Phase B.3: implement (HTTP GET issuerURL + "/.well-known/openid-configuration")
return nil, nil // placeholder for skeleton phase
}
// RefreshJWKS fetches JWKS URI, parse keys, populate jwks map.
// TODO Phase B.3: implement
func (c *OIDCClient) RefreshJWKS(ctx context.Context) error {
// TODO Phase B.3: implement (HTTP GET to JWKS URI from discovery, parse keys)
return nil // placeholder for skeleton phase
}
// ExchangeCode exchanges an authorization code for an access token and ID token.
// TODO Phase B.3: implement
func (c *OIDCClient) ExchangeCode(ctx context.Context, code, codeVerifier, redirectURI string) (*TokenResponse, error) {
// TODO Phase B.3: implement (POST to token_endpoint with code, code_verifier, redirect_uri)
return nil, nil // placeholder for skeleton phase
}
// ValidateIDToken verifies the signature and claims of an ID token.
// TODO Phase B.3: implement
func (c *OIDCClient) ValidateIDToken(ctx context.Context, idToken string) (*IDTokenClaims, error) {
// TODO Phase B.3: implement (verify signature with JWKS, validate claims)
return nil, nil // placeholder for skeleton phase
}