forked from sst/openauth
-
Notifications
You must be signed in to change notification settings - Fork 0
add grant-type assertions for jwt-bearer #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
nullfunc
wants to merge
26
commits into
defang
Choose a base branch
from
add-jwt-assertion-grant-type
base: defang
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 19 commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
11b1227
add gitlab and github actions oidc
nullfunc 57af85e
add grant-type for jwt assertion
nullfunc af53876
type enforcement updates
nullfunc c58ec05
error update
nullfunc e132d52
add gitlab and github actions oidc
nullfunc 4329d2b
add grant-type for jwt assertion
nullfunc 3e51d8d
type enforcement updates
nullfunc b8d187d
error update
nullfunc f196406
update gitlabs with oidc provider
nullfunc a210b19
Merge remote-tracking branch 'refs/remotes/origin/add-jwt-assertion-g…
nullfunc ec7834c
assert jwt-bearer
nullfunc 4fe5d3c
update tests
nullfunc 01fbc60
add trusted issuer check for jwt assertions
nullfunc d8b30bd
oidc provider and jwt assertion grant-types
nullfunc 966e6f5
remove trailing '/' from github issuer
nullfunc 15576b5
jwt grant updates to pass provider name
nullfunc a09fba3
fix missing param in test data
nullfunc 5e2f4f5
revert bun.lockb to original
nullfunc 977dd9d
auto: format code
github-actions[bot] 27477fe
update hono and exports/imports
nullfunc c6b227d
update to dependencies
nullfunc 3f6812e
review updates
nullfunc 44f971b
auto: format code
github-actions[bot] 7fe77c5
ignore audience if not defined. for github CI JWT the aud varies by r…
nullfunc 6cf959d
auto: format code
github-actions[bot] dcdb969
handle decode failure for better error
nullfunc File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
# JWT Bearer Token Validation | ||
|
||
## 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. | ||
|
||
## 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 | ||
|
||
## Configuration | ||
|
||
```typescript | ||
import { issuer } from "@openauthjs/openauth" | ||
import { OidcProvider } from "@openauthjs/openauth/provider/oidc" | ||
|
||
const app = issuer({ | ||
providers: { | ||
gitlab: OidcProvider({ | ||
clientID: "your-gitlab-app-id", | ||
issuer: "https://gitlab.com" | ||
}) | ||
}, | ||
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 }) | ||
} | ||
|
||
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=<jwt_token> | ||
``` | ||
|
||
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -49,6 +49,7 @@ import { | |
import { | ||
InvalidAccessTokenError, | ||
InvalidAuthorizationCodeError, | ||
InvalidJWTError, | ||
InvalidRefreshTokenError, | ||
InvalidSubjectError, | ||
} from "./error.js" | ||
|
@@ -451,6 +452,58 @@ export interface Client { | |
redirectURI: string, | ||
verifier?: string, | ||
): Promise<ExchangeSuccess | ExchangeError> | ||
/** | ||
* Exchange the jwt for access and refresh tokens. | ||
nullfunc marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
* | ||
* ```ts | ||
* const exchanged = await client.exchange(<code>, <redirect_uri>) | ||
* ``` | ||
* | ||
* 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( | ||
* <code>, | ||
* <redirect_uri>, | ||
* <challenge.verifier> | ||
* ) | ||
* ``` | ||
* | ||
* 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 | ||
* } | ||
* } | ||
nullfunc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* | ||
* const { access, refresh } = exchanged.tokens | ||
* ``` | ||
*/ | ||
exchangeJWT(assertion: string): Promise<ExchangeSuccess | ExchangeError> | ||
/** | ||
* Refreshes the tokens if they have expired. This is used in an SPA app to maintain the | ||
* session, without logging the user out. | ||
|
@@ -666,6 +719,34 @@ export function createClient(input: ClientInput): Client { | |
}, | ||
} | ||
}, | ||
async exchangeJWT( | ||
assertion: string, | ||
): Promise<ExchangeSuccess | ExchangeError> { | ||
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(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
} | ||
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, | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.