// 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 }