OAuth Identity Provider
authorization-policy-oauth owns
policy-selected login without a portal, its parser, callbacks, and opaque sessions.
That peer consumer reuses this provider's authentication protocol and trust checks.
Use refresh-token-implementation to
maintain opt-in portal renewal after verified upstream login. That consumer's
snapshot mode captures claims before transformations and never renews upstream
tokens. Keep callback state/nonce/PKCE and signature verification in this owner;
selected snapshot providers must disable upstream identity-token cookies.
Ownership and Entry Points
pkg/idp/config.go validates the shared provider parameter allowlist;
pkg/idp/provider.go constructs the concrete provider. OAuth behavior lives
under pkg/idp/oauth. Exercise this dispatcher as well as concrete helpers:
an OAuth Config field is not usable through the shared API until dispatch
accepts it and checks its type.
pkg/idp/oauth/parser.NewOAuthIdentityProviderConfigFromDirectives returns
*oauth.Config. pkg/idp/parser.NewOAuthIdentityProviderConfigFromDirectives
reuses that parser and returns *idp.IdentityProviderConfig for the shared
dispatcher or root Config.AddIdentityProvider. Both take the provider name
and encoded body statements. Keep grammar in the OAuth parser; the shared
adapter owns only normalized configuration conversion and allowlist validation.
See directive parsing for the complete
grammar, application examples, parser maintenance, defaults, and validation
boundaries.
authenticate.go owns redirects and code exchange; validator.go owns token
roles and trust claims; jwt.go owns signature verification and bounded retry;
jwks.go owns key conversion; jwks_store.go owns remote ingestion, static
pins, candidate selection, and refresh. provider.go owns setup/discovery.
The provider verifies upstream credentials. The portal subsequently signs a
separate credential using its existing KMS configuration. Keep upstream keys,
algorithms, issuer metadata, and key refresh independent of portal signing
selection, token settings, and portal JWKS publication. ES512 is the existing
autogeneration fallback, not a mandatory portal signing algorithm.
GitHub identity claims
The GitHub driver derives github_id only from the numeric id in its
successful authenticated /user response. Decode that value losslessly from
the original JSON as a positive unsigned 64-bit integer and emit a canonical
decimal string; a float64 conversion can authorize the wrong large ID. Preserve
metadata.id as a JSON number and the existing sub = github.com/<login>.
An absent ID remains compatible with older profile fixtures but emits no
github_id. Reject a supplied null, string, zero, negative, fraction, exponent
or overflow. Never accept an upstream field already named github_id as proof.
When user_org_filters enables the existing GitHub organization lookup,
project accepted organization login values into the string list github_orgs
as well as the existing github.com/<org>/members groups. For example, provider
body directive user_org_filters .* admits all returned organizations; narrower
regex filters restrict both outputs. Derive the list from fetched organization
records, never the profile's arbitrary extra fields or transformed roles.
Missing/disabled lookup, an empty or filtered result, an HTTP/API failure, or
invalid organization login types cannot grant an organization match. Existing
lookup failures remain nonfatal for login and produce no organization claim.
The lookup follows the profile's validated GitHub API organizations_url and
retains its existing response/visibility behavior, including its current single
page. GitHub's list-user-organizations endpoint
returns public memberships; adding read:org alone does not change that endpoint
to the authenticated-user API or expose private memberships. This feature adds
matching over those results, not private-membership discovery or pagination.
The transform parser and portal provider boundary are owned by
GitHub identity matchers.
Other drivers must not forward either reserved top-level claim.
The public factory assumes its embedding caller has authenticated the claim
source; a realm called github is not proof of the configured driver.
Validate changes with TestGithubIDFromProfile, TestGithubOrganizationClaims,
transform parser tests/examples, and the default-suite TLS consumer
TestE2EOAuthGithubTransforms. Its isolated subprocess confines proxy routing
for fixed GitHub hosts to local listeners and preserves production endpoints.
It covers large adjacent IDs, unchanged subjects/metadata, renamed accounts,
malformed IDs, exact/regex membership, org filters and lookup failures,
non-GitHub claim spoofing, and combined matchers through a protected resource.
make test TEST_DIR='./pkg/authn ./pkg/authn/transformer/... ./pkg/idp/oauth/... ./pkg/acl' COVERAGE_DIR='.coverage/github-transforms'
make test-automation
Ed25519 Verification Contract
internal/jwtutil registers the exact Ed25519 JOSE name and recognizes the
two concrete supported methods. The pinned JWT library provides EdDSA.
Both use pure Ed25519 over the original JWS bytes. Preserve exact method names
and never rewrite a signed header, prehash the message, or duplicate registry
initialization. KMS also consumes this helper.
The key representation follows RFC 8037;
the fully specified JOSE name follows RFC 9864.
New OKP keys require crv: Ed25519 and canonical unpadded base64url x with
exactly 32 bytes. Return a typed ed25519.PublicKey, never an HMAC byte secret.
Reject encryption use, incompatible public parameters, private d, and other
curves. Ed448 and X25519 are unsupported. Unknown unrelated extensions can be
ignored. For key_ops, omission allows verification; an explicit list must
contain verify, may also contain sign, and cannot contain duplicates or
other operations. Empty, null, and malformed lists are rejected for OKP.
Present empty or null alg/use values are invalid in remote OKP entries;
Go struct zero values still represent omitted optional fields.
| Ed25519 key restriction | Accepted JWT alg |
|---|
| JWK alg omitted | EdDSA and Ed25519 |
| JWK alg EdDSA | EdDSA |
| JWK alg Ed25519 | Ed25519 |
| Public PEM | EdDSA and Ed25519 |
JWT header values are case-sensitive. Keep the explicit RS/ES/PS/Ed allowlist,
concrete method checks, and typed verification candidates. HS and none receive
no verification keys. Preserve existing RSA-PSS verification with an
algorithm-unspecified RSA JWK. Explicit PS-labelled RSA JWKs remain outside
the legacy JWK parser's accepted metadata. Do not silently tighten legacy
RSA/EC alg/use/key_ops policy as part of Ed maintenance.
Remote decoding deliberately ignores legacy key_ops, including values the
old decoder did not inspect; apply its new typed validation only to OKP.
Configuration and Static Keys
Discovery uses the existing metadata_url and its jwks_uri. Advertised
id_token_signing_alg_values_supported values neither change the portal signer
nor become an access-token algorithm policy. Unknown advertised capabilities
do not prevent using supported keys.
jwks_keys / Config.JwksKeys maps explicit key IDs to public PEM paths.
NewJwksKeyFromPublicKeyPEM accepts Ed25519 PKIX/SPKI PUBLIC KEY, RSA PKIX
PUBLIC KEY, and RSA PKCS#1 RSA PUBLIC KEY. The RSA-specific exported constructor
retains its RSA-only contract. Private PEM, SSH, and certificate formats are
not added by this feature.
Configuration validation reads static keys; provisioning must also install
them into runtime verification state. Fully explicit authorization/token URLs
and static keys work without metadata or a disable flag. In combined mode,
fetch configured discovery while preserving explicit endpoint overrides.
Static IDs take precedence over remote entries and survive remote replacement.
key_verification_disabled retains its existing fetch-control meaning: static
JWT signatures are still verified. Do not prescribe disabled nonce, PKCE, or
TLS verification as a production prerequisite for Ed support.
The shared AuthCrunch configuration accepts the OAuth issuer and
access_token_audience string fields. An explicit issuer overrides discovered
issuer information. Do not derive issuer from base_auth_url. Static-only
users can supply issuer explicitly; absent issuer keeps the existing optional
issuer-check contract. The public parsers accept both settings as single-value
directives. Embedding-server block recognition and parser wiring are separate
consumer work; never edit sibling checkouts to implement or validate them.
Selection and Refresh
Remote JWK kid may be omitted. Explicit public constructors still require the
caller-supplied ID. A present JWT kid must be a nonempty string and restricts
candidate lookup to that ID; an unknown kid never falls back to other IDs.
Without kid, select compatible trusted candidates. Retain same-kid alternatives
and use the JWT library's VerificationKeySet for actual signature verification.
Apply each OKP entry's restrictions before deduplicating public material.
Snapshots are immutable after publication. keyMu protects snapshot/version
publication; keyFetchMu serializes refresh and its rate accounting. Network
work happens outside keyMu. A fetch version includes failures so concurrent
waiters reuse the outcome. Never mutate published maps or keys in place.
Limits are 1 MiB per metadata/JWKS response and 64 entries per remote or static
key set. Oversized documents fail rather than being truncated. Individual
invalid/unsupported remote entries do not poison valid siblings. A complete
empty or all-invalid keys array replaces the remote set with no usable entries
and returns an error; malformed documents and transport/HTTP failures preserve
the previous snapshot. Static pins are retained in either case.
Refresh allows four attempts, then a five-minute cooldown from the latest
attempt. Unknown keys and actual signature failures can cause at most one
refresh/retry per token validation. Signature failure supports same-kid
material replacement. Claims failures, unsupported methods, and failures for
an explicit static ID do not trigger refresh. Propagate request context to
request-driven JWKS fetching.
Removed remote keys stop verifying after a successful refresh observes their
removal. There is no periodic refresh or instantaneous revocation promise.
Upstream key removal does not revoke already-issued portal sessions.
Delayed setup publishes metadata before admitting authentication; setupMu
protects discovery changes and config reads. IdentityProvider.Close cancels and awaits
owned synchronous/delayed discovery, metadata/JWKS HTTP work, retry timers, and
the OAuth state worker. Registration of setup work and shutdown are serialized;
no worker may be added after shutdown begins or publish readiness after Close
returns. Configure calls serialize and reuse successful setup; closed providers
cannot be configured or authenticate again. Embedders drain requests before
disposal. The root Server.Close owns shared providers once; individual portals
do not own their lifetime. Direct provider consumers call Close themselves.
Browser State and Nonce Policy
state.go atomically publishes nonce, PKCE verifier and login binding. Bind a
five-minute, single-use callback to the initiating protected browser SessionID
and exact callback URL. Claim it atomically before processing any code or direct
token; delete it on every completion/error path. State is distinct from the
browser cookie, and a supplied state must never create its own browser binding.
IdentityProvider.CancelLogin(state, sessionIDHash, callback) atomically removes
state only for the exact callback and SHA-256 of its initiating SessionID. It is
idempotent and supports pending or claimed transactions. Keep this optional
provider capability separate from the shared idp.IdentityProvider interface;
direct authorization consumers require it to release canceled transactions.
Consumers must still prevent session issuance after their own logout or closure;
removing state does not promise to interrupt an already-running HTTP exchange.
Capture nonce policy when login starts. NonceDisabled omits nonce from the
authorization URL (including a preconfigured nonce parameter) and skips only the
nonce-claim check for that transaction. An enabled transaction requires its exact
stored nonce. Later config changes cannot weaken an existing transaction; legacy
state entries fail closed to nonce-required behavior. Missing state still fails.
Signature, issuer, audience, browser state, expiry, replay and PKCE validation
remain active when nonce compatibility is disabled.
The login binding also carries the embedding application's post-login destination:
the requests.Response.ReturnURL present when login starts is published with the
binding and is opaque to the provider. It never enters the authorization URL.
Successful completion also sets runtime ReturnURLBound, including an empty
destination, so the portal cannot fall back to another tab's cookie. The portal
revalidates the destination at grant and uses the shared 16384-byte bound.
Every callback request clears any ReturnURL it carried before the state is
claimed, and sets the bound value only on success, so neither a callback URL nor a
failed or unadmitted callback can choose a destination. The value is read before
the claim; it never changes once published. The destination shares the binding's
capacity, lifetime, single use and removal on completion, cancellation and expiry.
The embedding portal validates it before login and again before use. RedirectURL
stays reserved for the authorization endpoint.
Keep nonce_policy_test.go, state_binding_test.go, parser round-trip OAuth TLS
coverage and oauth_state_e2e_test.go for both settings, wrong/missing nonce,
wrong browser/callback, replay and malformed signed identity tokens.
TestStateBindingReturnURL and TestE2EOAuthTokenCallbackReturnsBoundDestination
cover the bound destination's lifecycle, interleaved callbacks, callback-carried
destinations, failed callbacks and unknown states.
Claims and Protocol Compatibility
An invalid ID token or selected identity access token fails authentication.
An unusable optional JWT access token contributes no claims. Opaque access
tokens retain their existing bearer/UserInfo behavior. Preserve the existing
issuer, audience, azp, time, state, nonce, and PKCE checks and role processing.
Do not turn optional legacy claims into new requirements without an explicit
compatibility decision. Malformed nonce/name values must return errors, not
panic. Several named drivers intentionally use UserInfo-oriented fetchClaims
instead of the common JWT path; preserve that dispatch.
Named-driver fetchClaims requires a nonempty string access token, a successful
UserInfo HTTP status, and at most 1 MiB of response JSON. Reject oversized bodies
instead of truncating into an accepted identity, and do not panic on malformed
token response types. Synthetic TLS tests cover these boundaries without calling
the named provider's live service.
Authorization-code exchange responses, including the Facebook variant, are also
limited to 1 MiB before JSON decoding. Reject an oversized document even when its
prefix is valid JSON. Treat error and error_description as optional strings;
malformed values fail without a panic and without copying aggregate provider data
into the returned error. Keep the real portal callback regression proving an
oversized token response issues no portal credential.
Generic OIDC UserInfo enrichment also requires a nonempty string access token, a
2xx response, and no more than 1 MiB of JSON before decoding. A failed or
oversized optional enrichment must not replace roles from the already verified
identity; login may continue with those verified claims. Keep the real TLS portal
regression proving an oversized UserInfo document cannot add a role and the
original verified role still authorizes. Test the exact size boundary and reject
provider-controlled access-token types without a panic.
Discord guild-list and guild-member enrichment requires 2xx responses and the
same 1 MiB bound for every request. Decode guild IDs, names, permissions, and
member role IDs into typed fields; reject malformed types, missing/empty IDs, and
the complete enrichment result rather than panicking or issuing partial groups.
Keep the isolated TLS/CONNECT E2E because these production endpoints are fixed
Discord hosts. It must cover valid admin groups, malformed provider types,
oversized documents, and non-success responses for both follow-up routes.
Google Cloud Identity group enrichment requires nonempty string access-token and
email inputs, a 2xx response, and the same 1 MiB limit. Validate every group
display name and all existing role elements before atomically publishing the
merged role list; a failure must leave prior roles unchanged. Keep its fixed-host
TLS/CONNECT E2E for malformed signed-claim types, non-success and oversized
responses, and its decoder/merge unit matrix.
GitHub's provider-supplied organizations_url is valid only on the HTTPS
api.github.com origin with the default TLS port, no userinfo or fragment, and a
nonempty path. Organization and fixed email follow-ups do not follow redirects;
both require 2xx responses and the same 1 MiB bound before JSON decoding. Keep
the isolated TLS/CONNECT E2E for arbitrary origins, redirect attempts,
non-success/oversized responses, and valid organization/email enrichment.
Cognito extension handling consumes the already signature/trust-verified claims
map; do not decode the raw JWT payload a second time. Validate custom roles,
groups, timezone, username, and zoneinfo types before publishing any derived
claims. Malformed signed values return an authentication error without partial
mutation or panic; custom:timezone deterministically overrides zoneinfo.
Keep both the atomic parser unit matrix and signed-RSA validation E2E.
Keep required/error JSON field validation in validateFetchedClaims before
mapping. Incorrect GitHub login/organization fields, Discord IDs and Facebook
identity/error types must not panic. Tests must distinguish parser rejection from
transport failure: include valid controls and observe the expected TLS request
and error, not just a nonnil error.
provider_direct_authorization_e2e_test.go covers provider cancellation capacity
and a synthetic LinkedIn authorization-policy journey. Named hard-coded hosts
are intercepted only in an isolated test subprocess with a local CONNECT proxy;
do not install a production transport override to enable the fixture.
user_claims_named_driver_e2e_test.go shares that isolated TLS fixture for malformed
named-driver JSON. Keep proxy targets fixed, clients bounded and hijacked
connections closed; setting a proxy after ProxyFromEnvironment has cached its
configuration does not reliably isolate a test in the main process.
Signed UserInfo/JWE, new grants, private_key_jwt, request objects, and a complete
OIDC at_hash/c_hash conformance upgrade are separate features.
Validation
Follow the repository testing lifecycle. Focused coverage belongs in
internal/jwtutil/ed25519_test.go, OAuth's ed25519_test.go,
jwks_store_test.go, provider_lifecycle_test.go, shutdown_test.go, and shared provider config
tests. Keep the existing KMS signing/default/refresh/export tests passing when
changing the shared adapter.
Parser unit tests and executable examples belong in pkg/idp/oauth/parser
and pkg/idp/parser, in package parser_test. Cover complete grammar, exact
token boundaries, duplicate aliases/states, redacted errors, immutable inputs,
driver defaults, static key files, and issuer/audience serialization. Root
config_oauth_identity_provider_test.go verifies parsed provider registration
and configuration reload.
pkg/authn/oauth_ed25519_e2e_test.go imports the shared public parser and reloads
its serialized output before exercising the public provider dispatcher,
local TLS OIDC endpoints, real state/nonce/PKCE code exchange, a portal with a
temporary local database, and an authorization gatekeeper. The fixture signs
upstream JWTs independently with standard-library crypto and uses unrelated
portal keys. It covers discovery/static/combined sources, opaque UserInfo,
verified access claims, signing compatibility, failures, and rollover. Local
self-signed TLS configuration is fixture-only. Use fresh browser sessions and
subjects where caches could obscure verification; stop listeners and workers.
pkg/authn/oauth_config_e2e_test.go uses that fixture to verify explicit and
discovered issuers, exact issuer matching, static-only behavior, explicit issuer
overrides, optional access-token audience/issuer rejection without role leakage,
and the existing azp fallback only when access audience is omitted. ID-token
audience remains the client ID. Check both successful protected-resource access
and absence of portal credentials after rejected identity tokens.
make test TEST_DIR='./internal/jwtutil ./pkg/idp/... ./pkg/kms' COVERAGE_DIR='.coverage/oauth-core'
make test TEST_DIR='./pkg/idp/... ./pkg/authn .' TEST='Test(NewOAuthIdentityProvider|OAuthIdentityProvider|ConfigOAuthIdentityProvider|E2EOAuth)|ExampleNewOAuthIdentityProvider' COVERAGE_DIR='.coverage/oauth-directives'
make test TEST_DIR='./pkg/authn' TEST='^TestE2EOAuth' COVERAGE_DIR='.coverage/oauth-e2e'
COVERAGE_DIR=.coverage/oauth-jwks-fuzz python3 assets/scripts/test_guard.py run \
go test -mod=readonly -race ./pkg/idp/oauth -run '^$' -fuzz '^FuzzOAuthJwks$' -fuzztime=10000x -parallel=2
make ci-check
Keep protocol/JWKS fuzzing bounded and test network refresh/coalescing
separately. Verify both successful login and rejection boundaries through E2E;
configuration-only tests do not establish static-key runtime support.