From b70b3322a233cd1c500caff9e3804f5bbfdad540 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 10 Feb 2025 15:03:52 +0100 Subject: [PATCH] fix: incorporate feedback --- .../oauth2/src/Oauth2AuthorizationServer.ts | 2 +- packages/oauth2/src/Oauth2Client.ts | 2 +- packages/oauth2/src/callbacks.ts | 22 ++- .../src/common/jwt/decode-jwt-header.ts | 8 +- packages/oauth2/src/common/jwt/decode-jwt.ts | 12 +- packages/oauth2/src/common/jwt/verify-jwt.ts | 13 ++ packages/oauth2/src/common/z-oauth2-error.ts | 8 + packages/oauth2/src/index.ts | 4 +- packages/oid4vci/src/Oid4vciClient.ts | 2 +- packages/oid4vci/src/Oid4vciIssuer.ts | 2 +- packages/openid4vp/src/Oid4vcVerifier.ts | 61 +++++++ packages/openid4vp/src/Oid4vpClient.ts | 49 ++++++ .../create-authorization-request.ts} | 64 +++++--- .../parse-authorization-request-params.ts | 67 ++++++++ .../resolve-authorization-request.ts | 82 ++++++++++ .../validate-authorization-request.ts} | 31 ++-- .../z-authorization-request.ts} | 15 +- .../create-authorization-response.ts} | 61 +++---- .../parse-authorization-response.ts | 146 +++++++++++++++++ .../submit-authorization-response.ts} | 22 +-- .../validate-authorization-response.ts | 100 ++++++++++++ ...alidate-openid4vp-auth-response-result.ts} | 10 +- .../z-authorization-response.ts} | 7 +- .../parse-client-identifier-scheme.ts | 57 ++++--- .../validate-verifier-attestation-jwt.ts | 149 ++++++------------ packages/openid4vp/src/index.ts | 72 +++++++-- .../validate-jar-request-against-session.ts | 12 +- .../handle-jar-request/verify-jar-request.ts | 81 +++++----- .../fetch-jar-request-object.ts | 18 ++- .../openid4vp/src/jar/z-jar-auth-request.ts | 25 +-- ...=> z-jar-authorization-server-metadata.ts} | 4 +- ...r-metadata.ts => z-jar-client-metadata.ts} | 4 +- .../src/jarm/jarm-auth-response-create.ts | 4 +- .../src/jarm/jarm-auth-response-send.ts | 2 +- .../jarm-validate-auth-response.ts | 9 +- .../verify-jarm-auth-response.ts | 42 ++--- .../openid4vp/src/jarm/jarm-extract-jwks.ts | 2 +- .../openid4vp/src/jarm/jarm-response-mode.ts | 10 ++ .../jarm-assert-metadata-supported.ts | 22 +-- ...> z-jarm-authorization-server-metadata.ts} | 5 +- ...-metadata.ts => z-jarm-client-metadata.ts} | 23 ++- ...arse-jarm-auth-response-direct-post-jwt.ts | 14 +- .../openid4vp/src/models/z-client-metadata.ts | 9 +- .../src/models/z-vp-formats-supported.ts | 8 +- packages/openid4vp/src/models/z-vp-formats.ts | 4 - .../parse-openid4vp-auth-request-params.ts | 55 ------- .../verify-openid4vp-auth-request.ts | 80 ---------- .../verify-openid4vp-auth-response.ts | 97 ------------ .../parse-transaction-data.ts | 20 ++- .../parse-presentations-from-vp-token.ts | 62 +++++--- packages/openid4vp/tsconfig.json | 3 +- packages/utils/package.json | 2 +- packages/utils/src/encoding.ts | 13 -- packages/utils/src/index.ts | 4 - packages/utils/src/parse.ts | 10 +- packages/utils/src/uri-encode-object.ts | 40 ----- packages/utils/src/x-www-form-url-encode.ts | 13 -- 57 files changed, 1031 insertions(+), 734 deletions(-) create mode 100644 packages/openid4vp/src/Oid4vcVerifier.ts create mode 100644 packages/openid4vp/src/Oid4vpClient.ts rename packages/openid4vp/src/{openid4vp-auth-request/create-openid4vp-auth-request.ts => authorization-request/create-authorization-request.ts} (58%) create mode 100644 packages/openid4vp/src/authorization-request/parse-authorization-request-params.ts create mode 100644 packages/openid4vp/src/authorization-request/resolve-authorization-request.ts rename packages/openid4vp/src/{openid4vp-auth-request/validate-openid4vp-auth-request.ts => authorization-request/validate-authorization-request.ts} (66%) rename packages/openid4vp/src/{openid4vp-auth-request/z-openid4vp-auth-request.ts => authorization-request/z-authorization-request.ts} (60%) rename packages/openid4vp/src/{openid4vp-auth-response/create-openid4vp-auth-response.ts => authorization-response/create-authorization-response.ts} (60%) create mode 100644 packages/openid4vp/src/authorization-response/parse-authorization-response.ts rename packages/openid4vp/src/{openid4vp-auth-response/submit-openid4vp-auth-response.ts => authorization-response/submit-authorization-response.ts} (54%) create mode 100644 packages/openid4vp/src/authorization-response/validate-authorization-response.ts rename packages/openid4vp/src/{openid4vp-auth-response/verify-openid4vp-auth-response-result.ts => authorization-response/validate-openid4vp-auth-response-result.ts} (65%) rename packages/openid4vp/src/{openid4vp-auth-response/z-openid4vp-auth-response.ts => authorization-response/z-authorization-response.ts} (61%) rename packages/openid4vp/src/jar/{z-jar-as-metadata.ts => z-jar-authorization-server-metadata.ts} (64%) rename packages/openid4vp/src/jar/{z-jar-dcr-metadata.ts => z-jar-client-metadata.ts} (53%) create mode 100644 packages/openid4vp/src/jarm/jarm-response-mode.ts rename packages/openid4vp/src/jarm/metadata/{z-jarm-as-metadata.ts => z-jarm-authorization-server-metadata.ts} (51%) rename packages/openid4vp/src/jarm/metadata/{z-jarm-dcr-metadata.ts => z-jarm-client-metadata.ts} (72%) delete mode 100644 packages/openid4vp/src/models/z-vp-formats.ts delete mode 100644 packages/openid4vp/src/openid4vp-auth-request/parse-openid4vp-auth-request-params.ts delete mode 100644 packages/openid4vp/src/openid4vp-auth-request/verify-openid4vp-auth-request.ts delete mode 100644 packages/openid4vp/src/openid4vp-auth-response/verify-openid4vp-auth-response.ts delete mode 100644 packages/utils/src/uri-encode-object.ts delete mode 100644 packages/utils/src/x-www-form-url-encode.ts diff --git a/packages/oauth2/src/Oauth2AuthorizationServer.ts b/packages/oauth2/src/Oauth2AuthorizationServer.ts index 05c30b2..ac196d5 100644 --- a/packages/oauth2/src/Oauth2AuthorizationServer.ts +++ b/packages/oauth2/src/Oauth2AuthorizationServer.ts @@ -37,7 +37,7 @@ export interface Oauth2AuthorizationServerOptions { /** * Callbacks required for the oauth2 authorization server */ - callbacks: Omit + callbacks: Omit } export class Oauth2AuthorizationServer { diff --git a/packages/oauth2/src/Oauth2Client.ts b/packages/oauth2/src/Oauth2Client.ts index ae7ddca..3edaec2 100644 --- a/packages/oauth2/src/Oauth2Client.ts +++ b/packages/oauth2/src/Oauth2Client.ts @@ -32,7 +32,7 @@ export interface Oauth2ClientOptions { /** * Callbacks required for the oauth2 client */ - callbacks: Omit + callbacks: Omit } export class Oauth2Client { diff --git a/packages/oauth2/src/callbacks.ts b/packages/oauth2/src/callbacks.ts index 3a86ad8..e473e79 100644 --- a/packages/oauth2/src/callbacks.ts +++ b/packages/oauth2/src/callbacks.ts @@ -39,25 +39,23 @@ export type VerifyJwtCallback = ( } > -export interface DecryptJwtCallbackOptions { +export interface DecryptJweCallbackOptions { jwk: Jwk } -export type DecryptJwtCallback = ( +export type DecryptJweCallback = ( jwe: string, - options?: DecryptJwtCallbackOptions + options?: DecryptJweCallbackOptions ) => OrPromise< | { decrypted: true decryptionJwk: Jwk payload: string - header: JwtHeader } | { decrypted: false decryptionJwk?: Jwk payload?: string - header?: JwtHeader } > @@ -91,7 +89,7 @@ export interface CallbackContext { /** * Decrypt jwe callback for decrypting of Json Web Encryptions */ - decryptJwt: DecryptJwtCallback + decryptJwe: DecryptJweCallback /** * Encrypt jwt callback for encrypting of Json Web Encryptions @@ -125,12 +123,10 @@ export interface CallbackContext { clientAuthentication: ClientAuthenticationCallback /** - * Get the DNS names from a X.509 certificate + * Get the DNS names and URI names from a X.509 certificate */ - getX509SanDnsNames?: (certificate: string) => string[] - - /** - * Get the URI names from a X.509 certificate - */ - getX509SanUriNames?: (certificate: string) => string[] + getX509CertificateMetadata?: (certificate: string) => { + sanDnsNames: string[] + sanUriNames: string[] + } } diff --git a/packages/oauth2/src/common/jwt/decode-jwt-header.ts b/packages/oauth2/src/common/jwt/decode-jwt-header.ts index d211cc9..d4c0621 100644 --- a/packages/oauth2/src/common/jwt/decode-jwt-header.ts +++ b/packages/oauth2/src/common/jwt/decode-jwt-header.ts @@ -6,7 +6,7 @@ import { stringToJsonWithErrorHandling, } from '@openid4vc/utils' import { Oauth2JwtParseError } from '../../error/Oauth2JwtParseError' -import type { InferSchemaOutput } from './decode-jwt' +import type { InferSchemaOrDefaultOutput } from './decode-jwt' import { zJwtHeader } from './z-jwt' export interface DecodeJwtHeaderOptions { @@ -23,7 +23,7 @@ export interface DecodeJwtHeaderOptions = { - header: InferSchemaOutput + header: InferSchemaOrDefaultOutput } export function decodeJwtHeader( @@ -41,10 +41,10 @@ export function decodeJwtHeader diff --git a/packages/oauth2/src/common/jwt/decode-jwt.ts b/packages/oauth2/src/common/jwt/decode-jwt.ts index a9720b6..d27019f 100644 --- a/packages/oauth2/src/common/jwt/decode-jwt.ts +++ b/packages/oauth2/src/common/jwt/decode-jwt.ts @@ -35,8 +35,8 @@ export type DecodeJwtResult< HeaderSchema extends BaseSchema | undefined = undefined, PayloadSchema extends BaseSchema | undefined = undefined, > = { - header: InferSchemaOutput - payload: InferSchemaOutput + header: InferSchemaOrDefaultOutput + payload: InferSchemaOrDefaultOutput signature: string } @@ -56,15 +56,15 @@ export function decodeJwt< 'Unable to parse jwt payload to JSON' ) } catch (error) { - throw new Oauth2JwtParseError('Error parsing JWT') + throw new Oauth2JwtParseError(`Error parsing JWT. ${error instanceof Error ? error.message : ''}`) } const { header } = decodeJwtHeader({ jwt: options.jwt, headerSchema: options.headerSchema }) const payload = parseWithErrorHandling(options.payloadSchema ?? zJwtPayload, payloadJson) return { - header: header as InferSchemaOutput, - payload: payload as InferSchemaOutput, + header: header as InferSchemaOrDefaultOutput, + payload: payload as InferSchemaOrDefaultOutput, signature: jwtParts[2], } } @@ -169,7 +169,7 @@ export function jwtSignerFromJwt({ header, payload }: Pick = T extends undefined ? false : true // Helper type to infer the output type based on whether a schema is provided -export type InferSchemaOutput< +export type InferSchemaOrDefaultOutput< ProvidedSchema extends BaseSchema | undefined, DefaultSchema extends BaseSchema, > = IsSchemaProvided extends true diff --git a/packages/oauth2/src/common/jwt/verify-jwt.ts b/packages/oauth2/src/common/jwt/verify-jwt.ts index 447b89a..80d4fc6 100644 --- a/packages/oauth2/src/common/jwt/verify-jwt.ts +++ b/packages/oauth2/src/common/jwt/verify-jwt.ts @@ -75,6 +75,11 @@ export interface VerifyJwtOptions { * Expected value for the 'sub' claim */ expectedSubject?: string + + /** + * The claims that are required to be present in the jwt. + */ + requiredClaims?: string[] } export interface VerifyJwtReturn { @@ -127,6 +132,14 @@ export async function verifyJwt(options: VerifyJwtOptions): Promise + callbacks: Omit } export class Oid4vciClient { diff --git a/packages/oid4vci/src/Oid4vciIssuer.ts b/packages/oid4vci/src/Oid4vciIssuer.ts index 981fdc3..71eca5a 100644 --- a/packages/oid4vci/src/Oid4vciIssuer.ts +++ b/packages/oid4vci/src/Oid4vciIssuer.ts @@ -37,7 +37,7 @@ export interface Oid4vciIssuerOptions { /** * Callbacks required for the oid4vc issuer */ - callbacks: Omit + callbacks: Omit } export class Oid4vciIssuer { diff --git a/packages/openid4vp/src/Oid4vcVerifier.ts b/packages/openid4vp/src/Oid4vcVerifier.ts new file mode 100644 index 0000000..87d868c --- /dev/null +++ b/packages/openid4vp/src/Oid4vcVerifier.ts @@ -0,0 +1,61 @@ +import type { CallbackContext } from '@openid4vc/oauth2' +import { + type CreateOpenid4vpAuthorizationRequestOptions, + createOpenid4vpAuthorizationRequest, +} from './authorization-request/create-authorization-request' +import { + type ParseOpenid4vpAuthRequestPayloadOptions, + parseOpenid4vpAuthorizationRequestPayload, +} from './authorization-request/parse-authorization-request-params' +import {} from './authorization-request/resolve-authorization-request' +import { + type ParseOpenid4vpAuthorizationResponseOptions, + parseOpenid4vpAuthorizationResponse, +} from './authorization-response/parse-authorization-response' +import { + type ValidateOpenid4vpAuthorizationResponseOptions, + validateOpenid4vpAuthorizationResponse, +} from './authorization-response/validate-authorization-response' +import type { ParseTransactionDataOptions } from './transaction-data/parse-transaction-data' +import { parseTransactionData } from './transaction-data/parse-transaction-data' +import { + type ParsePresentationsFromVpTokenOptions, + parsePresentationsFromVpToken, +} from './vp-token/parse-presentations-from-vp-token' + +export interface Oid4vciVerifierOptions { + /** + * Callbacks required for the oid4vc issuer + */ + callbacks: Omit +} + +export class Oid4vcVerifier { + public constructor(private options: Oid4vciVerifierOptions) {} + + public async createOpenId4vpAuthorizationRequest( + options: Omit + ) { + return createOpenid4vpAuthorizationRequest({ ...options, callbacks: this.options.callbacks }) + } + + public parseOpenid4vpAuthorizationRequestPayload(options: ParseOpenid4vpAuthRequestPayloadOptions) { + return parseOpenid4vpAuthorizationRequestPayload(options) + } + + public parseOpenid4vpAuthorizationResponse(options: ParseOpenid4vpAuthorizationResponseOptions) { + return parseOpenid4vpAuthorizationResponse(options) + } + + public validateOpenid4vpAuthorizationResponse(options: ValidateOpenid4vpAuthorizationResponseOptions) { + return validateOpenid4vpAuthorizationResponse(options) + } + + public parsePresentationsFromVpToken(options: ParsePresentationsFromVpTokenOptions) { + return parsePresentationsFromVpToken(options) + } + + public parseTransactionData(options: ParseTransactionDataOptions) { + return parseTransactionData(options) + } +} diff --git a/packages/openid4vp/src/Oid4vpClient.ts b/packages/openid4vp/src/Oid4vpClient.ts new file mode 100644 index 0000000..d27f51c --- /dev/null +++ b/packages/openid4vp/src/Oid4vpClient.ts @@ -0,0 +1,49 @@ +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 ResolveOpenid4vpAuthorizationRequestOptions, + resolveOpenid4vpAuthorizationRequest, +} from './authorization-request/resolve-authorization-request' +import { + type CreateOpenid4vpAuthorizationResponseOptions, + createOpenid4vpAuthorizationResponse, +} from './authorization-response/create-authorization-response' +import { + type SubmitOpenid4vpAuthorizationResponseOptions, + submitOpenid4vpAuthorizationResponse, +} from './authorization-response/submit-authorization-response' + +export interface Oid4vciClientOptions { + /** + * Callbacks required for the oid4vc issuer + */ + callbacks: Omit +} + +export class Oid4vpClient { + public constructor(private options: Oid4vciClientOptions) {} + + public parseOpenid4vpAuthorizationRequestPayload(options: ParseOpenid4vpAuthRequestPayloadOptions) { + return parseOpenid4vpAuthorizationRequestPayload(options) + } + + public async resolveOpenId4vpAuthorizationRequest( + options: Omit + ) { + return resolveOpenid4vpAuthorizationRequest({ ...options, callbacks: this.options.callbacks }) + } + + public async createOpenid4vpAuthorizationResponse( + options: Omit + ) { + return createOpenid4vpAuthorizationResponse({ ...options, callbacks: this.options.callbacks }) + } + + public async submitOpenid4vpAuthorizationResponse( + options: Omit + ) { + return submitOpenid4vpAuthorizationResponse({ ...options, callbacks: this.options.callbacks }) + } +} diff --git a/packages/openid4vp/src/openid4vp-auth-request/create-openid4vp-auth-request.ts b/packages/openid4vp/src/authorization-request/create-authorization-request.ts similarity index 58% rename from packages/openid4vp/src/openid4vp-auth-request/create-openid4vp-auth-request.ts rename to packages/openid4vp/src/authorization-request/create-authorization-request.ts index acb388b..ebbc294 100644 --- a/packages/openid4vp/src/openid4vp-auth-request/create-openid4vp-auth-request.ts +++ b/packages/openid4vp/src/authorization-request/create-authorization-request.ts @@ -1,14 +1,30 @@ import type { CallbackContext, JwtSigner } from '@openid4vc/oauth2' -import { uriEncodeObject } from '@openid4vc/utils' +import { URL, URLSearchParams, objectToQueryParams } from '@openid4vc/utils' import { createJarAuthRequest } from '../jar/create-jar-auth-request' -import { validateOpenid4vpAuthRequestParams } from './validate-openid4vp-auth-request' -import type { Openid4vpAuthRequest } from './z-openid4vp-auth-request' +import { + type WalletVerificationOptions, + validateOpenid4vpAuthorizationRequestPayload, +} from './validate-authorization-request' +import type { Openid4vpAuthorizationRequest } from './z-authorization-request' + +export interface CreateOpenid4vpAuthorizationRequestOptions { + scheme?: string + requestParams: Openid4vpAuthorizationRequest + jar?: { + requestUri: string + jwtSigner: JwtSigner + jweEncryptor?: JwtSigner + additionalJwtPayload?: Record + } + wallet?: WalletVerificationOptions + callbacks: Pick +} /** * Creates an OpenID4VP authorization request, optionally with a JWT Secured Authorization Request (JAR) * If the request is created after receiving wallet metadata via a POST to the request_uri endpoint, the wallet nonce needs to be provided * - * @param input Configuration options for creating the authorization request + * @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.jar Optional JWT Secured Authorization Request (JAR) configuration @@ -21,24 +37,10 @@ import type { Openid4vpAuthRequest } from './z-openid4vp-auth-request' * @param input.callbacks Callback functions for JWT operations * @returns Object containing the authorization request parameters, URI and optional JAR details */ -export async function createOpenid4vpAuthorizationRequest(input: { - scheme?: string - requestParams: Openid4vpAuthRequest - jar?: { - requestUri: string - jwtSigner: JwtSigner - jweEncryptor?: JwtSigner - additionalJwtPayload?: Record - } - wallet?: { - nonce?: string - } - callbacks: Pick -}) { - const { jar, scheme: _scheme, requestParams, wallet, callbacks } = input - const scheme = _scheme ?? 'openid4vp://' +export async function createOpenid4vpAuthorizationRequest(options: CreateOpenid4vpAuthorizationRequestOptions) { + const { jar, scheme = 'openid4vp://', requestParams, wallet, callbacks } = options - validateOpenid4vpAuthRequestParams(requestParams, { wallet: wallet }) + validateOpenid4vpAuthorizationRequestPayload({ params: requestParams, walletVerificationOptions: wallet }) let additionalJwtPayload: Record | undefined @@ -56,16 +58,28 @@ export async function createOpenid4vpAuthorizationRequest(input: { callbacks, }) + const url = new URL(scheme) + url.search = `?${new URLSearchParams([ + ...url.searchParams.entries(), + ...objectToQueryParams(jarResult.requestParams).entries(), + ]).toString()}` + return { - authRequestParams: jarResult.requestParams, - uri: `${scheme}?${uriEncodeObject(jarResult.requestParams)}`, + authRequestObject: jarResult.requestParams, + authRequest: url.toString(), jar: { ...jar, ...jarResult }, } } + const url = new URL(scheme) + url.search = `?${new URLSearchParams([ + ...url.searchParams.entries(), + ...objectToQueryParams(requestParams).entries(), + ]).toString()}` + return { - authRequestParams: requestParams, - uri: `${scheme}?${uriEncodeObject(requestParams)}`, + authRequestObject: requestParams, + authRequest: 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 new file mode 100644 index 0000000..5863af2 --- /dev/null +++ b/packages/openid4vp/src/authorization-request/parse-authorization-request-params.ts @@ -0,0 +1,67 @@ +import { Oauth2Error, decodeJwt } from '@openid4vc/oauth2' +import { URL } from '@openid4vc/utils' +import { parseWithErrorHandling } from '@openid4vc/utils' +import z from 'zod' +import { type JarAuthRequest, zJarAuthRequest } from '../jar/z-jar-auth-request' +import { type Openid4vpAuthorizationRequest, zOpenid4vpAuthorizationRequest } from './z-authorization-request' + +export interface ParsedJarOpenid4vpAuthRequest { + type: 'jar' + provided: 'uri' | 'jwt' | 'params' + params: JarAuthRequest +} + +export interface ParsedOpenid4vpAuthRequest { + type: 'openid4vp' + provided: 'uri' | 'jwt' | 'params' + params: Openid4vpAuthorizationRequest +} + +export interface ParseOpenid4vpAuthRequestPayloadOptions { + requestPayload: string | Record +} + +export function parseOpenid4vpAuthorizationRequestPayload( + options: ParseOpenid4vpAuthRequestPayloadOptions +): ParsedOpenid4vpAuthRequest | ParsedJarOpenid4vpAuthRequest { + const { requestPayload } = options + let provided: 'uri' | 'jwt' | 'params' = 'params' + + let params: Record + if (typeof requestPayload === 'string') { + if (requestPayload.includes('://')) { + const url = new URL(requestPayload) + params = Object.fromEntries(url.searchParams) + provided = 'uri' + } else { + const decoded = decodeJwt({ jwt: requestPayload }) + params = decoded.payload + provided = 'jwt' + } + } else { + params = requestPayload + } + + const parsedRequest = parseWithErrorHandling(z.union([zOpenid4vpAuthorizationRequest, zJarAuthRequest]), params) + const parsedOpenid4vpAuthRequest = zOpenid4vpAuthorizationRequest.safeParse(parsedRequest) + if (parsedOpenid4vpAuthRequest.success) { + return { + type: 'openid4vp', + provided, + params: parsedOpenid4vpAuthRequest.data, + } + } + + const parsedJarAuthRequest = zJarAuthRequest.safeParse(parsedRequest) + if (parsedJarAuthRequest.success) { + return { + type: 'jar', + provided, + params: parsedJarAuthRequest.data, + } + } + + throw new Oauth2Error( + 'Could not parse openid4vp auth request params. The received is neither a valid openid4vp auth request nor a valid jar auth request.' + ) +} diff --git a/packages/openid4vp/src/authorization-request/resolve-authorization-request.ts b/packages/openid4vp/src/authorization-request/resolve-authorization-request.ts new file mode 100644 index 0000000..37e7ec5 --- /dev/null +++ b/packages/openid4vp/src/authorization-request/resolve-authorization-request.ts @@ -0,0 +1,82 @@ +import { type CallbackContext, Oauth2Error } from '@openid4vc/oauth2' +import { parseWithErrorHandling } from '@openid4vc/utils' +import z from 'zod' +import { parseClientIdentifier } from '../client-identifier-scheme/parse-client-identifier-scheme' +import { verifyJarRequest } from '../jar/handle-jar-request/verify-jar-request' +import { type JarAuthRequest, zJarAuthRequest } from '../jar/z-jar-auth-request' +import { parseTransactionData } from '../transaction-data/parse-transaction-data' +import { + type WalletVerificationOptions, + validateOpenid4vpAuthorizationRequestPayload, +} from './validate-authorization-request' +import { type Openid4vpAuthorizationRequest, zOpenid4vpAuthorizationRequest } from './z-authorization-request' + +export interface ResolveOpenid4vpAuthorizationRequestOptions { + request: Openid4vpAuthorizationRequest | JarAuthRequest + wallet?: WalletVerificationOptions + callbacks: Pick +} + +export async function resolveOpenid4vpAuthorizationRequest(options: ResolveOpenid4vpAuthorizationRequestOptions) { + const { request, wallet, callbacks } = options + + let authRequestPayload: Openid4vpAuthorizationRequest + let jar: Awaited> | undefined + + const parsed = parseWithErrorHandling( + z.union([zOpenid4vpAuthorizationRequest, zJarAuthRequest]), + request, + 'Invalid authorization request. Could not parse openid4vp authorization request as openid4vp or jar auth request.' + ) + + const parsedOpenid4vpAuthorizationRequest = zOpenid4vpAuthorizationRequest.safeParse(request) + if (parsedOpenid4vpAuthorizationRequest.success) { + authRequestPayload = parsedOpenid4vpAuthorizationRequest.data + } else { + const parsedJarAuthRequest = zJarAuthRequest.parse(parsed) + jar = await verifyJarRequest({ jarRequestParams: parsedJarAuthRequest, callbacks, wallet }) + authRequestPayload = zOpenid4vpAuthorizationRequest.parse(jar.authRequestParams) + } + + validateOpenid4vpAuthorizationRequestPayload({ params: authRequestPayload, walletVerificationOptions: wallet }) + + const clientMeta = parseClientIdentifier({ request: authRequestPayload, jar, callbacks }) + + let pex: + | { + presentation_definition: unknown + presentation_definition_uri?: string + } + | undefined + + let dcql: { query: unknown } | undefined + + if (authRequestPayload.presentation_definition || authRequestPayload.presentation_definition_uri) { + if (authRequestPayload.presentation_definition_uri) { + throw new Oauth2Error('presentation_definition_uri is not supported') + } + pex = { + presentation_definition: authRequestPayload.presentation_definition, + presentation_definition_uri: authRequestPayload.presentation_definition_uri, + } + } + + if (authRequestPayload.dcql_query) { + dcql = { query: authRequestPayload.dcql_query } + } + + const transactionData = authRequestPayload.transaction_data + ? parseTransactionData({ transactionData: authRequestPayload.transaction_data }) + : undefined + + return { + transactionData, + payload: authRequestPayload, + jar, + client: { ...clientMeta }, + pex, + dcql, + } +} + +export type ResolvedOpenid4vpAuthRequest = Awaited> diff --git a/packages/openid4vp/src/openid4vp-auth-request/validate-openid4vp-auth-request.ts b/packages/openid4vp/src/authorization-request/validate-authorization-request.ts similarity index 66% rename from packages/openid4vp/src/openid4vp-auth-request/validate-openid4vp-auth-request.ts rename to packages/openid4vp/src/authorization-request/validate-authorization-request.ts index cfe5f8b..cc32756 100644 --- a/packages/openid4vp/src/openid4vp-auth-request/validate-openid4vp-auth-request.ts +++ b/packages/openid4vp/src/authorization-request/validate-authorization-request.ts @@ -1,17 +1,26 @@ import { Oauth2Error } from '@openid4vc/oauth2' -import type { Openid4vpAuthRequest } from './z-openid4vp-auth-request' +import { zHttpsUrl } from '@openid4vc/utils' +import type { WalletMetadata } from '../models/z-wallet-metadata' +import type { Openid4vpAuthorizationRequest } from './z-authorization-request' + +export interface WalletVerificationOptions { + expectedNonce?: string + metadata?: WalletMetadata +} + +export interface ValidateOpenid4vpAuthorizationRequestPayloadOptions { + params: Openid4vpAuthorizationRequest + walletVerificationOptions?: WalletVerificationOptions +} /** * Validate the OpenId4Vp Authorization Request parameters */ -export const validateOpenid4vpAuthRequestParams = ( - params: Openid4vpAuthRequest, - options: { - wallet?: { - nonce?: string - } - } +export const validateOpenid4vpAuthorizationRequestPayload = ( + options: ValidateOpenid4vpAuthorizationRequestPayloadOptions ) => { + const { params, walletVerificationOptions } = options + if (!params.redirect_uri && !params.response_uri) { throw new Oauth2Error('OpenId4Vp Authorization Request redirect_uri or response_uri is required.') } @@ -38,19 +47,19 @@ export const validateOpenid4vpAuthRequestParams = ( ) } - if (params.trust_chain && !params.client_id.startsWith('http://') && !params.client_id.startsWith('https://')) { + if (params.trust_chain && !zHttpsUrl.safeParse(params.client_id).success) { throw new Oauth2Error( 'OpenId4Vp Authorization Request trust_chain parameter MUST NOT be present if the client_id is not an OpenId Federation Entity Identifier starting with http:// or https://.' ) } - if (options.wallet?.nonce && !params.wallet_nonce) { + if (walletVerificationOptions?.expectedNonce && !params.wallet_nonce) { throw new Oauth2Error( 'OpenId4Vp Authorization Request wallet_nonce parameter is required when wallet_nonce is provided.' ) } - if (options.wallet?.nonce !== params.wallet_nonce) { + if (walletVerificationOptions?.expectedNonce !== params.wallet_nonce) { throw new Oauth2Error( 'OpenId4Vp Authorization Request wallet_nonce parameter does not match the wallet_nonce value passed by the Wallet.' ) diff --git a/packages/openid4vp/src/openid4vp-auth-request/z-openid4vp-auth-request.ts b/packages/openid4vp/src/authorization-request/z-authorization-request.ts similarity index 60% rename from packages/openid4vp/src/openid4vp-auth-request/z-openid4vp-auth-request.ts rename to packages/openid4vp/src/authorization-request/z-authorization-request.ts index 5850ecf..2fa51ca 100644 --- a/packages/openid4vp/src/openid4vp-auth-request/z-openid4vp-auth-request.ts +++ b/packages/openid4vp/src/authorization-request/z-authorization-request.ts @@ -1,20 +1,21 @@ +import { zHttpsUrl } from '@openid4vc/utils' import { z } from 'zod' import { zClientMetadata } from '../models/z-client-metadata' -export const zOpenid4vpAuthRequest = z +export const zOpenid4vpAuthorizationRequest = z .object({ response_type: z.literal('vp_token'), client_id: z.string(), - redirect_uri: z.string().optional(), - response_uri: z.string().optional(), - request_uri: z.string().optional(), + redirect_uri: zHttpsUrl.optional(), + response_uri: zHttpsUrl.optional(), + request_uri: zHttpsUrl.optional(), request_uri_method: z.enum(['post', 'get']).optional(), - response_mode: z.enum(['direct_post', 'direct_post.jwt', 'query', 'fragment']).optional().default('fragment'), + response_mode: z.enum(['direct_post', 'direct_post.jwt']).optional(), nonce: z.string(), wallet_nonce: z.string().optional(), scope: z.string().optional(), presentation_definition: z.object({}).passthrough().optional(), - presentation_definition_uri: z.string().optional(), + presentation_definition_uri: zHttpsUrl.optional(), dcql_query: z.object({}).passthrough().optional(), client_metadata: zClientMetadata.optional(), state: z.string().optional(), @@ -23,4 +24,4 @@ export const zOpenid4vpAuthRequest = z }) .passthrough() -export type Openid4vpAuthRequest = z.infer +export type Openid4vpAuthorizationRequest = z.infer diff --git a/packages/openid4vp/src/openid4vp-auth-response/create-openid4vp-auth-response.ts b/packages/openid4vp/src/authorization-response/create-authorization-response.ts similarity index 60% rename from packages/openid4vp/src/openid4vp-auth-response/create-openid4vp-auth-response.ts rename to packages/openid4vp/src/authorization-response/create-authorization-response.ts index 11bdf63..9712c6d 100644 --- a/packages/openid4vp/src/openid4vp-auth-response/create-openid4vp-auth-response.ts +++ b/packages/openid4vp/src/authorization-response/create-authorization-response.ts @@ -1,43 +1,48 @@ import { type CallbackContext, type JwtSigner, Oauth2Error } from '@openid4vc/oauth2' +import { dateToSeconds } from '@openid4vc/utils' +import { addSecondsToDate } from '../../../utils/src/date' +import type { Openid4vpAuthorizationRequest } from '../authorization-request/z-authorization-request' import { createJarmAuthResponse } from '../jarm/jarm-auth-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' -import type { JarmServerMetadata } from '../jarm/metadata/z-jarm-as-metadata' -import type { Openid4vpAuthRequest } from '../openid4vp-auth-request/z-openid4vp-auth-request' -import type { Openid4vpAuthResponse } from './z-openid4vp-auth-response' +import type { JarmServerMetadata } from '../jarm/metadata/z-jarm-authorization-server-metadata' +import type { Openid4vpAuthorizationResponse } from './z-authorization-response' -export async function createOpenid4vpAuthorizationResponse(options: { - requestParams: Pick - responseParams: Openid4vpAuthResponse & { state?: never } +export interface CreateOpenid4vpAuthorizationResponseOptions { + requestParams: Pick + responseParams: Openid4vpAuthorizationResponse & { state?: never } jarm?: { jwtSigner?: JwtSigner - jweEncryptor?: { - nonce: string - } + encryption?: { nonce: string } serverMetadata: JarmServerMetadata - iss?: string // The issuer URL of the authorization server that created the response - aud?: string // The client_id of the client the response is intended for - exp?: number // The expiration time of the JWT. A maximum JWT lifetime of 10 minutes is RECOMMENDED. + authorizationServer?: string // The issuer URL of the authorization server that created the response + audience?: string // The client_id of the client the response is intended for + expiresInSeconds?: number // The expiration time of the JWT. A maximum JWT lifetime of 10 minutes is RECOMMENDED. } callbacks: Pick -}) { +} + +export async function createOpenid4vpAuthorizationResponse(options: CreateOpenid4vpAuthorizationResponseOptions) { const { requestParams, responseParams, jarm, callbacks } = options const openid4vpAuthResponseParams = { ...responseParams, state: requestParams.state, - } satisfies Openid4vpAuthResponse + } satisfies Openid4vpAuthorizationResponse - if (!requestParams.response_mode.includes('jwt')) { - return { responseParams: openid4vpAuthResponseParams } + if (requestParams.response_mode && isJarmResponseMode(requestParams.response_mode) && !jarm) { + throw new Oauth2Error( + `Missing jarm options for creating Jarm response with response mode '${requestParams.response_mode}'` + ) } if (!jarm) { - throw new Oauth2Error(`JARM is required for response mode ${requestParams.response_mode}`) + return { responseParams: openid4vpAuthResponseParams } } if (!requestParams.client_metadata) { - throw new Oauth2Error('Missing client metadata in the request params to assert JARM metadata support.') + throw new Oauth2Error('Missing client metadata in the request params to assert Jarm metadata support.') } if (!requestParams.client_metadata.jwks) { @@ -60,36 +65,36 @@ export async function createOpenid4vpAuthorizationResponse(options: { // When the response is NOT only encrypted, the JWT payload needs to include the iss, aud and exp. let additionalJwtPayload: Record | undefined - if (jarm.jwtSigner) { - if (!jarm.iss) { + if (jarm?.jwtSigner) { + if (!jarm.authorizationServer) { throw new Oauth2Error('Missing required iss in JARM configuration for creating OpenID4VP authorization response.') } - if (!jarm.aud) { + if (!jarm.audience) { throw new Oauth2Error('Missing required aud in JARM configuration for creating OpenID4VP authorization response.') } additionalJwtPayload = { - iss: jarm.iss, - aud: jarm.aud, - exp: jarm.exp ?? Math.floor(Date.now() / 1000) + 60 * 10, // default: 10 minutes + iss: jarm.authorizationServer, + aud: jarm.audience, + exp: jarm.expiresInSeconds ?? dateToSeconds(addSecondsToDate(new Date(), 60 * 10)), // default: 10 minutes } } const jarmResponseParams = { ...openid4vpAuthResponseParams, ...additionalJwtPayload, - } satisfies Openid4vpAuthResponse + } satisfies Openid4vpAuthorizationResponse const result = await createJarmAuthResponse({ jarmAuthResponse: jarmResponseParams, - jwtSigner: jarm.jwtSigner, + jwtSigner: jarm?.jwtSigner, jwtEncryptor: - jarm.jweEncryptor && (supportedJarmMetadata.type === 'encrypt' || supportedJarmMetadata.type === 'sign_encrypt') + jarm?.encryption && (supportedJarmMetadata.type === 'encrypt' || supportedJarmMetadata.type === 'sign_encrypt') ? { method: 'jwk', publicJwk: clientMetaJwks.encJwk, - apu: jarm.jweEncryptor.nonce, + apu: jarm.encryption?.nonce, apv: requestParams.nonce, alg: supportedJarmMetadata.client_metadata.authorization_encrypted_response_alg, enc: supportedJarmMetadata.client_metadata.authorization_encrypted_response_enc, diff --git a/packages/openid4vp/src/authorization-response/parse-authorization-response.ts b/packages/openid4vp/src/authorization-response/parse-authorization-response.ts new file mode 100644 index 0000000..eabf2aa --- /dev/null +++ b/packages/openid4vp/src/authorization-response/parse-authorization-response.ts @@ -0,0 +1,146 @@ +import { + type CallbackContext, + Oauth2Error, + Oauth2ServerErrorResponseError, + decodeJwtHeader, + zCompactJwe, + zCompactJwt, + zJwtHeader, +} from '@openid4vc/oauth2' +import { decodeBase64, encodeToUtf8String, parseWithErrorHandling } from '@openid4vc/utils' +import z from 'zod' +import { parseOpenid4vpAuthorizationRequestPayload } from '../authorization-request/parse-authorization-request-params' +import { verifyJarmAuthorizationResponse } from '../jarm/jarm-auth-response/verify-jarm-auth-response' +import type { JarmAuthResponse, JarmAuthResponseEncryptedOnly } from '../jarm/jarm-auth-response/z-jarm-auth-response' +import { isJarmResponseMode } from '../jarm/jarm-response-mode' +import { validateOpenid4vpAuthorizationResponse } from './validate-authorization-response' +import { zOpenid4vpAuthorizationResponse } from './z-authorization-response' + +export interface ParseJarmAuthorizationResponseOptions { + jarmResponseJwt: string + callbacks: Pick & { + getOpenid4vpAuthorizationRequest: ( + authResponse: JarmAuthResponse | JarmAuthResponseEncryptedOnly + ) => Promise<{ authRequest: { client_id: string; nonce: string; state?: string } }> + } +} + +export async function parseJarmAuthorizationResponse(options: ParseJarmAuthorizationResponseOptions) { + const { jarmResponseJwt, callbacks } = options + + const jarmAuthorizationResponseJwt = parseWithErrorHandling( + z.union([zCompactJwt, zCompactJwe]), + jarmResponseJwt, + 'Invalid jarm authorization response jwt.' + ) + + const verifiedJarmResponse = await verifyJarmAuthorizationResponse({ jarmAuthorizationResponseJwt, callbacks }) + const zJarmHeader = z.object({ ...zJwtHeader.shape, apu: z.string().optional(), apv: z.string().optional() }) + const { header: jarmHeader } = decodeJwtHeader({ + jwt: jarmAuthorizationResponseJwt, + headerSchema: zJarmHeader, + }) + + const parsedAuthorizationRequest = parseOpenid4vpAuthorizationRequestPayload({ + requestPayload: verifiedJarmResponse.authRequest, + }) + + if (parsedAuthorizationRequest.type !== 'openid4vp') { + throw new Oauth2Error('Invalid authorization request. Could not parse openid4vp authorization request.') + } + + const authResponsePayload = parseWithErrorHandling( + zOpenid4vpAuthorizationResponse, + verifiedJarmResponse.jarmAuthResponse, + 'Invalid jarm authorization response.' + ) + const validateOpenId4vpResponse = validateOpenid4vpAuthorizationResponse({ + authorizationRequest: parsedAuthorizationRequest.params, + authorizationResponse: authResponsePayload, + }) + + const authRequestPayload = parsedAuthorizationRequest.params + if (!authRequestPayload.response_mode || !isJarmResponseMode(authRequestPayload.response_mode)) { + throw new Oauth2Error( + `Invalid response mode for jarm response. Response mode: '${authRequestPayload.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 !== authRequestPayload.nonce) { + throw new Oauth2Error('The nonce in the jarm header does not match the nonce in the request.') + } + } + + return { + ...validateOpenId4vpResponse, + jarm: { ...verifiedJarmResponse, jarmHeader, mdocGeneratedNonce }, + authResponsePayload, + authRequestPayload, + } +} + +export interface ParseOpenid4vpAuthorizationResponseOptions { + responsePayload: Record + callbacks: Pick & { + getOpenid4vpAuthorizationRequest: ( + authResponse: JarmAuthResponse | JarmAuthResponseEncryptedOnly + ) => Promise<{ authRequest: { client_id: string; nonce: string; state?: string } }> + } +} + +export async function parseOpenid4vpAuthorizationResponse(options: ParseOpenid4vpAuthorizationResponseOptions) { + const { responsePayload, callbacks } = options + + if (responsePayload.response) { + return parseJarmAuthorizationResponse({ jarmResponseJwt: responsePayload.response as string, callbacks }) + } + + const authorizationResponsePayload = responsePayload + + const authResponsePayload = parseWithErrorHandling( + zOpenid4vpAuthorizationResponse, + authorizationResponsePayload, + 'Invalid authorization response.' + ) + + const authRequest = await callbacks.getOpenid4vpAuthorizationRequest(authResponsePayload) + const parsedAuthRequest = parseOpenid4vpAuthorizationRequestPayload({ requestPayload: authRequest.authRequest }) + if (parsedAuthRequest.type !== 'openid4vp') { + throw new Oauth2Error('Invalid authorization request. Could not parse openid4vp authorization request.') + } + + const authRequestPayload = parsedAuthRequest.params + + const validateOpenId4vpResponse = validateOpenid4vpAuthorizationResponse({ + authorizationRequest: authRequestPayload, + authorizationResponse: authResponsePayload, + }) + + if (authRequestPayload.response_mode && isJarmResponseMode(authRequestPayload.response_mode)) { + throw new Oauth2ServerErrorResponseError( + { + error: 'invalid_request', + error_description: 'Invalid response mode for openid4vp response. Expected jarm response.', + }, + { + status: 400, + } + ) + } + + return { + ...validateOpenId4vpResponse, + authResponsePayload, + authRequestPayload, + jarm: undefined, + } +} + +export type ParsedOpenid4vpAuthorizationResponse = Awaited> diff --git a/packages/openid4vp/src/openid4vp-auth-response/submit-openid4vp-auth-response.ts b/packages/openid4vp/src/authorization-response/submit-authorization-response.ts similarity index 54% rename from packages/openid4vp/src/openid4vp-auth-response/submit-openid4vp-auth-response.ts rename to packages/openid4vp/src/authorization-response/submit-authorization-response.ts index 7fbcb1d..6f657e5 100644 --- a/packages/openid4vp/src/openid4vp-auth-response/submit-openid4vp-auth-response.ts +++ b/packages/openid4vp/src/authorization-response/submit-authorization-response.ts @@ -1,18 +1,20 @@ import type { CallbackContext } from '@openid4vc/oauth2' import { ContentType, defaultFetcher } from '@openid4vc/utils' -import { xWwwFormUrlEncodeObject } 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 type { Openid4vpAuthRequest } from '../openid4vp-auth-request/z-openid4vp-auth-request' -import type { Openid4vpAuthResponse } from './z-openid4vp-auth-response' +import type { Openid4vpAuthorizationResponse } from './z-authorization-response' -export async function submitOpenid4vpAuthorizationResponse(input: { - request: Pick - response: Openid4vpAuthResponse +export interface SubmitOpenid4vpAuthorizationResponseOptions { + request: Pick + response: Openid4vpAuthorizationResponse jarm?: { responseJwt: string } callbacks: Pick -}) { - const { request, response, jarm, callbacks } = input - const url = request.redirect_uri ?? request.response_uri +} + +export async function submitOpenid4vpAuthorizationResponse(options: SubmitOpenid4vpAuthorizationResponseOptions) { + const { request, response, jarm, callbacks } = options + const url = request.response_uri if (jarm) { return jarmAuthResponseSend({ @@ -27,7 +29,7 @@ export async function submitOpenid4vpAuthorizationResponse(input: { } const fetch = callbacks.fetch ?? defaultFetcher - const encodedResponse = xWwwFormUrlEncodeObject(response) + const encodedResponse = objectToQueryParams(response) 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 new file mode 100644 index 0000000..46ddf1c --- /dev/null +++ b/packages/openid4vp/src/authorization-response/validate-authorization-response.ts @@ -0,0 +1,100 @@ +import { Oauth2Error } from '@openid4vc/oauth2' +import type { Openid4vpAuthorizationRequest } from '../authorization-request/z-authorization-request' +import { + parseDcqlPresentationFromVpToken, + parsePresentationsFromVpToken, +} from '../vp-token/parse-presentations-from-vp-token' +import type { ValidateOpenid4VpAuthorizationResponseResult } from './validate-openid4vp-auth-response-result' +import type { Openid4vpAuthorizationResponse } from './z-authorization-response' + +export interface ValidateOpenid4vpAuthorizationResponseOptions { + authorizationRequest: Openid4vpAuthorizationRequest + authorizationResponse: Openid4vpAuthorizationResponse +} + +/** + * The following steps need to be performed outside of this library + * - verifying the presentations + * - validating the presentations against the presentation definition + * - checking the revocation status of the presentations + * - checking the nonce of the presentations matches the nonce of the request (for mdoc's) + */ +export function validateOpenid4vpAuthorizationResponse( + options: ValidateOpenid4vpAuthorizationResponseOptions +): ValidateOpenid4VpAuthorizationResponseResult { + const { authorizationRequest, authorizationResponse } = options + // todo i think the response prarms should also contain a nonce + if (!authorizationResponse.vp_token) { + throw new Oauth2Error('Failed to verify OpenId4Vp Authorization Response. vp_token is missing.') + } + + if (authorizationRequest.state !== authorizationResponse.state) { + throw new Oauth2Error('OpenId4Vp Authorization Response state mismatch.') + } + + // TODO: implement id_token handling + if (authorizationResponse.id_token) { + throw new Oauth2Error('OpenId4Vp Authorization Response id_token is not supported.') + } + + if (authorizationResponse.presentation_submission) { + if (!authorizationRequest.presentation_definition) { + throw new Oauth2Error('OpenId4Vp Authorization Request is missing the required presentation_definition.') + } + + // TODO: ENABLE THIS CHECK ALL THE TIME ONCE WE KNOW HOW TO GET THE NONCE FOR MDOCS AND ANONCREDS + const presentations = parsePresentationsFromVpToken({ vpToken: authorizationResponse.vp_token }) + if (presentations.every((p) => p.nonce) && !presentations.every((p) => p.nonce === authorizationRequest.nonce)) { + throw new Oauth2Error( + 'Presentation nonce mismatch. The nonce of some presentations does not match the nonce of the request.' + ) + } + + return { + type: 'pex', + pex: authorizationRequest.scope + ? { + scope: authorizationRequest.scope, + presentationSubmission: authorizationResponse.presentation_submission, + presentations, + } + : { + presentationDefinition: authorizationRequest.presentation_definition, + presentationSubmission: authorizationResponse.presentation_submission, + presentations, + }, + } + } + + if (authorizationRequest.dcql_query) { + if (Array.isArray(authorizationResponse.vp_token)) { + throw new Oauth2Error( + 'The OpenId4Vp Authorization Response contains multiple vp_token values. In combination with dcql this is not possible.' + ) + } + + if (typeof authorizationResponse.vp_token !== 'string') { + throw new Oauth2Error('If DCQL was used the vp_token must be a JSON-encoded object.') + } + + const presentation = parseDcqlPresentationFromVpToken({ vpToken: authorizationResponse.vp_token }) + // TODO: CHECK ALL THE NONCES ONCE WE KNOW HOW TO GET THE NONCE FOR MDOCS AND ANONCREDS + + return { + type: 'dcql', + dcql: authorizationRequest.scope + ? { + scope: authorizationRequest.scope, + presentation, + } + : { + query: authorizationRequest.dcql_query, + presentation, + }, + } + } + + throw new Oauth2Error( + 'Invalid OpenId4Vp Authorization Response. Response neither contains a presentation_submission nor a dcql presentation.' + ) +} diff --git a/packages/openid4vp/src/openid4vp-auth-response/verify-openid4vp-auth-response-result.ts b/packages/openid4vp/src/authorization-response/validate-openid4vp-auth-response-result.ts similarity index 65% rename from packages/openid4vp/src/openid4vp-auth-response/verify-openid4vp-auth-response-result.ts rename to packages/openid4vp/src/authorization-response/validate-openid4vp-auth-response-result.ts index 410e021..48ebaa7 100644 --- a/packages/openid4vp/src/openid4vp-auth-response/verify-openid4vp-auth-response-result.ts +++ b/packages/openid4vp/src/authorization-response/validate-openid4vp-auth-response-result.ts @@ -1,6 +1,6 @@ import type { VpTokenPresentationParseResult } from '../vp-token/parse-presentations-from-vp-token' -export interface VerifyOpenid4VpPexAuthorizationResponseResult { +export interface ValidateOpenid4VpPexAuthorizationResponseResult { type: 'pex' pex: { presentationSubmission: unknown @@ -11,13 +11,13 @@ export interface VerifyOpenid4VpPexAuthorizationResponseResult { ) } -export interface VerifyOpenid4VpDcqlAuthorizationResponseResult { +export interface ValidateOpenid4VpDcqlAuthorizationResponseResult { type: 'dcql' dcql: { presentation: Record } & ({ scope: string; query?: never } | { scope?: never; query: unknown }) } -export type VerifyOpenid4VpAuthorizationResponseResult = - | VerifyOpenid4VpPexAuthorizationResponseResult - | VerifyOpenid4VpDcqlAuthorizationResponseResult +export type ValidateOpenid4VpAuthorizationResponseResult = + | ValidateOpenid4VpPexAuthorizationResponseResult + | ValidateOpenid4VpDcqlAuthorizationResponseResult diff --git a/packages/openid4vp/src/openid4vp-auth-response/z-openid4vp-auth-response.ts b/packages/openid4vp/src/authorization-response/z-authorization-response.ts similarity index 61% rename from packages/openid4vp/src/openid4vp-auth-response/z-openid4vp-auth-response.ts rename to packages/openid4vp/src/authorization-response/z-authorization-response.ts index fea281e..f48f81a 100644 --- a/packages/openid4vp/src/openid4vp-auth-response/z-openid4vp-auth-response.ts +++ b/packages/openid4vp/src/authorization-response/z-authorization-response.ts @@ -1,10 +1,11 @@ import { z } from 'zod' +import { zVpToken } from '../vp-token/z-vp-token' -export const zOpenid4vpAuthResponse = z +export const zOpenid4vpAuthorizationResponse = z .object({ state: z.string().optional(), id_token: z.string().optional(), - vp_token: z.union([z.string(), z.array(z.string()), z.record(z.string(), z.unknown())]), + vp_token: zVpToken, presentation_submission: z.unknown().optional(), refresh_token: z.string().optional(), token_type: z.string().optional(), @@ -12,4 +13,4 @@ export const zOpenid4vpAuthResponse = z expires_in: z.number().optional(), }) .passthrough() -export type Openid4vpAuthResponse = z.infer +export type Openid4vpAuthorizationResponse = z.infer 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 0181ce5..f4069d0 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,8 +1,8 @@ import { Oauth2Error } from '@openid4vc/oauth2' import type { CallbackContext } from '../../../oauth2/src/callbacks' +import type { Openid4vpAuthorizationRequest } from '../authorization-request/z-authorization-request' import type { verifyJarRequest } from '../jar/handle-jar-request/verify-jar-request' import type { ClientMetadata } from '../models/z-client-metadata' -import type { Openid4vpAuthRequest } from '../openid4vp-auth-request/z-openid4vp-auth-request' import { type ClientIdScheme, zClientIdScheme } from './z-client-id-scheme' /** @@ -52,15 +52,17 @@ export interface ClientIdentifierParserConfig { requireSignatureFor?: ClientIdScheme[] } +export interface ClientIdentifierParserOptions { + request: Openid4vpAuthorizationRequest + jar?: Awaited> + callbacks: Partial> +} + /** * Parse and validate a client identifier */ export function parseClientIdentifier( - options: { - request: Openid4vpAuthRequest - jar?: Awaited> - callbacks: Partial> - }, + options: ClientIdentifierParserOptions, parserConfig?: ClientIdentifierParserConfig ): ParsedClientIdentifier { const { request, jar } = options @@ -139,14 +141,14 @@ export function parseClientIdentifier( } if (!clientId.startsWith('did:')) { - throw new Oauth2Error('Invalid client identifier. Client identifier must start with did:') + throw new Oauth2Error("Invalid client identifier. Client identifier must start with 'did:'") } - if (!jar.signerJwk.kid) { - throw new Oauth2Error('Missing required kid for client identifier scheme: did') + if (!jar.signer.publicJwk.kid) { + throw new Oauth2Error(`Missing required 'kid' for client identifier scheme: did`) } - if (!jar?.signerJwk.kid?.startsWith(clientId)) { + if (!jar.signer.publicJwk.kid?.startsWith(clientId)) { throw new Oauth2Error( 'With client identifier scheme "did" the JAR request must be signed by the same DID as the client identifier.' ) @@ -156,7 +158,7 @@ export function parseClientIdentifier( scheme, identifier: clientId, originalValue: clientId, - kid: jar.signerJwk.kid, + kid: jar.signer.publicJwk.kid, } } @@ -171,29 +173,40 @@ export function parseClientIdentifier( ) } - if (jar.jwtSigner.method !== 'x5c') { + if (jar.signer.method !== 'x5c') { throw new Oauth2Error( 'Something went wrong. The JWT signer method is not x5c but the client identifier scheme is x509_san_dns.' ) } - if (scheme === 'x509_san_dns' && options.callbacks.getX509SanDnsNames) { - const dnsNames = options.callbacks.getX509SanDnsNames(jar.jwtSigner.x5c[0]) - if (!dnsNames.includes(identifierPart)) { + if (scheme === 'x509_san_dns' && options.callbacks.getX509CertificateMetadata) { + const { sanDnsNames } = options.callbacks.getX509CertificateMetadata(jar.signer.x5c[0]) + if (!sanDnsNames.includes(identifierPart)) { throw new Oauth2Error('Invalid client identifier. Client identifier must be a valid DNS name.') } - } else if (scheme === 'x509_san_uri' && options.callbacks.getX509SanUriNames) { - const uriNames = options.callbacks.getX509SanUriNames(jar.jwtSigner.x5c[0]) - if (!uriNames.includes(identifierPart)) { + + const requestUri = (jar.authRequestParams.request_uri ?? jar.authRequestParams.response_uri) as string + if (getDomainFromUrl(requestUri) !== identifierPart) { + throw new Oauth2Error( + 'Invalid client identifier. The fully qualified domain name of the redirect_uri value MUST match the Client Identifier without the prefix x509_san_dns.' + ) + } + } else if (scheme === 'x509_san_uri' && options.callbacks.getX509CertificateMetadata) { + const { sanUriNames } = options.callbacks.getX509CertificateMetadata(jar.signer.x5c[0]) + if (!sanUriNames.includes(identifierPart)) { throw new Oauth2Error('Invalid client identifier. Client identifier must be a valid URI.') } + + if ((jar.authRequestParams.request_uri ?? jar.authRequestParams.response_uri) !== identifierPart) { + throw new Oauth2Error('The redirect_uri value MUST match the Client Identifier without the prefix x509_san_uri') + } } return { scheme, identifier: identifierPart, originalValue: clientId, - x5c: jar.jwtSigner.x5c, + x5c: jar.signer.x5c, } } @@ -209,3 +222,9 @@ export function parseClientIdentifier( originalValue: clientId, } } + +function getDomainFromUrl(url: string): string { + const regex = /[#/?]/ + const domain = url.split('://')[1].split(regex)[0] + return domain +} diff --git a/packages/openid4vp/src/client-identifier-scheme/validate-verifier-attestation-jwt.ts b/packages/openid4vp/src/client-identifier-scheme/validate-verifier-attestation-jwt.ts index 375a90f..6fe1116 100644 --- a/packages/openid4vp/src/client-identifier-scheme/validate-verifier-attestation-jwt.ts +++ b/packages/openid4vp/src/client-identifier-scheme/validate-verifier-attestation-jwt.ts @@ -1,40 +1,38 @@ import { type CallbackContext, type Jwk, - type JwtHeader, - type JwtPayload, type JwtSigner, Oauth2Error, decodeJwt, + verifyJwt, + zJwk, + zJwtHeader, } from '@openid4vc/oauth2' +import { jwtSignerFromJwt } from '@openid4vc/oauth2' +import z from 'zod' -/** - * Verifies a Verifier Attestation according to the OpenID4VP specification - * @param {Object} header - The decoded JWT header - * @param {Object} payload - The decoded JWT payload - * @param {Object} options - Additional verification options - * @param {number} [options.clockSkewSec=300] - Allowed clock skew in seconds - * @returns {Object} Result object with success boolean and any error message - */ -export async function verifyAttestation( - attestedJws: string, - options: { callbacks: Pick; attestationJwtCnfJwk: Jwk } -) { - const { callbacks, attestationJwtCnfJwk } = options - if (!attestationJwtCnfJwk.alg) { +export interface VerifyAttestationOptions { + attestedJwt: string + callbacks: Pick + expectedAttestationJwk: Jwk +} + +export async function verifyAttestation(options: VerifyAttestationOptions) { + const { callbacks, expectedAttestationJwk, attestedJwt } = options + if (!expectedAttestationJwk.alg) { throw new Oauth2Error('Invalid verifier attestation missing required alg property') } const jwtSigner: JwtSigner = { method: 'jwk', - alg: attestationJwtCnfJwk.alg, - publicJwk: attestationJwtCnfJwk, + alg: expectedAttestationJwk.alg, + publicJwk: expectedAttestationJwk, } - const { header, payload } = decodeJwt({ jwt: attestedJws }) + const { header, payload } = decodeJwt({ jwt: attestedJwt }) const verificationResult = await callbacks.verifyJwt(jwtSigner, { header, payload, - compact: attestedJws, + compact: attestedJwt, }) if (!verificationResult.verified) { @@ -44,82 +42,38 @@ export async function verifyAttestation( return verificationResult } -// Example usage: -/* -const result = verifyAttestationJWT({ - typ: 'verifier-attestation+jwt', - alg: 'ES256' -}, { - iss: 'https://issuer.example.com', - sub: 'client123', - exp: Math.floor(Date.now() / 1000) + 3600, - cnf: { - jwk: { - kty: 'EC', - crv: 'P-256', - x: 'MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4', - y: '4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM' - } - } -}); -*/ - -/** - * Verifies a Verifier Attestation JWT according to the OpenID4VP specification - * @param {Object} header - The decoded JWT header - * @param {Object} payload - The decoded JWT payload - * @param {Object} options - Additional verification options - * @param {number} [options.clockSkewSec=300] - Allowed clock skew in seconds - * @returns {Object} Result object with success boolean and any error message - */ -export async function verifyAttestationJWT( - jwt: { - signer: JwtSigner - header: JwtHeader - payload: JwtPayload - compact: string - }, - options: { clientId: string; clockSkewSec?: number; callbacks: Pick } -) { - const { header, payload, compact } = jwt +export interface VerifyAttestationJwtOptions { + attestationJwt: string + clientId: string + clockSkewSec?: number + callbacks: Pick +} +export async function verifyAttestationJWT(options: { + attestationJwt: string + clientId: string + clockSkewSec?: number + callbacks: Pick +}) { const errors = [] - const clockSkewSec = options.clockSkewSec || 300 // 5 minute default clock skew - const now = Math.floor(Date.now() / 1000) - // IT is not defined in openid4vp how to resolve the public key for verifying the attestation jwt - // it is just mentioned that the key may be resolved from the issuer - const verificationResult = await options.callbacks.verifyJwt(jwt.signer, { + const { header, payload } = decodeJwt({ + jwt: options.attestationJwt, + headerSchema: z.object({ ...zJwtHeader.shape, typ: z.literal('verifier-attestation+jwt') }), + }) + + const jwtSigner = jwtSignerFromJwt({ header, payload }) + const { signer } = await verifyJwt({ header, payload, - compact, + compact: options.attestationJwt, + signer: jwtSigner, + verifyJwtCallback: options.callbacks.verifyJwt, + now: new Date(), + expectedSubject: options.clientId, + allowedSkewInSeconds: options.clockSkewSec || 300, + requiredClaims: ['iss', 'sub', 'exp', 'cnf'], }) - if (!verificationResult.verified) { - errors.push('Invalid verifier attestation jwt. Signature verification failed.') - } - - // 1. Verify header has correct type - if (header.typ !== 'verifier-attestation+jwt') { - errors.push('Invalid typ header. Must be "verifier-attestation+jwt"') - } - - // 2. Verify required claims are present - const requiredClaims = ['iss', 'sub', 'exp', 'cnf'] - for (const claim of requiredClaims) { - if (!payload[claim]) { - errors.push(`Missing required claim: ${claim}`) - } - } - - // 3. Verify time-based claims - if (payload.exp && payload.exp <= now - clockSkewSec) { - errors.push('Token has expired') - } - - if (payload.nbf && payload.nbf > now + clockSkewSec) { - errors.push('Token cannot be used yet (nbf)') - } - // 4. Verify cnf claim structure if (payload.cnf) { if (!payload.cnf.jwk) { @@ -133,29 +87,18 @@ export async function verifyAttestationJWT( } } - // 5. Verify client_id format - if (payload.sub) { - if (typeof payload.sub !== 'string' || options.clientId !== payload.sub) { - errors.push(`sub claim must match the clientId '${options.clientId}'`) - } - } - const isValid = errors.length === 0 if (isValid) { return { isValid: true, - jwtHeader: header, - jwtPayload: payload, - // biome-ignore lint/style/noNonNullAssertion: - verifierPublicKey: payload.cnf?.jwk!, + signer, + verifierPublicKey: zJwk.parse(payload.cnf?.jwk), } as const } return { isValid: false, errors: errors, - jwtHeader: header, - jwtPayload: payload, verifierPublicKey: payload.cnf?.jwk, } as const } diff --git a/packages/openid4vp/src/index.ts b/packages/openid4vp/src/index.ts index de84eb3..8cf88e0 100644 --- a/packages/openid4vp/src/index.ts +++ b/packages/openid4vp/src/index.ts @@ -1,21 +1,63 @@ export { ClientIdScheme } from './client-identifier-scheme/z-client-id-scheme' -export { verifyJarmAuthResponse } from './jarm/jarm-auth-response/verify-jarm-auth-response' -export { JarmClientMetadata } from './jarm/metadata/z-jarm-dcr-metadata' -export { createOpenid4vpAuthorizationRequest } from './openid4vp-auth-request/create-openid4vp-auth-request' -export { parseOpenid4vpRequestParams } from './openid4vp-auth-request/parse-openid4vp-auth-request-params' -export { - verifyOpenid4vpAuthRequest, - VerifiedOpenid4vpAuthRequest, -} from './openid4vp-auth-request/verify-openid4vp-auth-request' -export type { Openid4vpAuthRequest } from './openid4vp-auth-request/z-openid4vp-auth-request' -export { validateOpenid4vpAuthRequestParams } from './openid4vp-auth-request/validate-openid4vp-auth-request' -export { createOpenid4vpAuthorizationResponse } from './openid4vp-auth-response/create-openid4vp-auth-response' -export { submitOpenid4vpAuthorizationResponse } from './openid4vp-auth-response/submit-openid4vp-auth-response' -export type { Openid4vpAuthResponse } from './openid4vp-auth-response/z-openid4vp-auth-response' -export { verifyOpenid4vpAuthorizationResponse } from './openid4vp-auth-response/verify-openid4vp-auth-response' -export { parseTransactionData } from './transaction-data/parse-transaction-data' +export { + verifyJarmAuthorizationResponse, + type VerifyJarmAuthorizationResponseOptions, +} from './jarm/jarm-auth-response/verify-jarm-auth-response' +export { zJarmClientMetadata, JarmClientMetadata } from './jarm/metadata/z-jarm-client-metadata' +export { + createOpenid4vpAuthorizationRequest, + CreateOpenid4vpAuthorizationRequestOptions, +} from './authorization-request/create-authorization-request' +export { + parseOpenid4vpAuthorizationRequestPayload, + ParseOpenid4vpAuthRequestPayloadOptions, +} from './authorization-request/parse-authorization-request-params' +export { + resolveOpenid4vpAuthorizationRequest, + ResolvedOpenid4vpAuthRequest, +} from './authorization-request/resolve-authorization-request' +export type { Openid4vpAuthorizationRequest } from './authorization-request/z-authorization-request' +export { + validateOpenid4vpAuthorizationRequestPayload, + ValidateOpenid4vpAuthorizationRequestPayloadOptions, +} from './authorization-request/validate-authorization-request' +export { + createOpenid4vpAuthorizationResponse, + CreateOpenid4vpAuthorizationResponseOptions, +} from './authorization-response/create-authorization-response' +export { + submitOpenid4vpAuthorizationResponse, + SubmitOpenid4vpAuthorizationResponseOptions, +} from './authorization-response/submit-authorization-response' +export type { Openid4vpAuthorizationResponse } from './authorization-response/z-authorization-response' +export { + validateOpenid4vpAuthorizationResponse, + ValidateOpenid4vpAuthorizationResponseOptions, +} from './authorization-response/validate-authorization-response' +export { + parseTransactionData, + ParseTransactionDataOptions, +} from './transaction-data/parse-transaction-data' export type { TransactionDataEntry } from './transaction-data/z-transaction-data' export { parsePresentationsFromVpToken, + ParsePresentationsFromVpTokenOptions, VpTokenPresentationParseResult, } from './vp-token/parse-presentations-from-vp-token' + +export { + parseOpenid4vpAuthorizationResponse, + ParseOpenid4vpAuthorizationResponseOptions, + ParsedOpenid4vpAuthorizationResponse, +} from './authorization-response/parse-authorization-response' + +export { + ValidateOpenid4VpPexAuthorizationResponseResult, + ValidateOpenid4VpDcqlAuthorizationResponseResult, + ValidateOpenid4VpAuthorizationResponseResult, +} from './authorization-response/validate-openid4vp-auth-response-result' + +export { Oid4vpClient } from './Oid4vpClient' +export { Oid4vcVerifier } from './Oid4vcVerifier' +export { zOpenid4vpAuthorizationResponse } from './authorization-response/z-authorization-response' +export { isJarmResponseMode } from './jarm/jarm-response-mode' diff --git a/packages/openid4vp/src/jar/handle-jar-request/validate-jar-request-against-session.ts b/packages/openid4vp/src/jar/handle-jar-request/validate-jar-request-against-session.ts index 8cec25d..9656f90 100644 --- a/packages/openid4vp/src/jar/handle-jar-request/validate-jar-request-against-session.ts +++ b/packages/openid4vp/src/jar/handle-jar-request/validate-jar-request-against-session.ts @@ -1,13 +1,13 @@ import { Oauth2Error } from '@openid4vc/oauth2' -export type JarMeta = { +export type JarMetadata = { ProtectedBy: 'signature' | 'signature_encryption' SendBy: 'value' | 'reference' } export interface ValidateJarRequestAgainstSessionOptions { - jarMeta: JarMeta - jarSessionMeta: JarMeta + jarMetadata: JarMetadata + jarSessionMetadata: JarMetadata } /** @@ -24,13 +24,13 @@ export interface ValidateJarRequestAgainstSessionOptions { * do not match the corresponding values in the session metadata. */ export async function validateJarRequestAgainstSession(options: ValidateJarRequestAgainstSessionOptions) { - const { jarMeta, jarSessionMeta } = options + const { jarMetadata, jarSessionMetadata } = options - if (jarSessionMeta.ProtectedBy !== jarMeta.ProtectedBy) { + if (jarSessionMetadata.ProtectedBy !== jarMetadata.ProtectedBy) { throw new Oauth2Error(`The protected_by value does not match the session's protected_by value.`) } - if (jarSessionMeta.SendBy !== jarMeta.SendBy) { + if (jarSessionMetadata.SendBy !== jarMetadata.SendBy) { throw new Oauth2Error(`The send_by value does not match the session's send_by value.`) } } 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 4bca647..ec5158d 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 @@ -1,56 +1,63 @@ import { type CallbackContext, type Jwk, - type JwtSigner, + type JwtSignerWithJwk, Oauth2Error, Oauth2ServerErrorResponseError, decodeJwt, jwtSignerFromJwt, + verifyJwt, zCompactJwe, zCompactJwt, } from '@openid4vc/oauth2' +import { zClientIdScheme } from '../../client-identifier-scheme/z-client-id-scheme' import type { WalletMetadata } from '../../models/z-wallet-metadata' 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, validateJarAuthRequest } from '../z-jar-auth-request' +import { type JarAuthRequest, validateJarRequestParams } from '../z-jar-auth-request' -/** - * Verifies a JAR (JWT Secured Authorization Request) request by validating, decrypting, and verifying signatures. - * - * @param options - The input parameters - * @param options.jarRequestParams - The JAR authorization request parameters - * @param options.callbacks - Context containing the relevant Jose crypto operations - * @returns The verified authorization request parameters and metadata - */ -export async function verifyJarRequest(options: { +export interface VerifyJarRequestOptions { jarRequestParams: JarAuthRequest - callbacks: Pick + callbacks: Pick wallet?: { metadata?: WalletMetadata nonce?: string } -}): Promise<{ +} + +export interface VerifyJarRequestReturn { authRequestParams: JarRequestObjectPayload sendBy: 'value' | 'reference' decryptionJwk?: Jwk - signerJwk: Jwk - jwtSigner: JwtSigner -}> { - const { jarRequestParams, callbacks, wallet } = options + signer: JwtSignerWithJwk +} - validateJarAuthRequest({ jarAuthRequest: jarRequestParams }) +/** + * Verifies a JAR (JWT Secured Authorization Request) request by validating, decrypting, and verifying signatures. + * + * @param options - The input parameters + * @param options.jarRequestParams - The JAR authorization request parameters + * @param options.callbacks - Context containing the relevant Jose crypto operations + * @returns The verified authorization request parameters and metadata + */ +export async function verifyJarRequest(options: VerifyJarRequestOptions): Promise { + const { callbacks, wallet = {} } = options + + const jarRequestParams = validateJarRequestParams(options) const sendBy = jarRequestParams.request ? 'value' : 'reference' + const clientIdentifierScheme = zClientIdScheme.parse(jarRequestParams.client_id.split(':')[0]) + + const method = jarRequestParams.request_uri_method ?? 'GET' const requestObject = jarRequestParams.request ?? - (await fetchJarRequestObject( - // biome-ignore lint/style/noNonNullAssertion: - jarRequestParams.request_uri!, - jarRequestParams.client_id.split(':')[0], - jarRequestParams.request_uri_method ?? 'GET', - wallet ?? {} - )) + (await fetchJarRequestObject({ + requestUri: jarRequestParams.request_uri, + clientIdentifierScheme, + method, + wallet, + })) const requestObjectIsEncrypted = zCompactJwe.safeParse(requestObject).success const { decryptionJwk, payload: decryptedRequestObject } = requestObjectIsEncrypted @@ -62,12 +69,12 @@ export async function verifyJarRequest(options: { throw new Oauth2Error('Jar Request Object is not a valid JWS.') } - const { authRequestParams, signerJwk, jwtSigner } = await verifyJarRequestObject({ + const { authRequestParams, signer } = await verifyJarRequestObject({ decryptedRequestObject, callbacks, }) if (!authRequestParams.client_id) { - throw new Oauth2Error('Jar Request Object is missing the required "client_id" field.') + throw new Oauth2Error(`Jar Request Object is missing the required 'client_id' field.`) } if (jarRequestParams.client_id !== authRequestParams.client_id) { @@ -77,15 +84,14 @@ export async function verifyJarRequest(options: { return { sendBy, authRequestParams, - signerJwk, + signer, decryptionJwk, - jwtSigner, } } async function decryptJarRequest(options: { jwe: string - callbacks: Pick + callbacks: Pick }) { const { jwe, callbacks } = options @@ -94,7 +100,7 @@ async function decryptJarRequest(options: { throw new Oauth2Error('Jar JWE is missing the protected header field "kid".') } - const decryptionResult = await callbacks.decryptJwt(jwe) + const decryptionResult = await callbacks.decryptJwe(jwe) if (!decryptionResult.decrypted) { throw new Oauth2ServerErrorResponseError({ error: 'invalid_request_object', @@ -114,14 +120,13 @@ async function verifyJarRequestObject(options: { const jwt = decodeJwt({ jwt: decryptedRequestObject, payloadSchema: zJarRequestObjectPayload }) const jwtSigner = jwtSignerFromJwt(jwt) - const { verified, signerJwk } = await callbacks.verifyJwt(jwtSigner, { - ...jwt, + const { signer } = await verifyJwt({ + verifyJwtCallback: callbacks.verifyJwt, compact: decryptedRequestObject, + header: jwt.header, + payload: jwt.payload, + signer: jwtSigner, }) - if (!verified) { - throw new Oauth2Error('Jar Request Object signature verification failed.') - } - - return { authRequestParams: jwt.payload, signerJwk, jwtSigner } + return { authRequestParams: 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 ab5d9e8..8f17807 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 @@ -1,6 +1,7 @@ import { Oauth2ServerErrorResponseError } from '@openid4vc/oauth2' -import { type BaseSchema, ContentType, type Fetch, createZodFetcher, xWwwFormUrlEncodeObject } from '@openid4vc/utils' +import { type BaseSchema, ContentType, type Fetch, createZodFetcher, objectToQueryParams } from '@openid4vc/utils' import { z } from 'zod' +import type { ClientIdScheme } from '../../client-identifier-scheme/z-client-id-scheme' import type { WalletMetadata } from '../../models/z-wallet-metadata' /** @@ -14,16 +15,17 @@ import type { WalletMetadata } from '../../models/z-wallet-metadata' * @throws {InvalidFetchResponseError} if no successful or 404 response * @throws {Error} if parsing json from response fails */ -export async function fetchJarRequestObject( - requestUri: string, - clientIdentifierScheme: string, - method: 'GET' | 'POST', +export async function fetchJarRequestObject(options: { + requestUri: string + clientIdentifierScheme: ClientIdScheme + method: 'GET' | 'POST' wallet: { metadata?: WalletMetadata nonce?: string - }, + } fetch?: Fetch -): Promise | null> { +}): Promise | null> { + const { requestUri, clientIdentifierScheme, method, wallet, fetch } = options const fetcher = createZodFetcher(fetch) let requestBody = wallet.metadata ? { wallet_metadata: wallet.metadata, wallet_nonce: wallet.nonce } : undefined @@ -42,7 +44,7 @@ export async function fetchJarRequestObject( Accept: `${ContentType.OAuthRequestObjectJwt}, ${ContentType.Jwt};q=0.9`, 'Content-Type': ContentType.XWwwFormUrlencoded, }, - body: method === 'POST' ? xWwwFormUrlEncodeObject(wallet.metadata ?? {}) : undefined, + body: method === 'POST' ? objectToQueryParams(wallet.metadata ?? {}) : undefined, }) if (!response.ok) { diff --git a/packages/openid4vp/src/jar/z-jar-auth-request.ts b/packages/openid4vp/src/jar/z-jar-auth-request.ts index ca28e5b..af5d273 100644 --- a/packages/openid4vp/src/jar/z-jar-auth-request.ts +++ b/packages/openid4vp/src/jar/z-jar-auth-request.ts @@ -2,28 +2,33 @@ import { Oauth2ServerErrorResponseError } from '@openid4vc/oauth2' import { zHttpsUrl } from '@openid4vc/utils' import { z } from 'zod' -export const zJarAuthRequest = z.object({ - request: z.optional(z.string()), - request_uri: z.optional(zHttpsUrl), - request_uri_method: z.optional(z.union([z.literal('GET'), z.literal('POST')])), - client_id: z.string(), -}) +export const zJarAuthRequest = z + .object({ + request: z.optional(z.string()), + request_uri: z.optional(zHttpsUrl), + request_uri_method: z.optional(z.union([z.literal('GET'), z.literal('POST')])), + client_id: z.string(), + }) + .passthrough() export type JarAuthRequest = z.infer -export function validateJarAuthRequest(input: { jarAuthRequest: JarAuthRequest }) { - const { jarAuthRequest } = input +export function validateJarRequestParams(options: { jarRequestParams: JarAuthRequest }) { + const { jarRequestParams } = options - if (jarAuthRequest.request && jarAuthRequest.request_uri) { + if (jarRequestParams.request && jarRequestParams.request_uri) { throw new Oauth2ServerErrorResponseError({ error: 'invalid_request_object', error_description: 'request and request_uri cannot both be present in a JAR request', }) } - if (!jarAuthRequest.request && !jarAuthRequest.request_uri) { + if (!jarRequestParams.request && !jarRequestParams.request_uri) { throw new Oauth2ServerErrorResponseError({ error: 'invalid_request_object', error_description: 'request or request_uri must be present', }) } + + return jarRequestParams as JarAuthRequest & + ({ request_uri: string; request?: never } | { request: string; request_uri?: never }) } diff --git a/packages/openid4vp/src/jar/z-jar-as-metadata.ts b/packages/openid4vp/src/jar/z-jar-authorization-server-metadata.ts similarity index 64% rename from packages/openid4vp/src/jar/z-jar-as-metadata.ts rename to packages/openid4vp/src/jar/z-jar-authorization-server-metadata.ts index 088e385..ce7b340 100644 --- a/packages/openid4vp/src/jar/z-jar-as-metadata.ts +++ b/packages/openid4vp/src/jar/z-jar-authorization-server-metadata.ts @@ -1,8 +1,8 @@ import { z } from 'zod' -export const zJarAsMetadata = z.object({ +export const zJarAuthorizationServerMetadata = z.object({ request_object_signing_alg_values_supported: z.optional(z.array(z.string())), request_object_encryption_alg_values_supported: z.optional(z.array(z.string())), request_object_encryption_enc_values_supported: z.optional(z.array(z.string())), }) -export type JarAsMetadata = z.infer +export type JarAuthorizationServerMetadata = z.infer diff --git a/packages/openid4vp/src/jar/z-jar-dcr-metadata.ts b/packages/openid4vp/src/jar/z-jar-client-metadata.ts similarity index 53% rename from packages/openid4vp/src/jar/z-jar-dcr-metadata.ts rename to packages/openid4vp/src/jar/z-jar-client-metadata.ts index 94af4d6..43014b6 100644 --- a/packages/openid4vp/src/jar/z-jar-dcr-metadata.ts +++ b/packages/openid4vp/src/jar/z-jar-client-metadata.ts @@ -1,8 +1,8 @@ import { z } from 'zod' -export const zJarDcrMetadata = z.object({ +export const zJarDynamicClientRegistrationMetadata = z.object({ request_object_signing_alg: z.optional(z.string()), request_object_encryption_alg: z.optional(z.string()), request_object_encryption_enc: z.optional(z.string()), }) -export type JarDcrMetadata = z.infer +export type JarDynamicClientRegistrationMetadata = z.infer diff --git a/packages/openid4vp/src/jarm/jarm-auth-response-create.ts b/packages/openid4vp/src/jarm/jarm-auth-response-create.ts index e7e84d7..79a4ddc 100644 --- a/packages/openid4vp/src/jarm/jarm-auth-response-create.ts +++ b/packages/openid4vp/src/jarm/jarm-auth-response-create.ts @@ -14,8 +14,8 @@ export interface CreateJarmAuthResponseOptions { callbacks: Pick } -export async function createJarmAuthResponse(input: CreateJarmAuthResponseOptions) { - const { jarmAuthResponse, jwtEncryptor, jwtSigner, callbacks } = input +export async function createJarmAuthResponse(options: CreateJarmAuthResponseOptions) { + const { jarmAuthResponse, jwtEncryptor, jwtSigner, callbacks } = options if (!jwtSigner && jwtEncryptor) { const { jwe } = await callbacks.encryptJwe(jwtEncryptor, JSON.stringify(jarmAuthResponse)) return { jarmAuthResponseJwt: jwe } diff --git a/packages/openid4vp/src/jarm/jarm-auth-response-send.ts b/packages/openid4vp/src/jarm/jarm-auth-response-send.ts index f332634..1153195 100644 --- a/packages/openid4vp/src/jarm/jarm-auth-response-send.ts +++ b/packages/openid4vp/src/jarm/jarm-auth-response-send.ts @@ -15,7 +15,7 @@ export const jarmAuthResponseSend = (options: JarmAuthResponseSendOptions) => { const responseEndpoint = authRequest.response_uri ?? authRequest.redirect_uri if (!responseEndpoint) { - throw new Oauth2Error('response_uri or redirect_uri is required') + throw new Oauth2Error(`Either 'response_uri' or 'redirect_uri' MUST be present in the authorization request`) } const responseEndpointUrl = new URL(responseEndpoint) diff --git a/packages/openid4vp/src/jarm/jarm-auth-response/jarm-validate-auth-response.ts b/packages/openid4vp/src/jarm/jarm-auth-response/jarm-validate-auth-response.ts index e0a704b..dfa240e 100644 --- a/packages/openid4vp/src/jarm/jarm-auth-response/jarm-validate-auth-response.ts +++ b/packages/openid4vp/src/jarm/jarm-auth-response/jarm-validate-auth-response.ts @@ -1,11 +1,12 @@ import { Oauth2Error } from '@openid4vc/oauth2' +import { dateToSeconds } from '@openid4vc/utils' import { type JarmAuthResponse, type JarmAuthResponseEncryptedOnly, zJarmAuthResponse } from './z-jarm-auth-response' -export const jarmAuthResponseValidate = (input: { +export const jarmAuthResponseValidate = (options: { authRequest: { client_id: string } authResponse: JarmAuthResponse | JarmAuthResponseEncryptedOnly }) => { - const { authRequest, authResponse } = input + const { authRequest, authResponse } = options // The traditional Jarm Validation Methods do not account for the encrypted response. if (!zJarmAuthResponse.safeParse(authResponse).success) { @@ -15,7 +16,7 @@ export const jarmAuthResponseValidate = (input: { // 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 (authRequest.client_id !== authResponse.aud) { throw new Oauth2Error( - `Invalid audience in jarm-auth-response. Expected '${ + `Invalid 'aud' claim in JARM authorization response. Expected '${ authRequest.client_id }' received '${JSON.stringify(authResponse.aud)}'.` ) @@ -23,7 +24,7 @@ export const jarmAuthResponseValidate = (input: { // 4. The client checks the JWT's exp element to determine if the JWT is still valid. If the check fails, the client MUST abort processing and refuse the response. // 120 seconds clock skew - if (authResponse.exp && authResponse.exp < Date.now() / 1000) { + if (authResponse.exp !== undefined && authResponse.exp < dateToSeconds()) { throw new Oauth2Error('Jarm auth response is expired.') } } 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 index 9ddd092..c064942 100644 --- 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 @@ -6,7 +6,9 @@ import { jwtSignerFromJwt, zCompactJwe, zCompactJwt, + zJwtHeader, } from '@openid4vc/oauth2' +import z from 'zod' import { jarmAuthResponseValidate } from './jarm-validate-auth-response' import { type JarmAuthResponse, @@ -24,7 +26,7 @@ import { */ const decryptJarmRequestData = async (options: { requestData: string - callbacks: Pick + callbacks: Pick }) => { const { requestData, callbacks } = options @@ -33,7 +35,7 @@ const decryptJarmRequestData = async (options: { throw new Oauth2Error('Jarm JWE is missing the protected header field "kid".') } - const result = await callbacks.decryptJwt(requestData) + const result = await callbacks.decryptJwe(requestData) if (!result.decrypted) { throw new Oauth2Error('Failed to decrypt jarm auth response.') } @@ -41,24 +43,27 @@ const decryptJarmRequestData = async (options: { return result.payload } +export interface VerifyJarmAuthorizationResponseOptions { + jarmAuthorizationResponseJwt: string + callbacks: Pick & { + getOpenid4vpAuthorizationRequest: ( + authResponse: JarmAuthResponse | JarmAuthResponseEncryptedOnly + ) => Promise<{ authRequest: { client_id: string; nonce: string; state?: string } }> + } +} + /** * 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 verifyJarmAuthResponse(options: { - jarmAuthResponseJwt: string - getAuthRequest: ( - authResponse: JarmAuthResponse | JarmAuthResponseEncryptedOnly - ) => Promise<{ authRequest: { client_id: string; nonce: string; state?: string } }> - callbacks: Pick -}) { - const { jarmAuthResponseJwt } = options - - const requestDataIsEncrypted = zCompactJwe.safeParse(jarmAuthResponseJwt).success +export async function verifyJarmAuthorizationResponse(options: VerifyJarmAuthorizationResponseOptions) { + const { jarmAuthorizationResponseJwt, callbacks } = options + + const requestDataIsEncrypted = zCompactJwe.safeParse(jarmAuthorizationResponseJwt).success const decryptedRequestData = requestDataIsEncrypted - ? await decryptJarmRequestData({ requestData: jarmAuthResponseJwt, callbacks: options.callbacks }) - : jarmAuthResponseJwt + ? await decryptJarmRequestData({ requestData: jarmAuthorizationResponseJwt, callbacks }) + : jarmAuthorizationResponseJwt const responseIsSigned = zCompactJwt.safeParse(decryptedRequestData).success if (!requestDataIsEncrypted && !responseIsSigned) { @@ -70,15 +75,12 @@ export async function verifyJarmAuthResponse(options: { if (responseIsSigned) { const { header: jwsProtectedHeader, payload: jwsPayload } = decodeJwt({ jwt: decryptedRequestData, + headerSchema: z.object({ ...zJwtHeader.shape, kid: z.string() }), }) const response = zJarmAuthResponse.parse(jwsPayload) - - if (!jwsProtectedHeader.kid) { - throw new Oauth2Error('Jarm JWS is missing the protected header field "kid".') - } - const jwtSigner = jwtSignerFromJwt({ header: jwsProtectedHeader, payload: jwsPayload }) + const verificationResult = await options.callbacks.verifyJwt(jwtSigner, { compact: decryptedRequestData, header: jwsProtectedHeader, @@ -95,7 +97,7 @@ export async function verifyJarmAuthResponse(options: { jarmAuthResponse = zJarmAuthResponseEncryptedOnly.parse(jsonRequestData) } - const { authRequest } = await options.getAuthRequest(jarmAuthResponse) + const { authRequest } = await callbacks.getOpenid4vpAuthorizationRequest(jarmAuthResponse) jarmAuthResponseValidate({ authRequest, authResponse: jarmAuthResponse }) diff --git a/packages/openid4vp/src/jarm/jarm-extract-jwks.ts b/packages/openid4vp/src/jarm/jarm-extract-jwks.ts index 41fecce..56d3091 100644 --- a/packages/openid4vp/src/jarm/jarm-extract-jwks.ts +++ b/packages/openid4vp/src/jarm/jarm-extract-jwks.ts @@ -1,5 +1,5 @@ import type { JwkSet } from '@openid4vc/oauth2' -import { type JarmClientMetadata, zJarmClientMetadataParsed } from './metadata/z-jarm-dcr-metadata' +import { type JarmClientMetadata, zJarmClientMetadataParsed } from './metadata/z-jarm-client-metadata' export function extractJwksFromClientMetadata(clientMetadata: JarmClientMetadata & { jwks: JwkSet }) { const parsed = zJarmClientMetadataParsed.parse(clientMetadata) diff --git a/packages/openid4vp/src/jarm/jarm-response-mode.ts b/packages/openid4vp/src/jarm/jarm-response-mode.ts new file mode 100644 index 0000000..6b0ae52 --- /dev/null +++ b/packages/openid4vp/src/jarm/jarm-response-mode.ts @@ -0,0 +1,10 @@ +import { z } from 'zod' + +export const jarmResponseMode = ['jwt', 'query.jwt', 'fragment.jwt', 'form_post.jwt', 'direct_post.jwt'] as const +export const zJarmResponseMode = z.enum(jarmResponseMode) + +export type JarmResponseMode = (typeof jarmResponseMode)[number] + +export const isJarmResponseMode = (responseMode: string): responseMode is JarmResponseMode => { + return jarmResponseMode.includes(responseMode as JarmResponseMode) +} diff --git a/packages/openid4vp/src/jarm/metadata/jarm-assert-metadata-supported.ts b/packages/openid4vp/src/jarm/metadata/jarm-assert-metadata-supported.ts index 8bfff10..f4681c4 100644 --- a/packages/openid4vp/src/jarm/metadata/jarm-assert-metadata-supported.ts +++ b/packages/openid4vp/src/jarm/metadata/jarm-assert-metadata-supported.ts @@ -1,29 +1,29 @@ import { Oauth2Error } from '@openid4vc/oauth2' -import type { JarmServerMetadata } from './z-jarm-as-metadata' -import { type JarmClientMetadata, zJarmClientMetadataParsed } from './z-jarm-dcr-metadata' +import type { JarmServerMetadata } from './z-jarm-authorization-server-metadata' +import { type JarmClientMetadata, zJarmClientMetadataParsed } from './z-jarm-client-metadata' interface AssertValueSupported { supported: T[] actual: T - error: Error + errorMessage: string } -function assertValueSupported(input: AssertValueSupported): T { - const { error, supported, actual } = input +function assertValueSupported(options: AssertValueSupported): T { + const { errorMessage, supported, actual } = options const intersection = supported.find((value) => value === actual) if (!intersection) { - throw error + throw new Oauth2Error(errorMessage) } return intersection } -export function jarmAssertMetadataSupported(input: { +export function jarmAssertMetadataSupported(options: { clientMetadata: JarmClientMetadata serverMetadata: JarmServerMetadata }) { - const { clientMetadata, serverMetadata } = input + const { clientMetadata, serverMetadata } = options const parsedClientMetadata = zJarmClientMetadataParsed.parse(clientMetadata) if (parsedClientMetadata.type === 'sign_encrypt' || parsedClientMetadata.type === 'encrypt') { @@ -31,7 +31,7 @@ export function jarmAssertMetadataSupported(input: { assertValueSupported({ supported: serverMetadata.authorization_encryption_alg_values_supported, actual: parsedClientMetadata.client_metadata.authorization_encrypted_response_alg, - error: new Oauth2Error('Invalid authorization_encryption_alg'), + errorMessage: 'Invalid authorization_encryption_alg', }) } @@ -39,7 +39,7 @@ export function jarmAssertMetadataSupported(input: { assertValueSupported({ supported: serverMetadata.authorization_encryption_enc_values_supported, actual: parsedClientMetadata.client_metadata.authorization_encrypted_response_enc, - error: new Oauth2Error('Invalid authorization_encryption_enc'), + errorMessage: 'Invalid authorization_encryption_enc', }) } } @@ -51,7 +51,7 @@ export function jarmAssertMetadataSupported(input: { assertValueSupported({ supported: serverMetadata.authorization_signing_alg_values_supported, actual: parsedClientMetadata.client_metadata.authorization_signed_response_alg, - error: new Oauth2Error('Invalid authorization_signed_response_alg'), + errorMessage: 'Invalid authorization_signed_response_alg', }) } diff --git a/packages/openid4vp/src/jarm/metadata/z-jarm-as-metadata.ts b/packages/openid4vp/src/jarm/metadata/z-jarm-authorization-server-metadata.ts similarity index 51% rename from packages/openid4vp/src/jarm/metadata/z-jarm-as-metadata.ts rename to packages/openid4vp/src/jarm/metadata/z-jarm-authorization-server-metadata.ts index e1404f6..e685d7c 100644 --- a/packages/openid4vp/src/jarm/metadata/z-jarm-as-metadata.ts +++ b/packages/openid4vp/src/jarm/metadata/z-jarm-authorization-server-metadata.ts @@ -1,8 +1,9 @@ +import { zAlgValueNotNone } from '@openid4vc/oauth2' import { z } from 'zod' export const zJarmServerMetadata = z.object({ - authorization_signing_alg_values_supported: z.array(z.string()), - authorization_encryption_alg_values_supported: z.array(z.string()), + authorization_signing_alg_values_supported: z.array(zAlgValueNotNone), + authorization_encryption_alg_values_supported: z.array(zAlgValueNotNone), authorization_encryption_enc_values_supported: z.array(z.string()), }) diff --git a/packages/openid4vp/src/jarm/metadata/z-jarm-dcr-metadata.ts b/packages/openid4vp/src/jarm/metadata/z-jarm-client-metadata.ts similarity index 72% rename from packages/openid4vp/src/jarm/metadata/z-jarm-dcr-metadata.ts rename to packages/openid4vp/src/jarm/metadata/z-jarm-client-metadata.ts index 012e901..6e8b210 100644 --- a/packages/openid4vp/src/jarm/metadata/z-jarm-dcr-metadata.ts +++ b/packages/openid4vp/src/jarm/metadata/z-jarm-client-metadata.ts @@ -1,8 +1,9 @@ -import { Oauth2Error } from '@openid4vc/oauth2' +import { Oauth2Error, zAlgValueNotNone } from '@openid4vc/oauth2' +import { parseWithErrorHandling } from '@openid4vc/utils' import { z } from 'zod' export const zJarmSignOnlyClientMetadata = z.object({ - authorization_signed_response_alg: z.string(), + authorization_signed_response_alg: zAlgValueNotNone, authorization_encrypted_response_alg: z.optional(z.never()), authorization_encrypted_response_enc: z.optional(z.never()), @@ -39,7 +40,13 @@ export const zJarmClientMetadata = z.object({ export type JarmClientMetadata = z.infer export const zJarmClientMetadataParsed = zJarmClientMetadata.transform((client_metadata) => { - const SignEncrypt = zJarmSignEncryptClientMetadata.safeParse(client_metadata) + const parsedClientMeta = parseWithErrorHandling( + z.union([zJarmEncryptOnlyClientMetadata, zJarmSignOnlyClientMetadata, zJarmSignEncryptClientMetadata]), + client_metadata, + 'Invalid jarm client metadata.' + ) + + const SignEncrypt = zJarmSignEncryptClientMetadata.safeParse(parsedClientMeta) if (SignEncrypt.success) { return { type: 'sign_encrypt', @@ -50,29 +57,29 @@ export const zJarmClientMetadataParsed = zJarmClientMetadata.transform((client_m } as const } - const encryptOnly = zJarmEncryptOnlyClientMetadata.safeParse(client_metadata) + const encryptOnly = zJarmEncryptOnlyClientMetadata.safeParse(parsedClientMeta) if (encryptOnly.success) { return { type: 'encrypt', client_metadata: { ...encryptOnly.data, - authorization_encrypted_response_enc: client_metadata.authorization_encrypted_response_enc ?? 'A128CBC-HS256', + authorization_encrypted_response_enc: parsedClientMeta.authorization_encrypted_response_enc ?? 'A128CBC-HS256', }, } as const } // this must be the last entry - const signOnly = zJarmSignOnlyClientMetadata.safeParse(client_metadata) + const signOnly = zJarmSignOnlyClientMetadata.safeParse(parsedClientMeta) if (signOnly.success) { return { type: 'sign', client_metadata: { ...signOnly.data, - authorization_signed_response_alg: client_metadata.authorization_signed_response_alg ?? 'RS256', + authorization_signed_response_alg: parsedClientMeta.authorization_signed_response_alg ?? 'RS256', }, } as const } - throw new Oauth2Error('Invalid client metadata') + throw new Oauth2Error('Invalid jarm client metadata. Failed to parse.') }) export type JarmClientMetadataParsed = z.infer diff --git a/packages/openid4vp/src/jarm/parse-jarm-auth-response-direct-post-jwt.ts b/packages/openid4vp/src/jarm/parse-jarm-auth-response-direct-post-jwt.ts index 030156c..9c1a7f6 100644 --- a/packages/openid4vp/src/jarm/parse-jarm-auth-response-direct-post-jwt.ts +++ b/packages/openid4vp/src/jarm/parse-jarm-auth-response-direct-post-jwt.ts @@ -1,5 +1,6 @@ import { Oauth2Error, zCompactJwe, zCompactJwt } from '@openid4vc/oauth2' import { ContentType, URLSearchParams } from '@openid4vc/utils' +import z from 'zod' export async function parseJarmAuthResponseDirectPostJwt(request: Request) { const contentType = request.headers.get('content-type') @@ -18,20 +19,15 @@ export async function parseJarmAuthResponseDirectPostJwt(request: Request) { const requestData = Object.fromEntries(urlSearchParams) if (!requestData.response) { - throw new Oauth2Error('Received invalid JARM request data. Response Jwt is missing.') + throw new Oauth2Error(`Received invalid JARM request data. The 'response' JWT/JWT value is missing.`) } - const isCompactJwt = zCompactJwt.safeParse(requestData.response) - if (isCompactJwt.success) { + const isJweOrJws = z.union([zCompactJwt, zCompactJwe]).safeParse(requestData.response) + if (isJweOrJws.success) { return { jarmAuthResponseJwt: requestData.response } } - const isCompactJwe = zCompactJwe.safeParse(requestData.response) - if (isCompactJwe.success) { - return { jarmAuthResponseJwt: requestData.response } - } - - throw new Oauth2Error('Received invalid JARM auth response. Expected JWE or JWS.', { + throw new Oauth2Error('Received invalid JARM auth response. Expected JWE or JWT.', { cause: requestData, }) } diff --git a/packages/openid4vp/src/models/z-client-metadata.ts b/packages/openid4vp/src/models/z-client-metadata.ts index 7a3accd..c79d01e 100644 --- a/packages/openid4vp/src/models/z-client-metadata.ts +++ b/packages/openid4vp/src/models/z-client-metadata.ts @@ -1,15 +1,18 @@ import { zJwkSet } from '@openid4vc/oauth2' +import { zHttpsUrl } from '@openid4vc/utils' import { z } from 'zod' -import { zJarmClientMetadata } from '../jarm/metadata/z-jarm-dcr-metadata' -import { zVpFormats } from './z-vp-formats' +import { zJarmClientMetadata } from '../jarm/metadata/z-jarm-client-metadata' +import { zVpFormatsSupported } from './z-vp-formats-supported' // Authoritative data the Wallet is able to obtain about the Client from other sources, // for example those from an OpenID Federation Entity Statement, take precedence over the values passed in client_metadata. export const zClientMetadata = z .object({ jwks: z.optional(zJwkSet), - vp_formats: z.optional(zVpFormats), + vp_formats: z.optional(zVpFormatsSupported), ...zJarmClientMetadata.shape, + logo_uri: zHttpsUrl.optional(), + client_name: z.string().optional(), }) .passthrough() export type ClientMetadata = z.infer diff --git a/packages/openid4vp/src/models/z-vp-formats-supported.ts b/packages/openid4vp/src/models/z-vp-formats-supported.ts index d43140b..ce9c68a 100644 --- a/packages/openid4vp/src/models/z-vp-formats-supported.ts +++ b/packages/openid4vp/src/models/z-vp-formats-supported.ts @@ -1,9 +1,11 @@ import { z } from 'zod' export const zVpFormatsSupported = z.record( z.string(), - z.object({ - alg_values_supported: z.optional(z.array(z.string())), - }) + z + .object({ + alg_values_supported: z.optional(z.array(z.string())), + }) + .passthrough() ) export type VpFormatsSupported = z.infer diff --git a/packages/openid4vp/src/models/z-vp-formats.ts b/packages/openid4vp/src/models/z-vp-formats.ts deleted file mode 100644 index 405f7e9..0000000 --- a/packages/openid4vp/src/models/z-vp-formats.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { z } from 'zod' - -export const zVpFormats = z.optional(z.record(z.string(), z.unknown())) -export type VpFormats = z.infer diff --git a/packages/openid4vp/src/openid4vp-auth-request/parse-openid4vp-auth-request-params.ts b/packages/openid4vp/src/openid4vp-auth-request/parse-openid4vp-auth-request-params.ts deleted file mode 100644 index 791d22d..0000000 --- a/packages/openid4vp/src/openid4vp-auth-request/parse-openid4vp-auth-request-params.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Oauth2Error, decodeJwt } from '@openid4vc/oauth2' -import { uriDecodeObject } from '@openid4vc/utils' -import { type JarAuthRequest, zJarAuthRequest } from '../jar/z-jar-auth-request' -import { type Openid4vpAuthRequest, zOpenid4vpAuthRequest } from './z-openid4vp-auth-request' - -export interface ParsedJarOpenid4vpAuthRequest { - type: 'jar' - provided: 'uri' | 'jwt' | 'params' - params: JarAuthRequest -} - -export interface ParsedOpenid4vpAuthRequest { - type: 'openid4vp' - provided: 'uri' | 'jwt' | 'params' - params: Openid4vpAuthRequest -} - -export function parseOpenid4vpRequestParams( - input: unknown -): ParsedOpenid4vpAuthRequest | ParsedJarOpenid4vpAuthRequest { - let params = input - let provided: 'uri' | 'jwt' | 'params' = 'params' - - if (typeof input === 'string') { - if (input.includes('://')) { - const data = input.split('://')[1] - params = uriDecodeObject(data) - provided = 'uri' - } else { - const decoded = decodeJwt({ jwt: input }) - params = decoded.payload - provided = 'jwt' - } - } - - const parsedOpenid4vpAuthRequest = zOpenid4vpAuthRequest.safeParse(params) - if (parsedOpenid4vpAuthRequest.success) { - return { - type: 'openid4vp', - provided, - params: parsedOpenid4vpAuthRequest.data, - } - } - - const parsedJarAuthRequest = zJarAuthRequest.safeParse(params) - if (parsedJarAuthRequest.success) { - return { - type: 'jar', - provided, - params: parsedJarAuthRequest.data, - } - } - - throw new Oauth2Error('Could not parse openid4vp auth request params.') -} diff --git a/packages/openid4vp/src/openid4vp-auth-request/verify-openid4vp-auth-request.ts b/packages/openid4vp/src/openid4vp-auth-request/verify-openid4vp-auth-request.ts deleted file mode 100644 index 45e3e67..0000000 --- a/packages/openid4vp/src/openid4vp-auth-request/verify-openid4vp-auth-request.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { CallbackContext } from '@openid4vc/oauth2' -import { parseClientIdentifier } from '../client-identifier-scheme/parse-client-identifier-scheme' -import { verifyJarRequest } from '../jar/handle-jar-request/verify-jar-request' -import { type JarAuthRequest, zJarAuthRequest } from '../jar/z-jar-auth-request' -import type { WalletMetadata } from '../models/z-wallet-metadata' -import { parseTransactionData } from '../transaction-data/parse-transaction-data' -import { validateOpenid4vpAuthRequestParams } from './validate-openid4vp-auth-request' -import { type Openid4vpAuthRequest, zOpenid4vpAuthRequest } from './z-openid4vp-auth-request' - -export async function verifyOpenid4vpAuthRequest( - params: Openid4vpAuthRequest | JarAuthRequest, - options: { - wallet?: { - nonce?: string - metadata?: WalletMetadata - } - callbacks: Pick - } -) { - const { wallet, callbacks } = options - - let authRequestParams: Openid4vpAuthRequest - let jar: Awaited> | undefined - - const parsedJarAuthRequest = zJarAuthRequest.safeParse(params) - if (parsedJarAuthRequest.success) { - jar = await verifyJarRequest({ jarRequestParams: parsedJarAuthRequest.data, callbacks, wallet }) - authRequestParams = zOpenid4vpAuthRequest.parse(jar.authRequestParams) - } else { - authRequestParams = params as Openid4vpAuthRequest - } - - validateOpenid4vpAuthRequestParams(authRequestParams, { wallet: options.wallet }) - - const clientMeta = parseClientIdentifier({ request: authRequestParams, jar, callbacks }) - - let pex: - | { - presentation_definition: unknown - presentation_definition_uri?: string - } - | undefined - - let dcql: - | { - query: unknown - } - | undefined - - if (authRequestParams.presentation_definition || authRequestParams.presentation_definition_uri) { - if (authRequestParams.presentation_definition_uri) { - throw new Error('presentation_definition_uri is not supported') - } - pex = { - presentation_definition: authRequestParams.presentation_definition, - presentation_definition_uri: authRequestParams.presentation_definition_uri, - } - } - - if (authRequestParams.dcql_query) { - dcql = { - query: authRequestParams.dcql_query, - } - } - - const transactionData = authRequestParams.transaction_data - ? parseTransactionData(authRequestParams.transaction_data) - : undefined - - return { - transactionData, - payload: authRequestParams, - jar, - client: { ...clientMeta }, - pex, - dcql, - } -} - -export type VerifiedOpenid4vpAuthRequest = Awaited> diff --git a/packages/openid4vp/src/openid4vp-auth-response/verify-openid4vp-auth-response.ts b/packages/openid4vp/src/openid4vp-auth-response/verify-openid4vp-auth-response.ts deleted file mode 100644 index b8f4fa4..0000000 --- a/packages/openid4vp/src/openid4vp-auth-response/verify-openid4vp-auth-response.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Oauth2Error } from '@openid4vc/oauth2' -import type { Openid4vpAuthRequest } from '../openid4vp-auth-request/z-openid4vp-auth-request' -import { - parseDcqlPresentationFromVpToken, - parsePresentationsFromVpToken, -} from '../vp-token/parse-presentations-from-vp-token' -import type { VerifyOpenid4VpAuthorizationResponseResult } from './verify-openid4vp-auth-response-result' -import type { Openid4vpAuthResponse } from './z-openid4vp-auth-response' - -/** - * The following steps need to be done manually - // validating the id token - // verifying the presentations - // validating the presentations against the presentation definition - // checking the revocation status of the presentations - // checking the nonce of the presentations matches the nonce of the request - */ -export function verifyOpenid4vpAuthorizationResponse(options: { - requestParams: Openid4vpAuthRequest - responseParams: Openid4vpAuthResponse -}): VerifyOpenid4VpAuthorizationResponseResult { - const { requestParams, responseParams } = options - // todo i think the response prarms should also contain a nonce - if (!responseParams.vp_token) { - throw new Oauth2Error('Failed to verify OpenId4Vp Authorization Response. vp_token is missing.') - } - - if (requestParams.state !== responseParams.state) { - throw new Oauth2Error('OpenId4Vp Authorization Response state mismatch.') - } - - // TODO: implement id_token handling - if (responseParams.id_token) { - throw new Oauth2Error('OpenId4Vp Authorization Response id_token is not supported.') - } - - if (responseParams.presentation_submission) { - if (!requestParams.presentation_definition) { - throw new Oauth2Error('OpenId4Vp Authorization Request is missing the required presentation_definition.') - } - - // TODO: ENABLE THIS CHECK ALL THE TIME ONCE WE KNOW HOW TO GET THE NONCE FOR MDOCS AND ANONCREDS - const presentations = parsePresentationsFromVpToken({ vpToken: responseParams.vp_token }) - if (presentations.every((p) => p.nonce) && !presentations.every((p) => p.nonce === requestParams.nonce)) { - throw new Oauth2Error( - 'Presentation nonce mismatch. The nonce of some presentations does not match the nonce of the request.' - ) - } - - return { - type: 'pex', - pex: requestParams.scope - ? { - scope: requestParams.scope, - presentationSubmission: responseParams.presentation_submission, - presentations, - } - : { - presentationDefinition: requestParams.presentation_definition, - presentationSubmission: responseParams.presentation_submission, - presentations, - }, - } - } - - if (requestParams.dcql_query) { - if (Array.isArray(responseParams.vp_token)) { - throw new Oauth2Error( - 'The OpenId4Vp Authorization Response contains multiple vp_token values. In combination with dcql this is not possible.' - ) - } - - if (typeof responseParams.vp_token !== 'string') { - throw new Oauth2Error('If DCQL was used the vp_token must be a JSON-encoded object.') - } - - const presentation = parseDcqlPresentationFromVpToken({ vpToken: responseParams.vp_token, path: '$' }) - // TODO: CHECK ALL THE NONCES ONCE WE KNOW HOW TO GET THE NONCE FOR MDOCS AND ANONCREDS - - return { - type: 'dcql', - dcql: requestParams.scope - ? { - scope: requestParams.scope, - presentation, - } - : { - query: requestParams.dcql_query, - presentation, - }, - } - } - - throw new Oauth2Error( - 'Invalid OpenId4Vp Authorization Response. Response neither contains a presentation_submission nor a dcql presentation.' - ) -} diff --git a/packages/openid4vp/src/transaction-data/parse-transaction-data.ts b/packages/openid4vp/src/transaction-data/parse-transaction-data.ts index 04b4aff..4539d28 100644 --- a/packages/openid4vp/src/transaction-data/parse-transaction-data.ts +++ b/packages/openid4vp/src/transaction-data/parse-transaction-data.ts @@ -1,15 +1,13 @@ -import { Oauth2Error } from '@openid4vc/oauth2' -import { decodeBase64, encodeToUtf8String } from '@openid4vc/utils' -import { parseIfJson } from '@openid4vc/utils' +import { decodeBase64, encodeToUtf8String, parseIfJson, parseWithErrorHandling } from '@openid4vc/utils' import { type TransactionData, zTransactionData } from './z-transaction-data' -export function parseTransactionData(transactionData: string[]): TransactionData { - const decoded = transactionData.map((tdEntry) => parseIfJson(encodeToUtf8String(decodeBase64(tdEntry)))) - const parsed = zTransactionData.safeParse(decoded) - - if (!parsed.success) { - throw new Oauth2Error('Failed to parse transaction data.') - } +export interface ParseTransactionDataOptions { + transactionData: string[] +} - return parsed.data +export function parseTransactionData(options: ParseTransactionDataOptions): TransactionData { + const { transactionData } = options + const decoded = transactionData.map((tdEntry) => parseIfJson(encodeToUtf8String(decodeBase64(tdEntry as string)))) + const parsed = parseWithErrorHandling(zTransactionData, decoded, 'Failed to parse transaction data.') + return parsed } diff --git a/packages/openid4vp/src/vp-token/parse-presentations-from-vp-token.ts b/packages/openid4vp/src/vp-token/parse-presentations-from-vp-token.ts index a6fcbc4..72a54f7 100644 --- a/packages/openid4vp/src/vp-token/parse-presentations-from-vp-token.ts +++ b/packages/openid4vp/src/vp-token/parse-presentations-from-vp-token.ts @@ -1,6 +1,6 @@ import { Oauth2Error, decodeJwt } from '@openid4vc/oauth2' import { zCompactJwt } from '@openid4vc/oauth2' -import { isObject, parseIfJson } from '@openid4vc/utils' +import { isObject, parseIfJson, parseWithErrorHandling } from '@openid4vc/utils' import { z } from 'zod' import type { VpToken } from './z-vp-token' @@ -8,20 +8,23 @@ export type VpTokenPresentationParseResult = | { format: 'dc+sd-jwt' | 'mso_mdoc' | 'jwt_vp_json' presentation: string - path: string + path?: string nonce?: string } | { format: 'ldp_vp' | 'ac_vp' presentation: Record - path: string + path?: string nonce?: string } -export function parsePresentationsFromVpToken(options: { vpToken: VpToken }): [ - VpTokenPresentationParseResult, - ...VpTokenPresentationParseResult[], -] { +export interface ParsePresentationsFromVpTokenOptions { + vpToken: VpToken +} + +export function parsePresentationsFromVpToken( + options: ParsePresentationsFromVpTokenOptions +): [VpTokenPresentationParseResult, ...VpTokenPresentationParseResult[]] { const { vpToken: _vpToken } = options const vpToken = parseIfJson(_vpToken) @@ -30,14 +33,14 @@ export function parsePresentationsFromVpToken(options: { vpToken: VpToken }): [ throw new Oauth2Error('Could not parse vp_token. vp_token is an empty array.') } - return vpToken.map((token, idx) => parseSinglePresentationsFromVpToken({ vpToken: token, path: `$[${idx}]` })) as [ + return vpToken.map((token, idx) => parseSinglePresentationFromVpToken({ vpToken: token, path: `$[${idx}]` })) as [ VpTokenPresentationParseResult, ...VpTokenPresentationParseResult[], ] } if (typeof vpToken === 'string' || typeof vpToken === 'object') { - return [parseSinglePresentationsFromVpToken({ vpToken, path: '$' })] + return [parseSinglePresentationFromVpToken({ vpToken, path: '$' })] } throw new Oauth2Error( @@ -45,44 +48,53 @@ export function parsePresentationsFromVpToken(options: { vpToken: VpToken }): [ ) } -export function parseDcqlPresentationFromVpToken(options: { - vpToken: unknown - path: string -}) { +export function parseDcqlPresentationFromVpToken(options: { vpToken: unknown }) { const { vpToken: _vpToken } = options const vpToken = parseIfJson(_vpToken) - if (!isObject(vpToken)) { - throw new Oauth2Error(`Could not parse vp_token. Expected a JSON object. Received: ${typeof vpToken}`) - } + const parsed = parseWithErrorHandling(z.object({}).passthrough(), vpToken) const dcqlPresentationRecord = Object.fromEntries( - Object.entries(vpToken).map(([key, value]) => { - return [key, parseSinglePresentationsFromVpToken({ vpToken: value, path: '$' })] + Object.entries(parsed).map(([key, value]) => { + return [key, parseSinglePresentationFromVpToken({ vpToken: value })] }) ) return dcqlPresentationRecord } -export function parseSinglePresentationsFromVpToken(options: { +export function parseSinglePresentationFromVpToken(options: { vpToken: unknown - path: string + path?: string }): VpTokenPresentationParseResult { - const { vpToken: _vpToken } = options + const { vpToken: _vpToken, path } = options const vpToken = parseIfJson(_vpToken) + const zLdpVpProof = z.object({ challenge: z.string().optional() }).passthrough() const ldpVpParseResult = z .object({ '@context': z.string().optional(), verifiableCredential: z.string().optional(), - proof: z.object({ challenge: z.string().optional() }).passthrough().optional(), + proof: z.union([zLdpVpProof, z.array(zLdpVpProof)]).optional(), }) .passthrough() .safeParse(vpToken) if (ldpVpParseResult.success && (ldpVpParseResult.data['@context'] || ldpVpParseResult.data.verifiableCredential)) { - const challenge = ldpVpParseResult.data.proof?.challenge + const challenge = Array.isArray(ldpVpParseResult.data.proof) + ? ldpVpParseResult.data.proof.map((proof) => proof.challenge) + : ldpVpParseResult.data.proof?.challenge + + // check if all nonces are the same + if (Array.isArray(challenge)) { + const allNoncesAreTheSame = challenge.every((nonce) => nonce === challenge[0]) + if (!allNoncesAreTheSame) { + throw new Oauth2Error( + 'Failed to parse presentation from vp_token. LDP presentation is missing the proof.challenge parameter.' + ) + } + } + if (!challenge) { throw new Oauth2Error( 'Failed to parse presentation from vp_token. LDP presentation is missing the proof.challenge parameter.' @@ -92,8 +104,8 @@ export function parseSinglePresentationsFromVpToken(options: { return { format: 'ldp_vp', presentation: ldpVpParseResult, - path: options.path, - nonce: challenge, + path, + nonce: Array.isArray(challenge) ? challenge[0] : challenge, } } diff --git a/packages/openid4vp/tsconfig.json b/packages/openid4vp/tsconfig.json index 8ef75ec..6026321 100644 --- a/packages/openid4vp/tsconfig.json +++ b/packages/openid4vp/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "outDir": "build", - "lib": ["ES2020", "DOM"] + "outDir": "build" } } diff --git a/packages/utils/package.json b/packages/utils/package.json index 1a88a88..436fe4a 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -24,7 +24,7 @@ } }, "scripts": { - "build": "tsup src/index.ts --format cjs,esm --dts --clean" + "build": "tsup src/index.ts --format cjs,esm --dts --clean --sourcemap" }, "dependencies": { "buffer": "catalog:", diff --git a/packages/utils/src/encoding.ts b/packages/utils/src/encoding.ts index 2af4828..f89f46a 100644 --- a/packages/utils/src/encoding.ts +++ b/packages/utils/src/encoding.ts @@ -35,16 +35,3 @@ export function encodeToBase64Url(data: Uint8Array | string) { function base64ToBase64Url(base64: string) { return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') } - -export function uriEncodeObject(obj: Record) { - return Object.entries(obj) - .map( - ([key, val]) => - `${key}=${encodeURIComponent( - typeof val === 'string' || typeof val === 'boolean' || typeof val === 'number' - ? val - : encodeURIComponent(JSON.stringify(val as Record)) - )}` - ) - .join('&') -} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index e34a130..d7fb19a 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -22,7 +22,6 @@ export { encodeToBase64, encodeToBase64Url, encodeToUtf8String, - uriEncodeObject, } from './encoding' export { mergeDeep } from './object' export { @@ -49,7 +48,4 @@ export { type WwwAuthenticateHeaderChallenge, } from './www-authenticate' -export { xWwwFormUrlEncodeObject } from './x-www-form-url-encode' -export { uriDecodeObject } from './uri-encode-object' - export { isObject } from './object' diff --git a/packages/utils/src/parse.ts b/packages/utils/src/parse.ts index bca8b6e..77c3c4c 100644 --- a/packages/utils/src/parse.ts +++ b/packages/utils/src/parse.ts @@ -16,17 +16,17 @@ export function stringToJsonWithErrorHandling(string: string, errorMessage?: str } } -export function parseIfJson(input: T): T | Record { - if (typeof input !== 'string') { - return input +export function parseIfJson(data: T): T | Record { + if (typeof data !== 'string') { + return data } try { // Try to parse the string as JSON - return JSON.parse(input) + return JSON.parse(data) } catch (error) {} - return input + return data } export function parseWithErrorHandling( diff --git a/packages/utils/src/uri-encode-object.ts b/packages/utils/src/uri-encode-object.ts deleted file mode 100644 index c749292..0000000 --- a/packages/utils/src/uri-encode-object.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { URLSearchParams } from './globals' -export const uriEncodeObject = (obj: Record) => { - return Object.entries(obj) - .map( - ([key, val]) => - `${key}=${encodeURIComponent( - typeof val === 'string' || typeof val === 'boolean' || typeof val === 'number' - ? val - : encodeURIComponent(JSON.stringify(val as Record)) - )}` - ) - .join('&') -} - -export const uriDecodeObject = (encodedStr: string): Record => { - const params = new URLSearchParams(encodedStr) - const result: Record = {} - - params.forEach((value, key) => { - try { - // Try to parse as JSON first for objects and arrays - result[key] = JSON.parse(decodeURIComponent(value)) - } catch { - // If parsing fails, handle primitive types - const decodedValue = decodeURIComponent(value) - - if (decodedValue === 'true') { - result[key] = true - } else if (decodedValue === 'false') { - result[key] = false - } else if (!Number.isNaN(Number(decodedValue))) { - result[key] = Number(decodedValue) - } else { - result[key] = decodedValue - } - } - }) - - return result -} diff --git a/packages/utils/src/x-www-form-url-encode.ts b/packages/utils/src/x-www-form-url-encode.ts deleted file mode 100644 index 7538934..0000000 --- a/packages/utils/src/x-www-form-url-encode.ts +++ /dev/null @@ -1,13 +0,0 @@ -export function xWwwFormUrlEncodeObject(object: Record) { - return Object.entries(object) - .map(([key, value]) => { - if (value === null || typeof value === 'function' || typeof value === 'symbol' || typeof value === 'undefined') { - throw new Error(`Invalid value type for key: ${key}`) - } - - const stringifiedValue = typeof value === 'object' ? JSON.stringify(value) : String(value) - - return `${encodeURIComponent(key)}=${encodeURIComponent(stringifiedValue)}` - }) - .join('&') -}