-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Timo Glastra <[email protected]>
- Loading branch information
1 parent
2e81cf1
commit d1bbf81
Showing
21 changed files
with
707 additions
and
26 deletions.
There are no files selected for viewing
This file contains 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 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 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 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
142 changes: 142 additions & 0 deletions
142
packages/oauth2/src/authorization-challenge/authorization-challenge.ts
This file contains 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,142 @@ | ||
import { | ||
ValidationError, | ||
createValibotFetcher, | ||
objectToQueryParams, | ||
parseWithErrorHandling, | ||
} from '@animo-id/oid4vc-utils' | ||
import * as v from 'valibot' | ||
import type { CallbackContext } from '../callbacks' | ||
import { ContentType } from '../common/content-type' | ||
import { Oauth2ClientAuthorizationChallengeError } from '../error/Oauth2ClientAuthorizationChallengeError' | ||
import { Oauth2Error } from '../error/Oauth2Error' | ||
import { Oauth2InvalidFetchResponseError } from '../error/Oauth2InvalidFetchResponseError' | ||
import type { AuthorizationServerMetadata } from '../metadata/authorization-server/v-authorization-server-metadata' | ||
import { createPkce } from '../pkce' | ||
import { | ||
type AuthorizationChallengeRequest, | ||
vAuthorizationChallengeErrorResponse, | ||
vAuthorizationChallengeRequest, | ||
vAuthorizationChallengeResponse, | ||
} from './v-authorization-challenge' | ||
|
||
export interface SendAuthorizationChallengeRequestOptions { | ||
/** | ||
* Callback context | ||
*/ | ||
callbacks: Pick<CallbackContext, 'fetch' | 'hash' | 'generateRandom'> | ||
|
||
/** | ||
* Metadata of the authorization server where to perform the authorization challenge | ||
*/ | ||
authorizationServerMetadata: AuthorizationServerMetadata | ||
|
||
/** | ||
* Previously established auth session | ||
*/ | ||
authSession?: string | ||
|
||
/** | ||
* The client id to use for the authorization challenge request | ||
*/ | ||
clientId?: string | ||
|
||
/** | ||
* Scope to request for the authorization challenge request | ||
*/ | ||
scope?: string | ||
|
||
/** | ||
* Presentation during issuance sessios if credentials were presented | ||
* as part of an issuance session | ||
*/ | ||
presentationDuringIssuanceSession?: string | ||
|
||
/** | ||
* Additional payload to include in the authorization challenge request. Items will be encoded and sent | ||
* using x-www-form-urlencoded format. Nested items (JSON) will be stringified and url encoded. | ||
*/ | ||
additionalRequestPayload?: Record<string, unknown> | ||
|
||
/** | ||
* Code verifier to use for pkce. If not provided a value will generated when pkce is supported | ||
*/ | ||
pkceCodeVerifier?: string | ||
} | ||
|
||
/** | ||
* Send an authorization challenge request. | ||
* | ||
* @throws {Oauth2ClientAuthorizationChallengeError} if the request failed and a {@link AuthorizationChallengeErrorResponse} is returned | ||
* @throws {Oauth2InvalidFetchResponseError} if the request failed but no error response could be parsed | ||
* @throws {ValidationError} if a successful response was received but an error occured during verification of the {@link AuthorizationChallengeResponse} | ||
*/ | ||
export async function sendAuthorizationChallengeRequest(options: SendAuthorizationChallengeRequestOptions) { | ||
const fetchWithValibot = createValibotFetcher(options.callbacks.fetch) | ||
|
||
const authorizationServerMetadata = options.authorizationServerMetadata | ||
if (!authorizationServerMetadata.authorization_challenge_endpoint) { | ||
throw new Oauth2Error( | ||
`Unable to send authorization challange. Authorization server '${authorizationServerMetadata.issuer}' has no 'authorization_challenge_endpoint'` | ||
) | ||
} | ||
|
||
// PKCE | ||
const pkce = authorizationServerMetadata.code_challenge_methods_supported | ||
? await createPkce({ | ||
allowedCodeChallengeMethods: authorizationServerMetadata.code_challenge_methods_supported, | ||
callbacks: options.callbacks, | ||
codeVerifier: options.pkceCodeVerifier, | ||
}) | ||
: undefined | ||
|
||
const authorizationChallengeRequest = parseWithErrorHandling(vAuthorizationChallengeRequest, { | ||
...options.additionalRequestPayload, | ||
auth_session: options.authSession, | ||
client_id: options.clientId, | ||
scope: options.scope, | ||
code_challenge: pkce?.codeChallenge, | ||
code_challenge_method: pkce?.codeChallengeMethod, | ||
presentation_during_issuance_session: options.presentationDuringIssuanceSession, | ||
} satisfies AuthorizationChallengeRequest) | ||
|
||
const { response, result } = await fetchWithValibot( | ||
vAuthorizationChallengeResponse, | ||
authorizationServerMetadata.authorization_challenge_endpoint, | ||
{ | ||
method: 'POST', | ||
body: objectToQueryParams(authorizationChallengeRequest), | ||
headers: { | ||
'Content-Type': ContentType.XWwwFormUrlencoded, | ||
}, | ||
} | ||
) | ||
|
||
if (!response.ok || !result) { | ||
const authorizationChallengeErrorResponse = v.safeParse( | ||
vAuthorizationChallengeErrorResponse, | ||
await response | ||
.clone() | ||
.json() | ||
.catch(() => null) | ||
) | ||
if (authorizationChallengeErrorResponse.success) { | ||
throw new Oauth2ClientAuthorizationChallengeError( | ||
`Error requesting authorization code from authorization challenge endpoint '${authorizationServerMetadata.authorization_challenge_endpoint}'. Received response with status ${response.status}`, | ||
authorizationChallengeErrorResponse.output, | ||
response | ||
) | ||
} | ||
|
||
throw new Oauth2InvalidFetchResponseError( | ||
`Error requesting authorization code from authorization challenge endpoint '${authorizationServerMetadata.authorization_challenge_endpoint}'. Received response with status ${response.status}`, | ||
await response.clone().text(), | ||
response | ||
) | ||
} | ||
|
||
if (!result.success) { | ||
throw new ValidationError('Error validating authorization challenge response', result.issues) | ||
} | ||
|
||
return { pkce, authorizationChallengeResponse: result.output } | ||
} |
33 changes: 33 additions & 0 deletions
33
packages/oauth2/src/authorization-challenge/v-authorization-challenge.ts
This file contains 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,33 @@ | ||
import { vInteger } from '@animo-id/oid4vc-utils' | ||
import * as v from 'valibot' | ||
import { vOauth2ErrorResponse } from '../common/v-oauth2-error' | ||
|
||
export const vAuthorizationChallengeRequest = v.looseObject({ | ||
client_id: v.optional(v.string()), | ||
scope: v.optional(v.string()), | ||
auth_session: v.optional(v.string()), | ||
|
||
// PKCE | ||
code_challenge: v.optional(v.string()), | ||
code_challenge_method: v.optional(v.string()), | ||
|
||
// DRAFT presentation during issuance | ||
presentation_during_issuance_session: v.optional(v.string()), | ||
}) | ||
export type AuthorizationChallengeRequest = v.InferOutput<typeof vAuthorizationChallengeRequest> | ||
|
||
export const vAuthorizationChallengeResponse = v.looseObject({ | ||
authorization_code: v.string(), | ||
}) | ||
export type AuthorizationChallengeResponse = v.InferOutput<typeof vAuthorizationChallengeResponse> | ||
|
||
export const vAuthorizationChallengeErrorResponse = v.looseObject({ | ||
...vOauth2ErrorResponse.entries, | ||
auth_session: v.optional(v.string()), | ||
request_uri: v.optional(v.string()), | ||
expires_in: v.optional(vInteger), | ||
|
||
// DRAFT: presentation during issuance | ||
presentation: v.optional(v.string()), | ||
}) | ||
export type AuthorizationChallengeErrorResponse = v.InferOutput<typeof vAuthorizationChallengeErrorResponse> |
This file contains 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 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 |
---|---|---|
@@ -1,17 +1,30 @@ | ||
import * as v from 'valibot' | ||
|
||
export enum Oauth2ErrorCodes { | ||
// Oauth2 | ||
InvalidRequest = 'invalid_request', | ||
InvalidToken = 'invalid_token', | ||
InsufficientScope = 'insufficient_scope', | ||
InvalidGrant = 'invalid_grant', | ||
InvalidClient = 'invalid_client', | ||
UnauthorizedClient = 'unauthorized_client', | ||
UnsupportedGrantType = 'unsupported_grant_type', | ||
InvalidScope = 'invalid_scope', | ||
|
||
// DPoP | ||
InvalidDpopProof = 'invalid_dpop_proof', | ||
UseDpopNonce = 'use_dpop_nonce', | ||
InvalidGrant = 'invalid_grant', | ||
|
||
// FaPI | ||
RedirectToWeb = 'redirect_to_web', | ||
InvalidSession = 'invalid_session', | ||
InsufficientAuthorization = 'insufficient_authorization', | ||
} | ||
|
||
export const vOauth2ErrorResponse = v.looseObject({ | ||
error: v.union([v.enum(Oauth2ErrorCodes), v.string()]), | ||
error_description: v.optional(v.string()), | ||
error_uri: v.optional(v.string()), | ||
}) | ||
|
||
export type Oauth2ErrorResponse = v.InferOutput<typeof vOauth2ErrorResponse> |
13 changes: 13 additions & 0 deletions
13
packages/oauth2/src/error/Oauth2ClientAuthorizationChallengeError.ts
This file contains 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,13 @@ | ||
import type { FetchResponse } from '@animo-id/oid4vc-utils' | ||
import type { AuthorizationChallengeErrorResponse } from '../authorization-challenge/v-authorization-challenge' | ||
import { Oauth2ClientErrorResponseError } from './Oauth2ClientErrorResponseError' | ||
|
||
export class Oauth2ClientAuthorizationChallengeError extends Oauth2ClientErrorResponseError { | ||
public constructor( | ||
message: string, | ||
public readonly errorResponse: AuthorizationChallengeErrorResponse, | ||
response: FetchResponse | ||
) { | ||
super(message, errorResponse, response) | ||
} | ||
} |
This file contains 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.