feat(auth): verifiers on one axis, and a reusable authenticator constructor - #3423
Conversation
…KS verifier Two things a service outside this repo cannot do today without reimplementing what the package already has. Every authenticator here is a thin literal over commonAuthenticator, but the constructors fix both the scheme name and the context key, and the type is internal. A service defining its own scheme — verifying under a name of its own, or recording something that is neither a user nor a team — has to write the header handling and the 401 stamping again. That is how a scheme ends up subtly different from the others rather than merely differently named, and it loses the telemetry these emit. NewAuthenticator takes the parts that vary and keeps the rest. AdminVerifier verifies a JWT against the issuers in a ProviderConfig and returns its claims. It resolves no identity and has nothing to do with admins; admin service tokens were simply its first caller. The name reads as a restriction that does not exist, which matters because the natural second caller is anything needing to know a token is validly signed before knowing who it names. It is IssuerVerifier now, with the old names kept as deprecated aliases so callers can move in their own time. dashboard-api moves to the new name in this change; nothing else referred to it.
PR SummaryMedium Risk Overview
Reviewed by Cursor Bugbot for commit 12b0229. Bugbot is set up for automated code reviews on this repo. Configure here. |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
LGTM — a clean, backward-compatible refactor with no functional behavior change.
Extended reasoning...
Overview
This PR adds a generic NewAuthenticator(AuthenticatorConfig[T]) constructor to packages/auth for external services that define their own security scheme, and renames AdminVerifier/NewAdminVerifier to IssuerVerifier/NewIssuerVerifier (better reflecting that the verifier resolves no identity). Both changes are additive/aliasing: AdminVerifier, NewAdminVerifier remain as type aliases and deprecated wrapper functions, so no existing caller breaks. dashboard-api/main.go is updated to use the new name; I confirmed via grep that no other file in the repo references the old names outside packages/auth internals, so the rename is safe.
Security risks
None identified. The underlying commonAuthenticator logic (header extraction, prefix handling, 401 stamping, telemetry) is untouched — NewAuthenticator just exposes the existing struct literal fields through a public config struct. The verifier rename is a pure type alias (type IssuerVerifier = token.AdminVerifier), so token.AdminVerifier's JWT/issuer verification behavior is unchanged.
Level of scrutiny
Light-to-moderate is appropriate: this touches the shared auth package, but the change is a mechanical constructor extraction plus a rename via type alias, not new authentication/authorization logic. I built and ran the full test suite for packages/auth locally (all passing) and confirmed no other callers of the old AdminVerifier/NewAdminVerifier names exist that could be affected by the rename.
Other factors
Four new unit tests cover the new constructor's behavior (scheme name/context setter application, prefix stripping, 401 stamping on missing header, optional SetContext). The PR author states go vet/golangci-lint pass and that they compiled an external caller's bootstrap shape against the new export. No architecture doc update was needed since no service responsibility, protocol, or topology changed.
Verification came at two levels and the useful middle one was missing. ProviderVerifier establishes signature, issuer, subject, and then the internal user behind them. AdminVerifier establishes only the signature — and reaches the keys differently, by appending the conventional JWKS path to the issuer rather than fetching the discovery document, so it never learns what issuer that document declares and never reads the token's iss claim. A caller that must read a subject before that subject is a user — signup, or anything else creating the identity it authenticates — fits neither. Reaching for AdminVerifier, as the closest thing available, silently drops the discovery fetch, the issuer cross-check and the iss claim on the one route that creates users. Reaching for ProviderVerifier means handing it a lookup that answers "nobody", which makes not-found a value and turns any missed check into an authenticated nobody. NewIdentityVerifier is that middle level: the same discovery and issuer validation as the resolving verifier, stopping at what the token asserts. Declining to resolve now weakens nothing about the token. Verify on such a verifier reports that it has no lookup rather than inventing a zero user, so the missing capability is unrepresentable rather than merely undocumented. The iss and sub extraction moves into VerifyIdentity and the resolving path is built on it, so those claims are read in one place instead of two. AdminVerifier becomes ServiceTokenVerifier, which is what it is: keys from a peer's JWKS endpoint, where there is no discovery to do. The earlier name in this branch, IssuerVerifier, was no better — it hid the same thing.
…nder them Nothing in this repo used the deprecated aliases, and belt pins this module by commit, so there is no caller to give time to. Keeping them would leave the wrong name reachable and the right one optional. The internal type follows: it is ServiceTokenVerifier throughout, and the comments and error strings say so. Leaving them on "admin JWT" would have left the rename half-done in exactly the places a reader looks when the type name has already told them otherwise. Its doc now leads with the property that actually constrains it — keys come from the issuer's conventional JWKS path, with no discovery document — since that, not the absent admin semantics, is what makes it the wrong choice for identity-provider tokens. NewAdminJWTAuthenticator keeps its name: AdminJWTAuth is a security scheme that genuinely exists.
ServiceTokenVerifier repeated the mistake it replaced. AdminVerifier named its first caller; ServiceTokenVerifier named a use case. Neither says what distinguishes the thing, which is where its keys come from. The three now sit on one axis and say so: JWKSVerifier keys from the issuer's JWKS path; claims OIDCVerifier keys via OIDC discovery; what the token asserts LinkedOIDCVerifier the above, resolved to an internal user Adopting those names forced a better shape. IdentityVerifier and ProviderVerifier were two names bound to one type, with the constructor deciding which methods worked and Verify failing at runtime when the verifier had no lookup. Two names for one type is a lie the compiler cannot catch — and it is worse under these names, since a LinkedOIDCVerifier that is assignable to an OIDCVerifier and back says the linking is optional. They are two types now, LinkedOIDCVerifier embedding OIDCVerifier, so linking is genuinely additive: the unlinked one has no Verify to call rather than one that reports it cannot. The runtime error is gone because the state it described is unrepresentable. Embedding brought its own hazard. A nil verifier is a supported state here — an unconfigured provider yields one and callers pass it along — but a promoted method computes the address of the embedded value, dereferencing the outer pointer before the inner nil check runs. VerifyIdentity is shadowed to deny rather than panic, with a test that panics without it. The single-issuer verifier gives up the OIDCVerifier name it held and becomes OIDCIssuerVerifier, which is what it is.
🤖 I have created a release *beep* *boop* --- ## 0.0.1 (2026-07-30) ### Features * add workspace admin API foundations ([#3314](#3314)) ([0f72030](0f72030)) * **api:** LD-gated ClickHouse read switcher ([#3061](#3061)) ([29e74ca](29e74ca)) * **api:** soft-delete build layers in DB on user delete ([#3121](#3121)) ([ee88776](ee88776)) * **auth:** support admin token team auth ([#2934](#2934)) ([5496666](5496666)) * **auth:** verifiers on one axis, and a reusable authenticator constructor ([#3423](#3423)) ([923b99b](923b99b)) * **dashboard-api:** add internal admin route for deleting a user ([#2986](#2986)) ([ecc1291](ecc1291)) * **dashboard-api:** add internal team creation ([#2824](#2824)) ([375051b](375051b)) * **dashboard-api:** add OIDC admin user bootstrap endpoint ([#2841](#2841)) ([6a7a59e](6a7a59e)) * **dashboard-api:** add Ory user profile provider and auth middleware fix ([#2840](#2840)) ([30d40d2](30d40d2)) * **dashboard-api:** add template tags handlers ([#2885](#2885)) ([bf52a4b](bf52a4b)) * **dashboard-api:** batch member sync route, and unenumerate project_type ([#3427](#3427)) ([cc16acf](cc16acf)) * **dashboard-api:** expose auth profile admin routes ([#2743](#2743)) ([b673a10](b673a10)) * **dashboard-api:** flag sandboxes past data retention ([#3102](#3102)) ([9b162bf](9b162bf)) * **dashboard-api:** implement upsertProjectLimits ([#3438](#3438)) ([ec1ed29](ec1ed29)) * **dashboard-api:** include build resources in /builds response ([#3009](#3009)) ([bf49c32](bf49c32)) * **dashboard-api:** map Ory SSO organizations to E2B teams ([#3094](#3094)) ([dbd098f](dbd098f)) * **dashboard-api:** populate Ory identity external_id on admin bootstrap ([#3062](#3062)) ([6c51232](6c51232)) * **dashboard-api:** project upsert, member sync and user purge ([#3442](#3442)) ([f997c39](f997c39)) * **dashboard-api:** templates list pagination ([#2904](#2904)) ([6882463](6882463)) * **db:** add project_limits, an override the limits owner can write ([#3429](#3429)) ([021c2a4](021c2a4)) * improve templates list sorting ([#2983](#2983)) ([51ad7ff](51ad7ff)) * **otel:** instrument auth service HTTP client with otelhttp ([#2722](#2722)) ([69b085d](69b085d)) * per-team events TTL limit (tier + addons) ([#3181](#3181)) ([f76b2cb](f76b2cb)) ### Bug Fixes * added api and orch ([#3454](#3454)) ([fda5e45](fda5e45)) * **api:** copy auth/internal into api and dashboard-api image builds ([#3323](#3323)) ([bda1fee](bda1fee)) * **api:** invalidate auth cache on API key deletion ([#3324](#3324)) ([8b02910](8b02910)) * correct 3 CVES ([#3218](#3218)) ([076823b](076823b)) * **dashboard-api:** avoid repeated Ory bootstrap provisioning ([#2940](#2940)) ([da5ce59](da5ce59)) * **dashboard-api:** drop removed read-replica accessor in provisioning tests ([#3340](#3340)) ([6addc91](6addc91)) * **dashboard-api:** pass signup metadata to billing provisioning ([#2978](#2978)) ([d0ea5b4](d0ea5b4)) * **dashboard-api:** set Ory external_id only after the bootstrap commit ([#3133](#3133)) ([00ad04b](00ad04b)) * push client-proxy, dashboard-api, and docker-reverse-proxy image… ([#2953](#2953)) ([1d930ee](1d930ee)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: e2b-release-please[bot] <298072688+e2b-release-please[bot]@users.noreply.github.com> Co-authored-by: Charlie Wyse <charlie.wyse@e2b.dev>
🤖 I have created a release *beep* *boop* --- ## [0.1.0](dashboard-api-v0.0.1...dashboard-api-v0.1.0) (2026-07-31) ### Features * add workspace admin API foundations ([#3314](#3314)) ([0f72030](0f72030)) * **api:** LD-gated ClickHouse read switcher ([#3061](#3061)) ([29e74ca](29e74ca)) * **api:** soft-delete build layers in DB on user delete ([#3121](#3121)) ([ee88776](ee88776)) * **auth:** support admin token team auth ([#2934](#2934)) ([5496666](5496666)) * **auth:** verifiers on one axis, and a reusable authenticator constructor ([#3423](#3423)) ([923b99b](923b99b)) * **dashboard-api:** add internal admin route for deleting a user ([#2986](#2986)) ([ecc1291](ecc1291)) * **dashboard-api:** add internal team creation ([#2824](#2824)) ([375051b](375051b)) * **dashboard-api:** add OIDC admin user bootstrap endpoint ([#2841](#2841)) ([6a7a59e](6a7a59e)) * **dashboard-api:** add Ory user profile provider and auth middleware fix ([#2840](#2840)) ([30d40d2](30d40d2)) * **dashboard-api:** add template tags handlers ([#2885](#2885)) ([bf52a4b](bf52a4b)) * **dashboard-api:** batch member sync route, and unenumerate project_type ([#3427](#3427)) ([cc16acf](cc16acf)) * **dashboard-api:** expose auth profile admin routes ([#2743](#2743)) ([b673a10](b673a10)) * **dashboard-api:** flag sandboxes past data retention ([#3102](#3102)) ([9b162bf](9b162bf)) * **dashboard-api:** implement upsertProjectLimits ([#3438](#3438)) ([ec1ed29](ec1ed29)) * **dashboard-api:** include build resources in /builds response ([#3009](#3009)) ([bf49c32](bf49c32)) * **dashboard-api:** map Ory SSO organizations to E2B teams ([#3094](#3094)) ([dbd098f](dbd098f)) * **dashboard-api:** populate Ory identity external_id on admin bootstrap ([#3062](#3062)) ([6c51232](6c51232)) * **dashboard-api:** project upsert, member sync and user purge ([#3442](#3442)) ([f997c39](f997c39)) * **dashboard-api:** templates list pagination ([#2904](#2904)) ([6882463](6882463)) * **db:** add project_limits, an override the limits owner can write ([#3429](#3429)) ([021c2a4](021c2a4)) * improve templates list sorting ([#2983](#2983)) ([51ad7ff](51ad7ff)) * **otel:** instrument auth service HTTP client with otelhttp ([#2722](#2722)) ([69b085d](69b085d)) * per-team events TTL limit (tier + addons) ([#3181](#3181)) ([f76b2cb](f76b2cb)) ### Bug Fixes * added api and orch ([#3454](#3454)) ([fda5e45](fda5e45)) * **api:** copy auth/internal into api and dashboard-api image builds ([#3323](#3323)) ([bda1fee](bda1fee)) * **api:** invalidate auth cache on API key deletion ([#3324](#3324)) ([8b02910](8b02910)) * correct 3 CVES ([#3218](#3218)) ([076823b](076823b)) * creating whitespace to test publish ([#3476](#3476)) ([5158cc9](5158cc9)) * **dashboard-api:** avoid repeated Ory bootstrap provisioning ([#2940](#2940)) ([da5ce59](da5ce59)) * **dashboard-api:** drop removed read-replica accessor in provisioning tests ([#3340](#3340)) ([6addc91](6addc91)) * **dashboard-api:** pass signup metadata to billing provisioning ([#2978](#2978)) ([d0ea5b4](d0ea5b4)) * **dashboard-api:** set Ory external_id only after the bootstrap commit ([#3133](#3133)) ([00ad04b](00ad04b)) * push client-proxy, dashboard-api, and docker-reverse-proxy image… ([#2953](#2953)) ([1d930ee](1d930ee)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: e2b-release-please[bot] <298072688+e2b-release-please[bot]@users.noreply.github.com>
…ructor (#3423) Makes `packages/auth` usable by a service that authenticates a route **before** the caller is a user — signup. Today no verifier fits that, so the closest one silently skips OIDC discovery, the issuer cross-check and the `iss` claim. ## Verifiers, now one axis | | keys from | establishes | |---|---|---| | `JWKSVerifier` | issuer's JWKS path | claims | | `OIDCVerifier` | OIDC discovery | what the token asserts | | `LinkedOIDCVerifier` | OIDC discovery | + the internal user | Each adds to the one before. Picking a lower rung is a choice about what you need, never a weaker check — discovery and issuer validation are identical across both OIDC levels. `LinkedOIDCVerifier` embeds `OIDCVerifier`, so linking is additive: the unlinked type has no `Verify` to call, rather than one that fails at runtime. ## Also - **`NewAuthenticator(AuthenticatorConfig[T])`** — build an authenticator for a scheme this package doesn't name. Every existing constructor is already a thin literal over `commonAuthenticator`, but they fix the scheme name and context key, and the type is `internal`. Named constructors unchanged. - **`iss`/`sub` extraction** moved into `VerifyIdentity`; the resolving path is built on it, so those claims are read in one place. ## Breaking | before | after | |---|---| | `ProviderVerifier` | `LinkedOIDCVerifier` | | `OIDCVerifier` (single-issuer) | `OIDCIssuerVerifier` | | `AdminVerifier` | `JWKSVerifier` | Aliases removed rather than deprecated — only belt used them, and it pins by commit. The `OIDCVerifier` reuse can't break silently: the new one takes a `ProviderConfig` where the old took a `JWTConfig`, so callers get a compile error. `NewAdminJWTAuthenticator` keeps its name — `AdminJWTAuth` is a real security scheme. ## Verification Behaviour unchanged for every current caller; `dashboard-api` verifies exactly as before. New tests cover the identity verifier (reports claims without a lookup, rejects a missing subject, rejects a discovery issuer mismatch), the authenticator constructor (scheme, prefix stripping, 401 on missing header, optional context setter), and that nil verifiers deny rather than panic — the last one panics without its guard, since promotion through an embedded value dereferences before the inner nil check. No `docs/ARCHITECTURE.md` change: no service, port, protocol, data store, flow or topology affected.
🤖 I have created a release *beep* *boop* --- ## 0.0.1 (2026-07-30) ### Features * add workspace admin API foundations ([#3314](#3314)) ([0f72030](0f72030)) * **api:** LD-gated ClickHouse read switcher ([#3061](#3061)) ([29e74ca](29e74ca)) * **api:** soft-delete build layers in DB on user delete ([#3121](#3121)) ([ee88776](ee88776)) * **auth:** support admin token team auth ([#2934](#2934)) ([5496666](5496666)) * **auth:** verifiers on one axis, and a reusable authenticator constructor ([#3423](#3423)) ([f68e713](f68e713)) * **dashboard-api:** add internal admin route for deleting a user ([#2986](#2986)) ([ecc1291](ecc1291)) * **dashboard-api:** add internal team creation ([#2824](#2824)) ([375051b](375051b)) * **dashboard-api:** add OIDC admin user bootstrap endpoint ([#2841](#2841)) ([6a7a59e](6a7a59e)) * **dashboard-api:** add Ory user profile provider and auth middleware fix ([#2840](#2840)) ([30d40d2](30d40d2)) * **dashboard-api:** add template tags handlers ([#2885](#2885)) ([bf52a4b](bf52a4b)) * **dashboard-api:** batch member sync route, and unenumerate project_type ([#3427](#3427)) ([6d8dc38](6d8dc38)) * **dashboard-api:** expose auth profile admin routes ([#2743](#2743)) ([b673a10](b673a10)) * **dashboard-api:** flag sandboxes past data retention ([#3102](#3102)) ([9b162bf](9b162bf)) * **dashboard-api:** implement upsertProjectLimits ([#3438](#3438)) ([f4ee390](f4ee390)) * **dashboard-api:** include build resources in /builds response ([#3009](#3009)) ([bf49c32](bf49c32)) * **dashboard-api:** map Ory SSO organizations to E2B teams ([#3094](#3094)) ([dbd098f](dbd098f)) * **dashboard-api:** populate Ory identity external_id on admin bootstrap ([#3062](#3062)) ([6c51232](6c51232)) * **dashboard-api:** project upsert, member sync and user purge ([#3442](#3442)) ([8c90702](8c90702)) * **dashboard-api:** templates list pagination ([#2904](#2904)) ([6882463](6882463)) * **db:** add project_limits, an override the limits owner can write ([#3429](#3429)) ([5ab6259](5ab6259)) * improve templates list sorting ([#2983](#2983)) ([51ad7ff](51ad7ff)) * **otel:** instrument auth service HTTP client with otelhttp ([#2722](#2722)) ([69b085d](69b085d)) * per-team events TTL limit (tier + addons) ([#3181](#3181)) ([f76b2cb](f76b2cb)) ### Bug Fixes * added api and orch ([#3454](#3454)) ([d56e0a8](d56e0a8)) * **api:** copy auth/internal into api and dashboard-api image builds ([#3323](#3323)) ([bda1fee](bda1fee)) * **api:** invalidate auth cache on API key deletion ([#3324](#3324)) ([8b02910](8b02910)) * correct 3 CVES ([#3218](#3218)) ([076823b](076823b)) * **dashboard-api:** avoid repeated Ory bootstrap provisioning ([#2940](#2940)) ([da5ce59](da5ce59)) * **dashboard-api:** drop removed read-replica accessor in provisioning tests ([#3340](#3340)) ([6addc91](6addc91)) * **dashboard-api:** pass signup metadata to billing provisioning ([#2978](#2978)) ([d0ea5b4](d0ea5b4)) * **dashboard-api:** set Ory external_id only after the bootstrap commit ([#3133](#3133)) ([00ad04b](00ad04b)) * push client-proxy, dashboard-api, and docker-reverse-proxy image… ([#2953](#2953)) ([1d930ee](1d930ee)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: e2b-release-please[bot] <298072688+e2b-release-please[bot]@users.noreply.github.com> Co-authored-by: Charlie Wyse <charlie.wyse@e2b.dev>
🤖 I have created a release *beep* *boop* --- ## [0.1.0](dashboard-api-v0.0.1...dashboard-api-v0.1.0) (2026-07-31) ### Features * add workspace admin API foundations ([#3314](#3314)) ([0f72030](0f72030)) * **api:** LD-gated ClickHouse read switcher ([#3061](#3061)) ([29e74ca](29e74ca)) * **api:** soft-delete build layers in DB on user delete ([#3121](#3121)) ([ee88776](ee88776)) * **auth:** support admin token team auth ([#2934](#2934)) ([5496666](5496666)) * **auth:** verifiers on one axis, and a reusable authenticator constructor ([#3423](#3423)) ([f68e713](f68e713)) * **dashboard-api:** add internal admin route for deleting a user ([#2986](#2986)) ([ecc1291](ecc1291)) * **dashboard-api:** add internal team creation ([#2824](#2824)) ([375051b](375051b)) * **dashboard-api:** add OIDC admin user bootstrap endpoint ([#2841](#2841)) ([6a7a59e](6a7a59e)) * **dashboard-api:** add Ory user profile provider and auth middleware fix ([#2840](#2840)) ([30d40d2](30d40d2)) * **dashboard-api:** add template tags handlers ([#2885](#2885)) ([bf52a4b](bf52a4b)) * **dashboard-api:** batch member sync route, and unenumerate project_type ([#3427](#3427)) ([6d8dc38](6d8dc38)) * **dashboard-api:** expose auth profile admin routes ([#2743](#2743)) ([b673a10](b673a10)) * **dashboard-api:** flag sandboxes past data retention ([#3102](#3102)) ([9b162bf](9b162bf)) * **dashboard-api:** implement upsertProjectLimits ([#3438](#3438)) ([f4ee390](f4ee390)) * **dashboard-api:** include build resources in /builds response ([#3009](#3009)) ([bf49c32](bf49c32)) * **dashboard-api:** map Ory SSO organizations to E2B teams ([#3094](#3094)) ([dbd098f](dbd098f)) * **dashboard-api:** populate Ory identity external_id on admin bootstrap ([#3062](#3062)) ([6c51232](6c51232)) * **dashboard-api:** project upsert, member sync and user purge ([#3442](#3442)) ([8c90702](8c90702)) * **dashboard-api:** templates list pagination ([#2904](#2904)) ([6882463](6882463)) * **db:** add project_limits, an override the limits owner can write ([#3429](#3429)) ([5ab6259](5ab6259)) * improve templates list sorting ([#2983](#2983)) ([51ad7ff](51ad7ff)) * **otel:** instrument auth service HTTP client with otelhttp ([#2722](#2722)) ([69b085d](69b085d)) * per-team events TTL limit (tier + addons) ([#3181](#3181)) ([f76b2cb](f76b2cb)) ### Bug Fixes * added api and orch ([#3454](#3454)) ([d56e0a8](d56e0a8)) * **api:** copy auth/internal into api and dashboard-api image builds ([#3323](#3323)) ([bda1fee](bda1fee)) * **api:** invalidate auth cache on API key deletion ([#3324](#3324)) ([8b02910](8b02910)) * correct 3 CVES ([#3218](#3218)) ([076823b](076823b)) * creating whitespace to test publish ([#3476](#3476)) ([6b4177f](6b4177f)) * **dashboard-api:** avoid repeated Ory bootstrap provisioning ([#2940](#2940)) ([da5ce59](da5ce59)) * **dashboard-api:** drop removed read-replica accessor in provisioning tests ([#3340](#3340)) ([6addc91](6addc91)) * **dashboard-api:** pass signup metadata to billing provisioning ([#2978](#2978)) ([d0ea5b4](d0ea5b4)) * **dashboard-api:** set Ory external_id only after the bootstrap commit ([#3133](#3133)) ([00ad04b](00ad04b)) * push client-proxy, dashboard-api, and docker-reverse-proxy image… ([#2953](#2953)) ([1d930ee](1d930ee)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: e2b-release-please[bot] <298072688+e2b-release-please[bot]@users.noreply.github.com>
…ructor (e2b-dev#3423) Makes `packages/auth` usable by a service that authenticates a route **before** the caller is a user — signup. Today no verifier fits that, so the closest one silently skips OIDC discovery, the issuer cross-check and the `iss` claim. ## Verifiers, now one axis | | keys from | establishes | |---|---|---| | `JWKSVerifier` | issuer's JWKS path | claims | | `OIDCVerifier` | OIDC discovery | what the token asserts | | `LinkedOIDCVerifier` | OIDC discovery | + the internal user | Each adds to the one before. Picking a lower rung is a choice about what you need, never a weaker check — discovery and issuer validation are identical across both OIDC levels. `LinkedOIDCVerifier` embeds `OIDCVerifier`, so linking is additive: the unlinked type has no `Verify` to call, rather than one that fails at runtime. ## Also - **`NewAuthenticator(AuthenticatorConfig[T])`** — build an authenticator for a scheme this package doesn't name. Every existing constructor is already a thin literal over `commonAuthenticator`, but they fix the scheme name and context key, and the type is `internal`. Named constructors unchanged. - **`iss`/`sub` extraction** moved into `VerifyIdentity`; the resolving path is built on it, so those claims are read in one place. ## Breaking | before | after | |---|---| | `ProviderVerifier` | `LinkedOIDCVerifier` | | `OIDCVerifier` (single-issuer) | `OIDCIssuerVerifier` | | `AdminVerifier` | `JWKSVerifier` | Aliases removed rather than deprecated — only belt used them, and it pins by commit. The `OIDCVerifier` reuse can't break silently: the new one takes a `ProviderConfig` where the old took a `JWTConfig`, so callers get a compile error. `NewAdminJWTAuthenticator` keeps its name — `AdminJWTAuth` is a real security scheme. ## Verification Behaviour unchanged for every current caller; `dashboard-api` verifies exactly as before. New tests cover the identity verifier (reports claims without a lookup, rejects a missing subject, rejects a discovery issuer mismatch), the authenticator constructor (scheme, prefix stripping, 401 on missing header, optional context setter), and that nil verifiers deny rather than panic — the last one panics without its guard, since promotion through an embedded value dereferences before the inner nil check. No `docs/ARCHITECTURE.md` change: no service, port, protocol, data store, flow or topology affected.
Makes
packages/authusable by a service that authenticates a route before the caller is a user — signup. Today no verifier fits that, so the closest one silently skips OIDC discovery, the issuer cross-check and theissclaim.Verifiers, now one axis
JWKSVerifierOIDCVerifierLinkedOIDCVerifierEach adds to the one before. Picking a lower rung is a choice about what you need, never a weaker check — discovery and issuer validation are identical across both OIDC levels.
LinkedOIDCVerifierembedsOIDCVerifier, so linking is additive: the unlinked type has noVerifyto call, rather than one that fails at runtime.Also
NewAuthenticator(AuthenticatorConfig[T])— build an authenticator for a scheme this package doesn't name. Every existing constructor is already a thin literal overcommonAuthenticator, but they fix the scheme name and context key, and the type isinternal. Named constructors unchanged.iss/subextraction moved intoVerifyIdentity; the resolving path is built on it, so those claims are read in one place.Breaking
ProviderVerifierLinkedOIDCVerifierOIDCVerifier(single-issuer)OIDCIssuerVerifierAdminVerifierJWKSVerifierAliases removed rather than deprecated — only belt used them, and it pins by commit. The
OIDCVerifierreuse can't break silently: the new one takes aProviderConfigwhere the old took aJWTConfig, so callers get a compile error.NewAdminJWTAuthenticatorkeeps its name —AdminJWTAuthis a real security scheme.Verification
Behaviour unchanged for every current caller;
dashboard-apiverifies exactly as before. New tests cover the identity verifier (reports claims without a lookup, rejects a missing subject, rejects a discovery issuer mismatch), the authenticator constructor (scheme, prefix stripping, 401 on missing header, optional context setter), and that nil verifiers deny rather than panic — the last one panics without its guard, since promotion through an embedded value dereferences before the inner nil check.No
docs/ARCHITECTURE.mdchange: no service, port, protocol, data store, flow or topology affected.