diff --git a/packages/oauth2/src/callbacks.ts b/packages/oauth2/src/callbacks.ts index dcebbbc..376dc00 100644 --- a/packages/oauth2/src/callbacks.ts +++ b/packages/oauth2/src/callbacks.ts @@ -44,7 +44,7 @@ export type VerifyJwtCallback = ( > export interface DecryptJweCallbackOptions { - jwk: Jwk + jwk?: Jwk } export type DecryptJweCallback = ( diff --git a/packages/openid4vp/src/Openid4vpClient.ts b/packages/openid4vp/src/Openid4vpClient.ts index 6c50772..4a9e690 100644 --- a/packages/openid4vp/src/Openid4vpClient.ts +++ b/packages/openid4vp/src/Openid4vpClient.ts @@ -1,7 +1,7 @@ import type { CallbackContext } from '@openid4vc/oauth2' import {} from './authorization-request/create-authorization-request' import { parseOpenid4vpAuthorizationRequestPayload } from './authorization-request/parse-authorization-request-params' -import type { ParseOpenid4vpAuthRequestPayloadOptions } from './authorization-request/parse-authorization-request-params' +import type { ParseOpenid4vpAuthorizationRequestPayloadOptions } from './authorization-request/parse-authorization-request-params' import { type ResolveOpenid4vpAuthorizationRequestOptions, resolveOpenid4vpAuthorizationRequest, @@ -25,7 +25,7 @@ export interface Openid4vpClientOptions { export class Openid4vpClient { public constructor(private options: Openid4vpClientOptions) {} - public parseOpenid4vpAuthorizationRequestPayload(options: ParseOpenid4vpAuthRequestPayloadOptions) { + public parseOpenid4vpAuthorizationRequestPayload(options: ParseOpenid4vpAuthorizationRequestPayloadOptions) { return parseOpenid4vpAuthorizationRequestPayload(options) } diff --git a/packages/openid4vp/src/Openid4vpVerifier.ts b/packages/openid4vp/src/Openid4vpVerifier.ts index 3227599..baf63c8 100644 --- a/packages/openid4vp/src/Openid4vpVerifier.ts +++ b/packages/openid4vp/src/Openid4vpVerifier.ts @@ -4,7 +4,7 @@ import { createOpenid4vpAuthorizationRequest, } from './authorization-request/create-authorization-request' import { - type ParseOpenid4vpAuthRequestPayloadOptions, + type ParseOpenid4vpAuthorizationRequestPayloadOptions, parseOpenid4vpAuthorizationRequestPayload, } from './authorization-request/parse-authorization-request-params' import { @@ -36,7 +36,7 @@ export class Openid4vpVerifier { return createOpenid4vpAuthorizationRequest({ ...options, callbacks: this.options.callbacks }) } - public parseOpenid4vpAuthorizationRequestPayload(options: ParseOpenid4vpAuthRequestPayloadOptions) { + public parseOpenid4vpAuthorizationRequestPayload(options: ParseOpenid4vpAuthorizationRequestPayloadOptions) { return parseOpenid4vpAuthorizationRequestPayload(options) } diff --git a/packages/openid4vp/src/authorization-request/create-authorization-request.ts b/packages/openid4vp/src/authorization-request/create-authorization-request.ts index a784ba3..007aa66 100644 --- a/packages/openid4vp/src/authorization-request/create-authorization-request.ts +++ b/packages/openid4vp/src/authorization-request/create-authorization-request.ts @@ -1,6 +1,6 @@ import { type CallbackContext, type JwtSigner, Oauth2Error } from '@openid4vc/oauth2' import { URL, URLSearchParams, objectToQueryParams, parseWithErrorHandling } from '@openid4vc/utils' -import { createJarAuthRequest } from '../jar/create-jar-auth-request' +import { createJarAuthorizationRequest } from '../jar/create-jar-authorization-request' import { type WalletVerificationOptions, validateOpenid4vpAuthorizationRequestPayload, @@ -15,7 +15,7 @@ import { export interface CreateOpenid4vpAuthorizationRequestOptions { scheme?: string - requestPayload: Openid4vpAuthorizationRequest | Openid4vpAuthorizationRequestDcApi + authorizationRequestPayload: Openid4vpAuthorizationRequest | Openid4vpAuthorizationRequestDcApi jar?: { requestUri?: string jwtSigner: JwtSigner @@ -31,7 +31,7 @@ export interface CreateOpenid4vpAuthorizationRequestOptions { * * @param options Configuration options for creating the authorization request * @param input.scheme Optional URI scheme to use (defaults to 'openid4vp://') - * @param input.requestParams The OpenID4VP authorization request parameters + * @param input.authorizationRequestPayload The OpenID4VP authorization request parameters * @param input.jar Optional JWT Secured Authorization Request (JAR) configuration * @param input.jar.requestUri The URI where the JAR will be accessible * @param input.jar.jwtSigner Function to sign the JAR JWT @@ -43,48 +43,49 @@ export interface CreateOpenid4vpAuthorizationRequestOptions { * @returns Object containing the authorization request parameters, URI and optional JAR details */ export async function createOpenid4vpAuthorizationRequest(options: CreateOpenid4vpAuthorizationRequestOptions) { - const { jar, scheme = 'openid4vp://', requestPayload, wallet, callbacks } = options + const { jar, scheme = 'openid4vp://', wallet, callbacks } = options let additionalJwtPayload: Record | undefined - let authRequestParams: Openid4vpAuthorizationRequest | Openid4vpAuthorizationRequestDcApi - if (isOpenid4vpAuthorizationRequestDcApi(requestPayload)) { - authRequestParams = parseWithErrorHandling( + let authorizationRequestPayload: Openid4vpAuthorizationRequest | Openid4vpAuthorizationRequestDcApi + if (isOpenid4vpAuthorizationRequestDcApi(options.authorizationRequestPayload)) { + authorizationRequestPayload = parseWithErrorHandling( zOpenid4vpAuthorizationRequestDcApi, - requestPayload, + options.authorizationRequestPayload, 'Invalid authorization request. Could not parse openid4vp dc_api authorization request.' ) - if (jar && !authRequestParams.expected_origins) { + if (jar && !authorizationRequestPayload.expected_origins) { throw new Oauth2Error( `The 'expected_origins' parameter MUST be present when using the dc_api response mode in combination with jar.` ) } validateOpenid4vpAuthorizationRequestDcApiPayload({ - params: authRequestParams, + params: authorizationRequestPayload, isJarRequest: Boolean(jar), - omitOriginValidation: true, + disableOriginValidation: true, }) } else { - authRequestParams = parseWithErrorHandling( + authorizationRequestPayload = parseWithErrorHandling( zOpenid4vpAuthorizationRequest, - requestPayload, + options.authorizationRequestPayload, 'Invalid authorization request. Could not parse openid4vp authorization request.' ) - validateOpenid4vpAuthorizationRequestPayload({ params: authRequestParams, walletVerificationOptions: wallet }) + validateOpenid4vpAuthorizationRequestPayload({ + params: authorizationRequestPayload, + walletVerificationOptions: wallet, + }) } if (jar) { if (!jar.additionalJwtPayload?.aud) { additionalJwtPayload = { ...jar.additionalJwtPayload, aud: jar.requestUri } } - } - if (jar) { - const jarResult = await createJarAuthRequest({ + const jarResult = await createJarAuthorizationRequest({ ...jar, - authRequestParams: requestPayload, + authorizationRequestPayload, additionalJwtPayload, callbacks, }) @@ -92,12 +93,13 @@ export async function createOpenid4vpAuthorizationRequest(options: CreateOpenid4 const url = new URL(scheme) url.search = `?${new URLSearchParams([ ...url.searchParams.entries(), - ...objectToQueryParams(jarResult.requestParams).entries(), + ...objectToQueryParams(jarResult.jarAuthorizationRequest).entries(), ]).toString()}` return { - authRequestObject: jarResult.requestParams, - authRequest: url.toString(), + authorizationRequestPayload, + authorizationRequestObject: jarResult.jarAuthorizationRequest, + authorizationRequest: url.toString(), jar: { ...jar, ...jarResult }, } } @@ -105,12 +107,13 @@ export async function createOpenid4vpAuthorizationRequest(options: CreateOpenid4 const url = new URL(scheme) url.search = `?${new URLSearchParams([ ...url.searchParams.entries(), - ...objectToQueryParams(requestPayload).entries(), + ...objectToQueryParams(authorizationRequestPayload).entries(), ]).toString()}` return { - authRequestObject: requestPayload, - authRequest: url.toString(), + authorizationRequestPayload, + authorizationRequestObject: authorizationRequestPayload, + authorizationRequest: url.toString(), jar: undefined, } } diff --git a/packages/openid4vp/src/authorization-request/parse-authorization-request-params.ts b/packages/openid4vp/src/authorization-request/parse-authorization-request-params.ts index dba8a2b..9a52d80 100644 --- a/packages/openid4vp/src/authorization-request/parse-authorization-request-params.ts +++ b/packages/openid4vp/src/authorization-request/parse-authorization-request-params.ts @@ -2,7 +2,11 @@ import { decodeJwt } from '@openid4vc/oauth2' import { URL } from '@openid4vc/utils' import { parseWithErrorHandling } from '@openid4vc/utils' import z from 'zod' -import { type JarAuthRequest, isJarAuthRequest, zJarAuthRequest } from '../jar/z-jar-auth-request' +import { + type JarAuthorizationRequest, + isJarAuthorizationRequest, + zJarAuthorizationRequest, +} from '../jar/z-jar-authorization-request' import { type Openid4vpAuthorizationRequest, zOpenid4vpAuthorizationRequest } from './z-authorization-request' import { type Openid4vpAuthorizationRequestDcApi, @@ -13,28 +17,28 @@ import { export interface ParsedJarRequest { type: 'jar' provided: 'uri' | 'jwt' | 'params' - params: JarAuthRequest + params: JarAuthorizationRequest } -export interface ParsedOpenid4vpAuthRequest { +export interface ParsedOpenid4vpAuthorizationRequest { type: 'openid4vp' provided: 'uri' | 'jwt' | 'params' params: Openid4vpAuthorizationRequest } -export interface ParsedOpenid4vpDcApiAuthRequest { +export interface ParsedOpenid4vpDcApiAuthorizationRequest { type: 'openid4vp_dc_api' provided: 'uri' | 'jwt' | 'params' params: Openid4vpAuthorizationRequestDcApi } -export interface ParseOpenid4vpAuthRequestPayloadOptions { +export interface ParseOpenid4vpAuthorizationRequestPayloadOptions { authorizationRequest: string | Record } export function parseOpenid4vpAuthorizationRequestPayload( - options: ParseOpenid4vpAuthRequestPayloadOptions -): ParsedOpenid4vpAuthRequest | ParsedJarRequest | ParsedOpenid4vpDcApiAuthRequest { + options: ParseOpenid4vpAuthorizationRequestPayloadOptions +): ParsedOpenid4vpAuthorizationRequest | ParsedJarRequest | ParsedOpenid4vpDcApiAuthorizationRequest { const { authorizationRequest } = options let provided: 'uri' | 'jwt' | 'params' = 'params' @@ -54,11 +58,11 @@ export function parseOpenid4vpAuthorizationRequestPayload( } const parsedRequest = parseWithErrorHandling( - z.union([zOpenid4vpAuthorizationRequest, zJarAuthRequest, zOpenid4vpAuthorizationRequestDcApi]), + z.union([zOpenid4vpAuthorizationRequest, zJarAuthorizationRequest, zOpenid4vpAuthorizationRequestDcApi]), params ) - if (isJarAuthRequest(parsedRequest)) { + if (isJarAuthorizationRequest(parsedRequest)) { return { type: 'jar', provided, diff --git a/packages/openid4vp/src/authorization-request/resolve-authorization-request.ts b/packages/openid4vp/src/authorization-request/resolve-authorization-request.ts index c1dda9c..d769a85 100644 --- a/packages/openid4vp/src/authorization-request/resolve-authorization-request.ts +++ b/packages/openid4vp/src/authorization-request/resolve-authorization-request.ts @@ -7,8 +7,12 @@ import { } from '../client-identifier-scheme/parse-client-identifier-scheme' import { fetchClientMetadata } from '../fetch-client-metadata' import { type VerifiedJarRequest, verifyJarRequest } from '../jar/handle-jar-request/verify-jar-request' -import { type JarAuthRequest, isJarAuthRequest, zJarAuthRequest } from '../jar/z-jar-auth-request' -import type { WalletMetadata } from '../models/z-wallet-metadata' +import { + type JarAuthorizationRequest, + isJarAuthorizationRequest, + zJarAuthorizationRequest, +} from '../jar/z-jar-authorization-request' +import type { PexPresentationDefinition } from '../models/z-pex' import { type ParsedTransactionDataEntry, parseTransactionData } from '../transaction-data/parse-transaction-data' import { type WalletVerificationOptions, @@ -23,83 +27,90 @@ import { } from './z-authorization-request-dc-api' export interface ResolveOpenid4vpAuthorizationRequestOptions { - requestPayload: Openid4vpAuthorizationRequest | JarAuthRequest + authorizationRequestPayload: Openid4vpAuthorizationRequest | JarAuthorizationRequest wallet?: WalletVerificationOptions origin?: string - omitOriginValidation?: boolean + disableOriginValidation?: boolean callbacks: Pick } -export type ResolvedOpenid4vpAuthRequest = { +export type ResolvedOpenid4vpAuthorizationRequest = { transactionData?: ParsedTransactionDataEntry[] - requestPayload: Openid4vpAuthorizationRequest | Openid4vpAuthorizationRequestDcApi + authorizationRequestPayload: Openid4vpAuthorizationRequest | Openid4vpAuthorizationRequestDcApi jar: VerifiedJarRequest | undefined client: ParsedClientIdentifier pex?: { - presentation_definition: unknown + presentation_definition?: PexPresentationDefinition presentation_definition_uri?: string } dcql?: { query: unknown } | undefined } export async function resolveOpenid4vpAuthorizationRequest( options: ResolveOpenid4vpAuthorizationRequestOptions -): Promise { - const { requestPayload, wallet, callbacks, origin, omitOriginValidation } = options +): Promise { + const { wallet, callbacks, origin, disableOriginValidation } = options - let authRequestPayload: + let authorizationRequestPayload: | Openid4vpAuthorizationRequest | (Openid4vpAuthorizationRequestDcApi & { presentation_definition_uri?: never }) const parsed = parseWithErrorHandling( - z.union([zOpenid4vpAuthorizationRequestDcApi, zOpenid4vpAuthorizationRequest, zJarAuthRequest]), - requestPayload, + z.union([zOpenid4vpAuthorizationRequestDcApi, zOpenid4vpAuthorizationRequest, zJarAuthorizationRequest]), + options.authorizationRequestPayload, 'Invalid authorization request. Could not parse openid4vp authorization request as openid4vp or jar auth request.' ) let jar: VerifiedJarRequest | undefined - if (isJarAuthRequest(parsed)) { + if (isJarAuthorizationRequest(parsed)) { jar = await verifyJarRequest({ jarRequestParams: parsed, callbacks, wallet }) - const parsedJarAuthRequestPayload = parseWithErrorHandling( + const parsedJarAuthorizationRequestPayload = parseWithErrorHandling( z.union([zOpenid4vpAuthorizationRequestDcApi, zOpenid4vpAuthorizationRequest]), - jar.authRequestParams, + jar.authorizationRequestParams, 'Invalid authorization request. Could not parse jar request payload as openid4vp auth request.' ) - authRequestPayload = validateOpenId4vpPayload({ - requestPayload: parsedJarAuthRequestPayload, + authorizationRequestPayload = validateOpenId4vpAuthorizationRequestPayload({ + authorizationRequestPayload: parsedJarAuthorizationRequestPayload, wallet, jar: true, origin, - omitOriginValidation, + disableOriginValidation, }) } else { - authRequestPayload = validateOpenId4vpPayload({ - requestPayload: parsed, + authorizationRequestPayload = validateOpenId4vpAuthorizationRequestPayload({ + authorizationRequestPayload: parsed, wallet, jar: false, origin, - omitOriginValidation, + disableOriginValidation, }) } - let clientMetadata: WalletMetadata | undefined - if (!isOpenid4vpAuthorizationRequestDcApi(authRequestPayload) && authRequestPayload.client_metadata_uri) { - clientMetadata = await fetchClientMetadata({ clientMetadataUri: authRequestPayload.client_metadata_uri }) + let clientMetadata = authorizationRequestPayload.client_metadata + if ( + !isOpenid4vpAuthorizationRequestDcApi(authorizationRequestPayload) && + !clientMetadata && + authorizationRequestPayload.client_metadata_uri + ) { + clientMetadata = await fetchClientMetadata({ clientMetadataUri: authorizationRequestPayload.client_metadata_uri }) } const clientMeta = parseClientIdentifier({ - request: { ...authRequestPayload, client_metadata: clientMetadata ?? authRequestPayload.client_metadata }, + authorizationRequestPayload: { + ...authorizationRequestPayload, + client_metadata: clientMetadata, + }, jar, callbacks, origin, }) - let pex: ResolvedOpenid4vpAuthRequest['pex'] | undefined - let dcql: ResolvedOpenid4vpAuthRequest['dcql'] | undefined + let pex: ResolvedOpenid4vpAuthorizationRequest['pex'] | undefined + let dcql: ResolvedOpenid4vpAuthorizationRequest['dcql'] | undefined - if (authRequestPayload.presentation_definition || authRequestPayload.presentation_definition_uri) { - if (authRequestPayload.presentation_definition_uri) { + if (authorizationRequestPayload.presentation_definition || authorizationRequestPayload.presentation_definition_uri) { + if (authorizationRequestPayload.presentation_definition_uri) { throw new Oauth2ServerErrorResponseError({ error: Oauth2ErrorCodes.InvalidRequest, error_description: 'Cannot fetch presentation definition from URI. Not supported.', @@ -107,49 +118,52 @@ export async function resolveOpenid4vpAuthorizationRequest( } pex = { - presentation_definition: authRequestPayload.presentation_definition, - presentation_definition_uri: authRequestPayload.presentation_definition_uri, + presentation_definition: authorizationRequestPayload.presentation_definition, + presentation_definition_uri: authorizationRequestPayload.presentation_definition_uri, } } - if (authRequestPayload.dcql_query) { - dcql = { query: authRequestPayload.dcql_query } + if (authorizationRequestPayload.dcql_query) { + dcql = { query: authorizationRequestPayload.dcql_query } } - const transactionData = authRequestPayload.transaction_data - ? parseTransactionData({ transactionData: authRequestPayload.transaction_data }) + const transactionData = authorizationRequestPayload.transaction_data + ? parseTransactionData({ transactionData: authorizationRequestPayload.transaction_data }) : undefined return { transactionData, - requestPayload: authRequestPayload, + authorizationRequestPayload, jar, - client: { ...clientMeta }, + client: clientMeta, pex, dcql, } } -function validateOpenId4vpPayload(options: { - requestPayload: Openid4vpAuthorizationRequest | Openid4vpAuthorizationRequestDcApi +function validateOpenId4vpAuthorizationRequestPayload(options: { + authorizationRequestPayload: Openid4vpAuthorizationRequest | Openid4vpAuthorizationRequestDcApi wallet?: WalletVerificationOptions jar: boolean origin?: string - omitOriginValidation?: boolean + disableOriginValidation?: boolean }) { - const { requestPayload, wallet, jar, origin, omitOriginValidation } = options + const { authorizationRequestPayload, wallet, jar, origin, disableOriginValidation } = options - if (isOpenid4vpAuthorizationRequestDcApi(requestPayload)) { + if (isOpenid4vpAuthorizationRequestDcApi(authorizationRequestPayload)) { validateOpenid4vpAuthorizationRequestDcApiPayload({ - params: requestPayload, + params: authorizationRequestPayload, isJarRequest: jar, - omitOriginValidation, + disableOriginValidation, origin, }) - return requestPayload + return authorizationRequestPayload } - validateOpenid4vpAuthorizationRequestPayload({ params: requestPayload, walletVerificationOptions: wallet }) - return requestPayload + validateOpenid4vpAuthorizationRequestPayload({ + params: authorizationRequestPayload, + walletVerificationOptions: wallet, + }) + return authorizationRequestPayload } diff --git a/packages/openid4vp/src/authorization-request/validate-authorization-request-dc-api.ts b/packages/openid4vp/src/authorization-request/validate-authorization-request-dc-api.ts index 6612357..1778754 100644 --- a/packages/openid4vp/src/authorization-request/validate-authorization-request-dc-api.ts +++ b/packages/openid4vp/src/authorization-request/validate-authorization-request-dc-api.ts @@ -4,7 +4,7 @@ import type { Openid4vpAuthorizationRequestDcApi } from './z-authorization-reque export interface ValidateOpenid4vpAuthorizationRequestDcApiPayloadOptions { params: Openid4vpAuthorizationRequestDcApi isJarRequest: boolean - omitOriginValidation?: boolean + disableOriginValidation?: boolean origin?: string } @@ -14,7 +14,7 @@ export interface ValidateOpenid4vpAuthorizationRequestDcApiPayloadOptions { export const validateOpenid4vpAuthorizationRequestDcApiPayload = ( options: ValidateOpenid4vpAuthorizationRequestDcApiPayloadOptions ) => { - const { params, isJarRequest, omitOriginValidation, origin } = options + const { params, isJarRequest, disableOriginValidation, origin } = options if (isJarRequest && !params.expected_origins) { throw new Oauth2ServerErrorResponseError({ @@ -23,15 +23,15 @@ export const validateOpenid4vpAuthorizationRequestDcApiPayload = ( }) } - if ([params.presentation_definition, params.dcql_query].filter(Boolean).length > 1) { + if ([params.presentation_definition, params.dcql_query].filter(Boolean).length !== 1) { throw new Oauth2ServerErrorResponseError({ error: Oauth2ErrorCodes.InvalidRequest, error_description: - 'Exactly one of the following parameters MUST be present in the Authorization Request: dcql_query, presentation_definition, presentation_definition_uri, or a scope value representing a Presentation Definition.', + 'Exactly one of the following parameters MUST be present in the Authorization Request: dcql_query or presentation_definition', }) } - if (params.expected_origins && !omitOriginValidation) { + if (params.expected_origins && !disableOriginValidation) { if (!origin) { throw new Oauth2ServerErrorResponseError({ error: Oauth2ErrorCodes.InvalidRequest, diff --git a/packages/openid4vp/src/authorization-request/z-authorization-request-dc-api.ts b/packages/openid4vp/src/authorization-request/z-authorization-request-dc-api.ts index 373aa9e..451d106 100644 --- a/packages/openid4vp/src/authorization-request/z-authorization-request-dc-api.ts +++ b/packages/openid4vp/src/authorization-request/z-authorization-request-dc-api.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import type { JarAuthRequest } from '../jar/z-jar-auth-request' +import type { JarAuthorizationRequest } from '../jar/z-jar-authorization-request' import { type Openid4vpAuthorizationRequest, zOpenid4vpAuthorizationRequest } from './z-authorization-request' export const zOpenid4vpAuthorizationRequestDcApi = zOpenid4vpAuthorizationRequest @@ -12,6 +12,7 @@ export const zOpenid4vpAuthorizationRequestDcApi = zOpenid4vpAuthorizationReques client_metadata: true, transaction_data: true, dcql_query: true, + trust_chain: true, }) .extend({ client_id: z.optional(z.string()), @@ -34,7 +35,7 @@ export const zOpenid4vpAuthorizationRequestDcApi = zOpenid4vpAuthorizationReques export type Openid4vpAuthorizationRequestDcApi = z.infer export function isOpenid4vpAuthorizationRequestDcApi( - request: Openid4vpAuthorizationRequest | Openid4vpAuthorizationRequestDcApi | JarAuthRequest + request: Openid4vpAuthorizationRequest | Openid4vpAuthorizationRequestDcApi | JarAuthorizationRequest ): request is Openid4vpAuthorizationRequestDcApi { return ( request.response_mode === 'dc_api' || diff --git a/packages/openid4vp/src/authorization-response/create-authorization-response.ts b/packages/openid4vp/src/authorization-response/create-authorization-response.ts index ee42499..ea3abd6 100644 --- a/packages/openid4vp/src/authorization-response/create-authorization-response.ts +++ b/packages/openid4vp/src/authorization-response/create-authorization-response.ts @@ -9,7 +9,7 @@ import { dateToSeconds } from '@openid4vc/utils' import { addSecondsToDate } from '../../../utils/src/date' import type { Openid4vpAuthorizationRequest } from '../authorization-request/z-authorization-request' import type { Openid4vpAuthorizationRequestDcApi } from '../authorization-request/z-authorization-request-dc-api' -import { createJarmAuthResponse } from '../jarm/jarm-auth-response-create' +import { createJarmAuthorizationResponse } from '../jarm/jarm-authorization-response-create' import { extractJwksFromClientMetadata } from '../jarm/jarm-extract-jwks' import { isJarmResponseMode } from '../jarm/jarm-response-mode' import { jarmAssertMetadataSupported } from '../jarm/metadata/jarm-assert-metadata-supported' @@ -17,10 +17,10 @@ import type { JarmServerMetadata } from '../jarm/metadata/z-jarm-authorization-s import type { Openid4vpAuthorizationResponse } from './z-authorization-response' export interface CreateOpenid4vpAuthorizationResponseOptions { - requestPayload: + authorizationRequestPayload: | Pick | Pick - responsePayload: Openid4vpAuthorizationResponse & { state?: never } + authorizationResponsePayload: Openid4vpAuthorizationResponse & { state?: never } jarm?: { jwtSigner?: JwtSigner encryption?: { nonce: string } @@ -33,36 +33,41 @@ export interface CreateOpenid4vpAuthorizationResponseOptions { } export interface CreateOpenid4vpAuthorizationResponseResult { - responsePayload: Openid4vpAuthorizationResponse + authorizationResponsePayload: Openid4vpAuthorizationResponse jarm?: { responseJwt: string } } export async function createOpenid4vpAuthorizationResponse( options: CreateOpenid4vpAuthorizationResponseOptions ): Promise { - const { requestPayload, jarm, callbacks } = options - const responsePayload = { - ...options.responsePayload, - ...('state' in requestPayload && { state: requestPayload.state }), + const { authorizationRequestPayload, jarm, callbacks } = options + + const authorizationResponsePayload = { + ...options.authorizationResponsePayload, + ...('state' in authorizationRequestPayload && { state: authorizationRequestPayload.state }), } satisfies Openid4vpAuthorizationResponse - if (requestPayload.response_mode && isJarmResponseMode(requestPayload.response_mode) && !jarm) { + if ( + authorizationRequestPayload.response_mode && + isJarmResponseMode(authorizationRequestPayload.response_mode) && + !jarm + ) { throw new Oauth2Error( - `Missing jarm options for creating Jarm response with response mode '${requestPayload.response_mode}'` + `Missing jarm options for creating Jarm response with response mode '${authorizationRequestPayload.response_mode}'` ) } if (!jarm) { return { - responsePayload, + authorizationResponsePayload, } } - if (!requestPayload.client_metadata) { + if (!authorizationRequestPayload.client_metadata) { throw new Oauth2Error('Missing client metadata in the request params to assert Jarm metadata support.') } - if (!requestPayload.client_metadata.jwks) { + if (!authorizationRequestPayload.client_metadata.jwks) { throw new Oauth2ServerErrorResponseError({ error: Oauth2ErrorCodes.InvalidRequest, error_description: 'Missing JWKS in client metadata. Cannot extract encryption JWK.', @@ -70,13 +75,13 @@ export async function createOpenid4vpAuthorizationResponse( } const supportedJarmMetadata = jarmAssertMetadataSupported({ - clientMetadata: requestPayload.client_metadata, + clientMetadata: authorizationRequestPayload.client_metadata, serverMetadata: jarm.serverMetadata, }) const clientMetaJwks = extractJwksFromClientMetadata({ - ...requestPayload.client_metadata, - jwks: requestPayload.client_metadata.jwks, + ...authorizationRequestPayload.client_metadata, + jwks: authorizationRequestPayload.client_metadata.jwks, }) if (!clientMetaJwks?.encJwk) { @@ -111,12 +116,12 @@ export async function createOpenid4vpAuthorizationResponse( } const jarmResponsePayload = { - ...responsePayload, + ...authorizationResponsePayload, ...additionalJwtPayload, } satisfies Openid4vpAuthorizationResponse - const result = await createJarmAuthResponse({ - jarmAuthResponse: jarmResponsePayload, + const result = await createJarmAuthorizationResponse({ + jarmAuthorizationResponse: jarmResponsePayload, jwtSigner: jarm?.jwtSigner, jweEncryptor: jarm?.encryption && (supportedJarmMetadata.type === 'encrypt' || supportedJarmMetadata.type === 'sign_encrypt') @@ -124,7 +129,7 @@ export async function createOpenid4vpAuthorizationResponse( method: 'jwk', publicJwk: clientMetaJwks.encJwk, apu: jarm.encryption?.nonce, - apv: requestPayload.nonce, + apv: authorizationRequestPayload.nonce, alg: supportedJarmMetadata.client_metadata.authorization_encrypted_response_alg, enc: supportedJarmMetadata.client_metadata.authorization_encrypted_response_enc, } @@ -136,7 +141,7 @@ export async function createOpenid4vpAuthorizationResponse( }) return { - responsePayload: jarmResponsePayload, - jarm: { responseJwt: result.jarmAuthResponseJwt }, + authorizationResponsePayload: jarmResponsePayload, + jarm: { responseJwt: result.jarmAuthorizationResponseJwt }, } } diff --git a/packages/openid4vp/src/authorization-response/parse-authorization-response.ts b/packages/openid4vp/src/authorization-response/parse-authorization-response.ts index 516c3f3..6d8d1b5 100644 --- a/packages/openid4vp/src/authorization-response/parse-authorization-response.ts +++ b/packages/openid4vp/src/authorization-response/parse-authorization-response.ts @@ -1,12 +1,9 @@ -import { type CallbackContext, Oauth2Error, Oauth2ServerErrorResponseError } from '@openid4vc/oauth2' -import { parseOpenid4vpAuthorizationRequestPayload } from '../authorization-request/parse-authorization-request-params' +import { type CallbackContext, Oauth2ServerErrorResponseError } from '@openid4vc/oauth2' import type { Openid4vpAuthorizationRequest } from '../authorization-request/z-authorization-request' import type { Openid4vpAuthorizationRequestDcApi } from '../authorization-request/z-authorization-request-dc-api' -import type { - GetOpenid4vpAuthorizationRequestCallback, - VerifiedJarmAuthorizationResponse, -} from '../jarm/jarm-auth-response/verify-jarm-auth-response' -import type { JarmHeader } from '../jarm/jarm-auth-response/z-jarm-auth-response' +import { getClientId } from '../client-identifier-scheme/parse-client-identifier-scheme' +import type { VerifiedJarmAuthorizationResponse } from '../jarm/jarm-authorization-response/verify-jarm-authorization-response' +import type { JarmHeader } from '../jarm/jarm-authorization-response/z-jarm-authorization-response' import { isJarmResponseMode } from '../jarm/jarm-response-mode' import { parseOpenid4VpAuthorizationResponsePayload } from './parse-authorization-response-payload' import { parseJarmAuthorizationResponse } from './parse-jarm-authorization-response' @@ -15,49 +12,45 @@ import type { ValidateOpenid4VpAuthorizationResponseResult } from './validate-au import type { Openid4vpAuthorizationResponse } from './z-authorization-response' export interface ParseOpenid4vpAuthorizationResponseOptions { - responsePayload: Record - callbacks: Pick & { - getOpenid4vpAuthorizationRequest: GetOpenid4vpAuthorizationRequestCallback - } + /** + * The authorization response as received from the wallet, and can optionally still be encrypted. + */ + authorizationResponse: Record + + authorizationRequestPayload: Openid4vpAuthorizationRequest | Openid4vpAuthorizationRequestDcApi + callbacks: Pick + + origin?: string } export type ParsedOpenid4vpAuthorizationResponse = ValidateOpenid4VpAuthorizationResponseResult & { authorizationResponsePayload: Openid4vpAuthorizationResponse - authorizationRequestPayload: Openid4vpAuthorizationRequest | Openid4vpAuthorizationRequestDcApi - expectedNonce: string - - // TODO: return this - // expectedTransactionDataHashes?: [] - jarm?: VerifiedJarmAuthorizationResponse & { jarmHeader: JarmHeader - mdocGeneratedNonce?: string } } export async function parseOpenid4vpAuthorizationResponse( options: ParseOpenid4vpAuthorizationResponseOptions ): Promise { - const { responsePayload, callbacks } = options - - if (responsePayload.response) { - return parseJarmAuthorizationResponse({ jarmResponseJwt: responsePayload.response as string, callbacks }) - } - - const authorizationResponsePayload = parseOpenid4VpAuthorizationResponsePayload(responsePayload) + const { authorizationResponse, callbacks, authorizationRequestPayload, origin } = options - const { authorizationRequest } = await callbacks.getOpenid4vpAuthorizationRequest(authorizationResponsePayload) - const parsedAuthRequest = parseOpenid4vpAuthorizationRequestPayload({ authorizationRequest: authorizationRequest }) - if (parsedAuthRequest.type !== 'openid4vp' && parsedAuthRequest.type !== 'openid4vp_dc_api') { - throw new Oauth2Error('Invalid authorization request. Could not parse openid4vp authorization request.') + const expectedClientId = getClientId({ authorizationRequestPayload, origin }) + if (authorizationResponse.response) { + return parseJarmAuthorizationResponse({ + jarmResponseJwt: authorizationResponse.response as string, + callbacks, + authorizationRequestPayload, + expectedClientId, + }) } - const authorizationRequestPayload = parsedAuthRequest.params + const authorizationResponsePayload = parseOpenid4VpAuthorizationResponsePayload(authorizationResponse) - const validateOpenId4vpResponse = validateOpenid4vpAuthorizationResponsePayload({ - requestPayload: authorizationRequestPayload, - responsePayload: authorizationResponsePayload, + const validatedOpenId4vpResponse = validateOpenid4vpAuthorizationResponsePayload({ + authorizationRequestPayload: authorizationRequestPayload, + authorizationResponsePayload: authorizationResponsePayload, }) if (authorizationRequestPayload.response_mode && isJarmResponseMode(authorizationRequestPayload.response_mode)) { @@ -73,11 +66,10 @@ export async function parseOpenid4vpAuthorizationResponse( } return { - ...validateOpenId4vpResponse, + ...validatedOpenId4vpResponse, expectedNonce: authorizationRequestPayload.nonce, authorizationResponsePayload, - authorizationRequestPayload, jarm: undefined, } } diff --git a/packages/openid4vp/src/authorization-response/parse-jarm-authorization-response.ts b/packages/openid4vp/src/authorization-response/parse-jarm-authorization-response.ts index 947ab39..c15b25b 100644 --- a/packages/openid4vp/src/authorization-response/parse-jarm-authorization-response.ts +++ b/packages/openid4vp/src/authorization-response/parse-jarm-authorization-response.ts @@ -1,12 +1,10 @@ import { type CallbackContext, Oauth2Error, decodeJwtHeader, zCompactJwe, zCompactJwt } from '@openid4vc/oauth2' -import { decodeBase64, encodeToUtf8String, parseWithErrorHandling } from '@openid4vc/utils' +import { parseWithErrorHandling } from '@openid4vc/utils' import z from 'zod' -import { parseOpenid4vpAuthorizationRequestPayload } from '../authorization-request/parse-authorization-request-params' -import { - type GetOpenid4vpAuthorizationRequestCallback, - verifyJarmAuthorizationResponse, -} from '../jarm/jarm-auth-response/verify-jarm-auth-response' -import { zJarmHeader } from '../jarm/jarm-auth-response/z-jarm-auth-response' +import type { Openid4vpAuthorizationRequest } from '../authorization-request/z-authorization-request' +import type { Openid4vpAuthorizationRequestDcApi } from '../authorization-request/z-authorization-request-dc-api' +import { verifyJarmAuthorizationResponse } from '../jarm/jarm-authorization-response/verify-jarm-authorization-response' +import { zJarmHeader } from '../jarm/jarm-authorization-response/z-jarm-authorization-response' import { isJarmResponseMode } from '../jarm/jarm-response-mode' import type { ParsedOpenid4vpAuthorizationResponse } from './parse-authorization-response' import { parseOpenid4VpAuthorizationResponsePayload } from './parse-authorization-response-payload' @@ -14,15 +12,16 @@ import { validateOpenid4vpAuthorizationResponsePayload } from './validate-author export interface ParseJarmAuthorizationResponseOptions { jarmResponseJwt: string - callbacks: Pick & { - getOpenid4vpAuthorizationRequest: GetOpenid4vpAuthorizationRequestCallback - } + authorizationRequestPayload: Openid4vpAuthorizationRequest | Openid4vpAuthorizationRequestDcApi + callbacks: Pick + + expectedClientId: string } export async function parseJarmAuthorizationResponse( options: ParseJarmAuthorizationResponseOptions ): Promise { - const { jarmResponseJwt, callbacks } = options + const { jarmResponseJwt, callbacks, authorizationRequestPayload, expectedClientId } = options const jarmAuthorizationResponseJwt = parseWithErrorHandling( z.union([zCompactJwt, zCompactJwe]), @@ -30,52 +29,37 @@ export async function parseJarmAuthorizationResponse( 'Invalid jarm authorization response jwt.' ) - const verifiedJarmResponse = await verifyJarmAuthorizationResponse({ jarmAuthorizationResponseJwt, callbacks }) + const verifiedJarmResponse = await verifyJarmAuthorizationResponse({ + jarmAuthorizationResponseJwt, + callbacks, + expectedClientId, + authorizationRequestPayload, + }) const { header: jarmHeader } = decodeJwtHeader({ jwt: jarmAuthorizationResponseJwt, headerSchema: zJarmHeader, }) - const parsedAuthorizationRequest = parseOpenid4vpAuthorizationRequestPayload({ - authorizationRequest: verifiedJarmResponse.authorizationRequest, - }) - - if (parsedAuthorizationRequest.type !== 'openid4vp' && parsedAuthorizationRequest.type !== 'openid4vp_dc_api') { - throw new Oauth2Error('Invalid authorization request. Could not parse openid4vp authorization request.') - } - - const authorizationResponsePayload = parseOpenid4VpAuthorizationResponsePayload(verifiedJarmResponse.jarmAuthResponse) + const authorizationResponsePayload = parseOpenid4VpAuthorizationResponsePayload( + verifiedJarmResponse.jarmAuthorizationResponse + ) const validateOpenId4vpResponse = validateOpenid4vpAuthorizationResponsePayload({ - requestPayload: parsedAuthorizationRequest.params, - responsePayload: authorizationResponsePayload, + authorizationRequestPayload: authorizationRequestPayload, + authorizationResponsePayload: authorizationResponsePayload, }) - const authorizationRequestPayload = parsedAuthorizationRequest.params if (!authorizationRequestPayload.response_mode || !isJarmResponseMode(authorizationRequestPayload.response_mode)) { throw new Oauth2Error( `Invalid response mode for jarm response. Response mode: '${authorizationRequestPayload.response_mode ?? 'fragment'}'` ) } - let mdocGeneratedNonce: string | undefined = undefined - - if (jarmHeader?.apu) { - mdocGeneratedNonce = encodeToUtf8String(decodeBase64(jarmHeader.apu)) - } - if (jarmHeader?.apv) { - const jarmRequestNonce = encodeToUtf8String(decodeBase64(jarmHeader.apv)) - if (jarmRequestNonce !== authorizationRequestPayload.nonce) { - throw new Oauth2Error('The nonce in the jarm header does not match the nonce in the request.') - } - } - return { ...validateOpenId4vpResponse, - jarm: { ...verifiedJarmResponse, jarmHeader, mdocGeneratedNonce }, + jarm: { ...verifiedJarmResponse, jarmHeader }, expectedNonce: authorizationRequestPayload.nonce, authorizationResponsePayload, - authorizationRequestPayload, } } diff --git a/packages/openid4vp/src/authorization-response/submit-authorization-response.ts b/packages/openid4vp/src/authorization-response/submit-authorization-response.ts index 6a8158b..2399324 100644 --- a/packages/openid4vp/src/authorization-response/submit-authorization-response.ts +++ b/packages/openid4vp/src/authorization-response/submit-authorization-response.ts @@ -2,24 +2,24 @@ import { type CallbackContext, Oauth2Error } from '@openid4vc/oauth2' import { ContentType, defaultFetcher } from '@openid4vc/utils' import { objectToQueryParams } from '@openid4vc/utils' import type { Openid4vpAuthorizationRequest } from '../authorization-request/z-authorization-request' -import { jarmAuthResponseSend } from '../jarm/jarm-auth-response-send' +import { jarmAuthorizationResponseSend } from '../jarm/jarm-authorizatino-response-send' import type { Openid4vpAuthorizationResponse } from './z-authorization-response' export interface SubmitOpenid4vpAuthorizationResponseOptions { - requestPayload: Pick - responsePayload: Openid4vpAuthorizationResponse + authorizationRequestPayload: Pick + authorizationResponsePayload: Openid4vpAuthorizationResponse jarm?: { responseJwt: string } callbacks: Pick } export async function submitOpenid4vpAuthorizationResponse(options: SubmitOpenid4vpAuthorizationResponseOptions) { - const { requestPayload, responsePayload, jarm, callbacks } = options - const url = requestPayload.response_uri + const { authorizationRequestPayload, authorizationResponsePayload, jarm, callbacks } = options + const url = authorizationRequestPayload.response_uri if (jarm) { - return jarmAuthResponseSend({ - authRequest: requestPayload, - jarmAuthResponseJwt: jarm.responseJwt, + return jarmAuthorizationResponseSend({ + authorizationRequestPayload, + jarmAuthorizationResponseJwt: jarm.responseJwt, callbacks, }) } @@ -31,7 +31,7 @@ export async function submitOpenid4vpAuthorizationResponse(options: SubmitOpenid } const fetch = callbacks.fetch ?? defaultFetcher - const encodedResponse = objectToQueryParams(responsePayload) + const encodedResponse = objectToQueryParams(authorizationResponsePayload) const submissionResponse = await fetch(url, { method: 'POST', body: encodedResponse, diff --git a/packages/openid4vp/src/authorization-response/validate-authorization-response.ts b/packages/openid4vp/src/authorization-response/validate-authorization-response.ts index 95c9789..7a2eac4 100644 --- a/packages/openid4vp/src/authorization-response/validate-authorization-response.ts +++ b/packages/openid4vp/src/authorization-response/validate-authorization-response.ts @@ -6,8 +6,8 @@ import type { ValidateOpenid4VpAuthorizationResponseResult } from './validate-au import type { Openid4vpAuthorizationResponse } from './z-authorization-response' export interface ValidateOpenid4vpAuthorizationResponseOptions { - requestPayload: Openid4vpAuthorizationRequest | Openid4vpAuthorizationRequestDcApi - responsePayload: Openid4vpAuthorizationResponse + authorizationRequestPayload: Openid4vpAuthorizationRequest | Openid4vpAuthorizationRequestDcApi + authorizationResponsePayload: Openid4vpAuthorizationResponse } /** @@ -20,52 +20,55 @@ export interface ValidateOpenid4vpAuthorizationResponseOptions { export function validateOpenid4vpAuthorizationResponsePayload( options: ValidateOpenid4vpAuthorizationResponseOptions ): ValidateOpenid4VpAuthorizationResponseResult { - const { requestPayload, responsePayload } = options + const { authorizationRequestPayload, authorizationResponsePayload } = options - if ('state' in requestPayload && requestPayload.state !== responsePayload.state) { + if ( + 'state' in authorizationRequestPayload && + authorizationRequestPayload.state !== authorizationResponsePayload.state + ) { throw new Oauth2Error('OpenId4Vp Authorization Response state mismatch.') } // TODO: implement id_token handling - if (responsePayload.id_token) { + if (authorizationResponsePayload.id_token) { throw new Oauth2Error('OpenId4Vp Authorization Response id_token is not supported.') } - if (responsePayload.presentation_submission) { - if (!requestPayload.presentation_definition) { + if (authorizationResponsePayload.presentation_submission) { + if (!authorizationRequestPayload.presentation_definition) { throw new Oauth2Error('OpenId4Vp Authorization Request is missing the required presentation_definition.') } return { type: 'pex', pex: - 'scope' in requestPayload && requestPayload.scope + 'scope' in authorizationRequestPayload && authorizationRequestPayload.scope ? { - scope: requestPayload.scope, - presentationSubmission: responsePayload.presentation_submission, - presentations: parsePexVpToken(responsePayload.vp_token), + scope: authorizationRequestPayload.scope, + presentationSubmission: authorizationResponsePayload.presentation_submission, + presentations: parsePexVpToken(authorizationResponsePayload.vp_token), } : { - presentationDefinition: requestPayload.presentation_definition, - presentationSubmission: responsePayload.presentation_submission, - presentations: parsePexVpToken(responsePayload.vp_token), + presentationDefinition: authorizationRequestPayload.presentation_definition, + presentationSubmission: authorizationResponsePayload.presentation_submission, + presentations: parsePexVpToken(authorizationResponsePayload.vp_token), }, } } - if (requestPayload.dcql_query) { - const presentations = parseDcqlVpToken(responsePayload.vp_token) + if (authorizationRequestPayload.dcql_query) { + const presentations = parseDcqlVpToken(authorizationResponsePayload.vp_token) return { type: 'dcql', dcql: - 'scope' in requestPayload && requestPayload.scope + 'scope' in authorizationRequestPayload && authorizationRequestPayload.scope ? { - scope: requestPayload.scope, + scope: authorizationRequestPayload.scope, presentations, } : { - query: requestPayload.dcql_query, + query: authorizationRequestPayload.dcql_query, presentations, }, } diff --git a/packages/openid4vp/src/client-identifier-scheme/parse-client-identifier-scheme.ts b/packages/openid4vp/src/client-identifier-scheme/parse-client-identifier-scheme.ts index 2c5469e..eef6013 100644 --- a/packages/openid4vp/src/client-identifier-scheme/parse-client-identifier-scheme.ts +++ b/packages/openid4vp/src/client-identifier-scheme/parse-client-identifier-scheme.ts @@ -1,4 +1,5 @@ import { Oauth2ErrorCodes, Oauth2ServerErrorResponseError, getGlobalConfig } from '@openid4vc/oauth2' +import { URL } from '@openid4vc/utils' import type { CallbackContext } from '../../../oauth2/src/callbacks' import type { Openid4vpAuthorizationRequest } from '../authorization-request/z-authorization-request' import { @@ -49,22 +50,20 @@ export type ParsedClientIdentifier = clientMetadata?: ClientMetadata } -/** - * Configuration options for the parser - */ -export interface ClientIdentifierParserConfig { - supportedSchemes?: ClientIdScheme[] -} - -export interface ClientIdentifierParserOptions { - request: Openid4vpAuthorizationRequest | Openid4vpAuthorizationRequestDcApi +export interface GetClientIdOptions { + authorizationRequestPayload: Openid4vpAuthorizationRequest | Openid4vpAuthorizationRequestDcApi jar?: VerifiedJarRequest origin?: string - callbacks: Partial> } -function getClientId(options: ClientIdentifierParserOptions) { - if (isOpenid4vpAuthorizationRequestDcApi(options.request)) { +export function getClientId(options: GetClientIdOptions) { + const version = parseAuthorizationRequestVersion(options.authorizationRequestPayload) + + if (version < 22) { + return getLegacyClientId(options) + } + + if (isOpenid4vpAuthorizationRequestDcApi(options.authorizationRequestPayload)) { if (!options.origin) { throw new Oauth2ServerErrorResponseError({ error: Oauth2ErrorCodes.InvalidRequest, @@ -73,25 +72,20 @@ function getClientId(options: ClientIdentifierParserOptions) { }) } - if (!options.jar || !options.request.client_id) return `web-origin:${options.origin}` + if (!options.jar || !options.authorizationRequestPayload.client_id) return `web-origin:${options.origin}` - return options.request.client_id + return options.authorizationRequestPayload.client_id } - return options.request.client_id + return options.authorizationRequestPayload.client_id } -function getLegacyClientId(options: ClientIdentifierParserOptions) { - const legacyClientIdScheme = options.request.client_id_scheme ?? 'pre-registered' +function getLegacyClientId(options: GetClientIdOptions) { + const legacyClientIdScheme = options.authorizationRequestPayload.client_id_scheme ?? 'pre-registered' - let clientIdScheme: ClientIdScheme - if (legacyClientIdScheme === 'entity_id') { - clientIdScheme = 'https' - } else { - clientIdScheme = legacyClientIdScheme - } + const clientIdScheme: ClientIdScheme = legacyClientIdScheme === 'entity_id' ? 'https' : legacyClientIdScheme - if (isOpenid4vpAuthorizationRequestDcApi(options.request)) { + if (isOpenid4vpAuthorizationRequestDcApi(options.authorizationRequestPayload)) { if (!options.origin) { throw new Oauth2ServerErrorResponseError({ error: Oauth2ErrorCodes.InvalidRequest, @@ -100,20 +94,34 @@ function getLegacyClientId(options: ClientIdentifierParserOptions) { }) } - if (!options.jar || !options.request.client_id) return `web-origin:${options.origin}` + if (!options.jar || !options.authorizationRequestPayload.client_id) return `web-origin:${options.origin}` - return `${clientIdScheme}:${options.request.client_id}` + return `${clientIdScheme}:${options.authorizationRequestPayload.client_id}` } if (clientIdScheme === 'https' || clientIdScheme === 'did') { - return options.request.client_id + return options.authorizationRequestPayload.client_id } if (clientIdScheme === 'pre-registered') { - return options.request.client_id + return options.authorizationRequestPayload.client_id } - return `${clientIdScheme}:${options.request.client_id}` + return `${clientIdScheme}:${options.authorizationRequestPayload.client_id}` +} + +/** + * Configuration options for the parser + */ +export interface ClientIdentifierParserConfig { + supportedSchemes?: ClientIdScheme[] +} + +export interface ClientIdentifierParserOptions { + authorizationRequestPayload: Openid4vpAuthorizationRequest | Openid4vpAuthorizationRequestDcApi + jar?: VerifiedJarRequest + origin?: string + callbacks: Partial> } /** @@ -123,38 +131,22 @@ export function parseClientIdentifier( options: ClientIdentifierParserOptions, parserConfig?: ClientIdentifierParserConfig ): ParsedClientIdentifier { - const { request, jar } = options - - const version = parseAuthorizationRequestVersion(request) - // this means that client_id_scheme is used - if (version < 22) { - const legacyClientIdScheme = request.client_id_scheme ?? 'pre-registered' - - let clientIdSchem: ClientIdScheme - if (legacyClientIdScheme) { - if (legacyClientIdScheme === 'entity_id') { - clientIdSchem = 'https' - } else { - clientIdSchem = legacyClientIdScheme - } - } - } - - const isDcApiRequest = isOpenid4vpAuthorizationRequestDcApi(request) - const clientId = version < 22 ? getLegacyClientId(options) : getClientId(options) + const { authorizationRequestPayload, jar } = options // By default require signatures for these schemes const parserConfigWithDefaults = { supportedSchemes: parserConfig?.supportedSchemes || Object.values(zClientIdScheme.options), } + const clientId = getClientId(options) + const colonIndex = clientId.indexOf(':') if (colonIndex === -1) { return { scheme: 'pre-registered', identifier: clientId, originalValue: clientId, - clientMetadata: request.client_metadata, + clientMetadata: authorizationRequestPayload.client_metadata, } } @@ -170,14 +162,6 @@ export function parseClientIdentifier( const scheme = schemePart as ClientIdScheme if (scheme === 'https') { - // https://github.com/openid/OpenID4VP/issues/436 - if (isDcApiRequest) { - throw new Oauth2ServerErrorResponseError({ - error: Oauth2ErrorCodes.InvalidRequest, - error_description: `The client identifier scheme 'https' is not supported when using the dc_api response mode.`, - }) - } - if (!clientId.startsWith('https://') && !(getGlobalConfig().allowInsecureUrls && clientId.startsWith('http://'))) { throw new Oauth2ServerErrorResponseError({ error: Oauth2ErrorCodes.InvalidRequest, @@ -190,7 +174,7 @@ export function parseClientIdentifier( scheme, identifier: clientId, originalValue: clientId, - trustChain: request.trust_chain, + trustChain: authorizationRequestPayload.trust_chain, } } @@ -202,7 +186,7 @@ export function parseClientIdentifier( }) } - if (isOpenid4vpAuthorizationRequestDcApi(request)) { + if (isOpenid4vpAuthorizationRequestDcApi(authorizationRequestPayload)) { throw new Oauth2ServerErrorResponseError({ error: Oauth2ErrorCodes.InvalidRequest, error_description: `The client identifier scheme 'redirect_uri' is not supported when using the dc_api response mode.`, @@ -213,7 +197,7 @@ export function parseClientIdentifier( scheme, identifier: identifierPart, originalValue: clientId, - redirectUri: (request.redirect_uri ?? request.response_uri) as string, + redirectUri: (authorizationRequestPayload.redirect_uri ?? authorizationRequestPayload.response_uri) as string, } } @@ -293,9 +277,9 @@ export function parseClientIdentifier( }) } - if (!isOpenid4vpAuthorizationRequestDcApi(request)) { - const uri = request.redirect_uri ?? request.response_uri - if (!uri || getDomainFromUrl(uri) !== identifierPart) { + if (!isOpenid4vpAuthorizationRequestDcApi(authorizationRequestPayload)) { + const uri = authorizationRequestPayload.redirect_uri ?? authorizationRequestPayload.response_uri + if (!uri || new URL(uri).hostname !== identifierPart) { throw new Oauth2ServerErrorResponseError({ error: Oauth2ErrorCodes.InvalidRequest, error_description: @@ -324,8 +308,8 @@ export function parseClientIdentifier( }) } - if (!isOpenid4vpAuthorizationRequestDcApi(request)) { - const uri = request.redirect_uri || request.response_uri + if (!isOpenid4vpAuthorizationRequestDcApi(authorizationRequestPayload)) { + const uri = authorizationRequestPayload.redirect_uri || authorizationRequestPayload.response_uri if (!uri || uri !== identifierPart) { throw new Oauth2ServerErrorResponseError({ error: Oauth2ErrorCodes.InvalidRequest, @@ -349,7 +333,7 @@ export function parseClientIdentifier( scheme, identifier: identifierPart, originalValue: clientId, - clientMetadata: request.client_metadata, + clientMetadata: authorizationRequestPayload.client_metadata, } } @@ -368,16 +352,3 @@ export function parseClientIdentifier( originalValue: clientId, } } - -function getDomainFromUrl(url: string): string { - try { - const regex = /[#/?]/ - const domain = url.split('://')[1].split(regex)[0] - return domain - } catch (error) { - throw new Oauth2ServerErrorResponseError({ - error: Oauth2ErrorCodes.ServerError, - error_description: `Url '${url}' is not a valid URL`, - }) - } -} diff --git a/packages/openid4vp/src/fetch-client-metadata.ts b/packages/openid4vp/src/fetch-client-metadata.ts index 04f33ff..c5d90da 100644 --- a/packages/openid4vp/src/fetch-client-metadata.ts +++ b/packages/openid4vp/src/fetch-client-metadata.ts @@ -1,16 +1,15 @@ import { Oauth2ErrorCodes, Oauth2ServerErrorResponseError } from '@openid4vc/oauth2' -import { type BaseSchema, ContentType, type Fetch, createZodFetcher } from '@openid4vc/utils' -import type { z } from 'zod' -import { zWalletMetadata } from './models/z-wallet-metadata' +import { ContentType, type Fetch, createZodFetcher } from '@openid4vc/utils' +import { type ClientMetadata, zClientMetadata } from './models/z-client-metadata' -export async function fetchClientMetadata(options: { +export async function fetchClientMetadata(options: { clientMetadataUri: string fetch?: Fetch -}): Promise | null> { +}): Promise { const { fetch, clientMetadataUri } = options const fetcher = createZodFetcher(fetch) - const { result, response } = await fetcher(zWalletMetadata, ContentType.Json, clientMetadataUri, { + const { result, response } = await fetcher(zClientMetadata, ContentType.Json, clientMetadataUri, { method: 'GET', headers: { Accept: ContentType.Json, diff --git a/packages/openid4vp/src/index.ts b/packages/openid4vp/src/index.ts index 6dc2945..1fdad9b 100644 --- a/packages/openid4vp/src/index.ts +++ b/packages/openid4vp/src/index.ts @@ -3,7 +3,7 @@ export { verifyJarmAuthorizationResponse, type VerifyJarmAuthorizationResponseOptions, type JarmMode, -} from './jarm/jarm-auth-response/verify-jarm-auth-response' +} from './jarm/jarm-authorization-response/verify-jarm-authorization-response' export { zJarmClientMetadata, JarmClientMetadata } from './jarm/metadata/z-jarm-client-metadata' export { createOpenid4vpAuthorizationRequest, @@ -11,12 +11,12 @@ export { } from './authorization-request/create-authorization-request' export { parseOpenid4vpAuthorizationRequestPayload, - ParseOpenid4vpAuthRequestPayloadOptions, + ParseOpenid4vpAuthorizationRequestPayloadOptions, } from './authorization-request/parse-authorization-request-params' export { resolveOpenid4vpAuthorizationRequest, ResolveOpenid4vpAuthorizationRequestOptions, - ResolvedOpenid4vpAuthRequest, + ResolvedOpenid4vpAuthorizationRequest, } from './authorization-request/resolve-authorization-request' export type { Openid4vpAuthorizationRequest } from './authorization-request/z-authorization-request' export { diff --git a/packages/openid4vp/src/jar/create-jar-auth-request.ts b/packages/openid4vp/src/jar/create-jar-authorization-request.ts similarity index 55% rename from packages/openid4vp/src/jar/create-jar-auth-request.ts rename to packages/openid4vp/src/jar/create-jar-authorization-request.ts index 929094e..c6d79d4 100644 --- a/packages/openid4vp/src/jar/create-jar-auth-request.ts +++ b/packages/openid4vp/src/jar/create-jar-authorization-request.ts @@ -6,10 +6,10 @@ import { type JwtSigner, jwtHeaderFromJwtSigner, } from '@openid4vc/oauth2' -import type { JarAuthRequest } from './z-jar-auth-request' +import type { JarAuthorizationRequest } from './z-jar-authorization-request' -export interface CreateJarAuthRequestOptions { - authRequestParams: JwtPayload & { client_id?: string } +export interface CreateJarAuthorizationRequestOptions { + authorizationRequestPayload: JwtPayload & { client_id?: string } jwtSigner: JwtSigner jweEncryptor?: JweEncryptor requestUri?: string @@ -21,35 +21,35 @@ export interface CreateJarAuthRequestOptions { * Creates a JAR (JWT Authorization Request) request object. * * @param options - The input parameters - * @param options.authRequestParams - The authorization request parameters + * @param options.authorizationRequestPayload - The authorization request parameters * @param options.jwtSigner - The JWT signer * @param options.jweEncryptor - The JWE encryptor (optional) if provided, the request object will be encrypted * @param options.requestUri - The request URI (optional) if provided, the request object needs to be fetched from the URI * @param options.callbacks - The callback context * @returns the requestParams, signerJwk, encryptionJwk, and requestObjectJwt */ -export async function createJarAuthRequest(options: CreateJarAuthRequestOptions) { - const { jwtSigner, jweEncryptor, authRequestParams, requestUri, callbacks } = options +export async function createJarAuthorizationRequest(options: CreateJarAuthorizationRequestOptions) { + const { jwtSigner, jweEncryptor, authorizationRequestPayload, requestUri, callbacks } = options - let requestObjectJwt: string | undefined + let authorizationRequestJwt: string | undefined let encryptionJwk: Jwk | undefined const { jwt, signerJwk } = await callbacks.signJwt(jwtSigner, { header: { ...jwtHeaderFromJwtSigner(jwtSigner), typ: 'oauth-authz-req+jwt' }, - payload: { ...options.additionalJwtPayload, ...authRequestParams }, + payload: { ...options.additionalJwtPayload, ...authorizationRequestPayload }, }) - requestObjectJwt = jwt + authorizationRequestJwt = jwt if (jweEncryptor) { - const encryptionResult = await callbacks.encryptJwe(jweEncryptor, requestObjectJwt) - requestObjectJwt = encryptionResult.jwe + const encryptionResult = await callbacks.encryptJwe(jweEncryptor, authorizationRequestJwt) + authorizationRequestJwt = encryptionResult.jwe encryptionJwk = encryptionResult.encryptionJwk } - const client_id = authRequestParams.client_id - const requestParams: JarAuthRequest = requestUri + const client_id = authorizationRequestPayload.client_id + const jarAuthorizationRequest: JarAuthorizationRequest = requestUri ? { client_id, request_uri: requestUri } - : { client_id, request: requestObjectJwt } + : { client_id, request: authorizationRequestJwt } - return { requestParams, signerJwk, encryptionJwk, requestObjectJwt } + return { jarAuthorizationRequest, signerJwk, encryptionJwk, authorizationRequestJwt } } diff --git a/packages/openid4vp/src/jar/handle-jar-request/verify-jar-request.ts b/packages/openid4vp/src/jar/handle-jar-request/verify-jar-request.ts index f6b77e4..34ce052 100644 --- a/packages/openid4vp/src/jar/handle-jar-request/verify-jar-request.ts +++ b/packages/openid4vp/src/jar/handle-jar-request/verify-jar-request.ts @@ -15,10 +15,10 @@ import type { WalletMetadata } from '../../models/z-wallet-metadata' import { parseAuthorizationRequestVersion } from '../../version' import { fetchJarRequestObject } from '../jar-request-object/fetch-jar-request-object' import { type JarRequestObjectPayload, zJarRequestObjectPayload } from '../jar-request-object/z-jar-request-object' -import { type JarAuthRequest, validateJarRequestParams } from '../z-jar-auth-request' +import { type JarAuthorizationRequest, validateJarRequestParams } from '../z-jar-authorization-request' export interface VerifyJarRequestOptions { - jarRequestParams: JarAuthRequest + jarRequestParams: JarAuthorizationRequest callbacks: Pick wallet?: { metadata?: WalletMetadata @@ -27,7 +27,7 @@ export interface VerifyJarRequestOptions { } export interface VerifiedJarRequest { - authRequestParams: JarRequestObjectPayload + authorizationRequestParams: JarRequestObjectPayload sendBy: 'value' | 'reference' decryptionJwk?: Jwk signer: JwtSignerWithJwk @@ -81,18 +81,18 @@ export async function verifyJarRequest(options: VerifyJarRequestOptions): Promis }) } - const { authRequestParams, signer } = await verifyJarRequestObject({ + const { authorizationRequestParams, signer } = await verifyJarRequestObject({ decryptedRequestObject, callbacks, }) - if (!authRequestParams.client_id) { + if (!authorizationRequestParams.client_id) { throw new Oauth2ServerErrorResponseError({ error: Oauth2ErrorCodes.InvalidRequestObject, error_description: 'Jar Request Object is missing the required "client_id" field.', }) } - if (jarRequestParams.client_id !== authRequestParams.client_id) { + if (jarRequestParams.client_id !== authorizationRequestParams.client_id) { throw new Oauth2ServerErrorResponseError({ error: Oauth2ErrorCodes.InvalidRequest, error_description: 'client_id does not match the request object client_id.', @@ -101,7 +101,7 @@ export async function verifyJarRequest(options: VerifyJarRequestOptions): Promis return { sendBy, - authRequestParams, + authorizationRequestParams, signer, decryptionJwk, } @@ -158,5 +158,5 @@ async function verifyJarRequestObject(options: { }) } - return { authRequestParams: jwt.payload, signer } + return { authorizationRequestParams: jwt.payload, signer } } diff --git a/packages/openid4vp/src/jar/jar-request-object/fetch-jar-request-object.ts b/packages/openid4vp/src/jar/jar-request-object/fetch-jar-request-object.ts index 2cd0565..9c9ff5b 100644 --- a/packages/openid4vp/src/jar/jar-request-object/fetch-jar-request-object.ts +++ b/packages/openid4vp/src/jar/jar-request-object/fetch-jar-request-object.ts @@ -38,10 +38,10 @@ export async function fetchJarRequestObject(options: requestBody = { ...requestBody, wallet_metadata: { ...rest } } } - const { result, response } = await fetcher(z.string(), ContentType.OAuthRequestObjectJwt, requestUri, { + const { result, response } = await fetcher(z.string(), ContentType.OAuthAuthorizationRequestJwt, requestUri, { method, headers: { - Accept: `${ContentType.OAuthRequestObjectJwt}, ${ContentType.Jwt};q=0.9`, + Accept: `${ContentType.OAuthAuthorizationRequestJwt}, ${ContentType.Jwt};q=0.9`, 'Content-Type': ContentType.XWwwFormUrlencoded, }, body: method === 'POST' ? objectToQueryParams(wallet.metadata ?? {}) : undefined, diff --git a/packages/openid4vp/src/jar/z-jar-auth-request.ts b/packages/openid4vp/src/jar/z-jar-authorization-request.ts similarity index 76% rename from packages/openid4vp/src/jar/z-jar-auth-request.ts rename to packages/openid4vp/src/jar/z-jar-authorization-request.ts index 0f82753..d19d323 100644 --- a/packages/openid4vp/src/jar/z-jar-auth-request.ts +++ b/packages/openid4vp/src/jar/z-jar-authorization-request.ts @@ -4,7 +4,7 @@ import { z } from 'zod' import type { Openid4vpAuthorizationRequest } from '../authorization-request/z-authorization-request' import type { Openid4vpAuthorizationRequestDcApi } from '../authorization-request/z-authorization-request-dc-api' -export const zJarAuthRequest = z +export const zJarAuthorizationRequest = z .object({ request: z.optional(z.string()), request_uri: z.optional(zHttpsUrl), @@ -12,9 +12,9 @@ export const zJarAuthRequest = z client_id: z.optional(z.string()), }) .passthrough() -export type JarAuthRequest = z.infer +export type JarAuthorizationRequest = z.infer -export function validateJarRequestParams(options: { jarRequestParams: JarAuthRequest }) { +export function validateJarRequestParams(options: { jarRequestParams: JarAuthorizationRequest }) { const { jarRequestParams } = options if (jarRequestParams.request && jarRequestParams.request_uri) { @@ -31,12 +31,12 @@ export function validateJarRequestParams(options: { jarRequestParams: JarAuthReq }) } - return jarRequestParams as JarAuthRequest & + return jarRequestParams as JarAuthorizationRequest & ({ request_uri: string; request?: never } | { request: string; request_uri?: never }) } -export function isJarAuthRequest( - request: Openid4vpAuthorizationRequest | JarAuthRequest | Openid4vpAuthorizationRequestDcApi -): request is JarAuthRequest { +export function isJarAuthorizationRequest( + request: Openid4vpAuthorizationRequest | JarAuthorizationRequest | Openid4vpAuthorizationRequestDcApi +): request is JarAuthorizationRequest { return 'request' in request || 'request_uri' in request } diff --git a/packages/openid4vp/src/jarm/jarm-auth-response/verify-jarm-auth-response.ts b/packages/openid4vp/src/jarm/jarm-auth-response/verify-jarm-auth-response.ts deleted file mode 100644 index 0f449fa..0000000 --- a/packages/openid4vp/src/jarm/jarm-auth-response/verify-jarm-auth-response.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { - type CallbackContext, - Oauth2Error, - decodeJwt, - decodeJwtHeader, - jwtSignerFromJwt, - zCompactJwe, - zCompactJwt, - zJwtHeader, -} from '@openid4vc/oauth2' -import z from 'zod' -import { jarmAuthResponseValidate } from './jarm-validate-auth-response' -import { - type JarmAuthResponse, - type JarmAuthResponseEncryptedOnly, - zJarmAuthResponse, - zJarmAuthResponseEncryptedOnly, -} from './z-jarm-auth-response' - -export enum JarmMode { - Signed = 'Signed', - Encrypted = 'Encrypted', - SignedEncrypted = 'SignedEncrypted', -} - -export type GetOpenid4vpAuthorizationRequestCallback = ( - authResponse: JarmAuthResponse | JarmAuthResponseEncryptedOnly -) => Promise<{ authorizationRequest: { client_id: string; nonce: string; state?: string } }> - -/** - * The client decrypts the JWT using the default key for the respective issuer or, - * if applicable, determined by the kid JWT header parameter. - * The key might be a private key, where the corresponding public key is registered - * with the expected issuer of the response ("use":"enc" via the client's metadata jwks or jwks_uri) - * or a key derived from its client secret (see Section 2.2). - */ -const decryptJarmRequestData = async (options: { - requestData: string - callbacks: Pick -}) => { - const { requestData, callbacks } = options - - const { header } = decodeJwtHeader({ jwt: requestData }) - if (!header.kid) { - throw new Oauth2Error('Jarm JWE is missing the protected header field "kid".') - } - - const result = await callbacks.decryptJwe(requestData) - if (!result.decrypted) { - throw new Oauth2Error('Failed to decrypt jarm auth response.') - } - - return result.payload -} - -export interface VerifyJarmAuthorizationResponseOptions { - jarmAuthorizationResponseJwt: string - callbacks: Pick & { - getOpenid4vpAuthorizationRequest: GetOpenid4vpAuthorizationRequestCallback - } -} - -export type VerifiedJarmAuthorizationResponse = Awaited> - -/** - * Validate a JARM direct_post.jwt compliant authentication response - * * The decryption key should be resolvable using the the protected header's 'kid' field - * * The signature verification jwk should be resolvable using the jws protected header's 'kid' field and the payload's 'iss' field. - */ -export async function verifyJarmAuthorizationResponse(options: VerifyJarmAuthorizationResponseOptions) { - const { jarmAuthorizationResponseJwt, callbacks } = options - - const requestDataIsEncrypted = zCompactJwe.safeParse(jarmAuthorizationResponseJwt).success - const decryptedRequestData = requestDataIsEncrypted - ? await decryptJarmRequestData({ requestData: jarmAuthorizationResponseJwt, callbacks }) - : jarmAuthorizationResponseJwt - - const responseIsSigned = zCompactJwt.safeParse(decryptedRequestData).success - if (!requestDataIsEncrypted && !responseIsSigned) { - throw new Oauth2Error('Jarm Auth Response must be either encrypted, signed, or signed and encrypted.') - } - - let jarmAuthResponse: JarmAuthResponse | JarmAuthResponseEncryptedOnly - - if (responseIsSigned) { - const { header: jwsProtectedHeader, payload: jwsPayload } = decodeJwt({ - jwt: decryptedRequestData, - headerSchema: z.object({ ...zJwtHeader.shape, kid: z.string() }), - }) - - const response = zJarmAuthResponse.parse(jwsPayload) - const jwtSigner = jwtSignerFromJwt({ header: jwsProtectedHeader, payload: jwsPayload }) - - const verificationResult = await options.callbacks.verifyJwt(jwtSigner, { - compact: decryptedRequestData, - header: jwsProtectedHeader, - payload: jwsPayload, - }) - - if (!verificationResult.verified) { - throw new Oauth2Error('Jarm Auth Response is not valid.') - } - - jarmAuthResponse = response - } else { - const jsonRequestData: unknown = JSON.parse(decryptedRequestData) - jarmAuthResponse = zJarmAuthResponseEncryptedOnly.parse(jsonRequestData) - } - - const { authorizationRequest } = await callbacks.getOpenid4vpAuthorizationRequest(jarmAuthResponse) - - jarmAuthResponseValidate({ - clientId: authorizationRequest.client_id, - authorizationResponse: jarmAuthResponse, - }) - const type: JarmMode = - requestDataIsEncrypted && responseIsSigned - ? JarmMode.SignedEncrypted - : requestDataIsEncrypted - ? JarmMode.Encrypted - : JarmMode.Signed - - const issuer = jarmAuthResponse.iss - return { authorizationRequest, jarmAuthResponse, type, issuer } -} diff --git a/packages/openid4vp/src/jarm/jarm-auth-response-send.ts b/packages/openid4vp/src/jarm/jarm-authorizatino-response-send.ts similarity index 62% rename from packages/openid4vp/src/jarm/jarm-auth-response-send.ts rename to packages/openid4vp/src/jarm/jarm-authorizatino-response-send.ts index 1153195..a8fc7cc 100644 --- a/packages/openid4vp/src/jarm/jarm-auth-response-send.ts +++ b/packages/openid4vp/src/jarm/jarm-authorizatino-response-send.ts @@ -1,25 +1,25 @@ import { type CallbackContext, Oauth2Error } from '@openid4vc/oauth2' import { ContentType, URL, defaultFetcher } from '@openid4vc/utils' -interface JarmAuthResponseSendOptions { - authRequest: { +interface JarmAuthorizationResponseSendOptions { + authorizationRequestPayload: { response_uri?: string redirect_uri?: string } - jarmAuthResponseJwt: string + jarmAuthorizationResponseJwt: string callbacks: Pick } -export const jarmAuthResponseSend = (options: JarmAuthResponseSendOptions) => { - const { authRequest, jarmAuthResponseJwt, callbacks } = options +export const jarmAuthorizationResponseSend = (options: JarmAuthorizationResponseSendOptions) => { + const { authorizationRequestPayload, jarmAuthorizationResponseJwt, callbacks } = options - const responseEndpoint = authRequest.response_uri ?? authRequest.redirect_uri + const responseEndpoint = authorizationRequestPayload.response_uri ?? authorizationRequestPayload.redirect_uri if (!responseEndpoint) { throw new Oauth2Error(`Either 'response_uri' or 'redirect_uri' MUST be present in the authorization request`) } const responseEndpointUrl = new URL(responseEndpoint) - return handleDirectPostJwt(responseEndpointUrl, jarmAuthResponseJwt, callbacks) + return handleDirectPostJwt(responseEndpointUrl, jarmAuthorizationResponseJwt, callbacks) } async function handleDirectPostJwt( diff --git a/packages/openid4vp/src/jarm/jarm-auth-response-create.ts b/packages/openid4vp/src/jarm/jarm-authorization-response-create.ts similarity index 51% rename from packages/openid4vp/src/jarm/jarm-auth-response-create.ts rename to packages/openid4vp/src/jarm/jarm-authorization-response-create.ts index 09f475d..68af367 100644 --- a/packages/openid4vp/src/jarm/jarm-auth-response-create.ts +++ b/packages/openid4vp/src/jarm/jarm-authorization-response-create.ts @@ -5,28 +5,31 @@ import { Oauth2Error, jwtHeaderFromJwtSigner, } from '@openid4vc/oauth2' -import type { JarmAuthResponse, JarmAuthResponseEncryptedOnly } from './jarm-auth-response/z-jarm-auth-response' +import type { + JarmAuthorizationResponse, + JarmAuthorizationResponseEncryptedOnly, +} from './jarm-authorization-response/z-jarm-authorization-response' -export interface CreateJarmAuthResponseOptions { - jarmAuthResponse: JarmAuthResponse | JarmAuthResponseEncryptedOnly +export interface CreateJarmAuthorizationResponseOptions { + jarmAuthorizationResponse: JarmAuthorizationResponse | JarmAuthorizationResponseEncryptedOnly jwtSigner?: JwtSigner jweEncryptor?: JweEncryptor callbacks: Pick } -export async function createJarmAuthResponse(options: CreateJarmAuthResponseOptions) { - const { jarmAuthResponse, jweEncryptor, jwtSigner, callbacks } = options +export async function createJarmAuthorizationResponse(options: CreateJarmAuthorizationResponseOptions) { + const { jarmAuthorizationResponse, jweEncryptor, jwtSigner, callbacks } = options if (!jwtSigner && jweEncryptor) { - const { jwe } = await callbacks.encryptJwe(jweEncryptor, JSON.stringify(jarmAuthResponse)) - return { jarmAuthResponseJwt: jwe } + const { jwe } = await callbacks.encryptJwe(jweEncryptor, JSON.stringify(jarmAuthorizationResponse)) + return { jarmAuthorizationResponseJwt: jwe } } if (jwtSigner && !jweEncryptor) { const signed = await callbacks.signJwt(jwtSigner, { header: jwtHeaderFromJwtSigner(jwtSigner), - payload: jarmAuthResponse, + payload: jarmAuthorizationResponse, }) - return { jarmAuthResponseJwt: signed.jwt } + return { jarmAuthorizationResponseJwt: signed.jwt } } if (!jwtSigner || !jweEncryptor) { @@ -34,10 +37,10 @@ export async function createJarmAuthResponse(options: CreateJarmAuthResponseOpti } const signed = await callbacks.signJwt(jwtSigner, { header: jwtHeaderFromJwtSigner(jwtSigner), - payload: jarmAuthResponse, + payload: jarmAuthorizationResponse, }) const encrypted = await callbacks.encryptJwe(jweEncryptor, signed.jwt) - return { jarmAuthResponseJwt: encrypted.jwe } + return { jarmAuthorizationResponseJwt: encrypted.jwe } } diff --git a/packages/openid4vp/src/jarm/jarm-auth-response/jarm-validate-auth-response.ts b/packages/openid4vp/src/jarm/jarm-authorization-response/jarm-validate-authorization-response.ts similarity index 64% rename from packages/openid4vp/src/jarm/jarm-auth-response/jarm-validate-auth-response.ts rename to packages/openid4vp/src/jarm/jarm-authorization-response/jarm-validate-authorization-response.ts index 306d7dd..5064751 100644 --- a/packages/openid4vp/src/jarm/jarm-auth-response/jarm-validate-auth-response.ts +++ b/packages/openid4vp/src/jarm/jarm-authorization-response/jarm-validate-authorization-response.ts @@ -1,23 +1,27 @@ import { Oauth2Error } from '@openid4vc/oauth2' import { dateToSeconds } from '@openid4vc/utils' -import { type JarmAuthResponse, type JarmAuthResponseEncryptedOnly, zJarmAuthResponse } from './z-jarm-auth-response' +import { + type JarmAuthorizationResponse, + type JarmAuthorizationResponseEncryptedOnly, + zJarmAuthorizationResponse, +} from './z-jarm-authorization-response' -export const jarmAuthResponseValidate = (options: { - clientId: string - authorizationResponse: JarmAuthResponse | JarmAuthResponseEncryptedOnly +export const jarmAuthorizationResponseValidate = (options: { + expectedClientId: string + authorizationResponse: JarmAuthorizationResponse | JarmAuthorizationResponseEncryptedOnly }) => { - const { clientId, authorizationResponse } = options + const { expectedClientId, authorizationResponse } = options // The traditional Jarm Validation Methods do not account for the encrypted response. - if (!zJarmAuthResponse.safeParse(authorizationResponse).success) { + if (!zJarmAuthorizationResponse.safeParse(authorizationResponse).success) { return } // 3. The client obtains the aud element from the JWT and checks whether it matches the client id the client used to identify itself in the corresponding authorization request. If the check fails, the client MUST abort processing and refuse the response. - if (clientId !== authorizationResponse.aud) { + if (expectedClientId !== authorizationResponse.aud) { throw new Oauth2Error( `Invalid 'aud' claim in JARM authorization response. Expected '${ - clientId + expectedClientId }' received '${JSON.stringify(authorizationResponse.aud)}'.` ) } diff --git a/packages/openid4vp/src/jarm/jarm-authorization-response/verify-jarm-authorization-response.ts b/packages/openid4vp/src/jarm/jarm-authorization-response/verify-jarm-authorization-response.ts new file mode 100644 index 0000000..75448da --- /dev/null +++ b/packages/openid4vp/src/jarm/jarm-authorization-response/verify-jarm-authorization-response.ts @@ -0,0 +1,140 @@ +import { + type CallbackContext, + Oauth2Error, + decodeJwt, + jwtSignerFromJwt, + zCompactJwe, + zCompactJwt, + zJwtHeader, +} from '@openid4vc/oauth2' +import z from 'zod' +import type { Openid4vpAuthorizationRequest } from '../../authorization-request/z-authorization-request' +import type { Openid4vpAuthorizationRequestDcApi } from '../../authorization-request/z-authorization-request-dc-api' +import { extractJwksFromClientMetadata } from '../jarm-extract-jwks' +import { jarmAuthorizationResponseValidate } from './jarm-validate-authorization-response' +import { + type JarmAuthorizationResponse, + type JarmAuthorizationResponseEncryptedOnly, + zJarmAuthorizationResponse, + zJarmAuthorizationResponseEncryptedOnly, +} from './z-jarm-authorization-response' + +export enum JarmMode { + Signed = 'Signed', + Encrypted = 'Encrypted', + SignedEncrypted = 'SignedEncrypted', +} + +/** + * The client decrypts the JWT using the default key for the respective issuer or, + * if applicable, determined by the kid JWT header parameter. + * The key might be a private key, where the corresponding public key is registered + * with the expected issuer of the response ("use":"enc" via the client's metadata jwks or jwks_uri) + * or a key derived from its client secret (see Section 2.2). + */ +const decryptJarmAuthorizationResponseJwt = async (options: { + jarmAuthorizationResponseJwt: string + callbacks: Pick + authorizationRequestPayload: Openid4vpAuthorizationRequest | Openid4vpAuthorizationRequestDcApi +}) => { + const { jarmAuthorizationResponseJwt, callbacks, authorizationRequestPayload } = options + + // NOTE: previously we required `kid` to be present in the JARM header, but not all implementations seem to + // add this, so we removed the check. For now we try to extract the JWK from the request, if we are not successfull + // (because e.g. the request used client_metadata_uri) the decryptJwe callback has to handle this edge case + // See https://github.com/openid/OpenID4VP/issues/441 + const encryptionJwk = authorizationRequestPayload.client_metadata?.jwks + ? extractJwksFromClientMetadata({ + ...authorizationRequestPayload.client_metadata, + jwks: authorizationRequestPayload.client_metadata.jwks, + }).encJwk + : undefined + + const result = await callbacks.decryptJwe(jarmAuthorizationResponseJwt, { jwk: encryptionJwk }) + if (!result.decrypted) { + throw new Oauth2Error('Failed to decrypt jarm auth response.') + } + + return result.payload +} + +export interface VerifyJarmAuthorizationResponseOptions { + jarmAuthorizationResponseJwt: string + + authorizationRequestPayload: Openid4vpAuthorizationRequest | Openid4vpAuthorizationRequestDcApi + + /** + * The client id of the authorization request. This should be the effective client id, + * meaning that if no client_id was present in the authorization request and DC API is used + * it should be `web-origin:` + */ + expectedClientId: string + + callbacks: Pick +} + +export type VerifiedJarmAuthorizationResponse = Awaited> + +/** + * Validate a JARM direct_post.jwt compliant authentication response + * * The decryption key should be resolvable using the the protected header's 'kid' field + * * The signature verification jwk should be resolvable using the jws protected header's 'kid' field and the payload's 'iss' field. + */ +export async function verifyJarmAuthorizationResponse(options: VerifyJarmAuthorizationResponseOptions) { + const { jarmAuthorizationResponseJwt, callbacks, expectedClientId, authorizationRequestPayload } = options + + const requestDataIsEncrypted = zCompactJwe.safeParse(jarmAuthorizationResponseJwt).success + const decryptedRequestData = requestDataIsEncrypted + ? await decryptJarmAuthorizationResponseJwt({ + jarmAuthorizationResponseJwt, + callbacks, + authorizationRequestPayload, + }) + : jarmAuthorizationResponseJwt + + const responseIsSigned = zCompactJwt.safeParse(decryptedRequestData).success + if (!requestDataIsEncrypted && !responseIsSigned) { + throw new Oauth2Error('Jarm Auth Response must be either encrypted, signed, or signed and encrypted.') + } + + let jarmAuthorizationResponse: JarmAuthorizationResponse | JarmAuthorizationResponseEncryptedOnly + + if (responseIsSigned) { + const { header: jwsProtectedHeader, payload: jwsPayload } = decodeJwt({ + jwt: decryptedRequestData, + headerSchema: z.object({ ...zJwtHeader.shape, kid: z.string() }), + }) + + const response = zJarmAuthorizationResponse.parse(jwsPayload) + const jwtSigner = jwtSignerFromJwt({ header: jwsProtectedHeader, payload: jwsPayload }) + + const verificationResult = await options.callbacks.verifyJwt(jwtSigner, { + compact: decryptedRequestData, + header: jwsProtectedHeader, + payload: jwsPayload, + }) + + if (!verificationResult.verified) { + throw new Oauth2Error('Jarm Auth Response is not valid.') + } + + jarmAuthorizationResponse = response + } else { + const jsonRequestData: unknown = JSON.parse(decryptedRequestData) + jarmAuthorizationResponse = zJarmAuthorizationResponseEncryptedOnly.parse(jsonRequestData) + } + + jarmAuthorizationResponseValidate({ + expectedClientId, + authorizationResponse: jarmAuthorizationResponse, + }) + const type: JarmMode = + requestDataIsEncrypted && responseIsSigned + ? JarmMode.SignedEncrypted + : requestDataIsEncrypted + ? JarmMode.Encrypted + : JarmMode.Signed + + const issuer = jarmAuthorizationResponse.iss + return { jarmAuthorizationResponse, type, issuer } +} diff --git a/packages/openid4vp/src/jarm/jarm-auth-response/z-jarm-auth-response.ts b/packages/openid4vp/src/jarm/jarm-authorization-response/z-jarm-authorization-response.ts similarity index 72% rename from packages/openid4vp/src/jarm/jarm-auth-response/z-jarm-auth-response.ts rename to packages/openid4vp/src/jarm/jarm-authorization-response/z-jarm-authorization-response.ts index ee83df4..4022347 100644 --- a/packages/openid4vp/src/jarm/jarm-auth-response/z-jarm-auth-response.ts +++ b/packages/openid4vp/src/jarm/jarm-authorization-response/z-jarm-authorization-response.ts @@ -4,7 +4,7 @@ import { z } from 'zod' export const zJarmHeader = z.object({ ...zJwtHeader.shape, apu: z.string().optional(), apv: z.string().optional() }) export type JarmHeader = z.infer -export const zJarmAuthResponse = z +export const zJarmAuthorizationResponse = z .object({ /** * iss: The issuer URL of the authorization server that created the response @@ -17,12 +17,12 @@ export const zJarmAuthResponse = z }) .passthrough() -export type JarmAuthResponse = z.infer +export type JarmAuthorizationResponse = z.infer -export const zJarmAuthResponseEncryptedOnly = z +export const zJarmAuthorizationResponseEncryptedOnly = z .object({ ...zJwtPayload.shape, state: z.optional(z.string()), }) .passthrough() -export type JarmAuthResponseEncryptedOnly = z.infer +export type JarmAuthorizationResponseEncryptedOnly = z.infer diff --git a/packages/openid4vp/src/jarm/parse-jarm-auth-response-direct-post-jwt.ts b/packages/openid4vp/src/jarm/parse-jarm-authorization-response-direct-post-jwt.ts similarity index 87% rename from packages/openid4vp/src/jarm/parse-jarm-auth-response-direct-post-jwt.ts rename to packages/openid4vp/src/jarm/parse-jarm-authorization-response-direct-post-jwt.ts index 9c1a7f6..9c50b70 100644 --- a/packages/openid4vp/src/jarm/parse-jarm-auth-response-direct-post-jwt.ts +++ b/packages/openid4vp/src/jarm/parse-jarm-authorization-response-direct-post-jwt.ts @@ -1,7 +1,8 @@ import { Oauth2Error, zCompactJwe, zCompactJwt } from '@openid4vc/oauth2' import { ContentType, URLSearchParams } from '@openid4vc/utils' import z from 'zod' -export async function parseJarmAuthResponseDirectPostJwt(request: Request) { + +export async function parseJarmAuthorizationResponseDirectPostJwt(request: Request) { const contentType = request.headers.get('content-type') if (!contentType) { @@ -24,7 +25,7 @@ export async function parseJarmAuthResponseDirectPostJwt(request: Request) { const isJweOrJws = z.union([zCompactJwt, zCompactJwe]).safeParse(requestData.response) if (isJweOrJws.success) { - return { jarmAuthResponseJwt: requestData.response } + return { jarmAuthorizationResponseJwt: requestData.response } } throw new Oauth2Error('Received invalid JARM auth response. Expected JWE or JWT.', { diff --git a/packages/openid4vp/src/version.ts b/packages/openid4vp/src/version.ts index 4f13394..d432446 100644 --- a/packages/openid4vp/src/version.ts +++ b/packages/openid4vp/src/version.ts @@ -14,31 +14,6 @@ export function parseAuthorizationRequestVersion( ): OpenId4VpVersion { const requirements: ['<' | '>=', OpenId4VpVersion][] = [] - // 23 - - const vp_formats = request.client_metadata?.vp_formats - // There might be some time we'd like to include both, as the update of the identifier can be somewhat tricky. - //if (vp_formats) { - //if (Object.keys(vp_formats).includes('vc+sd-jwt' satisfies CredentialFormat)) { - //requirements.push(['<', 23]) - //} - - //if (Object.keys(vp_formats).includes('dc+sd-jwt' satisfies CredentialFormat)) { - //requirements.push(['>=', 23]) - //} - - //if (Object.keys(vp_formats).includes('vc+sd-jwt' satisfies CredentialFormat)) { - //requirements.push(['>=', 21]) - //} - //} - - //if ( - //request.client_metadata?.vp_formats && - //Object.keys(request.client_metadata?.vp_formats).some(val => val === 'vc+sd-jwt') - //) { - //requirements.push(['>=', 21]) - //} - if ( isOpenid4vpAuthorizationRequestDcApi(request) && (request.response_mode === 'w3c_dc_api' || request.response_mode === 'w3c_dc_api.jwt') diff --git a/packages/openid4vp/tests/full-flow.test.ts b/packages/openid4vp/tests/full-flow.test.ts index f02dde3..9a0b0b6 100644 --- a/packages/openid4vp/tests/full-flow.test.ts +++ b/packages/openid4vp/tests/full-flow.test.ts @@ -68,7 +68,7 @@ describe('Full E2E openid4vp test', () => { test('openid4vp (unsigned)', async () => { callbacks.fetch = fetch - const requestPayload = { + const authorizationRequestPayload = { nonce: 'nonce', client_metadata: {}, response_mode: 'direct_post', @@ -79,7 +79,7 @@ describe('Full E2E openid4vp test', () => { } as const server.resetHandlers( - http.post(requestPayload.response_uri, async ({ request }) => { + http.post(authorizationRequestPayload.response_uri, async ({ request }) => { try { const formData = await request.formData() const rawResponsePayload = Object.fromEntries(formData.entries()) @@ -88,15 +88,15 @@ describe('Full E2E openid4vp test', () => { '{"orgeuuniversity":"uQADZ3ZlcnNpb25jMS4waWRvY3VtZW50c4GjZ2RvY1R5cGVxb3JnLmV1LnVuaXZlcnNpdHlsaXNzdWVyU2lnbmVkuQACam5hbWVTcGFjZXOhd2V1LmV1cm9wYS5lYy5ldWRpLnBpZC4xg9gYWGGkaGRpZ2VzdElEA3FlbGVtZW50SWRlbnRpZmllcmRuYW1lbGVsZW1lbnRWYWx1ZWhKb2huIERvZWZyYW5kb21YIEWyJGCZTVxZPQlUipZgJrFHAG953ShscUxOhqcVj5zZ2BhYY6RoZGlnZXN0SUQBcWVsZW1lbnRJZGVudGlmaWVyZmRlZ3JlZWxlbGVtZW50VmFsdWVoYmFjaGVsb3JmcmFuZG9tWCAK5tJW8iDu2_pFMZbncXHsVSoMPB-j6NHzTidrmAwglNgYWGakaGRpZ2VzdElEAnFlbGVtZW50SWRlbnRpZmllcmRkYXRlbGVsZW1lbnRWYWx1ZdkD7GoyMDI1LTAyLTI3ZnJhbmRvbVggHkxQ5P0ym4b-S2YhPULns-6950O_pu1f01YH4KZf7RFqaXNzdWVyQXV0aIRDoQEmogRYMXpEbmFlYVpVV3dVUmpyNVE1TTJnMnVjZjE2bjNwVUx4bzRDaHFuZktYTnJNMk53MUsYIYFZAR4wggEaMIHAoAMCAQICEBe3X5XsrOs2ZhTfjDA0whkwCgYIKoZIzj0EAwIwDTELMAkGA1UEAxMCREUwHhcNMjUwMjI3MDkxOTMzWhcNMjYwMjI3MDkxOTMzWjANMQswCQYDVQQDEwJERTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJaBQfh-jpNhVxqwlTlv39Gm3nkewRvcA4p9TRao8YlC271XGo2ojTBcNh-RX65ql--tNygiJh6BHNhz98VPcVCjAjAAMAoGCCqGSM49BAMCA0kAMEYCIQCx9YNNPzp7YPPdy4k3IVR_XLl6e7bnKS91cGEwArbMzgIhAIuglfUtfZM-ZYoEX1xYB47wMm666Trcykjag1sYMfgZWQIA2BhZAfu5AAZndmVyc2lvbmMxLjBvZGlnZXN0QWxnb3JpdGhtZ1NIQS0yNTZsdmFsdWVEaWdlc3RzoXdldS5ldXJvcGEuZWMuZXVkaS5waWQuMaUAWCAVPsuROLqNqsVsaaCrTJzdHdZ_IznS1nCh1qVKWZ2ezQFYIPeazaLvXv5M-s2h9713AG_QJcCZW-eu6UGzoGI6O9ZnAlggqnuFzR9wHj_51ftmjpqo5s-XIvjoLPw-5sfQ-IGtp1kDWCByQ8dnllyBLPNL1DgHqA0B8yAuvY-EoCVGmEAWMAbeowRYICrRJfwVhE9JJzLpa6tfc1LJ9rvv0sr2OzQ9ot-68CnNbWRldmljZUtleUluZm-5AAFpZGV2aWNlS2V5pAECIAEhWCAakKubHmMGh9_OHyUGEZ8102VOMM6j7C-MlEyHDyYJ1yJYIJbQibZhVZZ2ghRAClhsQO_fXpWcqRQKhriSZ4azbE2oZ2RvY1R5cGVxb3JnLmV1LnVuaXZlcnNpdHlsdmFsaWRpdHlJbmZvuQAEZnNpZ25lZMB0MjAyNS0wMi0yN1QwOToxOTozM1ppdmFsaWRGcm9twHQyMDI1LTAyLTI3VDA5OjE5OjMzWmp2YWxpZFVudGlswHQyMDI2LTAyLTI3VDA5OjE5OjMzWm5leHBlY3RlZFVwZGF0ZfdYQCFznxzCxRUqSe65YB3p1pjTEK7Sma4-JTUhnbwsdmtAoLBv5NMlu54mHj7oGCRBmN3G_un8GeX2opmG78yVdJNsZGV2aWNlU2lnbmVkuQACam5hbWVTcGFjZXPYGEGgamRldmljZUF1dGi5AAJvZGV2aWNlU2lnbmF0dXJlhEOhASag9lhA0kcpmpDK-lGZnNZ_cCjb_CbEz6UZ_MwymXE9r1j9YFrSpoahLj6dkprCZuaS1K79dvSZQTHw23KHzP_JAGFcj2lkZXZpY2VNYWP3ZnN0YXR1cwA","OpenBadgeCredentialDescriptor":"eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rcnpRUEJyNHB5cUM3NzZLS3RyejEzU2NoTTVlUFBic3N1UHVRWmI1dDR1S1EifQ.eyJ2Y3QiOiJPcGVuQmFkZ2VDcmVkZW50aWFsIiwiZGVncmVlIjoiYmFjaGVsb3IiLCJjbmYiOnsia2lkIjoiZGlkOmtleTp6Nk1rcEdSNGdzNFJjM1pwaDR2ajh3Um5qbkF4Z0FQU3hjUjhNQVZLdXRXc3BRemMjejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjIn0sImlzcyI6ImRpZDprZXk6ejZNa3J6UVBCcjRweXFDNzc2S0t0cnoxM1NjaE01ZVBQYnNzdVB1UVpiNXQ0dUtRIiwiaWF0IjoxNzQwNjQ3OTczLCJfc2QiOlsiSEtyNW1mYkE2OGRZY0JYZTVMRDREdFJHdjVoNWp0NEVDT2JSOWF5VkJCOCIsImlBYS1YVXhGaG1nU0g2SWhTOHZ2cm1TWF95VHJ2ZTQtZjFjTWRPLU41VUUiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.pP2G3YxexmDGpF-vfb04zMhVLLJGjkiVUiA-I-aLVdhNqzCjexOAu9xQOt0uTGT-4_ly_j66FXR2v4p0z9iyBw~WyI2NTgyNDY2MzM4MjYyODgyMjY2Nzc2MTkiLCJ1bml2ZXJzaXR5IiwiaW5uc2JydWNrIl0~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFZERTQSJ9.eyJpYXQiOjE3NDA2NDc5NzYsIm5vbmNlIjoiNDczODIzNzM5Nzg4MzU1NzEyMTc3MzUwIiwiYXVkIjoieDUwOV9zYW5fZG5zOmxvY2FsaG9zdDoxMjM0IiwidHJhbnNhY3Rpb25fZGF0YV9oYXNoZXMiOlsiWHd5VmQ3d0ZSRWRWV0xwbmk1UU5IZ2dOV1hvMko0TG41OHQyX2VjSjczcyJdLCJ0cmFuc2FjdGlvbl9kYXRhX2hhc2hlc19hbGciOiJzaGEtMjU2Iiwic2RfaGFzaCI6IkJFQ09xRm9OMjM0UldtRzEtTUlSWXl5SnpXTDg1XzRLdTdHNC1KcUl4V0UifQ.ZnDZBS8o_WPsTlvuUO0SnARUIvx1M8t9Fd6TiaQbMtT_0OA7QCvdpisSh9-NfLAb40frAED875W8RI3zi06-DQ"}', }) - const parsedResponse = parseOpenid4VpAuthorizationResponsePayload(rawResponsePayload) - expect(parsedResponse).toMatchObject({ + const authorizationResponsePayload = parseOpenid4VpAuthorizationResponsePayload(rawResponsePayload) + expect(authorizationResponsePayload).toMatchObject({ vp_token: '{"orgeuuniversity":"uQADZ3ZlcnNpb25jMS4waWRvY3VtZW50c4GjZ2RvY1R5cGVxb3JnLmV1LnVuaXZlcnNpdHlsaXNzdWVyU2lnbmVkuQACam5hbWVTcGFjZXOhd2V1LmV1cm9wYS5lYy5ldWRpLnBpZC4xg9gYWGGkaGRpZ2VzdElEA3FlbGVtZW50SWRlbnRpZmllcmRuYW1lbGVsZW1lbnRWYWx1ZWhKb2huIERvZWZyYW5kb21YIEWyJGCZTVxZPQlUipZgJrFHAG953ShscUxOhqcVj5zZ2BhYY6RoZGlnZXN0SUQBcWVsZW1lbnRJZGVudGlmaWVyZmRlZ3JlZWxlbGVtZW50VmFsdWVoYmFjaGVsb3JmcmFuZG9tWCAK5tJW8iDu2_pFMZbncXHsVSoMPB-j6NHzTidrmAwglNgYWGakaGRpZ2VzdElEAnFlbGVtZW50SWRlbnRpZmllcmRkYXRlbGVsZW1lbnRWYWx1ZdkD7GoyMDI1LTAyLTI3ZnJhbmRvbVggHkxQ5P0ym4b-S2YhPULns-6950O_pu1f01YH4KZf7RFqaXNzdWVyQXV0aIRDoQEmogRYMXpEbmFlYVpVV3dVUmpyNVE1TTJnMnVjZjE2bjNwVUx4bzRDaHFuZktYTnJNMk53MUsYIYFZAR4wggEaMIHAoAMCAQICEBe3X5XsrOs2ZhTfjDA0whkwCgYIKoZIzj0EAwIwDTELMAkGA1UEAxMCREUwHhcNMjUwMjI3MDkxOTMzWhcNMjYwMjI3MDkxOTMzWjANMQswCQYDVQQDEwJERTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJaBQfh-jpNhVxqwlTlv39Gm3nkewRvcA4p9TRao8YlC271XGo2ojTBcNh-RX65ql--tNygiJh6BHNhz98VPcVCjAjAAMAoGCCqGSM49BAMCA0kAMEYCIQCx9YNNPzp7YPPdy4k3IVR_XLl6e7bnKS91cGEwArbMzgIhAIuglfUtfZM-ZYoEX1xYB47wMm666Trcykjag1sYMfgZWQIA2BhZAfu5AAZndmVyc2lvbmMxLjBvZGlnZXN0QWxnb3JpdGhtZ1NIQS0yNTZsdmFsdWVEaWdlc3RzoXdldS5ldXJvcGEuZWMuZXVkaS5waWQuMaUAWCAVPsuROLqNqsVsaaCrTJzdHdZ_IznS1nCh1qVKWZ2ezQFYIPeazaLvXv5M-s2h9713AG_QJcCZW-eu6UGzoGI6O9ZnAlggqnuFzR9wHj_51ftmjpqo5s-XIvjoLPw-5sfQ-IGtp1kDWCByQ8dnllyBLPNL1DgHqA0B8yAuvY-EoCVGmEAWMAbeowRYICrRJfwVhE9JJzLpa6tfc1LJ9rvv0sr2OzQ9ot-68CnNbWRldmljZUtleUluZm-5AAFpZGV2aWNlS2V5pAECIAEhWCAakKubHmMGh9_OHyUGEZ8102VOMM6j7C-MlEyHDyYJ1yJYIJbQibZhVZZ2ghRAClhsQO_fXpWcqRQKhriSZ4azbE2oZ2RvY1R5cGVxb3JnLmV1LnVuaXZlcnNpdHlsdmFsaWRpdHlJbmZvuQAEZnNpZ25lZMB0MjAyNS0wMi0yN1QwOToxOTozM1ppdmFsaWRGcm9twHQyMDI1LTAyLTI3VDA5OjE5OjMzWmp2YWxpZFVudGlswHQyMDI2LTAyLTI3VDA5OjE5OjMzWm5leHBlY3RlZFVwZGF0ZfdYQCFznxzCxRUqSe65YB3p1pjTEK7Sma4-JTUhnbwsdmtAoLBv5NMlu54mHj7oGCRBmN3G_un8GeX2opmG78yVdJNsZGV2aWNlU2lnbmVkuQACam5hbWVTcGFjZXPYGEGgamRldmljZUF1dGi5AAJvZGV2aWNlU2lnbmF0dXJlhEOhASag9lhA0kcpmpDK-lGZnNZ_cCjb_CbEz6UZ_MwymXE9r1j9YFrSpoahLj6dkprCZuaS1K79dvSZQTHw23KHzP_JAGFcj2lkZXZpY2VNYWP3ZnN0YXR1cwA","OpenBadgeCredentialDescriptor":"eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rcnpRUEJyNHB5cUM3NzZLS3RyejEzU2NoTTVlUFBic3N1UHVRWmI1dDR1S1EifQ.eyJ2Y3QiOiJPcGVuQmFkZ2VDcmVkZW50aWFsIiwiZGVncmVlIjoiYmFjaGVsb3IiLCJjbmYiOnsia2lkIjoiZGlkOmtleTp6Nk1rcEdSNGdzNFJjM1pwaDR2ajh3Um5qbkF4Z0FQU3hjUjhNQVZLdXRXc3BRemMjejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjIn0sImlzcyI6ImRpZDprZXk6ejZNa3J6UVBCcjRweXFDNzc2S0t0cnoxM1NjaE01ZVBQYnNzdVB1UVpiNXQ0dUtRIiwiaWF0IjoxNzQwNjQ3OTczLCJfc2QiOlsiSEtyNW1mYkE2OGRZY0JYZTVMRDREdFJHdjVoNWp0NEVDT2JSOWF5VkJCOCIsImlBYS1YVXhGaG1nU0g2SWhTOHZ2cm1TWF95VHJ2ZTQtZjFjTWRPLU41VUUiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.pP2G3YxexmDGpF-vfb04zMhVLLJGjkiVUiA-I-aLVdhNqzCjexOAu9xQOt0uTGT-4_ly_j66FXR2v4p0z9iyBw~WyI2NTgyNDY2MzM4MjYyODgyMjY2Nzc2MTkiLCJ1bml2ZXJzaXR5IiwiaW5uc2JydWNrIl0~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFZERTQSJ9.eyJpYXQiOjE3NDA2NDc5NzYsIm5vbmNlIjoiNDczODIzNzM5Nzg4MzU1NzEyMTc3MzUwIiwiYXVkIjoieDUwOV9zYW5fZG5zOmxvY2FsaG9zdDoxMjM0IiwidHJhbnNhY3Rpb25fZGF0YV9oYXNoZXMiOlsiWHd5VmQ3d0ZSRWRWV0xwbmk1UU5IZ2dOV1hvMko0TG41OHQyX2VjSjczcyJdLCJ0cmFuc2FjdGlvbl9kYXRhX2hhc2hlc19hbGciOiJzaGEtMjU2Iiwic2RfaGFzaCI6IkJFQ09xRm9OMjM0UldtRzEtTUlSWXl5SnpXTDg1XzRLdTdHNC1KcUl4V0UifQ.ZnDZBS8o_WPsTlvuUO0SnARUIvx1M8t9Fd6TiaQbMtT_0OA7QCvdpisSh9-NfLAb40frAED875W8RI3zi06-DQ"}', }) const validatedResult = validateOpenid4vpAuthorizationResponsePayload({ - requestPayload: requestPayload, - responsePayload: parsedResponse, + authorizationRequestPayload, + authorizationResponsePayload, }) expect(validatedResult).toMatchObject({ @@ -122,26 +122,27 @@ describe('Full E2E openid4vp test', () => { // verifier const authorizationRequest = await createOpenid4vpAuthorizationRequest({ - requestPayload, + authorizationRequestPayload, callbacks, }) expect(authorizationRequest).toMatchObject({ - authRequestObject: requestPayload, - authRequest: - 'openid4vp://?nonce=nonce&client_metadata=%7B%7D&response_mode=direct_post&dcql_query=%7B%22credentials%22%3A%5B%7B%22id%22%3A%22orgeuuniversity%22%2C%22format%22%3A%22mso_mdoc%22%2C%22meta%22%3A%7B%22doctype_value%22%3A%22org.eu.university%22%7D%2C%22claims%22%3A%5B%7B%22namespace%22%3A%22eu.europa.ec.eudi.pid.1%22%2C%22claim_name%22%3A%22name%22%7D%2C%7B%22namespace%22%3A%22eu.europa.ec.eudi.pid.1%22%2C%22claim_name%22%3A%22degree%22%7D%2C%7B%22namespace%22%3A%22eu.europa.ec.eudi.pid.1%22%2C%22claim_name%22%3A%22date%22%7D%5D%7D%2C%7B%22id%22%3A%22OpenBadgeCredentialDescriptor%22%2C%22format%22%3A%22dc%2Bsd-jwt%22%2C%22meta%22%3A%7B%22vct_values%22%3A%5B%22OpenBadgeCredential%22%5D%7D%2C%22claims%22%3A%5B%7B%22path%22%3A%5B%22university%22%5D%7D%5D%7D%5D%7D&response_uri=https%3A%2F%2Fexample.com%2Fresponse_uri&response_type=vp_token&client_id=client_id', + authorizationRequestPayload, + authorizationRequestObject: authorizationRequestPayload, + authorizationRequest: + 'openid4vp://?response_type=vp_token&client_id=client_id&response_uri=https%3A%2F%2Fexample.com%2Fresponse_uri&response_mode=direct_post&nonce=nonce&dcql_query=%7B%22credentials%22%3A%5B%7B%22id%22%3A%22orgeuuniversity%22%2C%22format%22%3A%22mso_mdoc%22%2C%22meta%22%3A%7B%22doctype_value%22%3A%22org.eu.university%22%7D%2C%22claims%22%3A%5B%7B%22namespace%22%3A%22eu.europa.ec.eudi.pid.1%22%2C%22claim_name%22%3A%22name%22%7D%2C%7B%22namespace%22%3A%22eu.europa.ec.eudi.pid.1%22%2C%22claim_name%22%3A%22degree%22%7D%2C%7B%22namespace%22%3A%22eu.europa.ec.eudi.pid.1%22%2C%22claim_name%22%3A%22date%22%7D%5D%7D%2C%7B%22id%22%3A%22OpenBadgeCredentialDescriptor%22%2C%22format%22%3A%22dc%2Bsd-jwt%22%2C%22meta%22%3A%7B%22vct_values%22%3A%5B%22OpenBadgeCredential%22%5D%7D%2C%22claims%22%3A%5B%7B%22path%22%3A%5B%22university%22%5D%7D%5D%7D%5D%7D&client_metadata=%7B%7D', jar: undefined, }) // holder const resolved = await resolveOpenid4vpAuthorizationRequest({ - requestPayload, + authorizationRequestPayload, callbacks, }) expect(resolved).toMatchObject({ transactionData: undefined, - requestPayload: { + authorizationRequestPayload: { response_type: 'vp_token', client_id: 'client_id', response_uri: 'https://example.com/response_uri', @@ -164,13 +165,13 @@ describe('Full E2E openid4vp test', () => { }) const response = await createOpenid4vpAuthorizationResponse({ - requestPayload, - responsePayload: { vp_token: exampleVptoken }, + authorizationRequestPayload, + authorizationResponsePayload: { vp_token: exampleVptoken }, callbacks, }) expect(response).toMatchObject({ - responsePayload: { + authorizationResponsePayload: { vp_token: { orgeuuniversity: 'uQADZ3ZlcnNpb25jMS4waWRvY3VtZW50c4GjZ2RvY1R5cGVxb3JnLmV1LnVuaXZlcnNpdHlsaXNzdWVyU2lnbmVkuQACam5hbWVTcGFjZXOhd2V1LmV1cm9wYS5lYy5ldWRpLnBpZC4xg9gYWGGkaGRpZ2VzdElEA3FlbGVtZW50SWRlbnRpZmllcmRuYW1lbGVsZW1lbnRWYWx1ZWhKb2huIERvZWZyYW5kb21YIEWyJGCZTVxZPQlUipZgJrFHAG953ShscUxOhqcVj5zZ2BhYY6RoZGlnZXN0SUQBcWVsZW1lbnRJZGVudGlmaWVyZmRlZ3JlZWxlbGVtZW50VmFsdWVoYmFjaGVsb3JmcmFuZG9tWCAK5tJW8iDu2_pFMZbncXHsVSoMPB-j6NHzTidrmAwglNgYWGakaGRpZ2VzdElEAnFlbGVtZW50SWRlbnRpZmllcmRkYXRlbGVsZW1lbnRWYWx1ZdkD7GoyMDI1LTAyLTI3ZnJhbmRvbVggHkxQ5P0ym4b-S2YhPULns-6950O_pu1f01YH4KZf7RFqaXNzdWVyQXV0aIRDoQEmogRYMXpEbmFlYVpVV3dVUmpyNVE1TTJnMnVjZjE2bjNwVUx4bzRDaHFuZktYTnJNMk53MUsYIYFZAR4wggEaMIHAoAMCAQICEBe3X5XsrOs2ZhTfjDA0whkwCgYIKoZIzj0EAwIwDTELMAkGA1UEAxMCREUwHhcNMjUwMjI3MDkxOTMzWhcNMjYwMjI3MDkxOTMzWjANMQswCQYDVQQDEwJERTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJaBQfh-jpNhVxqwlTlv39Gm3nkewRvcA4p9TRao8YlC271XGo2ojTBcNh-RX65ql--tNygiJh6BHNhz98VPcVCjAjAAMAoGCCqGSM49BAMCA0kAMEYCIQCx9YNNPzp7YPPdy4k3IVR_XLl6e7bnKS91cGEwArbMzgIhAIuglfUtfZM-ZYoEX1xYB47wMm666Trcykjag1sYMfgZWQIA2BhZAfu5AAZndmVyc2lvbmMxLjBvZGlnZXN0QWxnb3JpdGhtZ1NIQS0yNTZsdmFsdWVEaWdlc3RzoXdldS5ldXJvcGEuZWMuZXVkaS5waWQuMaUAWCAVPsuROLqNqsVsaaCrTJzdHdZ_IznS1nCh1qVKWZ2ezQFYIPeazaLvXv5M-s2h9713AG_QJcCZW-eu6UGzoGI6O9ZnAlggqnuFzR9wHj_51ftmjpqo5s-XIvjoLPw-5sfQ-IGtp1kDWCByQ8dnllyBLPNL1DgHqA0B8yAuvY-EoCVGmEAWMAbeowRYICrRJfwVhE9JJzLpa6tfc1LJ9rvv0sr2OzQ9ot-68CnNbWRldmljZUtleUluZm-5AAFpZGV2aWNlS2V5pAECIAEhWCAakKubHmMGh9_OHyUGEZ8102VOMM6j7C-MlEyHDyYJ1yJYIJbQibZhVZZ2ghRAClhsQO_fXpWcqRQKhriSZ4azbE2oZ2RvY1R5cGVxb3JnLmV1LnVuaXZlcnNpdHlsdmFsaWRpdHlJbmZvuQAEZnNpZ25lZMB0MjAyNS0wMi0yN1QwOToxOTozM1ppdmFsaWRGcm9twHQyMDI1LTAyLTI3VDA5OjE5OjMzWmp2YWxpZFVudGlswHQyMDI2LTAyLTI3VDA5OjE5OjMzWm5leHBlY3RlZFVwZGF0ZfdYQCFznxzCxRUqSe65YB3p1pjTEK7Sma4-JTUhnbwsdmtAoLBv5NMlu54mHj7oGCRBmN3G_un8GeX2opmG78yVdJNsZGV2aWNlU2lnbmVkuQACam5hbWVTcGFjZXPYGEGgamRldmljZUF1dGi5AAJvZGV2aWNlU2lnbmF0dXJlhEOhASag9lhA0kcpmpDK-lGZnNZ_cCjb_CbEz6UZ_MwymXE9r1j9YFrSpoahLj6dkprCZuaS1K79dvSZQTHw23KHzP_JAGFcj2lkZXZpY2VNYWP3ZnN0YXR1cwA', @@ -181,8 +182,8 @@ describe('Full E2E openid4vp test', () => { }) const submissionResult = await submitOpenid4vpAuthorizationResponse({ - responsePayload: response.responsePayload, - requestPayload: requestPayload, + authorizationResponsePayload: response.authorizationResponsePayload, + authorizationRequestPayload, callbacks, }) diff --git a/packages/openid4vp/tests/parse-client-identifier-scheme.test.ts b/packages/openid4vp/tests/parse-client-identifier-scheme.test.ts index 2605cdf..7cbc0e1 100644 --- a/packages/openid4vp/tests/parse-client-identifier-scheme.test.ts +++ b/packages/openid4vp/tests/parse-client-identifier-scheme.test.ts @@ -5,7 +5,7 @@ describe('Correctly parses the client identifier', () => { describe('legacy client_id_scheme', () => { test(`correctly handles legacy client_id_schme 'entity_id'`, () => { const client = parseClientIdentifier({ - request: { + authorizationRequestPayload: { response_mode: 'direct_post', client_id: 'https://example.com', nonce: 'nonce', @@ -27,7 +27,7 @@ describe('Correctly parses the client identifier', () => { const client = parseClientIdentifier({ // @ts-expect-error jar: { signer: { publicJwk: { kid: 'did:example:123#key-1' } } }, - request: { + authorizationRequestPayload: { response_mode: 'direct_post', client_id: 'did:example:123#key-1', nonce: 'nonce', @@ -48,7 +48,7 @@ describe('Correctly parses the client identifier', () => { const client = parseClientIdentifier({ // @ts-expect-error jar: { signer: { method: 'x5c', x5c: ['certificate'] } }, - request: { + authorizationRequestPayload: { response_mode: 'direct_post', client_id: 'example.com', redirect_uri: 'https://example.com', @@ -72,7 +72,7 @@ describe('Correctly parses the client identifier', () => { const client = parseClientIdentifier({ // @ts-expect-error jar: { signer: { method: 'x5c', x5c: ['certificate'] } }, - request: { + authorizationRequestPayload: { response_mode: 'direct_post', client_id: 'https://example.com', redirect_uri: 'https://example.com', @@ -94,7 +94,7 @@ describe('Correctly parses the client identifier', () => { test('correctly assumes no client_id_scheme as pre-registered', () => { const client = parseClientIdentifier({ - request: { + authorizationRequestPayload: { response_mode: 'direct_post', client_id: 'pre-registered client', nonce: 'nonce', @@ -112,7 +112,7 @@ describe('Correctly parses the client identifier', () => { test('correctly applies pre-registered', () => { const client = parseClientIdentifier({ - request: { + authorizationRequestPayload: { response_mode: 'direct_post', client_id: 'pre-registered client', nonce: 'nonce', @@ -133,7 +133,7 @@ describe('Correctly parses the client identifier', () => { describe('client_id_scheme', () => { test(`correctly handles client_id_schme 'entity_id'`, () => { const client = parseClientIdentifier({ - request: { + authorizationRequestPayload: { response_mode: 'direct_post', client_id: 'https://example.com', nonce: 'nonce', @@ -154,7 +154,7 @@ describe('Correctly parses the client identifier', () => { const client = parseClientIdentifier({ // @ts-expect-error jar: { signer: { publicJwk: { kid: 'did:example:123#key-1' } } }, - request: { + authorizationRequestPayload: { response_mode: 'direct_post', client_id: 'did:example:123#key-1', nonce: 'nonce', @@ -174,7 +174,7 @@ describe('Correctly parses the client identifier', () => { const client = parseClientIdentifier({ // @ts-expect-error jar: { signer: { method: 'x5c', x5c: ['certificate'] } }, - request: { + authorizationRequestPayload: { response_mode: 'direct_post', client_id: 'x509_san_dns:example.com', redirect_uri: 'https://example.com', @@ -197,7 +197,7 @@ describe('Correctly parses the client identifier', () => { const client = parseClientIdentifier({ // @ts-expect-error jar: { signer: { method: 'x5c', x5c: ['certificate'] } }, - request: { + authorizationRequestPayload: { response_mode: 'direct_post', client_id: 'x509_san_uri:https://example.com', redirect_uri: 'https://example.com', @@ -218,7 +218,7 @@ describe('Correctly parses the client identifier', () => { test('correctly assumes no client_id_scheme as pre-registered', () => { const client = parseClientIdentifier({ - request: { + authorizationRequestPayload: { response_mode: 'direct_post', client_id: 'pre-registered client', nonce: 'nonce', @@ -236,7 +236,7 @@ describe('Correctly parses the client identifier', () => { test('correctly applies pre-registered', () => { const client = parseClientIdentifier({ - request: { + authorizationRequestPayload: { response_mode: 'direct_post', client_id: 'pre-registered:pre-registered client', nonce: 'nonce', diff --git a/packages/utils/src/content-type.ts b/packages/utils/src/content-type.ts index 3d71f7d..18f8eb8 100644 --- a/packages/utils/src/content-type.ts +++ b/packages/utils/src/content-type.ts @@ -4,7 +4,7 @@ export enum ContentType { XWwwFormUrlencoded = 'application/x-www-form-urlencoded', Json = 'application/json', JwkSet = 'application/jwk-set+json', - OAuthRequestObjectJwt = 'application/oauth-authz-req+jwt', + OAuthAuthorizationRequestJwt = 'application/oauth-authz-req+jwt', Jwt = 'application/jwt', } diff --git a/packages/utils/src/zod-fetcher.ts b/packages/utils/src/zod-fetcher.ts index 72b2be6..53eafd0 100644 --- a/packages/utils/src/zod-fetcher.ts +++ b/packages/utils/src/zod-fetcher.ts @@ -51,7 +51,7 @@ export function createZodFetcher(fetcher = defaultFetcher): ZodFetcher { ) } - if (expectedContentType === ContentType.OAuthRequestObjectJwt) { + if (expectedContentType === ContentType.OAuthAuthorizationRequestJwt) { return { response, result: response.ok ? schema.safeParse(await response.text()) : undefined,