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

feat: openid4vp alpha #21

Draft
wants to merge 20 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import type { CallbackContext, JwtSigner } from '@openid4vc/oauth2'
import { URL, URLSearchParams, objectToQueryParams } from '@openid4vc/utils'
import { type CallbackContext, type JwtSigner, Oauth2Error } from '@openid4vc/oauth2'
import { URL, URLSearchParams, objectToQueryParams, parseWithErrorHandling } from '@openid4vc/utils'
import { createJarAuthRequest } from '../jar/create-jar-auth-request'
import {
type WalletVerificationOptions,
validateOpenid4vpAuthorizationRequestPayload,
} from './validate-authorization-request'
import type { Openid4vpAuthorizationRequest } from './z-authorization-request'
import { type Openid4vpAuthorizationRequest, zOpenid4vpAuthorizationRequest } from './z-authorization-request'
import {
type Openid4vpAuthorizationRequestDcApi,
isOpenid4vpAuthorizationRequestDcApi,
zOpenid4vpAuthorizationRequestDcApi,
} from './z-authorization-request-dc-api'

export interface CreateOpenid4vpAuthorizationRequestOptions {
scheme?: string
requestParams: Openid4vpAuthorizationRequest
requestParams: Openid4vpAuthorizationRequest | Openid4vpAuthorizationRequestDcApi
jar?: {
requestUri: string
jwtSigner: JwtSigner
Expand Down Expand Up @@ -40,10 +45,31 @@ export interface CreateOpenid4vpAuthorizationRequestOptions {
export async function createOpenid4vpAuthorizationRequest(options: CreateOpenid4vpAuthorizationRequestOptions) {
const { jar, scheme = 'openid4vp://', requestParams, wallet, callbacks } = options

validateOpenid4vpAuthorizationRequestPayload({ params: requestParams, walletVerificationOptions: wallet })

let additionalJwtPayload: Record<string, unknown> | undefined

let authRequestParams: Openid4vpAuthorizationRequest | Openid4vpAuthorizationRequestDcApi
if (isOpenid4vpAuthorizationRequestDcApi(requestParams)) {
authRequestParams = parseWithErrorHandling(
zOpenid4vpAuthorizationRequestDcApi,
requestParams,
'Invalid authorization request. Could not parse openid4vp dc_api authorization request.'
)

if (jar && !authRequestParams.expected_origins) {
throw new Oauth2Error(
`The 'expected_origins' parameter MUST be present when using the dc_api response mode in combinaction with jar.`
)
}
} else {
authRequestParams = parseWithErrorHandling(
zOpenid4vpAuthorizationRequest,
requestParams,
'Invalid authorization request. Could not parse openid4vp authorization request.'
)
validateOpenid4vpAuthorizationRequestPayload({ params: authRequestParams, walletVerificationOptions: wallet })
authRequestParams = requestParams
}

if (jar) {
if (!jar.additionalJwtPayload?.aud) {
additionalJwtPayload = { ...jar.additionalJwtPayload, aud: jar.requestUri }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { Oauth2Error, decodeJwt } from '@openid4vc/oauth2'
import { 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 JarAuthRequest, isJarAuthRequest, zJarAuthRequest } from '../jar/z-jar-auth-request'
import { type Openid4vpAuthorizationRequest, zOpenid4vpAuthorizationRequest } from './z-authorization-request'
import {
type Openid4vpAuthorizationRequestDcApi,
isOpenid4vpAuthorizationRequestDcApi,
zOpenid4vpAuthorizationRequestDcApi,
} from './z-authorization-request-dc-api'

export interface ParsedJarOpenid4vpAuthRequest {
type: 'jar'
Expand All @@ -17,13 +22,19 @@ export interface ParsedOpenid4vpAuthRequest {
params: Openid4vpAuthorizationRequest
}

export interface ParsedOpenid4vpDcApiAuthRequest {
type: 'openid4vp_dc_api'
provided: 'uri' | 'jwt' | 'params'
params: Openid4vpAuthorizationRequestDcApi
}

export interface ParseOpenid4vpAuthRequestPayloadOptions {
requestPayload: string | Record<string, unknown>
}

export function parseOpenid4vpAuthorizationRequestPayload(
options: ParseOpenid4vpAuthRequestPayloadOptions
): ParsedOpenid4vpAuthRequest | ParsedJarOpenid4vpAuthRequest {
): ParsedOpenid4vpAuthRequest | ParsedJarOpenid4vpAuthRequest | ParsedOpenid4vpDcApiAuthRequest {
const { requestPayload } = options
let provided: 'uri' | 'jwt' | 'params' = 'params'

Expand All @@ -42,26 +53,30 @@ export function parseOpenid4vpAuthorizationRequestPayload(
params = requestPayload
}

const parsedRequest = parseWithErrorHandling(z.union([zOpenid4vpAuthorizationRequest, zJarAuthRequest]), params)
const parsedOpenid4vpAuthRequest = zOpenid4vpAuthorizationRequest.safeParse(parsedRequest)
if (parsedOpenid4vpAuthRequest.success) {
const parsedRequest = parseWithErrorHandling(
z.union([zOpenid4vpAuthorizationRequest, zJarAuthRequest, zOpenid4vpAuthorizationRequestDcApi]),
params
)

if (isOpenid4vpAuthorizationRequestDcApi(parsedRequest)) {
return {
type: 'openid4vp',
type: 'openid4vp_dc_api',
provided,
params: parsedOpenid4vpAuthRequest.data,
params: parsedRequest,
}
}

const parsedJarAuthRequest = zJarAuthRequest.safeParse(parsedRequest)
if (parsedJarAuthRequest.success) {
if (isJarAuthRequest(parsedRequest)) {
return {
type: 'jar',
provided,
params: parsedJarAuthRequest.data,
params: parsedRequest,
}
}

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.'
)
return {
type: 'openid4vp',
provided,
params: parsedRequest,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,57 @@ 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 { type JarAuthRequest, isJarAuthRequest, 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'
import {
type Openid4vpAuthorizationRequestDcApi,
isOpenid4vpAuthorizationRequestDcApi,
zOpenid4vpAuthorizationRequestDcApi,
} from './z-authorization-request-dc-api'

export interface ResolveOpenid4vpAuthorizationRequestOptions {
request: Openid4vpAuthorizationRequest | JarAuthRequest
wallet?: WalletVerificationOptions
origin?: string
callbacks: Pick<CallbackContext, 'verifyJwt' | 'decryptJwe' | 'getX509CertificateMetadata'>
}

export async function resolveOpenid4vpAuthorizationRequest(options: ResolveOpenid4vpAuthorizationRequestOptions) {
const { request, wallet, callbacks } = options
const { request, wallet, callbacks, origin } = options

let authRequestPayload: Openid4vpAuthorizationRequest
let authRequestPayload:
| Openid4vpAuthorizationRequest
| (Openid4vpAuthorizationRequestDcApi & { presentation_definition_uri?: never })
let jar: Awaited<ReturnType<typeof verifyJarRequest>> | undefined

const parsed = parseWithErrorHandling(
z.union([zOpenid4vpAuthorizationRequest, zJarAuthRequest]),
z.union([zOpenid4vpAuthorizationRequestDcApi, 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)
if (isJarAuthRequest(request)) {
const parsedJarAuthRequest = parseWithErrorHandling(
zJarAuthRequest,
parsed,
'Invalid authorization request. Could not parse jar auth request.'
)
jar = await verifyJarRequest({ jarRequestParams: parsedJarAuthRequest, callbacks, wallet })
authRequestPayload = zOpenid4vpAuthorizationRequest.parse(jar.authRequestParams)
authRequestPayload = parseOpenid4vpAuthorizationRequestPayload({
request: jar.authRequestParams,
wallet,
jar: true,
origin,
})
} else {
authRequestPayload = parseOpenid4vpAuthorizationRequestPayload({ request, wallet, jar: false, origin })
}

validateOpenid4vpAuthorizationRequestPayload({ params: authRequestPayload, walletVerificationOptions: wallet })

const clientMeta = parseClientIdentifier({ request: authRequestPayload, jar, callbacks })

let pex:
Expand Down Expand Up @@ -80,3 +94,51 @@ export async function resolveOpenid4vpAuthorizationRequest(options: ResolveOpeni
}

export type ResolvedOpenid4vpAuthRequest = Awaited<ReturnType<typeof resolveOpenid4vpAuthorizationRequest>>

function parseOpenid4vpAuthorizationRequestPayload(options: {
request: Record<string, unknown>
wallet?: WalletVerificationOptions
jar: boolean
origin?: string
}) {
const { request, wallet, jar, origin } = options

if (isOpenid4vpAuthorizationRequestDcApi(request)) {
const parsed = parseWithErrorHandling(
zOpenid4vpAuthorizationRequestDcApi,
request,
'Invalid authorization request. Could not parse openid4vp dc_api authorization request.'
)

if (jar && !request.expected_origins) {
throw new Oauth2Error(
`The 'expected_origins' parameter MUST be present when using the dc_api response mode in combinaction with jar.`
)
}

if (request.expected_origins) {
if (!origin) {
throw new Oauth2Error(
`The 'origin' validation parameter MUST be present when resolving an openid4vp dc_api authorization request.`
)
}

if (request.expected_origins && !request.expected_origins.includes(origin)) {
throw new Oauth2Error(
`The 'expected_origins' parameter MUST include the origin of the authorization request. Current: ${request.expected_origins}`
)
}
}

return parsed
}

const authRequestPayload = parseWithErrorHandling(
zOpenid4vpAuthorizationRequest,
request,
'Invalid authorization request. Could not parse openid4vp authorization request.'
)
validateOpenid4vpAuthorizationRequestPayload({ params: authRequestPayload, walletVerificationOptions: wallet })

return authRequestPayload
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { z } from 'zod'
import type { JarAuthRequest } from '../jar/z-jar-auth-request'
import { type Openid4vpAuthorizationRequest, zOpenid4vpAuthorizationRequest } from './z-authorization-request'

export const zOpenid4vpAuthorizationRequestDcApi = zOpenid4vpAuthorizationRequest
.pick({
client_id: true,
response_type: true,
response_mode: true,
nonce: true,
presentation_definition: true,
client_metadata: true,
transaction_data: true,
dcql_query: true,
})
.extend({
client_id: z.optional(z.string()),
expected_origins: z.array(z.string()).optional(),
response_mode: z.enum(['dc_api', 'dc_api.jwt']).optional(),
})
.strip()

export type Openid4vpAuthorizationRequestDcApi = z.infer<typeof zOpenid4vpAuthorizationRequestDcApi>

export function isOpenid4vpAuthorizationRequestDcApi(
request: Openid4vpAuthorizationRequest | JarAuthRequest | Openid4vpAuthorizationRequestDcApi
): request is Openid4vpAuthorizationRequestDcApi {
return request.response_mode === 'dc_api' || request.response_mode === 'dc_api.jwt'
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,28 @@ import {
import { decodeBase64, encodeToUtf8String, parseWithErrorHandling } from '@openid4vc/utils'
import z from 'zod'
import { parseOpenid4vpAuthorizationRequestPayload } from '../authorization-request/parse-authorization-request-params'
import { isOpenid4vpAuthorizationRequestDcApi } from '../authorization-request/z-authorization-request-dc-api'
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'
import {
isOpenid4vpAuthorizationResponseDcApi,
zOpenid4vpAuthorizationResponseDcApi,
} from './z-authorization-response-dc-api'

function parseOpenid4VpAuthorizationResponsePaylaod(payload: Record<string, unknown>) {
if (isOpenid4vpAuthorizationRequestDcApi(payload)) {
return parseWithErrorHandling(
zOpenid4vpAuthorizationResponseDcApi,
payload,
'Invalid openid4vp authorization response.'
)
}

return parseWithErrorHandling(zOpenid4vpAuthorizationResponse, payload, 'Invalid openid4vp authorization response.')
}

export interface ParseJarmAuthorizationResponseOptions {
jarmResponseJwt: string
Expand Down Expand Up @@ -49,14 +66,12 @@ export async function parseJarmAuthorizationResponse(options: ParseJarmAuthoriza
throw new Oauth2Error('Invalid authorization request. Could not parse openid4vp authorization request.')
}

const authResponsePayload = parseWithErrorHandling(
zOpenid4vpAuthorizationResponse,
verifiedJarmResponse.jarmAuthResponse,
'Invalid jarm authorization response.'
)
const authResponsePayload = parseOpenid4VpAuthorizationResponsePaylaod(verifiedJarmResponse.jarmAuthResponse)
const validateOpenId4vpResponse = validateOpenid4vpAuthorizationResponse({
authorizationRequest: parsedAuthorizationRequest.params,
authorizationResponse: authResponsePayload,
authorizationResponse: isOpenid4vpAuthorizationResponseDcApi(authResponsePayload)
? authResponsePayload.data
: authResponsePayload,
})

const authRequestPayload = parsedAuthorizationRequest.params
Expand Down Expand Up @@ -102,13 +117,7 @@ export async function parseOpenid4vpAuthorizationResponse(options: ParseOpenid4v
return parseJarmAuthorizationResponse({ jarmResponseJwt: responsePayload.response as string, callbacks })
}

const authorizationResponsePayload = responsePayload

const authResponsePayload = parseWithErrorHandling(
zOpenid4vpAuthorizationResponse,
authorizationResponsePayload,
'Invalid authorization response.'
)
const authResponsePayload = parseOpenid4VpAuthorizationResponsePaylaod(responsePayload)

const authRequest = await callbacks.getOpenid4vpAuthorizationRequest(authResponsePayload)
const parsedAuthRequest = parseOpenid4vpAuthorizationRequestPayload({ requestPayload: authRequest.authRequest })
Expand All @@ -120,7 +129,9 @@ export async function parseOpenid4vpAuthorizationResponse(options: ParseOpenid4v

const validateOpenId4vpResponse = validateOpenid4vpAuthorizationResponse({
authorizationRequest: authRequestPayload,
authorizationResponse: authResponsePayload,
authorizationResponse: isOpenid4vpAuthorizationResponseDcApi(authResponsePayload)
? authResponsePayload.data
: authResponsePayload,
})

if (authRequestPayload.response_mode && isJarmResponseMode(authRequestPayload.response_mode)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { z } from 'zod'
import { type Openid4vpAuthorizationResponse, zOpenid4vpAuthorizationResponse } from './z-authorization-response'

export const zOpenid4vpAuthorizationResponseDcApi = z
.object({
protocol: z.literal('openid4vp'),
data: zOpenid4vpAuthorizationResponse,
})
.passthrough()
export type Openid4vpAuthorizationResponseDcApi = z.infer<typeof zOpenid4vpAuthorizationResponseDcApi>

export function isOpenid4vpAuthorizationResponseDcApi(
response: Openid4vpAuthorizationResponse | Openid4vpAuthorizationResponseDcApi
): response is Openid4vpAuthorizationResponseDcApi {
return 'protocol' in response && 'data' in response
}
Loading
Loading