From 61a14e601d0bf35a809961bff9e6c89bc7cd0866 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Wed, 11 Dec 2024 16:45:39 +0100 Subject: [PATCH 1/3] chore: return unencrypted state value along with jarm response --- .../jarm-auth-response-send.ts | 34 +++++++++---------- packages/siop-oid4vp/lib/op/OP.ts | 1 + 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/jarm/lib/jarm-auth-response-send/jarm-auth-response-send.ts b/packages/jarm/lib/jarm-auth-response-send/jarm-auth-response-send.ts index 777b2caf..7bb77aeb 100644 --- a/packages/jarm/lib/jarm-auth-response-send/jarm-auth-response-send.ts +++ b/packages/jarm/lib/jarm-auth-response-send/jarm-auth-response-send.ts @@ -1,7 +1,7 @@ -import { appendFragmentParams, appendQueryParams } from '../utils.js'; -import type { JarmResponseMode, Openid4vpJarmResponseMode } from '../v-response-mode-registry.js'; -import { getJarmDefaultResponseMode, validateResponseMode } from '../v-response-mode-registry.js'; -import type { ResponseTypeOut } from '../v-response-type-registry.js'; +import { appendFragmentParams, appendQueryParams } from '../utils.js' +import type { JarmResponseMode, Openid4vpJarmResponseMode } from '../v-response-mode-registry.js' +import { getJarmDefaultResponseMode, validateResponseMode } from '../v-response-mode-registry.js' +import type { ResponseTypeOut } from '../v-response-type-registry.js' interface JarmAuthResponseSendInput { authRequestParams: { @@ -17,10 +17,11 @@ interface JarmAuthResponseSendInput { ); authResponse: string; + state: string; } export const jarmAuthResponseSend = async (input: JarmAuthResponseSendInput): Promise => { - const { authRequestParams, authResponse } = input; + const { authRequestParams, authResponse, state } = input; const responseEndpoint = 'response_uri' in authRequestParams ? new URL(authRequestParams.response_uri) : new URL(authRequestParams.redirect_uri); @@ -36,40 +37,39 @@ export const jarmAuthResponseSend = async (input: JarmAuthResponseSendInput): Pr switch (responseMode) { case 'direct_post.jwt': - return handleDirectPostJwt(responseEndpoint, authResponse); + return handleDirectPostJwt(responseEndpoint, authResponse, state); case 'query.jwt': - return handleQueryJwt(responseEndpoint, authResponse); + return handleQueryJwt(responseEndpoint, authResponse, state); case 'fragment.jwt': - return handleFragmentJwt(responseEndpoint, authResponse); + return handleFragmentJwt(responseEndpoint, authResponse, state); case 'form_post.jwt': throw new Error('Not implemented. form_post.jwt is not yet supported.'); } }; -async function handleDirectPostJwt(responseEndpoint: URL, responseJwt: string) { - const response = await fetch(responseEndpoint, { +async function handleDirectPostJwt(responseEndpoint: URL, responseJwt: string, state: string) { + const response = await fetch(responseEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: `response=${responseJwt}`, - }); - + body: `response=${responseJwt}&state=${state}` + }) return response; } -async function handleQueryJwt(responseEndpoint: URL, responseJwt: string) { +async function handleQueryJwt(responseEndpoint: URL, responseJwt: string, state: string) { const responseUrl = appendQueryParams({ url: responseEndpoint, - params: { response: responseJwt }, + params: { response: responseJwt, state }, }); const response = await fetch(responseUrl, { method: 'POST' }); return response; } -async function handleFragmentJwt(responseEndpoint: URL, responseJwt: string) { +async function handleFragmentJwt(responseEndpoint: URL, responseJwt: string, state: string) { const responseUrl = appendFragmentParams({ url: responseEndpoint, - fragments: { response: responseJwt }, + fragments: { response: responseJwt, state }, }); const response = await fetch(responseUrl, { method: 'POST' }); return response; diff --git a/packages/siop-oid4vp/lib/op/OP.ts b/packages/siop-oid4vp/lib/op/OP.ts index 0f188ccc..6ec4474d 100644 --- a/packages/siop-oid4vp/lib/op/OP.ts +++ b/packages/siop-oid4vp/lib/op/OP.ts @@ -246,6 +246,7 @@ export class OP { response_type: responseType, }, authResponse: response, + state: requestObjectPayload.state }) void this.emitEvent(AuthorizationEvents.ON_AUTH_RESPONSE_SENT_SUCCESS, { correlationId, subject: response }) return jarmResponse From 2dff0df4f3d9c0943b9e93ea2c9666fab43747c2 Mon Sep 17 00:00:00 2001 From: Niels Klomp Date: Sun, 22 Dec 2024 23:33:00 +0100 Subject: [PATCH 2/3] feat: MWALL-715 Create notification endpoint logic in Issuer --- packages/client/lib/OpenID4VCIClient.ts | 4 +- .../client/lib/OpenID4VCIClientV1_0_13.ts | 4 +- .../client/lib/functions/notifications.ts | 4 +- packages/did-auth-siop-adapter/package.json | 2 +- .../issuer-rest/lib/oid4vci-api-functions.ts | 37 ++++++--- packages/issuer/lib/VcIssuer.ts | 78 ++++++++++++++++--- packages/oid4vci-common/lib/events/index.ts | 1 + .../oid4vci-common/lib/types/Generic.types.ts | 2 +- .../lib/types/StateManager.types.ts | 6 +- packages/siop-oid4vp/package.json | 2 +- 10 files changed, 110 insertions(+), 30 deletions(-) diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index 0ec729d2..6769ebbd 100644 --- a/packages/client/lib/OpenID4VCIClient.ts +++ b/packages/client/lib/OpenID4VCIClient.ts @@ -28,7 +28,7 @@ import { getTypesFromObject, KID_JWK_X5C_ERROR, NotificationRequest, - NotificationResult, + NotificationResponseResult, OID4VCICredentialFormat, OpenId4VCIVersion, PKCEOpts, @@ -531,7 +531,7 @@ export class OpenID4VCIClient { credentialRequestOpts: Partial, request: NotificationRequest, accessToken?: string, - ): Promise { + ): Promise { return sendNotification(credentialRequestOpts, request, accessToken ?? this._state.accessToken ?? this._state.accessTokenResponse?.access_token); } diff --git a/packages/client/lib/OpenID4VCIClientV1_0_13.ts b/packages/client/lib/OpenID4VCIClientV1_0_13.ts index 2c1329d7..4aedcc9d 100644 --- a/packages/client/lib/OpenID4VCIClientV1_0_13.ts +++ b/packages/client/lib/OpenID4VCIClientV1_0_13.ts @@ -22,7 +22,7 @@ import { getTypesFromCredentialSupported, KID_JWK_X5C_ERROR, NotificationRequest, - NotificationResult, + NotificationResponseResult, OID4VCICredentialFormat, OpenId4VCIVersion, PKCEOpts, @@ -577,7 +577,7 @@ export class OpenID4VCIClientV1_0_13 { credentialRequestOpts: Partial, request: NotificationRequest, accessToken?: string, - ): Promise { + ): Promise { return sendNotification(credentialRequestOpts, request, accessToken ?? this._state.accessToken ?? this._state.accessTokenResponse?.access_token); } diff --git a/packages/client/lib/functions/notifications.ts b/packages/client/lib/functions/notifications.ts index 19db5642..935528e1 100644 --- a/packages/client/lib/functions/notifications.ts +++ b/packages/client/lib/functions/notifications.ts @@ -1,4 +1,4 @@ -import { NotificationErrorResponse, NotificationRequest, NotificationResult, post } from '@sphereon/oid4vci-common'; +import { NotificationErrorResponse, NotificationRequest, NotificationResponseResult, post } from '@sphereon/oid4vci-common'; import { CredentialRequestOpts } from '../CredentialRequestClient'; import { LOG } from '../types'; @@ -7,7 +7,7 @@ export async function sendNotification( credentialRequestOpts: Partial, request: NotificationRequest, accessToken?: string, -): Promise { +): Promise { LOG.info(`Sending status notification event '${request.event}' for id ${request.notification_id}`); if (!credentialRequestOpts.notificationEndpoint) { throw Error(`Cannot send notification when no notification endpoint is provided`); diff --git a/packages/did-auth-siop-adapter/package.json b/packages/did-auth-siop-adapter/package.json index decbd422..282f2b5d 100644 --- a/packages/did-auth-siop-adapter/package.json +++ b/packages/did-auth-siop-adapter/package.json @@ -20,7 +20,7 @@ }, "devDependencies": { "@types/jest": "^29.5.12", - "typescript": "5.4.5" + "typescript": "5.4.5" }, "engines": { "node": ">=18" diff --git a/packages/issuer-rest/lib/oid4vci-api-functions.ts b/packages/issuer-rest/lib/oid4vci-api-functions.ts index 4a045d54..e17b7ac3 100644 --- a/packages/issuer-rest/lib/oid4vci-api-functions.ts +++ b/packages/issuer-rest/lib/oid4vci-api-functions.ts @@ -1,6 +1,7 @@ import { uuidv4 } from '@sphereon/oid4vc-common' import { ACCESS_TOKEN_ISSUER_REQUIRED_ERROR, + AccessTokenRequest, adjustUrl, AuthorizationRequest, CredentialOfferRESTRequest, @@ -216,23 +217,39 @@ export function notificationEndpoint( }) try { const jwtResult = await validateJWT(jwt, { accessTokenVerificationCallback: opts.accessTokenVerificationCallback }) - EVENTS.emit(NotificationStatusEventNames.OID4VCI_NOTIFICATION_PROCESSED, { - eventName: NotificationStatusEventNames.OID4VCI_NOTIFICATION_PROCESSED, - id: uuidv4(), - data: notificationRequest, - initiator: jwtResult.jwt, - initiatorType: InitiatorType.EXTERNAL, - system: System.OID4VCI, - subsystem: SubSystem.API, + const accessToken = jwtResult.jwt.payload as AccessTokenRequest + const errorOrSession = await issuer.processNotification({ + preAuthorizedCode: accessToken['pre-authorized_code'], + /*TODO: authorizationCode*/ notification: notificationRequest, }) + if (errorOrSession instanceof Error) { + EVENTS.emit(NotificationStatusEventNames.OID4VCI_NOTIFICATION_ERROR, { + eventName: NotificationStatusEventNames.OID4VCI_NOTIFICATION_ERROR, + id: uuidv4(), + data: notificationRequest, + initiator: jwtResult.jwt, + initiatorType: InitiatorType.EXTERNAL, + system: System.OID4VCI, + subsystem: SubSystem.API, + }) + return sendErrorResponse(response, 400, errorOrSession.message) + } else { + EVENTS.emit(NotificationStatusEventNames.OID4VCI_NOTIFICATION_PROCESSED, { + eventName: NotificationStatusEventNames.OID4VCI_NOTIFICATION_PROCESSED, + id: uuidv4(), + data: notificationRequest, + initiator: jwtResult.jwt, + initiatorType: InitiatorType.EXTERNAL, + system: System.OID4VCI, + subsystem: SubSystem.API, + }) + } } catch (e) { LOG.warning(e) return sendErrorResponse(response, 400, { error: 'invalid_token', }) } - - // TODO Send event return response.status(204).send() } catch (e) { return sendErrorResponse( diff --git a/packages/issuer/lib/VcIssuer.ts b/packages/issuer/lib/VcIssuer.ts index 4a65eeaa..f8d35b82 100644 --- a/packages/issuer/lib/VcIssuer.ts +++ b/packages/issuer/lib/VcIssuer.ts @@ -30,6 +30,7 @@ import { KID_DID_NO_DID_ERROR, KID_JWK_X5C_ERROR, NO_ISS_IN_AUTHORIZATION_CODE_CONTEXT, + NotificationRequest, OID4VCICredentialFormat, OpenId4VCIVersion, PRE_AUTH_GRANT_LITERAL, @@ -46,6 +47,8 @@ import { assertValidPinNumber, createCredentialOfferObject, createCredentialOffe import { LookupStateManager } from './state-manager' import { CredentialDataSupplier, CredentialDataSupplierArgs, CredentialIssuanceInput, CredentialSignerCallback } from './types' +import { LOG } from './index' + export class VcIssuer { private readonly _issuerMetadata: CredentialIssuerMetadataOptsV1_0_13 private readonly _authorizationServerMetadata: AuthorizationServerMetadata @@ -94,6 +97,31 @@ export class VcIssuer { return new LookupStateManager(this.uris, this._credentialOfferSessions, 'uri').getAsserted(id) } + public async processNotification({ + preAuthorizedCode, + issuerState, + notification, + }: { + preAuthorizedCode?: string + issuerState?: string + notification: NotificationRequest + }): Promise { + const sessionId = preAuthorizedCode ?? issuerState + const session = sessionId ? await this.getCredentialOfferSessionById(sessionId) : undefined + if (!session || !sessionId) { + LOG.error(`No session or session id found ${sessionId}`) + return Error('invalid_notification_request') + } + if (notification.notification_id !== session.notification_id) { + LOG.error(`Notification id ${notification.notification_id} not found in session. session notification id ${session.notification_id}`) + return Error('invalid_notification_id') + } else if (session.notification) { + LOG.info(`Overwriting existing notification, as a new notification came in ${session.notification_id}`) + } + await this.updateSession({ preAuthorizedCode: preAuthorizedCode, issuerState: issuerState, notification }) + LOG.info(`Processed notification ${notification} for ${session.notification_id}`) + return session + } public async createCredentialOfferURI(opts: { grants?: CredentialOfferGrantInput credential_configuration_ids?: Array @@ -399,26 +427,49 @@ export class VcIssuer { } return response } catch (error: unknown) { - await this.updateErrorStatus({ preAuthorizedCode, issuerState, error }) + await this.updateSession({ preAuthorizedCode, issuerState, error }) throw error } } - private async updateErrorStatus({ + private async updateSession({ preAuthorizedCode, error, issuerState, + notification, }: { - preAuthorizedCode: string | undefined - issuerState: string | undefined - error: unknown + preAuthorizedCode?: string + issuerState?: string + error?: unknown + notification?: NotificationRequest }) { + let issueState: IssueStatus | undefined = undefined + if (error) { + issueState = IssueStatus.ERROR + } else if (notification) { + if (notification.event == 'credential_accepted') { + issueState = IssueStatus.NOTIFICATION_CREDENTIAL_ACCEPTED + } else if (notification.event == 'credential_deleted') { + issueState = IssueStatus.NOTIFICATION_CREDENTIAL_DELETED + } else if (notification.event == 'credential_failure') { + issueState = IssueStatus.NOTIFICATION_CREDENTIAL_FAILURE + } + } + if (preAuthorizedCode) { const preAuthSession = await this._credentialOfferSessions.get(preAuthorizedCode) if (preAuthSession) { preAuthSession.lastUpdatedAt = +new Date() - preAuthSession.status = IssueStatus.ERROR - preAuthSession.error = error instanceof Error ? error.message : error?.toString() + if (issueState) { + preAuthSession.status = issueState + } + if (error) { + preAuthSession.error = error instanceof Error ? error.message : error?.toString() + } + preAuthSession.notification_id + if (notification) { + preAuthSession.notification = notification + } await this._credentialOfferSessions.set(preAuthorizedCode, preAuthSession) } } @@ -426,8 +477,15 @@ export class VcIssuer { const authSession = await this._credentialOfferSessions.get(issuerState) if (authSession) { authSession.lastUpdatedAt = +new Date() - authSession.status = IssueStatus.ERROR - authSession.error = error instanceof Error ? error.message : error?.toString() + if (issueState) { + authSession.status = issueState + } + if (error) { + authSession.error = error instanceof Error ? error.message : error?.toString() + } + if (notification) { + authSession.notification = notification + } await this._credentialOfferSessions.set(issuerState, authSession) } } @@ -569,7 +627,7 @@ export class VcIssuer { return { jwtVerifyResult, preAuthorizedCode, preAuthSession, issuerState, authSession, cNonceState } } catch (error: unknown) { - await this.updateErrorStatus({ preAuthorizedCode, issuerState, error }) + await this.updateSession({ preAuthorizedCode, issuerState, error }) throw error } } diff --git a/packages/oid4vci-common/lib/events/index.ts b/packages/oid4vci-common/lib/events/index.ts index 50fc7fe6..93905f88 100644 --- a/packages/oid4vci-common/lib/events/index.ts +++ b/packages/oid4vci-common/lib/events/index.ts @@ -15,6 +15,7 @@ export enum CredentialEventNames { export enum NotificationStatusEventNames { OID4VCI_NOTIFICATION_RECEIVED = 'OID4VCI_NOTIFICATION_RECEIVED', OID4VCI_NOTIFICATION_PROCESSED = 'OID4VCI_NOTIFICATION_PROCESSED', + OID4VCI_NOTIFICATION_ERROR = 'OID4VCI_NOTIFICATION_ERROR', } export type LogEvents = 'oid4vciLog'; export const EVENTS = EventManager.instance(); diff --git a/packages/oid4vci-common/lib/types/Generic.types.ts b/packages/oid4vci-common/lib/types/Generic.types.ts index f6b8187d..25c8ebfd 100644 --- a/packages/oid4vci-common/lib/types/Generic.types.ts +++ b/packages/oid4vci-common/lib/types/Generic.types.ts @@ -416,7 +416,7 @@ export interface NotificationRequest { export type NotificationError = 'invalid_notification_id' | 'invalid_notification_request'; -export type NotificationResult = { +export type NotificationResponseResult = { error: boolean; response?: NotificationErrorResponse; }; diff --git a/packages/oid4vci-common/lib/types/StateManager.types.ts b/packages/oid4vci-common/lib/types/StateManager.types.ts index 1b041d0c..28cf53ed 100644 --- a/packages/oid4vci-common/lib/types/StateManager.types.ts +++ b/packages/oid4vci-common/lib/types/StateManager.types.ts @@ -1,5 +1,5 @@ import { AssertedUniformCredentialOffer } from './CredentialIssuance.types'; -import { CredentialDataSupplierInput } from './Generic.types'; +import { CredentialDataSupplierInput, NotificationRequest } from './Generic.types' export interface StateType { createdAt: number; @@ -14,6 +14,7 @@ export interface CredentialOfferSession extends StateType { error?: string; lastUpdatedAt: number; notification_id: string; + notification?: NotificationRequest; issuerState?: string; //todo: Probably good to hash it here, since it would come in from the client and we could match the hash and thus use the client value preAuthorizedCode?: string; //todo: Probably good to hash it here, since it would come in from the client and we could match the hash and thus use the client value } @@ -25,6 +26,9 @@ export enum IssueStatus { ACCESS_TOKEN_CREATED = 'ACCESS_TOKEN_CREATED', // Optional state, given the token endpoint could also be on a separate AS CREDENTIAL_REQUEST_RECEIVED = 'CREDENTIAL_REQUEST_RECEIVED', // Credential request received. Next state would either be error or issued CREDENTIAL_ISSUED = 'CREDENTIAL_ISSUED', + NOTIFICATION_CREDENTIAL_ACCEPTED = 'NOTIFICATION_CREDENTIAL_ACCEPTED', + NOTIFICATION_CREDENTIAL_DELETED = 'NOTIFICATION_CREDENTIAL_DELETED', + NOTIFICATION_CREDENTIAL_FAILURE = 'NOTIFICATION_CREDENTIAL_FAILURE', ERROR = 'ERROR', } diff --git a/packages/siop-oid4vp/package.json b/packages/siop-oid4vp/package.json index fcb225ed..20a7c680 100644 --- a/packages/siop-oid4vp/package.json +++ b/packages/siop-oid4vp/package.json @@ -50,7 +50,7 @@ "@transmute/ed25519-signature-2018": "^0.7.0-unstable.82", "@types/debug": "^4.1.12", "@types/jest": "^29.5.11", - "@types/language-tags": "^1.0.4", + "@types/language-tags": "^1.0.4", "@types/qs": "^6.9.11", "@typescript-eslint/eslint-plugin": "^5.52.0", "@typescript-eslint/parser": "^5.52.0", From 914d198c99df94c84ea83520e767b6b557ecd717 Mon Sep 17 00:00:00 2001 From: Niels Klomp Date: Fri, 27 Dec 2024 16:24:07 +0100 Subject: [PATCH 3/3] feat: MWALL-715 Add support for external AS --- packages/issuer-rest/lib/index.ts | 47 +++++++++++++++++ .../issuer-rest/lib/oid4vci-api-functions.ts | 51 +++++++++++++++++-- packages/issuer/lib/tokens/index.ts | 14 +++-- 3 files changed, 105 insertions(+), 7 deletions(-) diff --git a/packages/issuer-rest/lib/index.ts b/packages/issuer-rest/lib/index.ts index e4af28e5..b1f8153f 100644 --- a/packages/issuer-rest/lib/index.ts +++ b/packages/issuer-rest/lib/index.ts @@ -1,3 +1,50 @@ export * from './OID4VCIServer' export * from './oid4vci-api-functions' export * from './expressUtils' + + +/** + * Copied from openid-client + */ +export type ResponseType = 'code' | 'id_token' | 'code id_token' | 'none' | string; +export type ClientAuthMethod = + | 'client_secret_basic' + | 'client_secret_post' + | 'client_secret_jwt' + | 'private_key_jwt' + | 'tls_client_auth' + | 'self_signed_tls_client_auth' + | 'none'; +export interface ClientMetadata { + // important + client_id: string; + id_token_signed_response_alg?: string; + token_endpoint_auth_method?: ClientAuthMethod; + client_secret?: string; + redirect_uris?: string[]; + response_types?: ResponseType[]; + post_logout_redirect_uris?: string[]; + default_max_age?: number; + require_auth_time?: boolean; + tls_client_certificate_bound_access_tokens?: boolean; + request_object_signing_alg?: string; + + // less important + id_token_encrypted_response_alg?: string; + id_token_encrypted_response_enc?: string; + introspection_endpoint_auth_method?: ClientAuthMethod; + introspection_endpoint_auth_signing_alg?: string; + request_object_encryption_alg?: string; + request_object_encryption_enc?: string; + revocation_endpoint_auth_method?: ClientAuthMethod; + revocation_endpoint_auth_signing_alg?: string; + token_endpoint_auth_signing_alg?: string; + userinfo_encrypted_response_alg?: string; + userinfo_encrypted_response_enc?: string; + userinfo_signed_response_alg?: string; + authorization_encrypted_response_alg?: string; + authorization_encrypted_response_enc?: string; + authorization_signed_response_alg?: string; + + [key: string]: unknown; +} \ No newline at end of file diff --git a/packages/issuer-rest/lib/oid4vci-api-functions.ts b/packages/issuer-rest/lib/oid4vci-api-functions.ts index e17b7ac3..f8aa8a6f 100644 --- a/packages/issuer-rest/lib/oid4vci-api-functions.ts +++ b/packages/issuer-rest/lib/oid4vci-api-functions.ts @@ -1,9 +1,10 @@ -import { uuidv4 } from '@sphereon/oid4vc-common' +import { decodeJwt, decodeProtectedHeader, uuidv4 } from '@sphereon/oid4vc-common' import { ACCESS_TOKEN_ISSUER_REQUIRED_ERROR, AccessTokenRequest, adjustUrl, AuthorizationRequest, + CredentialIssuerMetadataOptsV1_0_13, CredentialOfferRESTRequest, CredentialRequestV1_0_13, determineGrantTypes, @@ -14,6 +15,9 @@ import { Grant, IssueStatusResponse, JWT_SIGNER_CALLBACK_REQUIRED_ERROR, + JWTHeader, + JWTVerifyCallback, + JwtVerifyResult, NotificationRequest, NotificationStatusEventNames, OpenId4VCIVersion, @@ -25,10 +29,11 @@ import { WellKnownEndpoints, } from '@sphereon/oid4vci-common' import { ITokenEndpointOpts, LOG, VcIssuer } from '@sphereon/oid4vci-issuer' -import { env, ISingleEndpointOpts, sendErrorResponse } from '@sphereon/ssi-express-support' +import { env, ISingleEndpointOpts, oidcDiscoverIssuer, oidcGetClient, sendErrorResponse } from '@sphereon/ssi-express-support' import { InitiatorType, SubSystem, System } from '@sphereon/ssi-types' import { NextFunction, Request, Response, Router } from 'express' + import { handleTokenRequest, verifyTokenRequest } from './IssuerTokenEndpoint' import { ICreateCredentialOfferEndpointOpts, @@ -38,6 +43,8 @@ import { } from './OID4VCIServer' import { validateRequestBody } from './expressUtils' +import { ClientMetadata } from './index' + const expiresIn = process.env.EXPIRES_IN ? parseInt(process.env.EXPIRES_IN) : 90 export function getIssueStatusEndpoint(router: Router, issuer: VcIssuer, opts: IGetIssueStatusEndpointOpts) { @@ -76,13 +83,17 @@ export function getIssueStatusEndpoint(router: Router, is }) } +function isExternalAS(issuerMetadata: CredentialIssuerMetadataOptsV1_0_13) { + return issuerMetadata.authorization_servers?.some((as) => !as.includes(issuerMetadata.credential_issuer)) +} + export function accessTokenEndpoint( router: Router, issuer: VcIssuer, opts: ITokenEndpointOpts & ISingleEndpointOpts & { baseUrl: string | URL }, ) { const tokenEndpoint = issuer.issuerMetadata.token_endpoint - const externalAS = issuer.issuerMetadata.authorization_servers + const externalAS = isExternalAS(issuer.issuerMetadata) if (externalAS) { LOG.log(`[OID4VCI] External Authorization Server ${tokenEndpoint} is being used. Not enabling issuer token endpoint`) return @@ -90,7 +101,11 @@ export function accessTokenEndpoint( LOG.log(`[OID4VCI] Token endpoint is not enabled`) return } - const accessTokenIssuer = opts?.accessTokenIssuer ?? process.env.ACCESS_TOKEN_ISSUER ?? issuer.issuerMetadata.credential_issuer + const accessTokenIssuer = + opts?.accessTokenIssuer ?? + process.env.ACCESS_TOKEN_ISSUER ?? + issuer.issuerMetadata.authorization_servers?.[0] ?? + issuer.issuerMetadata.credential_issuer const preAuthorizedCodeExpirationDuration = opts?.preAuthorizedCodeExpirationDuration ?? getNumberOrUndefined(process.env.PRE_AUTHORIZED_CODE_EXPIRATION_DURATION) ?? 300 @@ -484,3 +499,31 @@ export function getBasePath(url?: URL | string) { } return `/${trimBoth(basePath, '/')}` } + +export async function oidcAccessTokenVerifyCallback(opts: { + credentialIssuer: string + authorizationServer: string + clientMetadata?: ClientMetadata +}): Promise { + const callback = async (args: { jwt: string; kid?: string }): Promise => { + const introspection = await oidcClient.introspect(args.jwt) + if (!introspection.active) { + return Promise.reject(Error('Access token is not active or invalid')) + } + const jwt = { header: decodeProtectedHeader(args.jwt) as JWTHeader, payload: decodeJwt(args.jwt) } + + return { + jwt, + alg: jwt.header.alg, + ...(jwt.header.jwk && { jwk: jwt.header.jwk }), + ...(jwt.header.x5c && { x5c: jwt.header.x5c }), + ...(jwt.header.kid && { kid: jwt.header.kid }), + // We could resolve the did document here if the kid is a VM + } + } + + const clientMetadata = opts.clientMetadata ?? { client_id: opts.credentialIssuer } + const oidcIssuer = await oidcDiscoverIssuer({ issuerUrl: opts.authorizationServer }) + const oidcClient = await oidcGetClient(oidcIssuer.issuer, clientMetadata) + return callback +} \ No newline at end of file diff --git a/packages/issuer/lib/tokens/index.ts b/packages/issuer/lib/tokens/index.ts index 3e844f64..17ed04f3 100644 --- a/packages/issuer/lib/tokens/index.ts +++ b/packages/issuer/lib/tokens/index.ts @@ -37,18 +37,24 @@ export interface ITokenEndpointOpts { accessTokenSignerCallback?: JWTSignerCallback accessTokenVerificationCallback?: JWTVerifyCallback accessTokenIssuer?: string + accessTokenProvider?: AccessTokenProvider } +export type AccessTokenProvider = 'internal' | 'oidc' | 'oauth2' + export const generateAccessToken = async ( - opts: Required> & { + opts: Required> & { additionalClaims?: Record preAuthorizedCode?: string alg?: Alg dPoPJwk?: JWK }, ): Promise => { - const { dPoPJwk, accessTokenIssuer, alg, accessTokenSignerCallback, tokenExpiresIn, preAuthorizedCode, additionalClaims } = opts + const { dPoPJwk, accessTokenIssuer, alg, accessTokenSignerCallback, tokenExpiresIn, preAuthorizedCode, additionalClaims, accessTokenProvider = 'internal' } = opts // JWT uses seconds for iat and exp + if (accessTokenProvider !== 'internal') { + throw new TokenError(400, TokenErrorResponse.invalid_request, `Access token provider ${accessTokenProvider} is an external access token provider. We cannot generate tokens ourselves in this case`) + } const iat = new Date().getTime() / 1000 const exp = iat + tokenExpiresIn const cnf = dPoPJwk ? { cnf: { jkt: await calculateJwkThumbprint(dPoPJwk, 'sha256') } } : undefined @@ -202,11 +208,12 @@ export const createAccessTokenResponse = async ( // preAuthorizedCodeExpirationDuration?: number accessTokenSignerCallback: JWTSignerCallback accessTokenIssuer: string + accessTokenProvider?: AccessTokenProvider interval?: number dPoPJwk?: JWK }, ) => { - const { dPoPJwk, credentialOfferSessions, cNonces, cNonceExpiresIn, tokenExpiresIn, accessTokenIssuer, accessTokenSignerCallback, interval } = opts + const { dPoPJwk, credentialOfferSessions, cNonces, cNonceExpiresIn, tokenExpiresIn, accessTokenIssuer, accessTokenSignerCallback, interval, accessTokenProvider = 'internal' } = opts // Pre-auth flow const preAuthorizedCode = request[PRE_AUTH_CODE_LITERAL] as string @@ -219,6 +226,7 @@ export const createAccessTokenResponse = async ( preAuthorizedCode, accessTokenIssuer, dPoPJwk, + accessTokenProvider }) const response: AccessTokenResponse = {