Co-authored-by: Gabriel Radureau <arcodange@gmail.com> Co-committed-by: Gabriel Radureau <arcodange@gmail.com>
105 lines
3.8 KiB
Go
105 lines
3.8 KiB
Go
// 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
|
|
}
|