Skip to content

Commit

Permalink
feat: add support for code id_token response without FAPI 1.0 s_hash
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Oct 7, 2024
1 parent fee6790 commit eebb4f1
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 15 deletions.
12 changes: 5 additions & 7 deletions conformance/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,13 +376,11 @@ export const flow = (options?: MacroOptions) => {
if (usesJarm(variant)) {
params = await oauth.validateJwtAuthResponse(as, client, currentUrl, state)
} else if (response_type === 'code id_token') {
params = await oauth.validateDetachedSignatureResponse(
as,
client,
currentUrl,
nonce as string,
state,
)
params = await (
plan.name.startsWith('fapi1')
? oauth.validateDetachedSignatureResponse
: oauth.validateCodeIdTokenResponse
)(as, client, currentUrl, nonce as string, state)
} else {
params = oauth.validateAuthResponse(as, client, currentUrl, state)
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"_format": "find src test tap examples conformance -type f -name '*.ts' -o -name '*.mjs' -o -name '*.cjs' | xargs prettier",
"build": "npm run generate-build && tsc -p test && tsc -p examples && tsc -p conformance && tsc -p tap && npx --yes jsr publish --dry-run --allow-dirty",
"conformance": "bash -c 'source .node_flags.sh && ava --config conformance/ava.config.ts'",
"docs": "patch-package > /dev/null && typedoc",
"docs": "patch-package && typedoc",
"format": "npm run _format -- --check --write",
"format-check": "npm run _format -- --check",
"generate-build": "rm -rf build && tsc --sourceMap && tsc --declaration true --emitDeclarationOnly true --removeComments false",
Expand Down
78 changes: 72 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2219,7 +2219,7 @@ export class ResponseBodyError extends Error {
*/
override cause!: Record<string, JsonValue | undefined>

code: string
code: typeof RESPONSE_BODY_ERROR

/**
* Error code given in the JSON response
Expand Down Expand Up @@ -2284,7 +2284,7 @@ export class AuthorizationResponseError extends Error {
*/
override cause!: URLSearchParams

code: string
code: typeof AUTHORIZATION_RESPONSE_ERROR

/**
* Error code given in the Authorization Response
Expand Down Expand Up @@ -2337,7 +2337,7 @@ export class WWWAuthenticateChallengeError extends Error {
*/
override cause!: WWWAuthenticateChallenge[]

code: string
code: typeof WWW_AUTHENTICATE_CHALLENGE

/**
* The {@link !Response} that included a WWW-Authenticate HTTP Header challenges, its
Expand Down Expand Up @@ -4836,6 +4836,72 @@ export async function validateDetachedSignatureResponse(
expectedState?: string | typeof expectNoState,
maxAge?: number | typeof skipAuthTimeCheck,
options?: ValidateSignatureOptions,
): Promise<URLSearchParams> {
return validateHybridResponse(
as,
client,
parameters,
expectedNonce,
expectedState,
maxAge,
options,
true,
)
}

/**
* Same as {@link validateAuthResponse} but for `code id_token` authorization responses.
*
* @param as Authorization Server Metadata.
* @param client Client Metadata.
* @param parameters Authorization Response parameters as URLSearchParams or an instance of URL with
* parameters in a fragment/hash.
* @param expectedNonce Expected ID Token `nonce` claim value.
* @param expectedState Expected `state` parameter value. Default is {@link expectNoState}.
* @param maxAge ID Token {@link IDToken.auth_time `auth_time`} claim value will be checked to be
* present and conform to the `maxAge` value. Use of this option is required if you sent a
* `max_age` parameter in an authorization request. Default is
* {@link Client.default_max_age `client.default_max_age`} and falls back to
* {@link skipAuthTimeCheck}.
*
* @returns Validated Authorization Response parameters. Authorization Error Responses are rejected
* using {@link AuthorizationResponseError}.
*
* @group Authorization Code Grant w/ OpenID Connect (OIDC)
*
* @see [RFC 6749 - The OAuth 2.0 Authorization Framework](https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.2)
* @see [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth)
*/
export async function validateCodeIdTokenResponse(
as: AuthorizationServer,
client: Client,
parameters: URLSearchParams | URL,
expectedNonce: string,
expectedState?: string | typeof expectNoState,
maxAge?: number | typeof skipAuthTimeCheck,
options?: ValidateSignatureOptions,
): Promise<URLSearchParams> {
return validateHybridResponse(
as,
client,
parameters,
expectedNonce,
expectedState,
maxAge,
options,
false,
)
}

async function validateHybridResponse(
as: AuthorizationServer,
client: Client,
parameters: URLSearchParams | URL,
expectedNonce: string,
expectedState: string | typeof expectNoState | undefined,
maxAge: number | typeof skipAuthTimeCheck | undefined,
options: ValidateSignatureOptions | undefined,
fapi: boolean,
): Promise<URLSearchParams> {
assertAs(as)
assertClient(client)
Expand Down Expand Up @@ -4899,7 +4965,7 @@ export async function validateDetachedSignatureResponse(
]

const state = parameters.get('state')
if (typeof expectedState === 'string' || state !== null) {
if (fapi && (typeof expectedState === 'string' || state !== null)) {
requiredClaims.push('s_hash')
}

Expand Down Expand Up @@ -5005,7 +5071,7 @@ export async function validateDetachedSignatureResponse(
})
}

if (state !== null || claims.s_hash !== undefined) {
if ((fapi && state !== null) || claims.s_hash !== undefined) {
assertString(claims.s_hash, 'ID Token "s_hash" (state hash) claim value', INVALID_RESPONSE, {
claims,
})
Expand Down Expand Up @@ -5130,7 +5196,7 @@ export const expectNoState: unique symbol = Symbol()
* @group Authorization Code Grant w/ OpenID Connect (OIDC)
*
* @see [RFC 6749 - The OAuth 2.0 Authorization Framework](https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.2)
* @see [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication)
* @see [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth)
* @see [RFC 9207 - OAuth 2.0 Authorization Server Issuer Identification](https://www.rfc-editor.org/rfc/rfc9207.html)
*/
export function validateAuthResponse(
Expand Down
2 changes: 1 addition & 1 deletion tap/end2end.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export default (QUnit: QUnit) => {

let callbackParams: URLSearchParams
if (hybrid) {
callbackParams = await lib.validateDetachedSignatureResponse(
callbackParams = await lib.validateCodeIdTokenResponse(
as,
client,
currentUrl,
Expand Down

0 comments on commit eebb4f1

Please sign in to comment.