NewProvider constructs the fosite.OAuth2Provider that backs every OAuth endpoint in this server. Wires: - the e2a-prefixed HMAC strategy (ate2a_ / rte2a_ / oace_ tokens) - the four grant handlers we use (auth code, refresh, PKCE, revoke) - lifespans matching the legacy hand-rolled backend - PKCE-S2
(storage *Storage, issuerURL string, hmacSecret []byte)
| 80 | // Must match what clients fetch via /.well-known. Operators MUST set |
| 81 | // http.public_url; we read it once at startup, no per-request override. |
| 82 | func NewProvider(storage *Storage, issuerURL string, hmacSecret []byte) (fosite.OAuth2Provider, error) { |
| 83 | oauthSecret, err := deriveOAuthSigningKey(hmacSecret) |
| 84 | if err != nil { |
| 85 | return nil, err |
| 86 | } |
| 87 | cfg := &fosite.Config{ |
| 88 | // Token / code lifetimes — fosite writes these into the session's |
| 89 | // ExpiresAt map before persistence; our storage adapter reads |
| 90 | // them back out as the row's expires_at column. |
| 91 | AccessTokenLifespan: AccessTokenLifespan, |
| 92 | RefreshTokenLifespan: RefreshTokenLifespan, |
| 93 | AuthorizeCodeLifespan: AuthorizeCodeLifespan, |
| 94 | |
| 95 | // HMAC signing material. fosite gets the HKDF-derived subkey, |
| 96 | // never the master. deriveOAuthSigningKey already validated |
| 97 | // length above, so by the time we get here oauthSecret is |
| 98 | // guaranteed to be a fixed 32-byte slice. |
| 99 | GlobalSecret: oauthSecret, |
| 100 | RotatedGlobalSecrets: nil, |
| 101 | |
| 102 | // PKCE-S256 mandatory. EnforcePKCE=true makes fosite reject any |
| 103 | // authorize request without code_challenge; EnforcePKCEForPublic |
| 104 | // is redundant under EnforcePKCE but documents intent for the |
| 105 | // public-clients-only posture. EnablePKCEPlainChallengeMethod= |
| 106 | // false rejects code_challenge_method=plain (we only advertise |
| 107 | // S256 in discovery; this enforces it at the protocol layer). |
| 108 | EnforcePKCE: true, |
| 109 | EnforcePKCEForPublicClients: true, |
| 110 | EnablePKCEPlainChallengeMethod: false, |
| 111 | |
| 112 | // Scope matching: e2a's scopes are "agent" (runtime/inbox tier, the |
| 113 | // DCR-public default) and "account" (admin). ExactScope means a |
| 114 | // requested scope must literally equal one the client registered; |
| 115 | // HierarchicScope (the alternative) would let "agent:inbox" match |
| 116 | // "agent" as a parent — we don't want that drift today. |
| 117 | ScopeStrategy: fosite.ExactScopeStrategy, |
| 118 | AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, |
| 119 | |
| 120 | // Issue refresh tokens on EVERY authorize-code exchange. fosite's |
| 121 | // default ("offline" / "offline_access" required in granted |
| 122 | // scopes) is an OIDC convention; for our pure-OAuth MCP use |
| 123 | // case we want refresh on every grant. Empty list = always. |
| 124 | RefreshTokenScopes: []string{}, |
| 125 | |
| 126 | // Issuer for RFC 9207 (iss param on authorize response) and any |
| 127 | // future JWT iss claim. The discovery doc returns the same |
| 128 | // string; centralizing here means a deployment can't drift |
| 129 | // between what discovery advertises and what tokens carry. |
| 130 | IDTokenIssuer: issuerURL, |
| 131 | |
| 132 | // Don't leak server-side error context into client-facing |
| 133 | // error_description fields. fosite's default is true but for a |
| 134 | // public-facing AS we want the redacted output. |
| 135 | SendDebugMessagesToClients: false, |
| 136 | } |
| 137 | |
| 138 | hmac := &enigma.HMACStrategy{Config: cfg} |
| 139 | strategy := newPrefixedStrategy(hmac, cfg) |