From 11b1227ab0371d79a373aa55b56a17e210db2336 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Wed, 3 Sep 2025 13:25:08 -0700 Subject: [PATCH 01/25] add gitlab and github actions oidc --- packages/openauth/src/provider/github.ts | 21 ++++++++ packages/openauth/src/provider/gitlab.ts | 66 ++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 packages/openauth/src/provider/gitlab.ts diff --git a/packages/openauth/src/provider/github.ts b/packages/openauth/src/provider/github.ts index ca93ba3b..b913e005 100644 --- a/packages/openauth/src/provider/github.ts +++ b/packages/openauth/src/provider/github.ts @@ -18,8 +18,10 @@ */ import { Oauth2Provider, Oauth2WrappedConfig } from "./oauth2.js" +import { OidcProvider, OidcWrappedConfig } from "./oidc.js" export interface GithubConfig extends Oauth2WrappedConfig {} +export interface GithubOidcConfig extends OidcWrappedConfig {} /** * Create a Github OAuth2 provider. @@ -43,3 +45,22 @@ export function GithubProvider(config: GithubConfig) { }, }) } + +/** + * Create a Github OIDC provider. + * + * @param config - The config for the provider. + * @example + * ```ts + * GithubOidcProvider({ + * clientId: "1234567890" + * }) + * ``` + */ +export function GithubActionsOidcProvider(config: GithubOidcConfig) { + return OidcProvider({ + ...config, + type: "github", + issuer: "https://token.actions.githubusercontent.com/", + }) +} \ No newline at end of file diff --git a/packages/openauth/src/provider/gitlab.ts b/packages/openauth/src/provider/gitlab.ts new file mode 100644 index 00000000..a5fbccdb --- /dev/null +++ b/packages/openauth/src/provider/gitlab.ts @@ -0,0 +1,66 @@ +/** + * Use this provider to authenticate with Gitlab. + * + * ```ts {5-8} + * import { GitlabProvider } from "@openauthjs/openauth/provider/gitlab" + * + * export default issuer({ + * providers: { + * gitlab: GitlabProvider({ + * clientId: "1234567890", + * clientSecret: "0987654321" + * }) + * } + * }) + * ``` + * + * @packageDocumentation + */ + +import { Oauth2Provider, Oauth2WrappedConfig } from "./oauth2.js" +import { OidcProvider, OidcWrappedConfig } from "./oidc.js" + +export interface GitlabConfig extends Oauth2WrappedConfig {} +export interface GitlabOidcConfig extends OidcWrappedConfig {} + +/** + * Create a Gitlab OAuth2 provider. + * + * @param config - The config for the provider. + * @example + * ```ts + * GitlabProvider({ + * clientId: "1234567890", + * clientSecret: "0987654321" + * }) + * ``` + */ +export function GitlabProvider(config: GitlabConfig) { + return Oauth2Provider({ + ...config, + type: "gitlab", + endpoint: { + authorization: "https://gitlab.com/oauth/authorize", + token: "https://gitlab.com/oauth/token", + }, + }) +} + +/** + * Create a Gitlab OIDC provider. + * + * @param config - The config for the provider. + * @example + * ```ts + * GitlabOidcProvider({ + * clientId: "1234567890" + * }) + * ``` + */ +export function GitlabOidcProvider(config: GitlabOidcConfig) { + return OidcProvider({ + ...config, + type: "gitlab", + issuer: "https://gitlab.com", + }) +} \ No newline at end of file From 57af85eebef33f149c2e111fe3478f4a5246e13e Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Wed, 3 Sep 2025 16:27:21 -0700 Subject: [PATCH 02/25] add grant-type for jwt assertion --- bun.lockb | Bin 258568 -> 258568 bytes examples/jwt-bearer-validation.md | 118 ++++++++++++++++++++++++++++++ packages/openauth/src/issuer.ts | 75 ++++++++++++++++++- 3 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 examples/jwt-bearer-validation.md diff --git a/bun.lockb b/bun.lockb index 25694456e2ece350cbee3996b7060f86741b6b19..e18094741763fa17efd2be5422df59538457ba51 100755 GIT binary patch delta 21477 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 { + if (value.provider === "gitlab") { + // Handle GitLab OAuth login + const userID = /* map GitLab user to your system */ + return ctx.subject("user", { userID }) + } + + if (value.provider === "jwt-bearer") { + console.log("JWT Bearer token from:", value.issuer) + console.log("Full claims:", value.claims) + + // Validate the issuer - this is where YOU decide who to trust + const trustedIssuers = [ + "https://gitlab.com", // Your main GitLab instance + "https://accounts.google.com", // Google service accounts + "https://login.microsoftonline.com" // Azure AD + ] + + if (!trustedIssuers.includes(value.issuer)) { + throw new Error(`Untrusted issuer: ${value.issuer}`) + } + + // Handle different issuers differently + if (value.issuer === "https://gitlab.com") { + // JWT from GitLab (maybe from CI/CD pipeline) + const userID = /* lookup user from GitLab subject */ + return ctx.subject("user", { userID }) + } + + if (value.issuer === "https://accounts.google.com") { + // JWT from Google service account + const serviceID = /* extract service info */ + return ctx.subject("service", { serviceID }) + } + + // Add validation for additional custom claims + if (value.claims.custom_role !== "api_access") { + throw new Error("JWT missing required role") + } + + return ctx.subject("api_user", { + userID: value.subject, + issuer: value.issuer + }) + } + } +}) +``` + +## Token Exchange Flow + +1. **Client sends JWT assertion**: A client makes a POST request to `/token` with: + + ```http + grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion= + ``` + +2. **Signature verification**: OpenAuth fetches the issuer's JWKS from `${issuer}/.well-known/jwks.json` and verifies the JWT signature + +3. **Success callback**: OpenAuth calls your success callback with: + + ```typescript + { + provider: "jwt-bearer", + claims: JWTPayload, // Full JWT claims object + issuer: string, // The JWT issuer + subject: string, // The JWT subject (sub claim) + audience: string // The JWT audience (aud claim) + } + ``` + +4. **Issuer validation**: In your success callback, you decide which issuers to trust + +5. **Token generation**: If you approve the JWT, return `ctx.subject()` to generate final access/refresh tokens + +## Security Considerations + +**Why validate in the success callback?** + +- **Flexible validation**: You can implement custom logic for different issuers +- **Context-aware**: Access to full JWT claims for additional validation +- **Granular control**: Different handling per issuer (users vs services vs APIs) +- **Dynamic trust**: Trust decisions can be based on database lookups or external APIs +- **Consistent pattern**: Same validation approach as other OAuth providers + +**Best practices:** + +- **Use allowlists**: Explicitly list trusted issuers rather than trying to block bad ones +- **Validate additional claims**: Check roles, audiences, or custom claims as needed +- **Log JWT usage**: Monitor bearer token usage for security auditing +- **Handle errors gracefully**: Throw clear errors for untrusted issuers or invalid claims diff --git a/packages/openauth/src/issuer.ts b/packages/openauth/src/issuer.ts index 2092ad2f..5d1f6ad7 100644 --- a/packages/openauth/src/issuer.ts +++ b/packages/openauth/src/issuer.ts @@ -192,7 +192,7 @@ import { UnauthorizedClientError, UnknownStateError, } from "./error.js" -import { compactDecrypt, CompactEncrypt, jwtVerify, SignJWT } from "jose" +import { compactDecrypt, CompactEncrypt, createRemoteJWKSet, decodeJwt, jwtVerify, SignJWT } from "jose" import { Storage, StorageAdapter } from "./storage/storage.js" import { encryptionKeys, legacySigningKeys, signingKeys } from "./keys.js" import { validatePKCE } from "./pkce.js" @@ -1032,6 +1032,68 @@ export function issuer< ) } + // see https://datatracker.ietf.org/doc/html/rfc7521 and https://datatracker.ietf.org/doc/html/rfc7523 for jwt assertion grant-types spec + if (grantType === "urn:ietf:params:oauth:grant-type:jwt-bearer") { + const assertion = form.get("assertion") + if (!assertion) { + return c.json({ error: "missing `assertion` form value" }, 400) + } + + const claims = decodeJwt(assertion.toString()) + if (!claims) { + return c.json({ error: "missing jwt claims" }, 400) + } + + if (!claims.iss) { + return c.json({ error: "missing issuer in jwt claims" }, 400) + } + + // get the jwks for the assertion + const jwks = createRemoteJWKSet(new URL(`${claims.iss}/.well-known/jwks.json`)) + try { + const result = await jwtVerify(assertion.toString(), jwks, { + subject: claims.sub, + issuer: claims.iss, + audience: claims.aud + }) + } catch (err) { + return c.json({ error: "invalid jwt" }, 400) + } + + // Call the success callback to handle JWT bearer token validation + return input.success( + { + async subject(type, properties, opts) { + const tokens = await generateTokens(c, { + type: type as string, + subject: opts?.subject || claims.sub as string, + properties, + clientID: claims.aud as string, + scopes: parseScopes(scope), + ttl: { + access: opts?.ttl?.access ?? ((claims.exp as number) - Math.floor(Date.now() / 1000)), + refresh: opts?.ttl?.refresh ?? ttlRefresh, + }, + }) + return c.json({ + access_token: tokens.access, + refresh_token: tokens.refresh, + scope: parseScopes(scope)?.join(" "), + expires_in: tokens.expiresIn, + }) + }, + }, + { + provider: "jwt-bearer", + claims: claims, + issuer: claims.iss, + subject: claims.sub, + audience: claims.aud, + } as Result, + c.req.raw, + ) + } + throw new Error("Invalid grant_type") }, ) @@ -1155,8 +1217,15 @@ export function issuer< "~standard" ].validate(result.payload.properties) - if (!validated.issues && result.payload.mode === "access") { - return c.json(validated.value as SubjectSchema) + if (validated.issues) { + return c.json({ + error: "invalid_token", + error_description: "Invalid token", + }) + } + + if (result.payload.mode === "access" && 'value' in validated) { + return c.json(validated.value) } return c.json({ From af53876655110aedd1dd02371bc15021f0c442be Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Wed, 3 Sep 2025 22:34:46 -0700 Subject: [PATCH 03/25] type enforcement updates --- bun.lockb | Bin 258568 -> 257888 bytes package.json | 5 +++-- packages/openauth/src/issuer.ts | 10 +++++++--- packages/openauth/tsconfig.json | 3 ++- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/bun.lockb b/bun.lockb index e18094741763fa17efd2be5422df59538457ba51..f02a6996bde982afe5fd938cfa0b160fb1364d4d 100755 GIT binary patch delta 37540 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 delta 38009 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 SOR2{)>FCEfbskznmHz_>1=sxm diff --git a/package.json b/package.json index 5c5606eb..ef22a723 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,9 @@ "release": "bun run --filter=\"@openauthjs/openauth\" build && changeset publish" }, "devDependencies": { - "@tsconfig/node22": "22.0.0", - "@types/bun": "latest" + "@tsconfig/node22": "22.0.2", + "@types/bun": "1.2.21", + "@types/node": "22" }, "dependencies": { "@changesets/cli": "2.27.10", diff --git a/packages/openauth/src/issuer.ts b/packages/openauth/src/issuer.ts index 5d1f6ad7..e625b53a 100644 --- a/packages/openauth/src/issuer.ts +++ b/packages/openauth/src/issuer.ts @@ -597,10 +597,14 @@ export function issuer< ) }, forward(ctx, response) { + const headers: Record = {} + response.headers.forEach((value, name) => { + headers[name] = value + }) return ctx.newResponse( response.body, response.status as any, - Object.fromEntries(response.headers.entries()), + headers, ) }, async set(ctx, key, maxAge, value) { @@ -1001,7 +1005,7 @@ export function issuer< const response = await match.client({ clientID: clientID.toString(), clientSecret: clientSecret.toString(), - params: Object.fromEntries(form) as Record, + params: Object.fromEntries(Array.from(form.entries())) as Record, }) return input.success( { @@ -1225,7 +1229,7 @@ export function issuer< } if (result.payload.mode === "access" && 'value' in validated) { - return c.json(validated.value) + return c.json(validated.value as Record) } return c.json({ diff --git a/packages/openauth/tsconfig.json b/packages/openauth/tsconfig.json index b6e6b8c5..987a2153 100644 --- a/packages/openauth/tsconfig.json +++ b/packages/openauth/tsconfig.json @@ -7,7 +7,8 @@ "module": "NodeNext", "moduleResolution": "NodeNext", "jsx": "react-jsx", - "jsxImportSource": "hono/jsx" + "jsxImportSource": "hono/jsx", + "types": ["node", "bun"] }, "include": ["src"] } From c58ec0599e766b5ee43588aa9b71578f5b1d3e13 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 4 Sep 2025 11:01:36 -0700 Subject: [PATCH 04/25] error update --- packages/openauth/src/issuer.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/openauth/src/issuer.ts b/packages/openauth/src/issuer.ts index e625b53a..f5d91330 100644 --- a/packages/openauth/src/issuer.ts +++ b/packages/openauth/src/issuer.ts @@ -1055,13 +1055,13 @@ export function issuer< // get the jwks for the assertion const jwks = createRemoteJWKSet(new URL(`${claims.iss}/.well-known/jwks.json`)) try { - const result = await jwtVerify(assertion.toString(), jwks, { + await jwtVerify(assertion.toString(), jwks, { subject: claims.sub, issuer: claims.iss, - audience: claims.aud + audience: claims.aud, }) } catch (err) { - return c.json({ error: "invalid jwt" }, 400) + return c.json({ error: `invalid jwt - ${err instanceof Error ? err.message : String(err)}` }, 400) } // Call the success callback to handle JWT bearer token validation From e132d525c2115c46a4fe11b6adbf69251c838932 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Wed, 3 Sep 2025 13:25:08 -0700 Subject: [PATCH 05/25] add gitlab and github actions oidc --- packages/openauth/src/provider/github.ts | 21 +++++++++++++++++ packages/openauth/src/provider/gitlab.ts | 29 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/packages/openauth/src/provider/github.ts b/packages/openauth/src/provider/github.ts index ca93ba3b..b913e005 100644 --- a/packages/openauth/src/provider/github.ts +++ b/packages/openauth/src/provider/github.ts @@ -18,8 +18,10 @@ */ import { Oauth2Provider, Oauth2WrappedConfig } from "./oauth2.js" +import { OidcProvider, OidcWrappedConfig } from "./oidc.js" export interface GithubConfig extends Oauth2WrappedConfig {} +export interface GithubOidcConfig extends OidcWrappedConfig {} /** * Create a Github OAuth2 provider. @@ -43,3 +45,22 @@ export function GithubProvider(config: GithubConfig) { }, }) } + +/** + * Create a Github OIDC provider. + * + * @param config - The config for the provider. + * @example + * ```ts + * GithubOidcProvider({ + * clientId: "1234567890" + * }) + * ``` + */ +export function GithubActionsOidcProvider(config: GithubOidcConfig) { + return OidcProvider({ + ...config, + type: "github", + issuer: "https://token.actions.githubusercontent.com/", + }) +} \ No newline at end of file diff --git a/packages/openauth/src/provider/gitlab.ts b/packages/openauth/src/provider/gitlab.ts index 850d0b5c..6ee4ef14 100644 --- a/packages/openauth/src/provider/gitlab.ts +++ b/packages/openauth/src/provider/gitlab.ts @@ -18,8 +18,15 @@ */ import { Oauth2Provider, Oauth2WrappedConfig } from "./oauth2.js" +<<<<<<< HEAD export interface GitlabConfig extends Oauth2WrappedConfig {} +======= +import { OidcProvider, OidcWrappedConfig } from "./oidc.js" + +export interface GitlabConfig extends Oauth2WrappedConfig {} +export interface GitlabOidcConfig extends OidcWrappedConfig {} +>>>>>>> 11b1227 (add gitlab and github actions oidc) /** * Create a Gitlab OAuth2 provider. @@ -43,3 +50,25 @@ export function GitlabProvider(config: GitlabConfig) { }, }) } +<<<<<<< HEAD +======= + +/** + * Create a Gitlab OIDC provider. + * + * @param config - The config for the provider. + * @example + * ```ts + * GitlabOidcProvider({ + * clientId: "1234567890" + * }) + * ``` + */ +export function GitlabOidcProvider(config: GitlabOidcConfig) { + return OidcProvider({ + ...config, + type: "gitlab", + issuer: "https://gitlab.com", + }) +} +>>>>>>> 11b1227 (add gitlab and github actions oidc) From 4329d2b6dd3e060b8034df285557fa549f8df6c2 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Wed, 3 Sep 2025 16:27:21 -0700 Subject: [PATCH 06/25] add grant-type for jwt assertion --- bun.lockb | Bin 258568 -> 258568 bytes examples/jwt-bearer-validation.md | 118 ++++++++++++++++++++++++++++++ packages/openauth/src/issuer.ts | 75 ++++++++++++++++++- 3 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 examples/jwt-bearer-validation.md diff --git a/bun.lockb b/bun.lockb index 25694456e2ece350cbee3996b7060f86741b6b19..e18094741763fa17efd2be5422df59538457ba51 100755 GIT binary patch delta 21477 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 { + if (value.provider === "gitlab") { + // Handle GitLab OAuth login + const userID = /* map GitLab user to your system */ + return ctx.subject("user", { userID }) + } + + if (value.provider === "jwt-bearer") { + console.log("JWT Bearer token from:", value.issuer) + console.log("Full claims:", value.claims) + + // Validate the issuer - this is where YOU decide who to trust + const trustedIssuers = [ + "https://gitlab.com", // Your main GitLab instance + "https://accounts.google.com", // Google service accounts + "https://login.microsoftonline.com" // Azure AD + ] + + if (!trustedIssuers.includes(value.issuer)) { + throw new Error(`Untrusted issuer: ${value.issuer}`) + } + + // Handle different issuers differently + if (value.issuer === "https://gitlab.com") { + // JWT from GitLab (maybe from CI/CD pipeline) + const userID = /* lookup user from GitLab subject */ + return ctx.subject("user", { userID }) + } + + if (value.issuer === "https://accounts.google.com") { + // JWT from Google service account + const serviceID = /* extract service info */ + return ctx.subject("service", { serviceID }) + } + + // Add validation for additional custom claims + if (value.claims.custom_role !== "api_access") { + throw new Error("JWT missing required role") + } + + return ctx.subject("api_user", { + userID: value.subject, + issuer: value.issuer + }) + } + } +}) +``` + +## Token Exchange Flow + +1. **Client sends JWT assertion**: A client makes a POST request to `/token` with: + + ```http + grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion= + ``` + +2. **Signature verification**: OpenAuth fetches the issuer's JWKS from `${issuer}/.well-known/jwks.json` and verifies the JWT signature + +3. **Success callback**: OpenAuth calls your success callback with: + + ```typescript + { + provider: "jwt-bearer", + claims: JWTPayload, // Full JWT claims object + issuer: string, // The JWT issuer + subject: string, // The JWT subject (sub claim) + audience: string // The JWT audience (aud claim) + } + ``` + +4. **Issuer validation**: In your success callback, you decide which issuers to trust + +5. **Token generation**: If you approve the JWT, return `ctx.subject()` to generate final access/refresh tokens + +## Security Considerations + +**Why validate in the success callback?** + +- **Flexible validation**: You can implement custom logic for different issuers +- **Context-aware**: Access to full JWT claims for additional validation +- **Granular control**: Different handling per issuer (users vs services vs APIs) +- **Dynamic trust**: Trust decisions can be based on database lookups or external APIs +- **Consistent pattern**: Same validation approach as other OAuth providers + +**Best practices:** + +- **Use allowlists**: Explicitly list trusted issuers rather than trying to block bad ones +- **Validate additional claims**: Check roles, audiences, or custom claims as needed +- **Log JWT usage**: Monitor bearer token usage for security auditing +- **Handle errors gracefully**: Throw clear errors for untrusted issuers or invalid claims diff --git a/packages/openauth/src/issuer.ts b/packages/openauth/src/issuer.ts index 2092ad2f..5d1f6ad7 100644 --- a/packages/openauth/src/issuer.ts +++ b/packages/openauth/src/issuer.ts @@ -192,7 +192,7 @@ import { UnauthorizedClientError, UnknownStateError, } from "./error.js" -import { compactDecrypt, CompactEncrypt, jwtVerify, SignJWT } from "jose" +import { compactDecrypt, CompactEncrypt, createRemoteJWKSet, decodeJwt, jwtVerify, SignJWT } from "jose" import { Storage, StorageAdapter } from "./storage/storage.js" import { encryptionKeys, legacySigningKeys, signingKeys } from "./keys.js" import { validatePKCE } from "./pkce.js" @@ -1032,6 +1032,68 @@ export function issuer< ) } + // see https://datatracker.ietf.org/doc/html/rfc7521 and https://datatracker.ietf.org/doc/html/rfc7523 for jwt assertion grant-types spec + if (grantType === "urn:ietf:params:oauth:grant-type:jwt-bearer") { + const assertion = form.get("assertion") + if (!assertion) { + return c.json({ error: "missing `assertion` form value" }, 400) + } + + const claims = decodeJwt(assertion.toString()) + if (!claims) { + return c.json({ error: "missing jwt claims" }, 400) + } + + if (!claims.iss) { + return c.json({ error: "missing issuer in jwt claims" }, 400) + } + + // get the jwks for the assertion + const jwks = createRemoteJWKSet(new URL(`${claims.iss}/.well-known/jwks.json`)) + try { + const result = await jwtVerify(assertion.toString(), jwks, { + subject: claims.sub, + issuer: claims.iss, + audience: claims.aud + }) + } catch (err) { + return c.json({ error: "invalid jwt" }, 400) + } + + // Call the success callback to handle JWT bearer token validation + return input.success( + { + async subject(type, properties, opts) { + const tokens = await generateTokens(c, { + type: type as string, + subject: opts?.subject || claims.sub as string, + properties, + clientID: claims.aud as string, + scopes: parseScopes(scope), + ttl: { + access: opts?.ttl?.access ?? ((claims.exp as number) - Math.floor(Date.now() / 1000)), + refresh: opts?.ttl?.refresh ?? ttlRefresh, + }, + }) + return c.json({ + access_token: tokens.access, + refresh_token: tokens.refresh, + scope: parseScopes(scope)?.join(" "), + expires_in: tokens.expiresIn, + }) + }, + }, + { + provider: "jwt-bearer", + claims: claims, + issuer: claims.iss, + subject: claims.sub, + audience: claims.aud, + } as Result, + c.req.raw, + ) + } + throw new Error("Invalid grant_type") }, ) @@ -1155,8 +1217,15 @@ export function issuer< "~standard" ].validate(result.payload.properties) - if (!validated.issues && result.payload.mode === "access") { - return c.json(validated.value as SubjectSchema) + if (validated.issues) { + return c.json({ + error: "invalid_token", + error_description: "Invalid token", + }) + } + + if (result.payload.mode === "access" && 'value' in validated) { + return c.json(validated.value) } return c.json({ From 3e51d8d251fc071caab424e30943214d3da039f8 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Wed, 3 Sep 2025 22:34:46 -0700 Subject: [PATCH 07/25] type enforcement updates --- bun.lockb | Bin 258568 -> 257888 bytes package.json | 5 +++-- packages/openauth/src/issuer.ts | 10 +++++++--- packages/openauth/tsconfig.json | 3 ++- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/bun.lockb b/bun.lockb index e18094741763fa17efd2be5422df59538457ba51..f02a6996bde982afe5fd938cfa0b160fb1364d4d 100755 GIT binary patch delta 37540 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 delta 38009 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 SOR2{)>FCEfbskznmHz_>1=sxm diff --git a/package.json b/package.json index 5c5606eb..ef22a723 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,9 @@ "release": "bun run --filter=\"@openauthjs/openauth\" build && changeset publish" }, "devDependencies": { - "@tsconfig/node22": "22.0.0", - "@types/bun": "latest" + "@tsconfig/node22": "22.0.2", + "@types/bun": "1.2.21", + "@types/node": "22" }, "dependencies": { "@changesets/cli": "2.27.10", diff --git a/packages/openauth/src/issuer.ts b/packages/openauth/src/issuer.ts index 5d1f6ad7..e625b53a 100644 --- a/packages/openauth/src/issuer.ts +++ b/packages/openauth/src/issuer.ts @@ -597,10 +597,14 @@ export function issuer< ) }, forward(ctx, response) { + const headers: Record = {} + response.headers.forEach((value, name) => { + headers[name] = value + }) return ctx.newResponse( response.body, response.status as any, - Object.fromEntries(response.headers.entries()), + headers, ) }, async set(ctx, key, maxAge, value) { @@ -1001,7 +1005,7 @@ export function issuer< const response = await match.client({ clientID: clientID.toString(), clientSecret: clientSecret.toString(), - params: Object.fromEntries(form) as Record, + params: Object.fromEntries(Array.from(form.entries())) as Record, }) return input.success( { @@ -1225,7 +1229,7 @@ export function issuer< } if (result.payload.mode === "access" && 'value' in validated) { - return c.json(validated.value) + return c.json(validated.value as Record) } return c.json({ diff --git a/packages/openauth/tsconfig.json b/packages/openauth/tsconfig.json index b6e6b8c5..987a2153 100644 --- a/packages/openauth/tsconfig.json +++ b/packages/openauth/tsconfig.json @@ -7,7 +7,8 @@ "module": "NodeNext", "moduleResolution": "NodeNext", "jsx": "react-jsx", - "jsxImportSource": "hono/jsx" + "jsxImportSource": "hono/jsx", + "types": ["node", "bun"] }, "include": ["src"] } From b8d187db0ecdc3eef8ba42fac185d7e6a6e8df99 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 4 Sep 2025 11:01:36 -0700 Subject: [PATCH 08/25] error update --- packages/openauth/src/issuer.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/openauth/src/issuer.ts b/packages/openauth/src/issuer.ts index e625b53a..f5d91330 100644 --- a/packages/openauth/src/issuer.ts +++ b/packages/openauth/src/issuer.ts @@ -1055,13 +1055,13 @@ export function issuer< // get the jwks for the assertion const jwks = createRemoteJWKSet(new URL(`${claims.iss}/.well-known/jwks.json`)) try { - const result = await jwtVerify(assertion.toString(), jwks, { + await jwtVerify(assertion.toString(), jwks, { subject: claims.sub, issuer: claims.iss, - audience: claims.aud + audience: claims.aud, }) } catch (err) { - return c.json({ error: "invalid jwt" }, 400) + return c.json({ error: `invalid jwt - ${err instanceof Error ? err.message : String(err)}` }, 400) } // Call the success callback to handle JWT bearer token validation From f1964066287958284142ae30eaaad45e8cb48b34 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 4 Sep 2025 11:45:07 -0700 Subject: [PATCH 09/25] update gitlabs with oidc provider --- packages/openauth/src/provider/gitlab.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/openauth/src/provider/gitlab.ts b/packages/openauth/src/provider/gitlab.ts index 6ee4ef14..fb9fd398 100644 --- a/packages/openauth/src/provider/gitlab.ts +++ b/packages/openauth/src/provider/gitlab.ts @@ -18,15 +18,10 @@ */ import { Oauth2Provider, Oauth2WrappedConfig } from "./oauth2.js" -<<<<<<< HEAD - -export interface GitlabConfig extends Oauth2WrappedConfig {} -======= import { OidcProvider, OidcWrappedConfig } from "./oidc.js" export interface GitlabConfig extends Oauth2WrappedConfig {} export interface GitlabOidcConfig extends OidcWrappedConfig {} ->>>>>>> 11b1227 (add gitlab and github actions oidc) /** * Create a Gitlab OAuth2 provider. @@ -50,8 +45,6 @@ export function GitlabProvider(config: GitlabConfig) { }, }) } -<<<<<<< HEAD -======= /** * Create a Gitlab OIDC provider. @@ -71,4 +64,3 @@ export function GitlabOidcProvider(config: GitlabOidcConfig) { issuer: "https://gitlab.com", }) } ->>>>>>> 11b1227 (add gitlab and github actions oidc) From ec7834c80967ddf5017c4a66880656b42275b30d Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 4 Sep 2025 11:58:17 -0700 Subject: [PATCH 10/25] assert jwt-bearer --- packages/openauth/test/issuer.test.ts | 73 +++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/packages/openauth/test/issuer.test.ts b/packages/openauth/test/issuer.test.ts index 369d4649..e20fddfc 100644 --- a/packages/openauth/test/issuer.test.ts +++ b/packages/openauth/test/issuer.test.ts @@ -167,6 +167,79 @@ describe("client credentials flow", () => { }) }) +describe("jwt-bearer grant type", () => { + test("success", async () => { + // Create a JWT assertion for the test + const { SignJWT } = await import("jose") + const now = Math.floor(Date.now() / 1000) + const jwt = await new SignJWT({ + sub: "123", + iss: "myuser", + aud: "https://auth.example.com/token", + exp: now + 60, + provider: "dummy", + email: "foo@bar.com", + }) + .setProtectedHeader({ alg: "HS256" }) + .sign(new TextEncoder().encode("test-secret")) + + const response = await auth.request("https://auth.example.com/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", + assertion: jwt, + provider: "dummy", + }).toString(), + }) + + expect(response.status).toBe(200) + const tokens = await response.json() + expect(tokens).toStrictEqual({ + access_token: expectNonEmptyString, + refresh_token: expectNonEmptyString, + expires_in: expect.any(Number), + }) + + const client = createClient({ + issuer: "https://auth.example.com", + clientID: "myuser", + fetch: (a, b) => Promise.resolve(auth.request(a, b)), + }) + const verified = await client.verify(subjects, tokens.access_token) + if (verified.err) throw verified.err + expect(verified).toStrictEqual({ + aud: "myuser", + subject: { + type: "user", + properties: { + userID: "123", + }, + }, + }) + }) + + test("failure with invalid assertion", async () => { + const response = await auth.request("https://auth.example.com/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", + assertion: "invalid.jwt.token", + provider: "dummy", + }).toString(), + }) + + expect(response.status).toBe(400) + const error = await response.json() + expect(error.error).toBe("invalid_grant") + }) +}) + describe("refresh token", () => { let tokens: { access: string; refresh: string } let client: ReturnType From 4fe5d3c477de6218abe879d1954c49f1150d4b21 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 4 Sep 2025 11:58:57 -0700 Subject: [PATCH 11/25] update tests --- packages/openauth/test/issuer.test.ts | 126 ++++++++++++++++---------- 1 file changed, 79 insertions(+), 47 deletions(-) diff --git a/packages/openauth/test/issuer.test.ts b/packages/openauth/test/issuer.test.ts index e20fddfc..70bcaf77 100644 --- a/packages/openauth/test/issuer.test.ts +++ b/packages/openauth/test/issuer.test.ts @@ -56,6 +56,16 @@ const issuerConfig = { userID: "123", }) } + if (value.provider === "jwt-bearer") { + // Validate trusted issuers + const trustedIssuers = ["https://trusted-issuer.example.com"] + if (!trustedIssuers.includes(value.issuer)) { + throw new Error(`Untrusted issuer: ${value.issuer}`) + } + return ctx.subject("user", { + userID: value.subject, + }) + } throw new Error("Invalid provider: " + value.provider) }, } @@ -169,56 +179,79 @@ describe("client credentials flow", () => { describe("jwt-bearer grant type", () => { test("success", async () => { - // Create a JWT assertion for the test - const { SignJWT } = await import("jose") - const now = Math.floor(Date.now() / 1000) - const jwt = await new SignJWT({ - sub: "123", - iss: "myuser", - aud: "https://auth.example.com/token", - exp: now + 60, - provider: "dummy", - email: "foo@bar.com", + // Generate a key pair for testing + const { generateKeyPair, SignJWT } = await import("jose") + const { privateKey, publicKey } = await generateKeyPair("RS256", { + modulusLength: 2048, }) - .setProtectedHeader({ alg: "HS256" }) - .sign(new TextEncoder().encode("test-secret")) + + // Mock the JWKS endpoint + const originalFetch = global.fetch + ;(global as any).fetch = async (url: string | URL, init?: RequestInit): Promise => { + if (url.toString() === "https://trusted-issuer.example.com/.well-known/jwks.json") { + const jwks = await import("jose").then(jose => + jose.exportJWK(publicKey).then(jwk => ({ + keys: [{ ...jwk, kid: "test-key", use: "sig", alg: "RS256" }] + })) + ) + return new Response(JSON.stringify(jwks), { + headers: { "Content-Type": "application/json" } + }) + } + return originalFetch(url, init) + } - const response = await auth.request("https://auth.example.com/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", - assertion: jwt, + try { + const now = Math.floor(Date.now() / 1000) + const jwt = await new SignJWT({ + sub: "123", + iss: "https://trusted-issuer.example.com", + aud: "https://auth.example.com/token", + exp: now + 60, provider: "dummy", - }).toString(), - }) + email: "foo@bar.com", + }) + .setProtectedHeader({ alg: "RS256", kid: "test-key" }) + .sign(privateKey) - expect(response.status).toBe(200) - const tokens = await response.json() - expect(tokens).toStrictEqual({ - access_token: expectNonEmptyString, - refresh_token: expectNonEmptyString, - expires_in: expect.any(Number), - }) + const response = await auth.request("https://auth.example.com/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", + assertion: jwt, + }).toString(), + }) - const client = createClient({ - issuer: "https://auth.example.com", - clientID: "myuser", - fetch: (a, b) => Promise.resolve(auth.request(a, b)), - }) - const verified = await client.verify(subjects, tokens.access_token) - if (verified.err) throw verified.err - expect(verified).toStrictEqual({ - aud: "myuser", - subject: { - type: "user", - properties: { - userID: "123", + expect(response.status).toBe(200) + const tokens = await response.json() + expect(tokens).toStrictEqual({ + access_token: expectNonEmptyString, + refresh_token: expectNonEmptyString, + expires_in: expect.any(Number), + }) + + const client = createClient({ + issuer: "https://auth.example.com", + clientID: "https://auth.example.com/token", + fetch: (a, b) => Promise.resolve(auth.request(a, b)), + }) + const verified = await client.verify(subjects, tokens.access_token) + if (verified.err) throw verified.err + expect(verified).toStrictEqual({ + aud: "https://auth.example.com/token", + subject: { + type: "user", + properties: { + userID: "123", + }, }, - }, - }) + }) + } finally { + ;(global as any).fetch = originalFetch + } }) test("failure with invalid assertion", async () => { @@ -230,13 +263,12 @@ describe("jwt-bearer grant type", () => { body: new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion: "invalid.jwt.token", - provider: "dummy", }).toString(), }) expect(response.status).toBe(400) - const error = await response.json() - expect(error.error).toBe("invalid_grant") + const responseText = await response.text() + expect(responseText).toContain("unknown state") // Expecting error about invalid JWT }) }) From 01fbc60241d4cb7bc9221f4077931591e2090c51 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 4 Sep 2025 12:49:40 -0700 Subject: [PATCH 12/25] add trusted issuer check for jwt assertions --- packages/openauth/src/issuer.ts | 25 ++++++++++++++++++++ packages/openauth/test/issuer.test.ts | 33 +++++++++++++-------------- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/packages/openauth/src/issuer.ts b/packages/openauth/src/issuer.ts index f5d91330..c0ee5186 100644 --- a/packages/openauth/src/issuer.ts +++ b/packages/openauth/src/issuer.ts @@ -362,6 +362,24 @@ export interface IssuerInput< */ retention?: number } + /** + * List of trusted JWT issuers for JWT bearer token flow. + * + * When specified, only JWTs from these issuers will be accepted for the + * urn:ietf:params:oauth:grant-type:jwt-bearer grant type. + * + * @example + * ```ts + * { + * trustedIssuers: [ + * "https://gitlab.com", + * "https://github.com", + * "https://accounts.google.com" + * ] + * } + * ``` + */ + trustedIssuers?: string[] /** * Optionally, configure the UI that's displayed when the user visits the root URL of the * of the OpenAuth server. @@ -1052,6 +1070,13 @@ export function issuer< return c.json({ error: "missing issuer in jwt claims" }, 400) } + // Validate trusted issuers if configured + if (input.trustedIssuers && input.trustedIssuers.length > 0) { + if (!input.trustedIssuers.includes(claims.iss)) { + return c.json({ error: `untrusted issuer: ${claims.iss}` }, 400) + } + } + // get the jwks for the assertion const jwks = createRemoteJWKSet(new URL(`${claims.iss}/.well-known/jwks.json`)) try { diff --git a/packages/openauth/test/issuer.test.ts b/packages/openauth/test/issuer.test.ts index 70bcaf77..54bb5ae6 100644 --- a/packages/openauth/test/issuer.test.ts +++ b/packages/openauth/test/issuer.test.ts @@ -7,6 +7,7 @@ import { test, } from "bun:test" import { object, string } from "valibot" +import { generateKeyPair, SignJWT, exportJWK } from "jose" import { createClient } from "../src/client.js" import { issuer } from "../src/issuer.js" import { Provider } from "../src/provider/provider.js" @@ -24,6 +25,7 @@ const issuerConfig = { storage, subjects, allow: async () => true, + trustedIssuers: ["https://external-issuer.com"], // Add trusted issuers configuration ttl: { access: 60, refresh: 6000, @@ -57,11 +59,7 @@ const issuerConfig = { }) } if (value.provider === "jwt-bearer") { - // Validate trusted issuers - const trustedIssuers = ["https://trusted-issuer.example.com"] - if (!trustedIssuers.includes(value.issuer)) { - throw new Error(`Untrusted issuer: ${value.issuer}`) - } + // No need to validate issuers here since we're using trustedIssuers config return ctx.subject("user", { userID: value.subject, }) @@ -179,39 +177,40 @@ describe("client credentials flow", () => { describe("jwt-bearer grant type", () => { test("success", async () => { + const encryptAlgo = "RS256" // Generate a key pair for testing - const { generateKeyPair, SignJWT } = await import("jose") - const { privateKey, publicKey } = await generateKeyPair("RS256", { + const { privateKey, publicKey } = await generateKeyPair(encryptAlgo, { modulusLength: 2048, }) // Mock the JWKS endpoint const originalFetch = global.fetch + + // Override global fetch to mock the JWKS endpoint ;(global as any).fetch = async (url: string | URL, init?: RequestInit): Promise => { - if (url.toString() === "https://trusted-issuer.example.com/.well-known/jwks.json") { - const jwks = await import("jose").then(jose => - jose.exportJWK(publicKey).then(jwk => ({ - keys: [{ ...jwk, kid: "test-key", use: "sig", alg: "RS256" }] - })) - ) + if (url.toString() === "https://external-issuer.com/.well-known/jwks.json") { + const jwk = await exportJWK(publicKey) + const jwks = { + keys: [{ ...jwk, kid: "test-key", use: "sig", alg: encryptAlgo }] + } return new Response(JSON.stringify(jwks), { headers: { "Content-Type": "application/json" } }) } - return originalFetch(url, init) + return originalFetch.call(global, url, init) } try { const now = Math.floor(Date.now() / 1000) const jwt = await new SignJWT({ sub: "123", - iss: "https://trusted-issuer.example.com", + iss: "https://external-issuer.com", aud: "https://auth.example.com/token", exp: now + 60, provider: "dummy", email: "foo@bar.com", }) - .setProtectedHeader({ alg: "RS256", kid: "test-key" }) + .setProtectedHeader({ alg: encryptAlgo, kid: "test-key" }) .sign(privateKey) const response = await auth.request("https://auth.example.com/token", { @@ -250,7 +249,7 @@ describe("jwt-bearer grant type", () => { }, }) } finally { - ;(global as any).fetch = originalFetch + (global as any).fetch = originalFetch } }) From d8b30bd24063a669ed62593404f45aea6cbd84b3 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Fri, 5 Sep 2025 00:35:11 -0700 Subject: [PATCH 13/25] oidc provider and jwt assertion grant-types --- packages/openauth/src/client.ts | 83 +++++++++++++ packages/openauth/src/error.ts | 9 ++ packages/openauth/src/issuer.ts | 95 +++++++++------ packages/openauth/src/provider/code.ts | 2 +- packages/openauth/src/provider/oauth2.ts | 1 - packages/openauth/src/provider/oidc.ts | 36 +++++- packages/openauth/test/issuer.test.ts | 145 ++++++++++++----------- 7 files changed, 253 insertions(+), 118 deletions(-) diff --git a/packages/openauth/src/client.ts b/packages/openauth/src/client.ts index 83013232..588bef13 100644 --- a/packages/openauth/src/client.ts +++ b/packages/openauth/src/client.ts @@ -49,6 +49,7 @@ import { import { InvalidAccessTokenError, InvalidAuthorizationCodeError, + InvalidJWTError, InvalidRefreshTokenError, InvalidSubjectError, } from "./error.js" @@ -450,6 +451,60 @@ export interface Client { code: string, redirectURI: string, verifier?: string, + ): Promise + /** + * Exchange the jwt for access and refresh tokens. + * + * ```ts + * const exchanged = await client.exchange(, ) + * ``` + * + * You call this after the user has been redirected back to your app after the OAuth flow. + * + * :::tip + * For SSR sites, the code is returned in the query parameter. + * ::: + * + * So the code comes from the query parameter in the redirect URI. The redirect URI here is + * the one that you passed in to the `authorize` call when starting the flow. + * + * :::tip + * For SPA sites, the code is returned through the URL hash. + * ::: + * + * If you used the PKCE flow for an SPA app, the code is returned as a part of the redirect URL + * hash. + * + * ```ts {4} + * const exchanged = await client.exchange( + * , + * , + * + * ) + * ``` + * + * You also need to pass in the previously stored challenge verifier. + * + * This method returns the access and refresh tokens. Or if it fails, it returns an error that + * you can handle depending on the error. + * + * ```ts + * import { InvalidAuthorizationCodeError } from "@openauthjs/openauth/error" + * + * if (exchanged.err) { + * if (exchanged.err instanceof InvalidAuthorizationCodeError) { + * // handle invalid code error + * } + * else { + * // handle other errors + * } + * } + * + * const { access, refresh } = exchanged.tokens + * ``` + */ + exchangeJWT( + assertion: string, ): Promise /** * Refreshes the tokens if they have expired. This is used in an SPA app to maintain the @@ -666,6 +721,34 @@ export function createClient(input: ClientInput): Client { }, } }, + async exchangeJWT( + assertion: string, + ): Promise { + const tokens = await f(issuer + "/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + assertion, + grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", + }).toString(), + }) + const json = (await tokens.json()) as any + if (!tokens.ok) { + return { + err: new InvalidJWTError(), + } + } + return { + err: false, + tokens: { + access: json.access_token as string, + refresh: json.refresh_token as string, + expiresIn: json.expires_in as number, + }, + } + }, async refresh( refresh: string, opts?: RefreshOptions, diff --git a/packages/openauth/src/error.ts b/packages/openauth/src/error.ts index b35de0b5..e5986cfb 100644 --- a/packages/openauth/src/error.ts +++ b/packages/openauth/src/error.ts @@ -118,3 +118,12 @@ export class InvalidAuthorizationCodeError extends Error { super("Invalid authorization code") } } + +/** + * The JWT is invalid. + */ +export class InvalidJWTError extends Error { + constructor() { + super("Invalid JWT") + } +} diff --git a/packages/openauth/src/issuer.ts b/packages/openauth/src/issuer.ts index c0ee5186..a2f14790 100644 --- a/packages/openauth/src/issuer.ts +++ b/packages/openauth/src/issuer.ts @@ -192,7 +192,7 @@ import { UnauthorizedClientError, UnknownStateError, } from "./error.js" -import { compactDecrypt, CompactEncrypt, createRemoteJWKSet, decodeJwt, jwtVerify, SignJWT } from "jose" +import { compactDecrypt, CompactEncrypt, decodeJwt, jwtVerify, SignJWT } from "jose" import { Storage, StorageAdapter } from "./storage/storage.js" import { encryptionKeys, legacySigningKeys, signingKeys } from "./keys.js" import { validatePKCE } from "./pkce.js" @@ -204,13 +204,22 @@ import { setTheme, Theme } from "./ui/theme.js" import { getRelativeUrl, isDomainMatch, lazy } from "./util.js" import { cors } from "hono/cors" import { logger } from "hono/logger" +import { OidcProvider } from "./provider/oidc.js" /** @internal */ export const aws = awsHandle +interface ResponseLike { + json(): Promise + ok: Response["ok"] +} +type FetchLike = (...args: any[]) => Promise + + export interface IssuerInput< Providers extends Record>, Subjects extends SubjectSchema, + OidcProviders extends Record>, Result = { [key in keyof Providers]: Prettify< { @@ -283,6 +292,37 @@ export interface IssuerInput< * ``` */ providers: Providers + + /** + * The Oidc Providers that you want your OpenAuth server to support. + * + * @example + * + * ```ts title="issuer.ts" + * import { GithubProvider } from "@openauthjs/openauth/provider/github" + * + * issuer({ + * oidcProviders: { + * github: GithubActionOidcProvider() + * } + * }) + * ``` + * + * The key is just a string that you can use to identify the provider. It's passed back to + * the `success` callback. + * + * You can also specify multiple providers. + * + * ```ts + * { + * oidcProviders: { + * github: GithubActionOidcProvider(), + * } + * } + * ``` + */ + oidcProviders?: OidcProviders + /** * Array containing a list of the OAuth 2.0 [RFC6749] "scope" values that this authorization server advertises. * @@ -361,25 +401,9 @@ export interface IssuerInput< * @default 0s */ retention?: number + + fetch?: FetchLike } - /** - * List of trusted JWT issuers for JWT bearer token flow. - * - * When specified, only JWTs from these issuers will be accepted for the - * urn:ietf:params:oauth:grant-type:jwt-bearer grant type. - * - * @example - * ```ts - * { - * trustedIssuers: [ - * "https://gitlab.com", - * "https://github.com", - * "https://accounts.google.com" - * ] - * } - * ``` - */ - trustedIssuers?: string[] /** * Optionally, configure the UI that's displayed when the user visits the root URL of the * of the OpenAuth server. @@ -475,6 +499,7 @@ export interface IssuerInput< export function issuer< Providers extends Record>, Subjects extends SubjectSchema, + OidcProviders extends Record>, Result = { [key in keyof Providers]: Prettify< { @@ -482,7 +507,7 @@ export function issuer< } & (Providers[key] extends Provider ? T : {}) > }[keyof Providers], ->(input: IssuerInput) { +>(input: IssuerInput) { const error = input.error ?? function (err) { @@ -1070,25 +1095,19 @@ export function issuer< return c.json({ error: "missing issuer in jwt claims" }, 400) } - // Validate trusted issuers if configured - if (input.trustedIssuers && input.trustedIssuers.length > 0) { - if (!input.trustedIssuers.includes(claims.iss)) { - return c.json({ error: `untrusted issuer: ${claims.iss}` }, 400) + let oidcProvider + for (const provider in input.oidcProviders) { + if (input.oidcProviders[provider]?.issuer === claims.iss) { + oidcProvider = input.oidcProviders[provider] + break } } - // get the jwks for the assertion - const jwks = createRemoteJWKSet(new URL(`${claims.iss}/.well-known/jwks.json`)) - try { - await jwtVerify(assertion.toString(), jwks, { - subject: claims.sub, - issuer: claims.iss, - audience: claims.aud, - }) - } catch (err) { - return c.json({ error: `invalid jwt - ${err instanceof Error ? err.message : String(err)}` }, 400) + if (!oidcProvider) { + return c.json({ error: "no matching oidc provider found for issuer" }, 400) } - + + await oidcProvider.verifyIdToken(assertion.toString()) // Call the success callback to handle JWT bearer token validation return input.success( { @@ -1098,7 +1117,7 @@ export function issuer< subject: opts?.subject || claims.sub as string, properties, clientID: claims.aud as string, - scopes: parseScopes(scope), + // scopes: parseScopes(scope), validated? ttl: { access: opts?.ttl?.access ?? ((claims.exp as number) - Math.floor(Date.now() / 1000)), refresh: opts?.ttl?.refresh ?? ttlRefresh, @@ -1107,7 +1126,7 @@ export function issuer< return c.json({ access_token: tokens.access, refresh_token: tokens.refresh, - scope: parseScopes(scope)?.join(" "), + // scope: parseScopes(scope)?.join(" "), expires_in: tokens.expiresIn, }) }, @@ -1184,7 +1203,7 @@ export function issuer< await auth.set(c, "authorization", 60 * 60 * 24, authorization) if (provider) return c.redirect(`/${provider}/authorize`) const providers = Object.keys(input.providers) - // if (providers.length === 1) return c.redirect(`/${providers[0]}/authorize`) + if (providers.length === 1) return c.redirect(`/${providers[0]}/authorize`) return auth.forward( c, await select()( diff --git a/packages/openauth/src/provider/code.ts b/packages/openauth/src/provider/code.ts index e8f7b709..6e0bba4e 100644 --- a/packages/openauth/src/provider/code.ts +++ b/packages/openauth/src/provider/code.ts @@ -173,7 +173,7 @@ export function CodeProvider< const action = fd.get("action")?.toString() if (action === "request" || action === "resend") { - const claims = Object.fromEntries(fd) as Claims + const claims = Object.fromEntries(fd.entries()) as Claims delete claims.action const err = await config.sendCode(claims, code) if (err) return transition(c, { type: "start" }, fd, err) diff --git a/packages/openauth/src/provider/oauth2.ts b/packages/openauth/src/provider/oauth2.ts index 5a0f6583..b3ae11fc 100644 --- a/packages/openauth/src/provider/oauth2.ts +++ b/packages/openauth/src/provider/oauth2.ts @@ -184,7 +184,6 @@ export function Oauth2Provider( let idTokenPayload: Record | null = null if (config.endpoint.jwks) { const jwksEndpoint = new URL(config.endpoint.jwks) - // @ts-expect-error bun/node mismatch const jwks = createRemoteJWKSet(jwksEndpoint) const { payload } = await jwtVerify(json.id_token, jwks, { audience: config.clientID, diff --git a/packages/openauth/src/provider/oidc.ts b/packages/openauth/src/provider/oidc.ts index 8e21fde7..d1e61a93 100644 --- a/packages/openauth/src/provider/oidc.ts +++ b/packages/openauth/src/provider/oidc.ts @@ -24,6 +24,14 @@ import { OauthError } from "../error.js" import { Provider } from "./provider.js" import { JWTPayload } from "hono/utils/jwt/types" import { getRelativeUrl, lazy } from "../util.js" +import { verify } from "crypto" + +interface ResponseLike { + json(): Promise + ok: Response["ok"] + text(): Promise +} +type FetchLike = (...args: any[]) => Promise export interface OidcConfig { /** @@ -77,6 +85,8 @@ export interface OidcConfig { * ``` */ query?: Record + + fetch?: FetchLike } /** @@ -99,14 +109,20 @@ export interface IdTokenResponse { raw: Record } +export interface OidcProvider extends Provider { + issuer: string + verifyIdToken: (id_token: string) => Promise<{ payload: JWTPayload; protectedHeader: Record }> +} + export function OidcProvider( config: OidcConfig, -): Provider<{ id: JWTPayload; clientID: string }> { +): OidcProvider<{ id: JWTPayload; clientID: string }> { const query = config.query || {} const scopes = config.scopes || [] + const f = config.fetch || fetch const wk = lazy(() => - fetch(config.issuer + "/.well-known/openid-configuration").then( + f(config.issuer + "/.well-known/openid-configuration").then( async (r) => { if (!r.ok) throw new Error(await r.text()) return r.json() as Promise @@ -118,14 +134,22 @@ export function OidcProvider( wk() .then((r) => r.jwks_uri) .then(async (uri) => { - const r = await fetch(uri) + const r = await f(uri) if (!r.ok) throw new Error(await r.text()) return createLocalJWKSet((await r.json()) as JSONWebKeySet) }), ) + const verifyIdToken = async (id_token: string) => { + return jwtVerify(id_token, await jwks(), { + audience: config.clientID, + issuer: config.issuer, + }) + } + return { type: config.type || "oidc", + issuer: config.issuer, init(routes, ctx) { routes.get("/authorize", async (c) => { const provider: ProviderState = { @@ -163,9 +187,8 @@ export function OidcProvider( const idToken = body.get("id_token") if (!idToken) throw new OauthError("invalid_request", "Missing id_token") - const result = await jwtVerify(idToken.toString(), await jwks(), { - audience: config.clientID, - }) + + const result = await verifyIdToken(idToken.toString()) if (result.payload.nonce !== provider.nonce) { throw new OauthError("invalid_request", "Invalid nonce") } @@ -175,5 +198,6 @@ export function OidcProvider( }) }) }, + verifyIdToken, } } diff --git a/packages/openauth/test/issuer.test.ts b/packages/openauth/test/issuer.test.ts index 54bb5ae6..7c695876 100644 --- a/packages/openauth/test/issuer.test.ts +++ b/packages/openauth/test/issuer.test.ts @@ -13,6 +13,7 @@ import { issuer } from "../src/issuer.js" import { Provider } from "../src/provider/provider.js" import { MemoryStorage } from "../src/storage/memory.js" import { createSubjects } from "../src/subject.js" +import { OidcProvider } from "../src/provider/oidc.js" const subjects = createSubjects({ user: object({ @@ -21,17 +22,50 @@ const subjects = createSubjects({ }) let storage = MemoryStorage() + +const encryptAlgo = "RS256" +// Generate a key pair for testing +const { privateKey, publicKey } = await generateKeyPair(encryptAlgo, { + modulusLength: 2048, +}) + +const mockProvider: OidcProvider = OidcProvider({ + clientID: "https://auth.example.com/token", + issuer: "https://external-issuer.com", + fetch: async (url: string | URL, init?: RequestInit): Promise => { + if (url.toString() === "https://external-issuer.com/.well-known/openid-configuration") { + return new Response(JSON.stringify({ + issuer: "https://external-issuer.com", + authorization_endpoint: "https://external-issuer.com/authorize", + jwks_uri: "https://external-issuer.com/.well-known/jwks.json", + })) + } + + if (url.toString() === "https://external-issuer.com/.well-known/jwks.json") { + const jwk = await exportJWK(publicKey) + const jwks = { + keys: [{ ...jwk, kid: "test-key", use: "sig", alg: encryptAlgo }] + } + return new Response(JSON.stringify(jwks), { + headers: { "Content-Type": "application/json" } + }) + } + return new Response("Not Found", { status: 404 }) +}}) + const issuerConfig = { storage, subjects, allow: async () => true, - trustedIssuers: ["https://external-issuer.com"], // Add trusted issuers configuration + oidcProviders: {mockProvider}, ttl: { access: 60, refresh: 6000, refreshReuse: 60, refreshRetention: 6000, + }, + providers: { dummy: { type: "dummy", @@ -50,7 +84,7 @@ const issuerConfig = { email: "foo@bar.com", } }, - } satisfies Provider<{ email: string }>, + } }, success: async (ctx, value) => { if (value.provider === "dummy") { @@ -177,80 +211,47 @@ describe("client credentials flow", () => { describe("jwt-bearer grant type", () => { test("success", async () => { - const encryptAlgo = "RS256" - // Generate a key pair for testing - const { privateKey, publicKey } = await generateKeyPair(encryptAlgo, { - modulusLength: 2048, - }) - + // Mock the JWKS endpoint - const originalFetch = global.fetch - - // Override global fetch to mock the JWKS endpoint - ;(global as any).fetch = async (url: string | URL, init?: RequestInit): Promise => { - if (url.toString() === "https://external-issuer.com/.well-known/jwks.json") { - const jwk = await exportJWK(publicKey) - const jwks = { - keys: [{ ...jwk, kid: "test-key", use: "sig", alg: encryptAlgo }] - } - return new Response(JSON.stringify(jwks), { - headers: { "Content-Type": "application/json" } - }) - } - return originalFetch.call(global, url, init) - } - - try { - const now = Math.floor(Date.now() / 1000) - const jwt = await new SignJWT({ - sub: "123", - iss: "https://external-issuer.com", - aud: "https://auth.example.com/token", - exp: now + 60, - provider: "dummy", - email: "foo@bar.com", - }) - .setProtectedHeader({ alg: encryptAlgo, kid: "test-key" }) - .sign(privateKey) - - const response = await auth.request("https://auth.example.com/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", - assertion: jwt, - }).toString(), - }) + const client = createClient({ + issuer: "https://external-issuer.com", + clientID: "https://auth.example.com/token", // This should match the 'aud' claim in the JWT + fetch: async (url: string | URL, init?: RequestInit): Promise => { + return auth.request(url, init) + }}) + + const now = Math.floor(Date.now() / 1000) + const jwt = await new SignJWT({ + sub: "123", + iss: "https://external-issuer.com", + aud: "https://auth.example.com/token", + exp: now + 60, + provider: "dummy", + email: "foo@bar.com", + }) + .setProtectedHeader({ alg: encryptAlgo, kid: "test-key" }) + .sign(privateKey) - expect(response.status).toBe(200) - const tokens = await response.json() - expect(tokens).toStrictEqual({ - access_token: expectNonEmptyString, - refresh_token: expectNonEmptyString, - expires_in: expect.any(Number), - }) + const result = await client.exchangeJWT(jwt) + if (result.err) throw result.err + const tokens = result.tokens + expect(tokens).toStrictEqual({ + access: expectNonEmptyString, + refresh: expectNonEmptyString, + expiresIn: expect.any(Number), + }) - const client = createClient({ - issuer: "https://auth.example.com", - clientID: "https://auth.example.com/token", - fetch: (a, b) => Promise.resolve(auth.request(a, b)), - }) - const verified = await client.verify(subjects, tokens.access_token) - if (verified.err) throw verified.err - expect(verified).toStrictEqual({ - aud: "https://auth.example.com/token", - subject: { - type: "user", - properties: { - userID: "123", - }, + const verified = await client.verify(subjects, tokens.access) + if (verified.err) throw verified.err + expect(verified).toStrictEqual({ + aud: "https://auth.example.com/token", + subject: { + type: "user", + properties: { + userID: "123", }, - }) - } finally { - (global as any).fetch = originalFetch - } + }, + }) }) test("failure with invalid assertion", async () => { From 966e6f52b8a1d56b1e5ae398a1f7192744e49919 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Fri, 5 Sep 2025 12:18:30 -0700 Subject: [PATCH 14/25] remove trailing '/' from github issuer --- packages/openauth/src/provider/github.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/openauth/src/provider/github.ts b/packages/openauth/src/provider/github.ts index b913e005..20495b29 100644 --- a/packages/openauth/src/provider/github.ts +++ b/packages/openauth/src/provider/github.ts @@ -61,6 +61,6 @@ export function GithubActionsOidcProvider(config: GithubOidcConfig) { return OidcProvider({ ...config, type: "github", - issuer: "https://token.actions.githubusercontent.com/", + issuer: "https://token.actions.githubusercontent.com", }) } \ No newline at end of file From 15576b530a249b68657f6e315f8e5beea38afe8a Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Mon, 8 Sep 2025 13:20:15 -0700 Subject: [PATCH 15/25] jwt grant updates to pass provider name --- packages/openauth/src/issuer.ts | 2 +- packages/openauth/src/provider/oidc.ts | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/openauth/src/issuer.ts b/packages/openauth/src/issuer.ts index a2f14790..c9de2d55 100644 --- a/packages/openauth/src/issuer.ts +++ b/packages/openauth/src/issuer.ts @@ -1132,7 +1132,7 @@ export function issuer< }, }, { - provider: "jwt-bearer", + provider: oidcProvider.type, claims: claims, issuer: claims.iss, subject: claims.sub, diff --git a/packages/openauth/src/provider/oidc.ts b/packages/openauth/src/provider/oidc.ts index d1e61a93..380d0c4f 100644 --- a/packages/openauth/src/provider/oidc.ts +++ b/packages/openauth/src/provider/oidc.ts @@ -73,6 +73,18 @@ export interface OidcConfig { * ``` */ scopes?: string[] + /** + * The expected audience for JWT verification. + * If not provided, defaults to clientID. + * + * @example + * ```ts + * { + * audience: "https://github.com/owner/repo" + * } + * ``` + */ + audience?: string /** * Any additional parameters that you want to pass to the authorization endpoint. * @example @@ -142,7 +154,7 @@ export function OidcProvider( const verifyIdToken = async (id_token: string) => { return jwtVerify(id_token, await jwks(), { - audience: config.clientID, + audience: config.audience || config.clientID, issuer: config.issuer, }) } From a09fba3efb3eeb5237134f8193742ea67c947202 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Mon, 8 Sep 2025 15:09:41 -0700 Subject: [PATCH 16/25] fix missing param in test data --- packages/openauth/test/issuer.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/openauth/test/issuer.test.ts b/packages/openauth/test/issuer.test.ts index 7c695876..8a215a67 100644 --- a/packages/openauth/test/issuer.test.ts +++ b/packages/openauth/test/issuer.test.ts @@ -32,6 +32,7 @@ const { privateKey, publicKey } = await generateKeyPair(encryptAlgo, { const mockProvider: OidcProvider = OidcProvider({ clientID: "https://auth.example.com/token", issuer: "https://external-issuer.com", + type: "jwt-bearer", fetch: async (url: string | URL, init?: RequestInit): Promise => { if (url.toString() === "https://external-issuer.com/.well-known/openid-configuration") { return new Response(JSON.stringify({ From 5e2f4f58f7ae088f70f34338c579e89b3a5dd3dd Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Mon, 8 Sep 2025 15:23:46 -0700 Subject: [PATCH 17/25] revert bun.lockb to original --- bun.lockb | Bin 257888 -> 258568 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/bun.lockb b/bun.lockb index f02a6996bde982afe5fd938cfa0b160fb1364d4d..25694456e2ece350cbee3996b7060f86741b6b19 100755 GIT binary patch delta 39092 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%0Dh>W=)TFwPw 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 From 977dd9d98915851aeec8222734203a354aace479 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 8 Sep 2025 22:37:00 +0000 Subject: [PATCH 18/25] auto: format code --- examples/jwt-bearer-validation.md | 24 ++++----- packages/openauth/src/client.ts | 6 +-- packages/openauth/src/issuer.ts | 37 ++++++++------ packages/openauth/src/provider/github.ts | 2 +- packages/openauth/src/provider/gitlab.ts | 2 +- packages/openauth/src/provider/oidc.ts | 18 +++---- packages/openauth/test/issuer.test.ts | 64 ++++++++++++++---------- 7 files changed, 85 insertions(+), 68 deletions(-) diff --git a/examples/jwt-bearer-validation.md b/examples/jwt-bearer-validation.md index fc39c292..78c4f691 100644 --- a/examples/jwt-bearer-validation.md +++ b/examples/jwt-bearer-validation.md @@ -19,7 +19,7 @@ import { OidcProvider } from "@openauthjs/openauth/provider/oidc" const app = issuer({ providers: { gitlab: OidcProvider({ - clientID: "your-gitlab-app-id", + clientID: "your-gitlab-app-id", issuer: "https://gitlab.com" }) }, @@ -31,43 +31,43 @@ const app = issuer({ const userID = /* map GitLab user to your system */ return ctx.subject("user", { userID }) } - + if (value.provider === "jwt-bearer") { console.log("JWT Bearer token from:", value.issuer) console.log("Full claims:", value.claims) - + // Validate the issuer - this is where YOU decide who to trust const trustedIssuers = [ "https://gitlab.com", // Your main GitLab instance - "https://accounts.google.com", // Google service accounts + "https://accounts.google.com", // Google service accounts "https://login.microsoftonline.com" // Azure AD ] - + if (!trustedIssuers.includes(value.issuer)) { throw new Error(`Untrusted issuer: ${value.issuer}`) } - + // Handle different issuers differently if (value.issuer === "https://gitlab.com") { // JWT from GitLab (maybe from CI/CD pipeline) const userID = /* lookup user from GitLab subject */ return ctx.subject("user", { userID }) } - + if (value.issuer === "https://accounts.google.com") { // JWT from Google service account const serviceID = /* extract service info */ return ctx.subject("service", { serviceID }) } - + // Add validation for additional custom claims if (value.claims.custom_role !== "api_access") { throw new Error("JWT missing required role") } - - return ctx.subject("api_user", { + + return ctx.subject("api_user", { userID: value.subject, - issuer: value.issuer + issuer: value.issuer }) } } @@ -91,7 +91,7 @@ const app = issuer({ provider: "jwt-bearer", claims: JWTPayload, // Full JWT claims object issuer: string, // The JWT issuer - subject: string, // The JWT subject (sub claim) + subject: string, // The JWT subject (sub claim) audience: string // The JWT audience (aud claim) } ``` diff --git a/packages/openauth/src/client.ts b/packages/openauth/src/client.ts index 588bef13..19ede047 100644 --- a/packages/openauth/src/client.ts +++ b/packages/openauth/src/client.ts @@ -452,7 +452,7 @@ export interface Client { redirectURI: string, verifier?: string, ): Promise - /** + /** * Exchange the jwt for access and refresh tokens. * * ```ts @@ -503,9 +503,7 @@ export interface Client { * const { access, refresh } = exchanged.tokens * ``` */ - exchangeJWT( - assertion: string, - ): Promise + exchangeJWT(assertion: string): Promise /** * Refreshes the tokens if they have expired. This is used in an SPA app to maintain the * session, without logging the user out. diff --git a/packages/openauth/src/issuer.ts b/packages/openauth/src/issuer.ts index c9de2d55..8db74c56 100644 --- a/packages/openauth/src/issuer.ts +++ b/packages/openauth/src/issuer.ts @@ -192,7 +192,13 @@ import { UnauthorizedClientError, UnknownStateError, } from "./error.js" -import { compactDecrypt, CompactEncrypt, decodeJwt, jwtVerify, SignJWT } from "jose" +import { + compactDecrypt, + CompactEncrypt, + decodeJwt, + jwtVerify, + SignJWT, +} from "jose" import { Storage, StorageAdapter } from "./storage/storage.js" import { encryptionKeys, legacySigningKeys, signingKeys } from "./keys.js" import { validatePKCE } from "./pkce.js" @@ -215,7 +221,6 @@ interface ResponseLike { } type FetchLike = (...args: any[]) => Promise - export interface IssuerInput< Providers extends Record>, Subjects extends SubjectSchema, @@ -644,11 +649,7 @@ export function issuer< response.headers.forEach((value, name) => { headers[name] = value }) - return ctx.newResponse( - response.body, - response.status as any, - headers, - ) + return ctx.newResponse(response.body, response.status as any, headers) }, async set(ctx, key, maxAge, value) { setCookie(ctx, key, await encrypt(value), { @@ -1048,7 +1049,10 @@ export function issuer< const response = await match.client({ clientID: clientID.toString(), clientSecret: clientSecret.toString(), - params: Object.fromEntries(Array.from(form.entries())) as Record, + params: Object.fromEntries(Array.from(form.entries())) as Record< + string, + string + >, }) return input.success( { @@ -1104,7 +1108,10 @@ export function issuer< } if (!oidcProvider) { - return c.json({ error: "no matching oidc provider found for issuer" }, 400) + return c.json( + { error: "no matching oidc provider found for issuer" }, + 400, + ) } await oidcProvider.verifyIdToken(assertion.toString()) @@ -1114,12 +1121,14 @@ export function issuer< async subject(type, properties, opts) { const tokens = await generateTokens(c, { type: type as string, - subject: opts?.subject || claims.sub as string, + subject: opts?.subject || (claims.sub as string), properties, clientID: claims.aud as string, // scopes: parseScopes(scope), validated? ttl: { - access: opts?.ttl?.access ?? ((claims.exp as number) - Math.floor(Date.now() / 1000)), + access: + opts?.ttl?.access ?? + (claims.exp as number) - Math.floor(Date.now() / 1000), refresh: opts?.ttl?.refresh ?? ttlRefresh, }, }) @@ -1140,7 +1149,7 @@ export function issuer< } as Result, c.req.raw, ) - } + } throw new Error("Invalid grant_type") }, @@ -1267,12 +1276,12 @@ export function issuer< if (validated.issues) { return c.json({ - error: "invalid_token", + error: "invalid_token", error_description: "Invalid token", }) } - if (result.payload.mode === "access" && 'value' in validated) { + if (result.payload.mode === "access" && "value" in validated) { return c.json(validated.value as Record) } diff --git a/packages/openauth/src/provider/github.ts b/packages/openauth/src/provider/github.ts index 20495b29..99036c6e 100644 --- a/packages/openauth/src/provider/github.ts +++ b/packages/openauth/src/provider/github.ts @@ -63,4 +63,4 @@ export function GithubActionsOidcProvider(config: GithubOidcConfig) { type: "github", issuer: "https://token.actions.githubusercontent.com", }) -} \ No newline at end of file +} diff --git a/packages/openauth/src/provider/gitlab.ts b/packages/openauth/src/provider/gitlab.ts index a5fbccdb..fb9fd398 100644 --- a/packages/openauth/src/provider/gitlab.ts +++ b/packages/openauth/src/provider/gitlab.ts @@ -63,4 +63,4 @@ export function GitlabOidcProvider(config: GitlabOidcConfig) { type: "gitlab", issuer: "https://gitlab.com", }) -} \ No newline at end of file +} diff --git a/packages/openauth/src/provider/oidc.ts b/packages/openauth/src/provider/oidc.ts index 380d0c4f..a5f25bf5 100644 --- a/packages/openauth/src/provider/oidc.ts +++ b/packages/openauth/src/provider/oidc.ts @@ -97,7 +97,7 @@ export interface OidcConfig { * ``` */ query?: Record - + fetch?: FetchLike } @@ -123,7 +123,9 @@ export interface IdTokenResponse { export interface OidcProvider extends Provider { issuer: string - verifyIdToken: (id_token: string) => Promise<{ payload: JWTPayload; protectedHeader: Record }> + verifyIdToken: ( + id_token: string, + ) => Promise<{ payload: JWTPayload; protectedHeader: Record }> } export function OidcProvider( @@ -134,12 +136,10 @@ export function OidcProvider( const f = config.fetch || fetch const wk = lazy(() => - f(config.issuer + "/.well-known/openid-configuration").then( - async (r) => { - if (!r.ok) throw new Error(await r.text()) - return r.json() as Promise - }, - ), + f(config.issuer + "/.well-known/openid-configuration").then(async (r) => { + if (!r.ok) throw new Error(await r.text()) + return r.json() as Promise + }), ) const jwks = lazy(() => @@ -200,7 +200,7 @@ export function OidcProvider( if (!idToken) throw new OauthError("invalid_request", "Missing id_token") - const result = await verifyIdToken(idToken.toString()) + const result = await verifyIdToken(idToken.toString()) if (result.payload.nonce !== provider.nonce) { throw new OauthError("invalid_request", "Invalid nonce") } diff --git a/packages/openauth/test/issuer.test.ts b/packages/openauth/test/issuer.test.ts index 8a215a67..41998845 100644 --- a/packages/openauth/test/issuer.test.ts +++ b/packages/openauth/test/issuer.test.ts @@ -30,41 +30,48 @@ const { privateKey, publicKey } = await generateKeyPair(encryptAlgo, { }) const mockProvider: OidcProvider = OidcProvider({ - clientID: "https://auth.example.com/token", - issuer: "https://external-issuer.com", - type: "jwt-bearer", - fetch: async (url: string | URL, init?: RequestInit): Promise => { - if (url.toString() === "https://external-issuer.com/.well-known/openid-configuration") { - return new Response(JSON.stringify({ + clientID: "https://auth.example.com/token", + issuer: "https://external-issuer.com", + type: "jwt-bearer", + fetch: async (url: string | URL, init?: RequestInit): Promise => { + if ( + url.toString() === + "https://external-issuer.com/.well-known/openid-configuration" + ) { + return new Response( + JSON.stringify({ issuer: "https://external-issuer.com", authorization_endpoint: "https://external-issuer.com/authorize", jwks_uri: "https://external-issuer.com/.well-known/jwks.json", - })) - } + }), + ) + } - if (url.toString() === "https://external-issuer.com/.well-known/jwks.json") { - const jwk = await exportJWK(publicKey) - const jwks = { - keys: [{ ...jwk, kid: "test-key", use: "sig", alg: encryptAlgo }] - } - return new Response(JSON.stringify(jwks), { - headers: { "Content-Type": "application/json" } - }) + if ( + url.toString() === "https://external-issuer.com/.well-known/jwks.json" + ) { + const jwk = await exportJWK(publicKey) + const jwks = { + keys: [{ ...jwk, kid: "test-key", use: "sig", alg: encryptAlgo }], } - return new Response("Not Found", { status: 404 }) -}}) - + return new Response(JSON.stringify(jwks), { + headers: { "Content-Type": "application/json" }, + }) + } + return new Response("Not Found", { status: 404 }) + }, +}) + const issuerConfig = { storage, subjects, allow: async () => true, - oidcProviders: {mockProvider}, + oidcProviders: { mockProvider }, ttl: { access: 60, refresh: 6000, refreshReuse: 60, refreshRetention: 6000, - }, providers: { @@ -85,7 +92,7 @@ const issuerConfig = { email: "foo@bar.com", } }, - } + }, }, success: async (ctx, value) => { if (value.provider === "dummy") { @@ -212,20 +219,23 @@ describe("client credentials flow", () => { describe("jwt-bearer grant type", () => { test("success", async () => { - // Mock the JWKS endpoint const client = createClient({ issuer: "https://external-issuer.com", clientID: "https://auth.example.com/token", // This should match the 'aud' claim in the JWT - fetch: async (url: string | URL, init?: RequestInit): Promise => { - return auth.request(url, init) - }}) + fetch: async ( + url: string | URL, + init?: RequestInit, + ): Promise => { + return auth.request(url, init) + }, + }) const now = Math.floor(Date.now() / 1000) const jwt = await new SignJWT({ sub: "123", iss: "https://external-issuer.com", - aud: "https://auth.example.com/token", + aud: "https://auth.example.com/token", exp: now + 60, provider: "dummy", email: "foo@bar.com", From 27477fec83bf185db10fc00352a79ad2870ecf83 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Mon, 8 Sep 2025 17:41:40 -0700 Subject: [PATCH 19/25] update hono and exports/imports --- packages/openauth/package.json | 18 +++++++++++------- packages/openauth/tsconfig.json | 1 + 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/openauth/package.json b/packages/openauth/package.json index d71c4c4d..33ae658a 100644 --- a/packages/openauth/package.json +++ b/packages/openauth/package.json @@ -12,23 +12,27 @@ "@tsconfig/node22": "22.0.0", "@types/node": "22.10.1", "arctic": "2.2.2", - "hono": "4.6.9", + "hono": "4.9.6", "ioredis": "5.4.1", "typescript": "5.6.3", "valibot": "1.0.0-beta.15" }, "exports": { ".": { - "import": "./dist/esm/index.js", - "types": "./dist/types/index.d.ts" + "import": "./src/index.ts", + "types": "./src/index.ts" }, "./*": { - "import": "./dist/esm/*.js", - "types": "./dist/types/*.d.ts" + "import": "./src/*.ts", + "types": "./src/*.ts" }, "./ui": { - "import": "./dist/esm/ui/index.js", - "types": "./dist/types/ui/index.d.ts" + "import": "./src/ui/index.ts", + "types": "./src/ui/index.ts" + }, + "./ui/*": { + "import": "./src/ui/*.tsx", + "types": "./src/ui/*.tsx" } }, "peerDependencies": { diff --git a/packages/openauth/tsconfig.json b/packages/openauth/tsconfig.json index 987a2153..dca5481b 100644 --- a/packages/openauth/tsconfig.json +++ b/packages/openauth/tsconfig.json @@ -4,6 +4,7 @@ "outDir": "dist", "declaration": true, "skipLibCheck": true, + "lib": ["dom", "esnext"], // or ["dom", "es2023"] "module": "NodeNext", "moduleResolution": "NodeNext", "jsx": "react-jsx", From c6b227d72be67bbb98f686ce834e50e77cf0cdb3 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Mon, 8 Sep 2025 17:52:42 -0700 Subject: [PATCH 20/25] update to dependencies --- packages/openauth/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/openauth/package.json b/packages/openauth/package.json index 33ae658a..4a39029f 100644 --- a/packages/openauth/package.json +++ b/packages/openauth/package.json @@ -11,9 +11,8 @@ "@cloudflare/workers-types": "4.20241205.0", "@tsconfig/node22": "22.0.0", "@types/node": "22.10.1", - "arctic": "2.2.2", + "arctic": "2.3.4", "hono": "4.9.6", - "ioredis": "5.4.1", "typescript": "5.6.3", "valibot": "1.0.0-beta.15" }, @@ -36,13 +35,14 @@ } }, "peerDependencies": { - "arctic": "^2.2.2", + "arctic": "^2.3.4", "hono": "^4.0.0" }, "dependencies": { "@standard-schema/spec": "1.0.0-beta.3", "aws4fetch": "1.0.20", - "jose": "5.9.6" + "jose": "5.9.6", + "ioredis": "5.4.1" }, "files": [ "src", From 3f6812eecbb70276d7d6f1654b39413af6a07205 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Tue, 9 Sep 2025 15:11:31 -0700 Subject: [PATCH 21/25] review updates --- examples/jwt-bearer-validation.md | 149 +++++++++++++++---------- package.json | 3 +- packages/openauth/src/client.ts | 39 +++---- packages/openauth/src/issuer.ts | 52 ++++----- packages/openauth/src/provider/oidc.ts | 6 + packages/openauth/tsconfig.json | 2 +- 6 files changed, 140 insertions(+), 111 deletions(-) diff --git a/examples/jwt-bearer-validation.md b/examples/jwt-bearer-validation.md index 78c4f691..af0f771b 100644 --- a/examples/jwt-bearer-validation.md +++ b/examples/jwt-bearer-validation.md @@ -2,74 +2,104 @@ ## Overview -When using the `urn:ietf:params:oauth:grant-type:jwt-bearer` grant type, OpenAuth validates the JWT signature and then calls your success callback where you can validate the issuer and map claims to your user system. +When using the `urn:ietf:params:oauth:grant-type:jwt-bearer` grant type, OpenAuth automatically validates JWT signatures using JWKS and calls your success callback to handle the validated JWT claims. ## Validation Process -1. **JWT signature verification**: OpenAuth fetches the issuer's JWKS and verifies the JWT signature -2. **Success callback**: Your success callback receives the JWT claims where you can validate the issuer -3. **Token generation**: If you approve the JWT, return `ctx.subject()` to generate final access/refresh tokens +1. **JWT decoding**: OpenAuth decodes the JWT assertion to extract claims +2. **OIDC provider matching**: Finds a matching OIDC provider based on the JWT issuer +3. **JWT signature verification**: Automatically fetches the issuer's JWKS and verifies the JWT signature using the provider's `verifyIdToken()` method +4. **Success callback**: Your success callback receives the validated JWT claims +5. **Token generation**: Return `ctx.subject()` to generate final access/refresh tokens ## Configuration +Configure `oidcProviders` for each JWT issuer you want to accept: + ```typescript import { issuer } from "@openauthjs/openauth" -import { OidcProvider } from "@openauthjs/openauth/provider/oidc" +import { OidcProvider } from "@openauthjs/openauth/provider/oidc" +import { GitHubProvider } from "@openauthjs/openauth/provider/github" const app = issuer({ - providers: { + // OIDC providers for JWT bearer validation + oidcProviders: { gitlab: OidcProvider({ - clientID: "your-gitlab-app-id", - issuer: "https://gitlab.com" + clientID: "https://gitlab.com", // Must match JWT 'aud' claim + issuer: "https://gitlab.com", // Must match JWT 'iss' claim + provider: "gitlab" // Provider type identifier + }), + github: OidcProvider({ + clientID: "github-actions", + issuer: "https://token.actions.githubusercontent.com", + provider: "github" }) }, + + // Regular OAuth providers for interactive login + providers: { + github: GitHubProvider({ + clientId: process.env.GITHUB_CLIENT_ID!, + clientSecret: process.env.GITHUB_CLIENT_SECRET! + }) + }, + subjects: { /* your subjects */ }, storage: /* your storage */, + success: async (ctx, value) => { - if (value.provider === "gitlab") { - // Handle GitLab OAuth login - const userID = /* map GitLab user to your system */ - return ctx.subject("user", { userID }) + // Handle regular OAuth providers + if (value.provider === "github") { + const providerData = await getGithubData(value.tokenset.access) + const { user } = await upsertUser(providerData) + return ctx.subject("user", { + id: user.id, + tenant: user.defaultTenant, + hasura: { + "x-hasura-allowed-roles": ["user"], + "x-hasura-default-role": "user", + "x-hasura-user-id": user.id, + }, + externalTenants: user.tenants.map(t => t.id), + githubOrgs: providerData.orgs?.map(org => org.name) + }, { + subject: user.id + }) } - if (value.provider === "jwt-bearer") { + // Handle JWT bearer tokens + if (!value.tokenset) { console.log("JWT Bearer token from:", value.issuer) - console.log("Full claims:", value.claims) - - // Validate the issuer - this is where YOU decide who to trust - const trustedIssuers = [ - "https://gitlab.com", // Your main GitLab instance - "https://accounts.google.com", // Google service accounts - "https://login.microsoftonline.com" // Azure AD - ] + console.log("JWT claims:", value.claims) - if (!trustedIssuers.includes(value.issuer)) { - throw new Error(`Untrusted issuer: ${value.issuer}`) - } - - // Handle different issuers differently + // The JWT signature is already validated by OpenAuth using JWKS + // Map different issuers to appropriate subjects + if (value.issuer === "https://gitlab.com") { - // JWT from GitLab (maybe from CI/CD pipeline) - const userID = /* lookup user from GitLab subject */ - return ctx.subject("user", { userID }) - } - - if (value.issuer === "https://accounts.google.com") { - // JWT from Google service account - const serviceID = /* extract service info */ - return ctx.subject("service", { serviceID }) + // JWT from GitLab CI/CD pipeline + return ctx.subject("service", { + id: value.subject, + issuer: value.issuer, + }) } - // Add validation for additional custom claims - if (value.claims.custom_role !== "api_access") { - throw new Error("JWT missing required role") + if (value.issuer === "https://token.actions.githubusercontent.com") { + // JWT from GitHub CI Action + return ctx.subject("service", { + id: value.subject, + issuer: value.issuer, + }) } + // Default: map to API user if no specific handling return ctx.subject("api_user", { - userID: value.subject, - issuer: value.issuer + id: value.subject, + issuer: value.issuer, + audience: value.audience }) } + + throw new Error(`Unsupported provider: ${value.provider}`) } }) ``` @@ -82,37 +112,38 @@ const app = issuer({ grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion= ``` -2. **Signature verification**: OpenAuth fetches the issuer's JWKS from `${issuer}/.well-known/jwks.json` and verifies the JWT signature +2. **OIDC provider matching**: OpenAuth finds the matching OIDC provider by comparing the JWT `iss` claim with configured provider issuers + +3. **Signature verification**: OpenAuth uses the matched OIDC provider to verify the JWT signature (automatically fetches JWKS) -3. **Success callback**: OpenAuth calls your success callback with: +4. **Success callback**: OpenAuth calls your success callback with: ```typescript { - provider: "jwt-bearer", - claims: JWTPayload, // Full JWT claims object - issuer: string, // The JWT issuer - subject: string, // The JWT subject (sub claim) - audience: string // The JWT audience (aud claim) + provider: string, // OIDC provider type (from config.type) + claims: JWTPayload, // Full JWT claims object + issuer: string, // The JWT issuer (iss claim) + subject: string, // The JWT subject (sub claim) + audience: string // The JWT audience (aud claim) } ``` -4. **Issuer validation**: In your success callback, you decide which issuers to trust - -5. **Token generation**: If you approve the JWT, return `ctx.subject()` to generate final access/refresh tokens +5. **Token generation**: Return `ctx.subject()` to generate final access/refresh tokens ## Security Considerations -**Why validate in the success callback?** +**OIDC provider configuration acts as allowlist:** -- **Flexible validation**: You can implement custom logic for different issuers -- **Context-aware**: Access to full JWT claims for additional validation -- **Granular control**: Different handling per issuer (users vs services vs APIs) -- **Dynamic trust**: Trust decisions can be based on database lookups or external APIs -- **Consistent pattern**: Same validation approach as other OAuth providers +- **Explicit trust**: Only JWTs from configured `oidcProviders` are accepted +- **Automatic validation**: JWT signature verification is handled automatically +- **No additional issuer validation needed**: The OIDC provider matching already ensures trusted issuers +- **JWKS fetching**: OpenAuth automatically fetches and caches JWKS for signature verification **Best practices:** -- **Use allowlists**: Explicitly list trusted issuers rather than trying to block bad ones -- **Validate additional claims**: Check roles, audiences, or custom claims as needed +- **Configure specific issuers**: Only add OIDC providers for issuers you trust +- **Match audience claims**: Ensure JWT `aud` claim matches your `clientID` configuration +- **Validate additional claims**: Check roles, scopes, or custom claims in the success callback +- **Use specific types**: Create different subject types for different use cases (users vs services) - **Log JWT usage**: Monitor bearer token usage for security auditing -- **Handle errors gracefully**: Throw clear errors for untrusted issuers or invalid claims +- **Handle claim validation**: Throw clear errors for missing or invalid claims diff --git a/package.json b/package.json index ef22a723..fd43e07e 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,7 @@ }, "devDependencies": { "@tsconfig/node22": "22.0.2", - "@types/bun": "1.2.21", - "@types/node": "22" + "@types/bun": "1.2.21" }, "dependencies": { "@changesets/cli": "2.27.10", diff --git a/packages/openauth/src/client.ts b/packages/openauth/src/client.ts index 19ede047..d4aebd4c 100644 --- a/packages/openauth/src/client.ts +++ b/packages/openauth/src/client.ts @@ -453,47 +453,38 @@ export interface Client { verifier?: string, ): Promise /** - * Exchange the jwt for access and refresh tokens. + * Exchange a JWT assertion for access and refresh tokens using the JWT Bearer grant type. * * ```ts - * const exchanged = await client.exchange(, ) + * const exchanged = await client.exchangeJWT() * ``` * - * You call this after the user has been redirected back to your app after the OAuth flow. - * - * :::tip - * For SSR sites, the code is returned in the query parameter. - * ::: - * - * So the code comes from the query parameter in the redirect URI. The redirect URI here is - * the one that you passed in to the `authorize` call when starting the flow. + * This implements the JWT Bearer grant type (RFC 7523) where you exchange a signed JWT + * for OpenAuth access and refresh tokens. * * :::tip - * For SPA sites, the code is returned through the URL hash. + * The JWT must be signed by a trusted issuer configured in your OpenAuth server. * ::: * - * If you used the PKCE flow for an SPA app, the code is returned as a part of the redirect URL - * hash. + * The JWT assertion should contain standard claims like `iss` (issuer), `sub` (subject), + * `aud` (audience), and `exp` (expiration). The issuer must match one of your configured + * OIDC providers. * - * ```ts {4} - * const exchanged = await client.exchange( - * , - * , - * - * ) + * ```ts + * // Example: exchanging a GitLab CI JWT + * const gitlabJWT = process.env.OIDC_TOKEN + * const exchanged = await client.exchangeJWT(gitlabJWT) * ``` * - * You also need to pass in the previously stored challenge verifier. - * * This method returns the access and refresh tokens. Or if it fails, it returns an error that * you can handle depending on the error. * * ```ts - * import { InvalidAuthorizationCodeError } from "@openauthjs/openauth/error" + * import { InvalidJWTError } from "@openauthjs/openauth/error" * * if (exchanged.err) { - * if (exchanged.err instanceof InvalidAuthorizationCodeError) { - * // handle invalid code error + * if (exchanged.err instanceof InvalidJWTError) { + * // handle invalid JWT error (signature verification failed, untrusted issuer, etc.) * } * else { * // handle other errors diff --git a/packages/openauth/src/issuer.ts b/packages/openauth/src/issuer.ts index 8db74c56..7cd2e974 100644 --- a/packages/openauth/src/issuer.ts +++ b/packages/openauth/src/issuer.ts @@ -215,12 +215,6 @@ import { OidcProvider } from "./provider/oidc.js" /** @internal */ export const aws = awsHandle -interface ResponseLike { - json(): Promise - ok: Response["ok"] -} -type FetchLike = (...args: any[]) => Promise - export interface IssuerInput< Providers extends Record>, Subjects extends SubjectSchema, @@ -406,8 +400,6 @@ export interface IssuerInput< * @default 0s */ retention?: number - - fetch?: FetchLike } /** * Optionally, configure the UI that's displayed when the user visits the root URL of the @@ -1087,16 +1079,25 @@ export function issuer< if (grantType === "urn:ietf:params:oauth:grant-type:jwt-bearer") { const assertion = form.get("assertion") if (!assertion) { - return c.json({ error: "missing `assertion` form value" }, 400) + return c.json({ + error: "invalid_grant", + error_description: "Missing assertion parameter" + }, 400) } const claims = decodeJwt(assertion.toString()) if (!claims) { - return c.json({ error: "missing jwt claims" }, 400) + return c.json({ + error: "invalid_grant", + error_description: "JWT assertion could not be decoded" + }, 400) } if (!claims.iss) { - return c.json({ error: "missing issuer in jwt claims" }, 400) + return c.json({ + error: "invalid_grant", + error_description: "JWT assertion missing required issuer claim" + }, 400) } let oidcProvider @@ -1108,13 +1109,21 @@ export function issuer< } if (!oidcProvider) { - return c.json( - { error: "no matching oidc provider found for issuer" }, - 400, - ) + return c.json({ + error: "invalid_grant", + error_description: "JWT assertion from untrusted issuer" + }, 400) + } + + try { + await oidcProvider.verifyIdToken(assertion.toString()) + } catch (error) { + return c.json({ + error: "invalid_grant", + error_description: "JWT assertion signature verification failed or token expired" + }, 400) } - await oidcProvider.verifyIdToken(assertion.toString()) // Call the success callback to handle JWT bearer token validation return input.success( { @@ -1274,15 +1283,8 @@ export function issuer< "~standard" ].validate(result.payload.properties) - if (validated.issues) { - return c.json({ - error: "invalid_token", - error_description: "Invalid token", - }) - } - - if (result.payload.mode === "access" && "value" in validated) { - return c.json(validated.value as Record) + if (!validated.issues && result.payload.mode === "access") { + return c.json(validated.value as SubjectSchema) } return c.json({ diff --git a/packages/openauth/src/provider/oidc.ts b/packages/openauth/src/provider/oidc.ts index a5f25bf5..13aa5732 100644 --- a/packages/openauth/src/provider/oidc.ts +++ b/packages/openauth/src/provider/oidc.ts @@ -98,6 +98,12 @@ export interface OidcConfig { */ query?: Record + /** + * Optionally, override the internally used fetch function. + * + * This is useful if you are using a polyfilled fetch function in your application and you + * want the client to use it too. + */ fetch?: FetchLike } diff --git a/packages/openauth/tsconfig.json b/packages/openauth/tsconfig.json index dca5481b..0d8538dd 100644 --- a/packages/openauth/tsconfig.json +++ b/packages/openauth/tsconfig.json @@ -9,7 +9,7 @@ "moduleResolution": "NodeNext", "jsx": "react-jsx", "jsxImportSource": "hono/jsx", - "types": ["node", "bun"] + "types": ["bun"] }, "include": ["src"] } From 44f971b3f2f6a76bba6eb070b7c54d95e332722b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 9 Sep 2025 22:11:51 +0000 Subject: [PATCH 22/25] auto: format code --- examples/jwt-bearer-validation.md | 18 +++++----- packages/openauth/src/client.ts | 4 +-- packages/openauth/src/issuer.ts | 56 ++++++++++++++++++++----------- 3 files changed, 47 insertions(+), 31 deletions(-) diff --git a/examples/jwt-bearer-validation.md b/examples/jwt-bearer-validation.md index af0f771b..9dc16957 100644 --- a/examples/jwt-bearer-validation.md +++ b/examples/jwt-bearer-validation.md @@ -18,7 +18,7 @@ Configure `oidcProviders` for each JWT issuer you want to accept: ```typescript import { issuer } from "@openauthjs/openauth" -import { OidcProvider } from "@openauthjs/openauth/provider/oidc" +import { OidcProvider } from "@openauthjs/openauth/provider/oidc" import { GitHubProvider } from "@openauthjs/openauth/provider/github" const app = issuer({ @@ -26,7 +26,7 @@ const app = issuer({ oidcProviders: { gitlab: OidcProvider({ clientID: "https://gitlab.com", // Must match JWT 'aud' claim - issuer: "https://gitlab.com", // Must match JWT 'iss' claim + issuer: "https://gitlab.com", // Must match JWT 'iss' claim provider: "gitlab" // Provider type identifier }), github: OidcProvider({ @@ -35,7 +35,7 @@ const app = issuer({ provider: "github" }) }, - + // Regular OAuth providers for interactive login providers: { github: GitHubProvider({ @@ -43,10 +43,10 @@ const app = issuer({ clientSecret: process.env.GITHUB_CLIENT_SECRET! }) }, - + subjects: { /* your subjects */ }, storage: /* your storage */, - + success: async (ctx, value) => { // Handle regular OAuth providers if (value.provider === "github") { @@ -57,7 +57,7 @@ const app = issuer({ tenant: user.defaultTenant, hasura: { "x-hasura-allowed-roles": ["user"], - "x-hasura-default-role": "user", + "x-hasura-default-role": "user", "x-hasura-user-id": user.id, }, externalTenants: user.tenants.map(t => t.id), @@ -74,7 +74,7 @@ const app = issuer({ // The JWT signature is already validated by OpenAuth using JWKS // Map different issuers to appropriate subjects - + if (value.issuer === "https://gitlab.com") { // JWT from GitLab CI/CD pipeline return ctx.subject("service", { @@ -98,7 +98,7 @@ const app = issuer({ audience: value.audience }) } - + throw new Error(`Unsupported provider: ${value.provider}`) } }) @@ -123,7 +123,7 @@ const app = issuer({ provider: string, // OIDC provider type (from config.type) claims: JWTPayload, // Full JWT claims object issuer: string, // The JWT issuer (iss claim) - subject: string, // The JWT subject (sub claim) + subject: string, // The JWT subject (sub claim) audience: string // The JWT audience (aud claim) } ``` diff --git a/packages/openauth/src/client.ts b/packages/openauth/src/client.ts index d4aebd4c..24f0718f 100644 --- a/packages/openauth/src/client.ts +++ b/packages/openauth/src/client.ts @@ -459,14 +459,14 @@ export interface Client { * const exchanged = await client.exchangeJWT() * ``` * - * This implements the JWT Bearer grant type (RFC 7523) where you exchange a signed JWT + * This implements the JWT Bearer grant type (RFC 7523) where you exchange a signed JWT * for OpenAuth access and refresh tokens. * * :::tip * The JWT must be signed by a trusted issuer configured in your OpenAuth server. * ::: * - * The JWT assertion should contain standard claims like `iss` (issuer), `sub` (subject), + * The JWT assertion should contain standard claims like `iss` (issuer), `sub` (subject), * `aud` (audience), and `exp` (expiration). The issuer must match one of your configured * OIDC providers. * diff --git a/packages/openauth/src/issuer.ts b/packages/openauth/src/issuer.ts index 7cd2e974..6215d125 100644 --- a/packages/openauth/src/issuer.ts +++ b/packages/openauth/src/issuer.ts @@ -1079,25 +1079,34 @@ export function issuer< if (grantType === "urn:ietf:params:oauth:grant-type:jwt-bearer") { const assertion = form.get("assertion") if (!assertion) { - return c.json({ - error: "invalid_grant", - error_description: "Missing assertion parameter" - }, 400) + return c.json( + { + error: "invalid_grant", + error_description: "Missing assertion parameter", + }, + 400, + ) } const claims = decodeJwt(assertion.toString()) if (!claims) { - return c.json({ - error: "invalid_grant", - error_description: "JWT assertion could not be decoded" - }, 400) + return c.json( + { + error: "invalid_grant", + error_description: "JWT assertion could not be decoded", + }, + 400, + ) } if (!claims.iss) { - return c.json({ - error: "invalid_grant", - error_description: "JWT assertion missing required issuer claim" - }, 400) + return c.json( + { + error: "invalid_grant", + error_description: "JWT assertion missing required issuer claim", + }, + 400, + ) } let oidcProvider @@ -1109,19 +1118,26 @@ export function issuer< } if (!oidcProvider) { - return c.json({ - error: "invalid_grant", - error_description: "JWT assertion from untrusted issuer" - }, 400) + return c.json( + { + error: "invalid_grant", + error_description: "JWT assertion from untrusted issuer", + }, + 400, + ) } try { await oidcProvider.verifyIdToken(assertion.toString()) } catch (error) { - return c.json({ - error: "invalid_grant", - error_description: "JWT assertion signature verification failed or token expired" - }, 400) + return c.json( + { + error: "invalid_grant", + error_description: + "JWT assertion signature verification failed or token expired", + }, + 400, + ) } // Call the success callback to handle JWT bearer token validation From 7fe77c59272f79e0a4a282e00422184723ba21aa Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Tue, 9 Sep 2025 21:33:14 -0700 Subject: [PATCH 23/25] ignore audience if not defined. for github CI JWT the aud varies by repo and owner. --- packages/openauth/src/provider/oidc.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/openauth/src/provider/oidc.ts b/packages/openauth/src/provider/oidc.ts index 13aa5732..d50eda7d 100644 --- a/packages/openauth/src/provider/oidc.ts +++ b/packages/openauth/src/provider/oidc.ts @@ -159,10 +159,17 @@ export function OidcProvider( ) const verifyIdToken = async (id_token: string) => { - return jwtVerify(id_token, await jwks(), { - audience: config.audience || config.clientID, + console.log("Verifying ID token with config:", config); + const verifyOptions: any = { issuer: config.issuer, - }) + } + + // Only include audience validation if audience is specified + if (config.audience) { + verifyOptions.audience = config.audience + } + + return jwtVerify(id_token, await jwks(), verifyOptions) } return { From 6cf959de5dc8f964f66e8502ed6368ee832e899d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 10 Sep 2025 04:33:43 +0000 Subject: [PATCH 24/25] auto: format code --- packages/openauth/src/provider/oidc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/openauth/src/provider/oidc.ts b/packages/openauth/src/provider/oidc.ts index d50eda7d..b1a3d5ac 100644 --- a/packages/openauth/src/provider/oidc.ts +++ b/packages/openauth/src/provider/oidc.ts @@ -159,7 +159,7 @@ export function OidcProvider( ) const verifyIdToken = async (id_token: string) => { - console.log("Verifying ID token with config:", config); + console.log("Verifying ID token with config:", config) const verifyOptions: any = { issuer: config.issuer, } From dcdb96964848b34a8037b0bc153dcd07399c21d7 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Wed, 10 Sep 2025 10:06:17 -0700 Subject: [PATCH 25/25] handle decode failure for better error --- packages/openauth/src/issuer.ts | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/openauth/src/issuer.ts b/packages/openauth/src/issuer.ts index 6215d125..69936e1c 100644 --- a/packages/openauth/src/issuer.ts +++ b/packages/openauth/src/issuer.ts @@ -1088,8 +1088,19 @@ export function issuer< ) } - const claims = decodeJwt(assertion.toString()) - if (!claims) { + let claims + try { + claims = decodeJwt(assertion.toString()) + if (!claims) { + return c.json( + { + error: "invalid_grant", + error_description: "JWT assertion could not be decoded", + }, + 400, + ) + } + } catch (error) { return c.json( { error: "invalid_grant", @@ -1099,6 +1110,16 @@ export function issuer< ) } + if (claims == undefined) { + return c.json( + { + error: "invalid_grant", + error_description: "no claims found in JWT assertion", + }, + 400, + ) + } + if (!claims.iss) { return c.json( {