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 #21

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8b84e2c
feat: openid4vp alpha
auer-martin Jan 26, 2025
3413eb6
feat: dcql support
auer-martin Feb 4, 2025
5ec2bad
Merge branch 'main' of github.com:animo/oid4vc-ts
auer-martin Feb 5, 2025
84a8dec
fix: incorporate feedback
auer-martin Feb 10, 2025
7663c71
feat: web-origin support
auer-martin Feb 16, 2025
64623b4
fix: web-origin
auer-martin Feb 17, 2025
6c0ac2e
fix: allow dcql_query to be an object
auer-martin Feb 17, 2025
99cdec6
feat: add dc api response params
auer-martin Feb 17, 2025
53aec5b
fix: client-id-scheme validation
auer-martin Feb 17, 2025
7e08a19
fix: web-origin client-id handling
auer-martin Feb 17, 2025
52a3f3f
fix: remove watch
TimoGlastra Feb 17, 2025
65366cb
fix: export callbacks
TimoGlastra Feb 17, 2025
3a058b1
fix: pass origin
TimoGlastra Feb 18, 2025
e574612
fix: only use actual client_id, not inferred
TimoGlastra Feb 18, 2025
083b286
fix: error-handling
auer-martin Feb 18, 2025
c4172cf
Merge branch 'openid4vp-alpha' of https://github.com/auer-martin/oid4…
auer-martin Feb 18, 2025
2128263
fix: remove js import
auer-martin Feb 18, 2025
e152043
fix: v11 conversion
auer-martin Feb 18, 2025
f7a5276
fix: support web-origin scheme
TimoGlastra Feb 18, 2025
8828bfa
chore: export type
TimoGlastra Feb 18, 2025
9d7a5f0
fix: allow response with dc api
TimoGlastra Feb 23, 2025
223eb72
fix: allow any object for dcql and presentation definition
TimoGlastra Feb 19, 2025
841ce30
prepare for merge
TimoGlastra Feb 19, 2025
b4a7226
docs(changeset): renamed oid4vci to openid4vci. This includes the pac…
TimoGlastra Feb 19, 2025
127d91d
docs(changeset): feat: initial version of openid4vp
TimoGlastra Feb 19, 2025
58c5d8f
align version of oid4vp
TimoGlastra Feb 19, 2025
68885bb
styling
TimoGlastra Feb 19, 2025
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