Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: remove presentation decoding #50

Merged
merged 2 commits into from
Mar 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions packages/openid4vp/src/Openid4vpVerifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,11 @@ import {
} from './authorization-response/parse-authorization-response'
import {
type ValidateOpenid4vpAuthorizationResponseOptions,
validateOpenid4vpAuthorizationResponse,
validateOpenid4vpAuthorizationResponsePayload,
} 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'
import { parseDcqlVpToken, parsePexVpToken } from './vp-token/parse-vp-token'

export interface Openid4vpVerifierOptions {
/**
Expand All @@ -46,12 +43,16 @@ export class Openid4vpVerifier {
return parseOpenid4vpAuthorizationResponse(options)
}

public validateOpenid4vpAuthorizationResponse(options: ValidateOpenid4vpAuthorizationResponseOptions) {
return validateOpenid4vpAuthorizationResponse(options)
public validateOpenid4vpAuthorizationResponsePayload(options: ValidateOpenid4vpAuthorizationResponseOptions) {
return validateOpenid4vpAuthorizationResponsePayload(options)
}

public parsePexVpToken(vpToken: unknown) {
return parsePexVpToken(vpToken)
}

public parsePresentationsFromVpToken(options: ParsePresentationsFromVpTokenOptions) {
return parsePresentationsFromVpToken(options)
public parseDcqlVpToken(vpToken: unknown) {
return parseDcqlVpToken(vpToken)
}

public parseTransactionData(options: ParseTransactionDataOptions) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { type CallbackContext, Oauth2Error, Oauth2ServerErrorResponseError } from '@openid4vc/oauth2'
import { parseOpenid4vpAuthorizationRequestPayload } from '../authorization-request/parse-authorization-request-params'
import type { GetOpenid4vpAuthorizationRequestCallback } from '../jarm/jarm-auth-response/verify-jarm-auth-response'
import type { Openid4vpAuthorizationRequest } from '../authorization-request/z-authorization-request'
import type { Openid4vpAuthorizationRequestDcApi } from '../authorization-request/z-authorization-request-dc-api'
import type {
GetOpenid4vpAuthorizationRequestCallback,
VerifiedJarmAuthorizationResponse,
} from '../jarm/jarm-auth-response/verify-jarm-auth-response'
import type { JarmHeader } from '../jarm/jarm-auth-response/z-jarm-auth-response'
import { isJarmResponseMode } from '../jarm/jarm-response-mode'
import { parseOpenid4VpAuthorizationResponsePayload } from './parse-authorization-response-payload'
import { parseJarmAuthorizationResponse } from './parse-jarm-authorization-response'
import { validateOpenid4vpAuthorizationResponse } from './validate-authorization-response'
import { validateOpenid4vpAuthorizationResponsePayload } from './validate-authorization-response'
import type { ValidateOpenid4VpAuthorizationResponseResult } from './validate-authorization-response-result'
import type { Openid4vpAuthorizationResponse } from './z-authorization-response'

export interface ParseOpenid4vpAuthorizationResponseOptions {
responsePayload: Record<string, unknown>
Expand All @@ -13,29 +21,46 @@ export interface ParseOpenid4vpAuthorizationResponseOptions {
}
}

export async function parseOpenid4vpAuthorizationResponse(options: ParseOpenid4vpAuthorizationResponseOptions) {
export type ParsedOpenid4vpAuthorizationResponse = ValidateOpenid4VpAuthorizationResponseResult & {
authorizationResponsePayload: Openid4vpAuthorizationResponse
authorizationRequestPayload: Openid4vpAuthorizationRequest | Openid4vpAuthorizationRequestDcApi

expectedNonce: string

// TODO: return this
// expectedTransactionDataHashes?: []

jarm?: VerifiedJarmAuthorizationResponse & {
jarmHeader: JarmHeader
mdocGeneratedNonce?: string
}
}

export async function parseOpenid4vpAuthorizationResponse(
options: ParseOpenid4vpAuthorizationResponseOptions
): Promise<ParsedOpenid4vpAuthorizationResponse> {
const { responsePayload, callbacks } = options

if (responsePayload.response) {
return parseJarmAuthorizationResponse({ jarmResponseJwt: responsePayload.response as string, callbacks })
}

const authResponsePayload = parseOpenid4VpAuthorizationResponsePayload(responsePayload)
const authorizationResponsePayload = parseOpenid4VpAuthorizationResponsePayload(responsePayload)

const { authorizationRequest } = await callbacks.getOpenid4vpAuthorizationRequest(authResponsePayload)
const { authorizationRequest } = await callbacks.getOpenid4vpAuthorizationRequest(authorizationResponsePayload)
const parsedAuthRequest = parseOpenid4vpAuthorizationRequestPayload({ authorizationRequest: authorizationRequest })
if (parsedAuthRequest.type !== 'openid4vp' && parsedAuthRequest.type !== 'openid4vp_dc_api') {
throw new Oauth2Error('Invalid authorization request. Could not parse openid4vp authorization request.')
}

const authRequestPayload = parsedAuthRequest.params
const authorizationRequestPayload = parsedAuthRequest.params

const validateOpenId4vpResponse = validateOpenid4vpAuthorizationResponse({
requestPayload: authRequestPayload,
responsePayload: authResponsePayload,
const validateOpenId4vpResponse = validateOpenid4vpAuthorizationResponsePayload({
requestPayload: authorizationRequestPayload,
responsePayload: authorizationResponsePayload,
})

if (authRequestPayload.response_mode && isJarmResponseMode(authRequestPayload.response_mode)) {
if (authorizationRequestPayload.response_mode && isJarmResponseMode(authorizationRequestPayload.response_mode)) {
throw new Oauth2ServerErrorResponseError(
{
error: 'invalid_request',
Expand All @@ -49,10 +74,10 @@ export async function parseOpenid4vpAuthorizationResponse(options: ParseOpenid4v

return {
...validateOpenId4vpResponse,
authResponsePayload,
authRequestPayload,
expectedNonce: authorizationRequestPayload.nonce,

authorizationResponsePayload,
authorizationRequestPayload,
jarm: undefined,
}
}

export type ParsedOpenid4vpAuthorizationResponse = Awaited<ReturnType<typeof parseOpenid4vpAuthorizationResponse>>
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import {
type CallbackContext,
Oauth2Error,
decodeJwtHeader,
zCompactJwe,
zCompactJwt,
zJwtHeader,
} from '@openid4vc/oauth2'
import { type CallbackContext, Oauth2Error, decodeJwtHeader, zCompactJwe, zCompactJwt } from '@openid4vc/oauth2'
import { decodeBase64, encodeToUtf8String, parseWithErrorHandling } from '@openid4vc/utils'
import z from 'zod'
import { parseOpenid4vpAuthorizationRequestPayload } from '../authorization-request/parse-authorization-request-params'
import {
type GetOpenid4vpAuthorizationRequestCallback,
verifyJarmAuthorizationResponse,
} from '../jarm/jarm-auth-response/verify-jarm-auth-response'
import { zJarmHeader } from '../jarm/jarm-auth-response/z-jarm-auth-response'
import { isJarmResponseMode } from '../jarm/jarm-response-mode'
import type { ParsedOpenid4vpAuthorizationResponse } from './parse-authorization-response'
import { parseOpenid4VpAuthorizationResponsePayload } from './parse-authorization-response-payload'
import { validateOpenid4vpAuthorizationResponse } from './validate-authorization-response'
import { validateOpenid4vpAuthorizationResponsePayload } from './validate-authorization-response'

export interface ParseJarmAuthorizationResponseOptions {
jarmResponseJwt: string
Expand All @@ -24,7 +19,9 @@ export interface ParseJarmAuthorizationResponseOptions {
}
}

export async function parseJarmAuthorizationResponse(options: ParseJarmAuthorizationResponseOptions) {
export async function parseJarmAuthorizationResponse(
options: ParseJarmAuthorizationResponseOptions
): Promise<ParsedOpenid4vpAuthorizationResponse> {
const { jarmResponseJwt, callbacks } = options

const jarmAuthorizationResponseJwt = parseWithErrorHandling(
Expand All @@ -34,7 +31,7 @@ export async function parseJarmAuthorizationResponse(options: ParseJarmAuthoriza
)

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,
Expand All @@ -48,16 +45,16 @@ export async function parseJarmAuthorizationResponse(options: ParseJarmAuthoriza
throw new Oauth2Error('Invalid authorization request. Could not parse openid4vp authorization request.')
}

const authResponsePayload = parseOpenid4VpAuthorizationResponsePayload(verifiedJarmResponse.jarmAuthResponse)
const validateOpenId4vpResponse = validateOpenid4vpAuthorizationResponse({
const authorizationResponsePayload = parseOpenid4VpAuthorizationResponsePayload(verifiedJarmResponse.jarmAuthResponse)
const validateOpenId4vpResponse = validateOpenid4vpAuthorizationResponsePayload({
requestPayload: parsedAuthorizationRequest.params,
responsePayload: authResponsePayload,
responsePayload: authorizationResponsePayload,
})

const authRequestPayload = parsedAuthorizationRequest.params
if (!authRequestPayload.response_mode || !isJarmResponseMode(authRequestPayload.response_mode)) {
const authorizationRequestPayload = parsedAuthorizationRequest.params
if (!authorizationRequestPayload.response_mode || !isJarmResponseMode(authorizationRequestPayload.response_mode)) {
throw new Oauth2Error(
`Invalid response mode for jarm response. Response mode: '${authRequestPayload.response_mode ?? 'fragment'}'`
`Invalid response mode for jarm response. Response mode: '${authorizationRequestPayload.response_mode ?? 'fragment'}'`
)
}

Expand All @@ -68,15 +65,17 @@ export async function parseJarmAuthorizationResponse(options: ParseJarmAuthoriza
}
if (jarmHeader?.apv) {
const jarmRequestNonce = encodeToUtf8String(decodeBase64(jarmHeader.apv))
if (jarmRequestNonce !== authRequestPayload.nonce) {
if (jarmRequestNonce !== authorizationRequestPayload.nonce) {
throw new Oauth2Error('The nonce in the jarm header does not match the nonce in the request.')
}
}

return {
...validateOpenId4vpResponse,
jarm: { ...verifiedJarmResponse, jarmHeader, mdocGeneratedNonce },
authResponsePayload,
authRequestPayload,

expectedNonce: authorizationRequestPayload.nonce,
authorizationResponsePayload,
authorizationRequestPayload,
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { VpTokenPresentationParseResult } from '../vp-token/parse-presentations-from-vp-token'
import type { PexPresentationSubmission } from '../models/z-pex'
import type { VpTokenDcql, VpTokenPexEntry } from '../vp-token/z-vp-token'

export interface ValidateOpenid4VpPexAuthorizationResponseResult {
type: 'pex'

pex: {
presentationSubmission: unknown
presentations: [VpTokenPresentationParseResult, ...VpTokenPresentationParseResult[]]
presentationSubmission: PexPresentationSubmission
presentations: [VpTokenPexEntry, ...VpTokenPexEntry[]]
} & (
| { scope: string; presentationDefinition?: never }
| { scope?: never; presentationDefinition: Record<string, unknown> | string }
Expand All @@ -14,7 +16,7 @@ export interface ValidateOpenid4VpPexAuthorizationResponseResult {
export interface ValidateOpenid4VpDcqlAuthorizationResponseResult {
type: 'dcql'
dcql: {
presentation: Record<string, VpTokenPresentationParseResult>
presentations: VpTokenDcql
} & ({ scope: string; query?: never } | { scope?: never; query: unknown })
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { Oauth2Error } from '@openid4vc/oauth2'
import type { Openid4vpAuthorizationRequest } from '../authorization-request/z-authorization-request'
import type { Openid4vpAuthorizationRequestDcApi } from '../authorization-request/z-authorization-request-dc-api'
import {
parseDcqlPresentationFromVpToken,
parsePresentationsFromVpToken,
} from '../vp-token/parse-presentations-from-vp-token'
import { parseDcqlVpToken, parsePexVpToken } from '../vp-token/parse-vp-token'
import type { ValidateOpenid4VpAuthorizationResponseResult } from './validate-authorization-response-result'
import type { Openid4vpAuthorizationResponse } from './z-authorization-response'

Expand All @@ -20,13 +17,10 @@ export interface ValidateOpenid4vpAuthorizationResponseOptions {
* - 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(
export function validateOpenid4vpAuthorizationResponsePayload(
options: ValidateOpenid4vpAuthorizationResponseOptions
): ValidateOpenid4VpAuthorizationResponseResult {
const { requestPayload, responsePayload } = options
if (!responsePayload.vp_token) {
throw new Oauth2Error('Failed to verify OpenId4Vp Authorization Response. vp_token is missing.')
}

if ('state' in requestPayload && requestPayload.state !== responsePayload.state) {
throw new Oauth2Error('OpenId4Vp Authorization Response state mismatch.')
Expand All @@ -42,70 +36,42 @@ export function validateOpenid4vpAuthorizationResponse(
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: responsePayload.vp_token })
if (presentations.every((p) => p.nonce) && !presentations.every((p) => p.nonce === requestPayload.nonce)) {
throw new Oauth2Error(
'Presentation nonce mismatch. The nonce of some presentations does not match the nonce of the request.'
)
}

return {
type: 'pex',
pex:
'scope' in requestPayload && requestPayload.scope
? {
scope: requestPayload.scope,
presentationSubmission: responsePayload.presentation_submission,
presentations,
presentations: parsePexVpToken(responsePayload.vp_token),
}
: {
presentationDefinition: requestPayload.presentation_definition,
presentationSubmission: responsePayload.presentation_submission,
presentations,
presentations: parsePexVpToken(responsePayload.vp_token),
},
}
}

if (requestPayload.dcql_query) {
if (Array.isArray(responsePayload.vp_token)) {
throw new Oauth2Error(
'The OpenId4Vp Authorization Response contains multiple vp_token values. In combination with dcql this is not possible.'
)
}

if (typeof responsePayload.vp_token !== 'string' && typeof responsePayload.vp_token !== 'object') {
throw new Oauth2Error('With DCQL the vp_token must be a JSON-encoded object.')
}

const presentation = parseDcqlPresentationFromVpToken({ vpToken: responsePayload.vp_token })

// TODO: CHECK ALL THE NONCES ONCE WE KNOW HOW TO GET THE NONCE FOR MDOCS AND ANONCREDS
if (
Object.values(presentation).every((p) => p.nonce) &&
!Object.values(presentation).every((p) => p.nonce === requestPayload.nonce)
) {
throw new Oauth2Error(
'Presentation nonce mismatch. The nonce of some presentations does not match the nonce of the request.'
)
}
const presentations = parseDcqlVpToken(responsePayload.vp_token)

return {
type: 'dcql',
dcql:
'scope' in requestPayload && requestPayload.scope
? {
scope: requestPayload.scope,
presentation,
presentations,
}
: {
query: requestPayload.dcql_query,
presentation,
presentations,
},
}
}

throw new Oauth2Error(
'Invalid OpenId4Vp Authorization Response. Response neither contains a presentation_submission nor a dcql_query.'
'Invalid OpenId4Vp Authorization Response. Response neither contains a presentation_submission nor request contains a dcql_query.'
)
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { z } from 'zod'
import { zPexPresentationSubmission } from '../models/z-pex'
import { zVpToken } from '../vp-token/z-vp-token'

export const zOpenid4vpAuthorizationResponse = z
.object({
state: z.string().optional(),
id_token: z.string().optional(),
vp_token: zVpToken,
presentation_submission: z.unknown().optional(),
presentation_submission: zPexPresentationSubmission.optional(),
refresh_token: z.string().optional(),
token_type: z.string().optional(),
access_token: z.string().optional(),
Expand Down
9 changes: 4 additions & 5 deletions packages/openid4vp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export {
SubmitOpenid4vpAuthorizationResponseOptions,
} from './authorization-response/submit-authorization-response'
export {
validateOpenid4vpAuthorizationResponse,
validateOpenid4vpAuthorizationResponsePayload,
ValidateOpenid4vpAuthorizationResponseOptions,
} from './authorization-response/validate-authorization-response'
export {
Expand All @@ -43,10 +43,9 @@ export {
} 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'
parsePexVpToken,
parseDcqlVpToken,
} from './vp-token/parse-vp-token'

export {
parseOpenid4vpAuthorizationResponse,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ export interface VerifyJarmAuthorizationResponseOptions {
}
}

export type VerifiedJarmAuthorizationResponse = Awaited<ReturnType<typeof verifyJarmAuthorizationResponse>>

/**
* Validate a JARM direct_post.jwt compliant authentication response
* * The decryption key should be resolvable using the the protected header's 'kid' field
Expand Down
Loading
Loading