refactor(oidc): drop single-subject allowlist, authenticate-only (ADR-012)
ADR-011's single-user authz (ID-token subject must equal AllowedSubject, else 403) is replaced by ADR-012's model: Dex authentication is the only gate — any Dex-authenticated subject may establish a session. Whether that subject has a tapir user, and routing to registration if not, is decided downstream in internal/web (next commit). Removals (noted): oidc.Config.AllowedSubject + its required-field check + the callback 403 branch; config.Config.AllowedSubject + TAPIR_ALLOWED_SUBJECT env wiring; the AllowedSubject arg in cmdServe. ui-spec.md updated to reflect the supersession. Sessions, cookie signing, login/callback/logout unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+17
-24
@@ -4,11 +4,13 @@
|
||||
// interface, so swapping the stub for Dex is a wiring choice in cmd/tapir, not
|
||||
// a code change (ADR-003).
|
||||
//
|
||||
// Authentication is real (Dex OIDC); authorization is single-user — the ID
|
||||
// token's subject must equal Config.AllowedSubject or the request is refused
|
||||
// with 403. Sessions are server-side (in-memory, fine for the single Stage-0
|
||||
// replica) addressed by an HMAC-signed (HS256) HttpOnly Secure SameSite=Lax
|
||||
// cookie with a short TTL and sliding refresh. Tokens are never logged.
|
||||
// Authentication is real (Dex OIDC) and is the only gate: any Dex-authenticated
|
||||
// subject may sign in (ADR-012 dropped ADR-011's single-subject allowlist).
|
||||
// Authorization/registration is layered on top in internal/web (an authenticated
|
||||
// subject with no tapir user is routed to registration). Sessions are server-side
|
||||
// (in-memory, fine for the single Stage-1 replica) addressed by an HMAC-signed
|
||||
// (HS256) HttpOnly Secure SameSite=Lax cookie with a short TTL and sliding
|
||||
// refresh. Tokens are never logged.
|
||||
//
|
||||
// This is mcp-chassis's cousin but NOT the same code: mcp-chassis validates
|
||||
// inbound Bearer JWTs for MCP APIs; this is a browser session login.
|
||||
@@ -28,8 +30,8 @@ import (
|
||||
)
|
||||
|
||||
// Config is the OIDC + session configuration. cmd/tapir maps these from
|
||||
// TAPIR_OIDC_*/TAPIR_DEX_*/TAPIR_SESSION_SECRET/TAPIR_ALLOWED_SUBJECT; this
|
||||
// package takes the resolved struct.
|
||||
// TAPIR_OIDC_*/TAPIR_DEX_*/TAPIR_SESSION_SECRET; this package takes the resolved
|
||||
// struct.
|
||||
type Config struct {
|
||||
// Issuer is the Dex issuer URL, e.g. https://auth.d-ma.be. Discovery
|
||||
// (.well-known/openid-configuration) runs against it in New.
|
||||
@@ -42,9 +44,6 @@ type Config struct {
|
||||
RedirectURL string
|
||||
// SessionSecret keys the HS256 session-cookie signature. Never logged.
|
||||
SessionSecret string
|
||||
// AllowedSubject is the single Dex subject permitted to sign in. Everyone
|
||||
// else is refused 403 (single-user authz, ADR-011).
|
||||
AllowedSubject string
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -102,12 +101,11 @@ func WithInsecureCookies() Option {
|
||||
// discovery request only.
|
||||
func New(ctx context.Context, cfg Config, opts ...Option) (*DexAuth, error) {
|
||||
for name, val := range map[string]string{
|
||||
"issuer": cfg.Issuer,
|
||||
"client id": cfg.ClientID,
|
||||
"client secret": cfg.ClientSecret,
|
||||
"redirect url": cfg.RedirectURL,
|
||||
"session secret": cfg.SessionSecret,
|
||||
"allowed subject": cfg.AllowedSubject,
|
||||
"issuer": cfg.Issuer,
|
||||
"client id": cfg.ClientID,
|
||||
"client secret": cfg.ClientSecret,
|
||||
"redirect url": cfg.RedirectURL,
|
||||
"session secret": cfg.SessionSecret,
|
||||
} {
|
||||
if strings.TrimSpace(val) == "" {
|
||||
return nil, fmt.Errorf("oidc: missing %s", name)
|
||||
@@ -242,14 +240,9 @@ func (d *DexAuth) handleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Single-user authz: only the allowlisted subject may sign in. On mismatch
|
||||
// we echo the caller's own subject (an opaque id, not a secret) so the
|
||||
// maintainer can bootstrap TAPIR_ALLOWED_SUBJECT on first login.
|
||||
if idToken.Subject != d.cfg.AllowedSubject {
|
||||
http.Error(w, "forbidden — not the allowlisted subject. your subject is: "+idToken.Subject, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Authentication is the only gate (ADR-012): any Dex-authenticated subject may
|
||||
// establish a session. Whether that subject has a tapir user — and routing to
|
||||
// registration if not — is decided downstream in internal/web, not here.
|
||||
var claims struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user