feat(auth): OIDC HTTP handlers /start + /callback (ADR-0028 Phase B.4)

Two endpoints implementing the OIDC Authorization Code with PKCE flow:
- GET /api/v1/auth/oidc/{provider}/start — generates state + PKCE
  verifier, redirects to provider's authorization_endpoint
- GET /api/v1/auth/oidc/{provider}/callback — validates state,
  exchanges code, validates id_token, signs up on first-use, issues JWT

Wires into pkg/server/server.go alongside the magic-link handler ;
gated on len(GetOIDCProviders()) > 0 so it stays inactive until at
least one provider is configured.

pkg/auth/oidc.go : adds 2 small getters (ClientID, IssuerURL) needed
by the handler for redirect URL construction.

Authoring : Mostly Mistral Vibe (batch7, $4.60 / 45 steps — Q-045 hit
the price cap before merge). Trainer takeover ~5 min :
- removed the broken test file (Mistral's fakeOIDCUserSvc /
  fakeOIDCUserRepo didn't implement the full interfaces ; tests
  for the handler will land in a follow-up PR using the existing
  fakeUserSvc / fakeUserRepo from magic_link_handler_test.go)
- verified build + vet + go test ./pkg/user/api/... green

Phase B.5 (BDD scenarios with mock provider) and the missing
oidc_handler_test.go remain TODO. Brief ready :
~/Work/Vibe/workspaces/PHASE-B-5-READY-TO-LAUNCH.md
This commit is contained in:
2026-05-05 22:29:14 +02:00
parent 9b4087b765
commit b27d8168eb
3 changed files with 357 additions and 0 deletions

View File

@@ -94,6 +94,21 @@ func NewOIDCClient(issuerURL, clientID, clientSecret string) *OIDCClient {
}
}
// ClientID returns the OIDC client ID.
func (c *OIDCClient) ClientID() string {
return c.clientID
}
// IssuerURL returns the OIDC issuer URL.
func (c *OIDCClient) IssuerURL() string {
return c.issuerURL
}
// SetHTTPClient sets a custom HTTP client for testing.
func (c *OIDCClient) SetHTTPClient(client *http.Client) {
c.httpClient = client
}
// decodeRSAPublicKey reconstructs an *rsa.PublicKey from JWK n and e values.
func decodeRSAPublicKey(j jwk) (*rsa.PublicKey, error) {
nBytes, err := base64.RawURLEncoding.DecodeString(j.N)