From 9c273b94a5373f9949b0d717e151e9f378307a3f Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Thu, 9 Jan 2025 13:20:29 +0100 Subject: [PATCH 01/26] feat: added support for first party applications --- packages/issuer-rest/lib/OID4VCIServer.ts | 29 ++++- .../issuer-rest/lib/oid4vci-api-functions.ts | 108 ++++++++++++++++- packages/issuer/lib/VcIssuer.ts | 9 +- .../AuthorizationServerMetadataBuilder.ts | 5 + .../lib/builder/IssuerMetadataBuilderV1_13.ts | 17 ++- .../lib/types/Authorization.types.ts | 112 ++++++++++++++++++ .../oid4vci-common/lib/types/Generic.types.ts | 1 + .../lib/types/ServerMetadata.ts | 2 + .../lib/types/StateManager.types.ts | 1 + .../oid4vci-common/lib/types/v1_0_13.types.ts | 2 + packages/siop-oid4vp/lib/__tests__/IT.spec.ts | 2 + .../lib/authorization-response/Payload.ts | 1 + .../lib/authorization-response/types.ts | 1 + packages/siop-oid4vp/lib/op/OP.ts | 1 + packages/siop-oid4vp/lib/rp/RP.ts | 6 +- .../AuthorizationResponseOpts.schema.ts | 3 + packages/siop-oid4vp/lib/types/SIOP.types.ts | 1 + 17 files changed, 291 insertions(+), 10 deletions(-) diff --git a/packages/issuer-rest/lib/OID4VCIServer.ts b/packages/issuer-rest/lib/OID4VCIServer.ts index d8d2cd0f..ea2b74a7 100644 --- a/packages/issuer-rest/lib/OID4VCIServer.ts +++ b/packages/issuer-rest/lib/OID4VCIServer.ts @@ -12,6 +12,7 @@ import express, { Express } from 'express' import { accessTokenEndpoint, + authorizationChallengeEndpoint, createCredentialOfferEndpoint, getBasePath, getCredentialEndpoint, @@ -84,6 +85,13 @@ export interface IGetIssueStatusEndpointOpts extends ISingleEndpointOpts { baseUrl: string | URL } +export interface IAuthorizationChallengeEndpointOpts extends ISingleEndpointOpts { + // TODO docs + // optional state so when rest is not used one could sync the state + createAuthRequestUriCallback: (presentationDefinitionId: string, state?: string) => Promise + verifyAuthResponseCallback: (correlationId: string) => Promise // TODO authorizationResponsePayload: any, -> AuthorizationResponse +} + export interface IOID4VCIServerOpts extends HasEndpointOpts { endpointOpts?: { tokenEndpointOpts?: ITokenEndpointOpts @@ -92,6 +100,7 @@ export interface IOID4VCIServerOpts extends HasEndpointOpts { getCredentialOfferOpts?: IGetCredentialOfferEndpointOpts getStatusOpts?: IGetIssueStatusEndpointOpts parOpts?: ISingleEndpointOpts + authorizationChallengeOpts?: IAuthorizationChallengeEndpointOpts } baseUrl?: string } @@ -129,6 +138,20 @@ export class OID4VCIServer { if (this.isStatusEndpointEnabled(opts?.endpointOpts?.getStatusOpts)) { getIssueStatusEndpoint(this.router, this.issuer, { ...opts?.endpointOpts?.getStatusOpts, baseUrl: this.baseUrl }) } + if (this.isAuthorizationChallengeEndpointEnabled(opts?.endpointOpts?.authorizationChallengeOpts)) { + // TODO errors or just continue without endpoint + if (!opts?.endpointOpts?.authorizationChallengeOpts?.createAuthRequestUriCallback) { + throw Error( + `Unable to enable authorization challenge endpoint. No createAuthRequestUriCallback present in authorization challenge options`, + ) + } + if (!opts?.endpointOpts?.authorizationChallengeOpts?.verifyAuthResponseCallback) { + throw Error( + `Unable to enable authorization challenge endpoint. No verifyAuthResponseCallback present in authorization challenge options`, + ) + } + authorizationChallengeEndpoint(this.router, this.issuer, { ...opts?.endpointOpts?.authorizationChallengeOpts, baseUrl: this.baseUrl }) + } this._app.use(getBasePath(this.baseUrl), this._router) } @@ -160,7 +183,11 @@ export class OID4VCIServer { } private isStatusEndpointEnabled(statusEndpointOpts?: IGetIssueStatusEndpointOpts) { - return statusEndpointOpts?.enabled !== false || process.env.STATUS_ENDPOINT_ENABLED === 'false' + return statusEndpointOpts?.enabled !== false || process.env.STATUS_ENDPOINT_ENABLED !== 'false' + } + + private isAuthorizationChallengeEndpointEnabled(authorizationChallengeEndpointOpts?: IAuthorizationChallengeEndpointOpts) { + return authorizationChallengeEndpointOpts?.enabled !== false || process.env.AUTHORIZATION_CHALLENGE_ENDPOINT_ENABLED !== 'false' } private assertAccessTokenHandling(tokenEndpointOpts?: ITokenEndpointOpts) { diff --git a/packages/issuer-rest/lib/oid4vci-api-functions.ts b/packages/issuer-rest/lib/oid4vci-api-functions.ts index 4a045d54..c04b5c4c 100644 --- a/packages/issuer-rest/lib/oid4vci-api-functions.ts +++ b/packages/issuer-rest/lib/oid4vci-api-functions.ts @@ -2,13 +2,18 @@ import { uuidv4 } from '@sphereon/oid4vc-common' import { ACCESS_TOKEN_ISSUER_REQUIRED_ERROR, adjustUrl, + AuthorizationChallengeCodeResponse, + AuthorizationChallengeError, + AuthorizationChallengeErrorResponse, AuthorizationRequest, + CommonAuthorizationChallengeRequest, CredentialOfferRESTRequest, CredentialRequestV1_0_13, determineGrantTypes, determineSpecVersionFromOffer, EVENTS, extractBearerToken, + generateRandomString, getNumberOrUndefined, Grant, IssueStatusResponse, @@ -21,19 +26,24 @@ import { trimEnd, trimStart, validateJWT, - WellKnownEndpoints, + WellKnownEndpoints } from '@sphereon/oid4vci-common' -import { ITokenEndpointOpts, LOG, VcIssuer } from '@sphereon/oid4vci-issuer' +import { + ITokenEndpointOpts, + LOG, + VcIssuer +} from '@sphereon/oid4vci-issuer' import { env, ISingleEndpointOpts, 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 { + IAuthorizationChallengeEndpointOpts, ICreateCredentialOfferEndpointOpts, ICreateCredentialOfferURIResponse, IGetCredentialOfferEndpointOpts, - IGetIssueStatusEndpointOpts, + IGetIssueStatusEndpointOpts } from './OID4VCIServer' import { validateRequestBody } from './expressUtils' @@ -75,6 +85,98 @@ export function getIssueStatusEndpoint(router: Router, is }) } +export function authorizationChallengeEndpoint( + router: Router, + issuer: VcIssuer, + opts: IAuthorizationChallengeEndpointOpts & { baseUrl: string | URL }, +) { + const endpoint = issuer.issuerMetadata.authorization_challenge_endpoint + const baseUrl = getBaseUrl(opts.baseUrl) + if (!endpoint) { + LOG.warning('authorization challenge endpoint disabled as no "authorization_challenge_endpoint" has been configured in issuer metadata') + return + } + const path = determinePath(baseUrl, endpoint, { stripBasePath: true }) + LOG.log(`[OID4VCI] authorization challenge endpoint at ${path}`) + router.post(path, async (request: Request, response: Response) => { + const authorizationChallengeRequest = request.body as CommonAuthorizationChallengeRequest // TODO we need the request object here + const { + client_id, + issuer_state, + auth_session, + presentation_during_issuance_session, + definition_id + } = authorizationChallengeRequest + + try { + if (!client_id && !auth_session) { + const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = { + error: AuthorizationChallengeError.invalid_request + } + return Promise.reject(authorizationChallengeErrorResponse) + } + + if (!auth_session && issuer_state) { + const session = await issuer.credentialOfferSessions.get(issuer_state) + if (!session) { + const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = { + error: AuthorizationChallengeError.invalid_session + } + return Promise.reject(authorizationChallengeErrorResponse) + } + + if (!definition_id) { + const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = { + error: AuthorizationChallengeError.invalid_request + } + return Promise.reject(authorizationChallengeErrorResponse) + } + + const authRequestURI = await opts.createAuthRequestUriCallback(definition_id, issuer_state) + const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = { + error: AuthorizationChallengeError.insufficient_authorization, + auth_session: issuer_state, + presentation: authRequestURI + } + return Promise.reject(authorizationChallengeErrorResponse) + } + + if (auth_session && presentation_during_issuance_session) { + const session = await issuer.credentialOfferSessions.get(auth_session) + if (!session) { + const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = { + error: AuthorizationChallengeError.invalid_session + } + return Promise.reject(authorizationChallengeErrorResponse) + } + + const verifiedResponse = await opts.verifyAuthResponseCallback(presentation_during_issuance_session) + if (verifiedResponse) { + const authorizationCode = generateRandomString(16, 'base64url') + session.authorizationCode = authorizationCode + await issuer.credentialOfferSessions.set(auth_session, session) + const authorizationChallengeCodeResponse: AuthorizationChallengeCodeResponse = { + authorization_code: authorizationCode + } + return response.send(authorizationChallengeCodeResponse) + } + } + + const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = { + error: AuthorizationChallengeError.invalid_request + } + return Promise.reject(authorizationChallengeErrorResponse) + } catch (e) { + return sendErrorResponse( + response, + 400, + (e as Error), + e, + ) + } + }) +} + export function accessTokenEndpoint( router: Router, issuer: VcIssuer, diff --git a/packages/issuer/lib/VcIssuer.ts b/packages/issuer/lib/VcIssuer.ts index 4a65eeaa..da46d987 100644 --- a/packages/issuer/lib/VcIssuer.ts +++ b/packages/issuer/lib/VcIssuer.ts @@ -40,7 +40,14 @@ import { TYP_ERROR, URIState, } from '@sphereon/oid4vci-common' -import { CompactSdJwtVc, CredentialMapper, InitiatorType, SubSystem, System, W3CVerifiableCredential } from '@sphereon/ssi-types' +import { + CompactSdJwtVc, + CredentialMapper, + InitiatorType, + SubSystem, + System, + W3CVerifiableCredential +} from '@sphereon/ssi-types' import { assertValidPinNumber, createCredentialOfferObject, createCredentialOfferURIFromObject, CredentialOfferGrantInput } from './functions' import { LookupStateManager } from './state-manager' diff --git a/packages/issuer/lib/builder/AuthorizationServerMetadataBuilder.ts b/packages/issuer/lib/builder/AuthorizationServerMetadataBuilder.ts index 74cefe77..e2a81045 100644 --- a/packages/issuer/lib/builder/AuthorizationServerMetadataBuilder.ts +++ b/packages/issuer/lib/builder/AuthorizationServerMetadataBuilder.ts @@ -25,6 +25,11 @@ export class AuthorizationServerMetadataBuilder { return this } + public withAuthorizationChallengeEndpoint(endpoint: string): AuthorizationServerMetadataBuilder { + this.metadata.authorization_challenge_endpoint = endpoint + return this + } + public withTokenEndpoint(endpoint: string): AuthorizationServerMetadataBuilder { this.metadata.token_endpoint = endpoint return this diff --git a/packages/issuer/lib/builder/IssuerMetadataBuilderV1_13.ts b/packages/issuer/lib/builder/IssuerMetadataBuilderV1_13.ts index 7be368c4..13627087 100644 --- a/packages/issuer/lib/builder/IssuerMetadataBuilderV1_13.ts +++ b/packages/issuer/lib/builder/IssuerMetadataBuilderV1_13.ts @@ -4,8 +4,8 @@ import { CredentialSupportedBuilderV1_13 } from './CredentialSupportedBuilderV1_ import { DisplayBuilder } from './DisplayBuilder' export class IssuerMetadataBuilderV1_13 { - credentialEndpoint: string | undefined - credentialIssuer: string | undefined + credentialEndpoint?: string + credentialIssuer?: string supportedBuilders: CredentialSupportedBuilderV1_13[] = [] credentialConfigurationsSupported: Record = {} displayBuilders: DisplayBuilder[] = [] @@ -13,6 +13,7 @@ export class IssuerMetadataBuilderV1_13 { batchCredentialEndpoint?: string authorizationServers?: string[] tokenEndpoint?: string + authorizationChallengeEndpoint?: string public withBatchCredentialEndpoint(batchCredentialEndpoint: string) { this.batchCredentialEndpoint = batchCredentialEndpoint @@ -32,6 +33,11 @@ export class IssuerMetadataBuilderV1_13 { return this } + public withAuthorizationChallengeEndpoint(authorizationChallengeEndpoint: string) { + this.authorizationChallengeEndpoint = authorizationChallengeEndpoint + return this + } + public withTokenEndpoint(tokenEndpoint: string) { this.tokenEndpoint = tokenEndpoint return this @@ -105,14 +111,17 @@ export class IssuerMetadataBuilderV1_13 { display.push(...this.display) display.push(...this.displayBuilders.map((builder) => builder.build())) - return { + const issuerMetadata: IssuerMetadataV1_0_13 = { credential_issuer: this.credentialIssuer, credential_endpoint: this.credentialEndpoint, credential_configurations_supported, // batch_credential_endpoint: this.batchCredentialEndpoint; // not implemented yet ...(this.authorizationServers && { authorization_servers: this.authorizationServers }), ...(this.tokenEndpoint && { token_endpoint: this.tokenEndpoint }), + ...(this.authorizationChallengeEndpoint && { authorization_challenge_endpoint: this.authorizationChallengeEndpoint }), ...(display.length > 0 && { display }), - } as IssuerMetadataV1_0_13 + } + + return issuerMetadata } } diff --git a/packages/oid4vci-common/lib/types/Authorization.types.ts b/packages/oid4vci-common/lib/types/Authorization.types.ts index 10a510c8..c13c1b9b 100644 --- a/packages/oid4vci-common/lib/types/Authorization.types.ts +++ b/packages/oid4vci-common/lib/types/Authorization.types.ts @@ -70,6 +70,118 @@ export interface CommonAuthorizationRequest { issuer_state?: string; } +// https://www.ietf.org/archive/id/draft-parecki-oauth-first-party-apps-02.html#name-authorization-challenge-req +export interface CommonAuthorizationChallengeRequest { + /** + * REQUIRED if the client is not authenticating with the authorization server and if no auth_session is included.. + */ + client_id?: string; + /** + * OPTIONAL. String value identifying a certain processing context at the Credential Issuer. A value for this parameter is typically passed in + * an issuance initation request from the Credential Issuer to the Wallet. This request parameter is used to pass the + * issuer_state value back to the Credential Issuer. + */ + issuer_state?: string + /** + * The value of the scope parameter is expressed as a list of space-delimited, case-sensitive strings. + */ + scope?: string; // TODO what we do with this + /** + * OPTIONAL. A random string or a JWE. The auth session allows the authorization server to associate subsequent + * requests by this client with an ongoing authorization request sequence. The client MUST include the + * auth_session in follow-up requests to the authorization challenge endpoint if it receives one along with + * the error response. + */ + auth_session?: string + /** + * OPTIONAL. If the "code_challenge_method" from Section 4.3 was "S256", the + * received "code_verifier" is hashed by SHA-256, base64url-encoded, and + * then compared to the "code_challenge", i.e.: + * BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge + * + * If the "code_challenge_method" from Section 4.3 was "plain", they are + * compared directly, i.e.: + * code_verifier == code_challenge. + */ + code_challenge?: string; // TODO what we do with this + /** + * OPTIONAL. value must be set either to "S256" or a value defined by a cryptographically secure + */ + code_challenge_method?: CodeChallengeMethod; // TODO what we do with this + /** + * OPTIONAL. A presentation definition id used to create the authorization request uri that will be used to authorize with the RP + */ + definition_id?: string + /** + * OPTIONAL. String containing information about the session when credential presentation is happening during issuance of another + * credential. The content of this parameter is opaque to the wallet. When this parameter is present the Wallet MUST use this parameter in + * the subsequent Authorization Challenge Request. This allows the Issuer to determine which it can be used by to prevent session + * fixation attacks. The Response URI MAY return this parameter in response to successful Authorization Responses or for Error + * Responses. + */ + presentation_during_issuance_session?: string; +} + +// https://www.ietf.org/archive/id/draft-parecki-oauth-first-party-apps-02.html#name-error-response +export interface AuthorizationChallengeErrorResponse { + /** + * A single ASCII error code of type AuthorizationChallengeError. + */ + error: AuthorizationChallengeError + /** + * OPTIONAL. OPTIONAL. Human-readable ASCII text providing additional information, used + * to assist the client developer in understanding the error that occurred. Values for the error_description + * parameter MUST NOT include characters outside the set %x20-21 / %x23-5B / %x5D-7E. + */ + error_description?: string + /** + * OPTIONAL. A URI identifying a human-readable web page with information about the error, used + * to provide the client developer with additional information about the error. Values for the error_uri + * parameter MUST conform to the URI-reference syntax and thus MUST NOT include characters outside the + * set %x21 / %x23-5B / %x5D-7E. + */ + error_uri?: string + /** + * OPTIONAL. A random string or a JWE. The auth session allows the authorization server to associate subsequent + * requests by this client with an ongoing authorization request sequence. The client MUST include the + * auth_session in follow-up requests to the authorization challenge endpoint if it receives one along with + * the error response. + */ + auth_session?: string + /** + * OPTIONAL. The request URI corresponding to the authorization request posted. This URI is a single-use reference + * to the respective request data in the subsequent authorization request. + */ + request_uri?: string + /** + * OPTIONAL. A JSON number that represents the lifetime of the request URI in seconds as a positive integer. + */ + expires_in?: number + /** + * String containing the OID4VP request URI. The Wallet will use this URI to start the OID4VP flow. + */ + presentation?: string +} + +// https://www.ietf.org/archive/id/draft-parecki-oauth-first-party-apps-02.html#name-authorization-challenge-res +export interface AuthorizationChallengeCodeResponse { + /** + * The authorization code issued by the authorization server. + */ + authorization_code: string +} + +// https://www.ietf.org/archive/id/draft-parecki-oauth-first-party-apps-02.html#name-error-response +export enum AuthorizationChallengeError { + invalid_request = 'invalid_request', + invalid_client = 'invalid_client', + unauthorized_client = 'unauthorized_client', + invalid_session = 'invalid_session', + invalid_scope = 'invalid_scope', + insufficient_authorization = 'insufficient_authorization', + redirect_to_web = 'redirect_to_web', +} + /** * string type added for conformity with our previous code in the client */ diff --git a/packages/oid4vci-common/lib/types/Generic.types.ts b/packages/oid4vci-common/lib/types/Generic.types.ts index f6b8187d..9a422de6 100644 --- a/packages/oid4vci-common/lib/types/Generic.types.ts +++ b/packages/oid4vci-common/lib/types/Generic.types.ts @@ -67,6 +67,7 @@ export interface CredentialIssuerMetadataOpts { authorization_server?: string; // OPTIONAL. Identifier of the OAuth 2.0 Authorization Server (as defined in [RFC8414]) the Credential Issuer relies on for authorization. If this element is omitted, the entity providing the Credential Issuer is also acting as the AS, i.e. the Credential Issuer's identifier is used as the OAuth 2.0 Issuer value to obtain the Authorization Server metadata as per [RFC8414]. token_endpoint?: string; notification_endpoint?: string; + authorization_challenge_endpoint?: string // OPTIONAL URL of the Credential Issuer's Authorization Challange Endpoint. This URL MUST use the https scheme and MAY contain port, path and query parameter components. Described on https://www.ietf.org/archive/id/draft-parecki-oauth-first-party-apps-02.html#name-authorization-challenge-end display?: MetadataDisplay[]; // An array of objects, where each object contains display properties of a Credential Issuer for a certain language. Below is a non-exhaustive list of valid parameters that MAY be included: credential_supplier_config?: CredentialSupplierConfig; } diff --git a/packages/oid4vci-common/lib/types/ServerMetadata.ts b/packages/oid4vci-common/lib/types/ServerMetadata.ts index 238a8e08..637ac50d 100644 --- a/packages/oid4vci-common/lib/types/ServerMetadata.ts +++ b/packages/oid4vci-common/lib/types/ServerMetadata.ts @@ -53,6 +53,7 @@ export type PKCECodeChallengeMethod = 'plain' | 'S256'; export interface AuthorizationServerMetadata extends DynamicRegistrationClientMetadata { issuer: string; authorization_endpoint?: string; + authorization_challenge_endpoint?: string; token_endpoint?: string; token_endpoint_auth_methods_supported?: Array; token_endpoint_auth_signing_alg_values_supported?: Array; @@ -147,4 +148,5 @@ export interface EndpointMetadata { deferred_credential_endpoint?: string; authorization_server?: string; authorization_endpoint?: string; // Can be undefined in pre-auth flow + authorization_challenge_endpoint?: string; } diff --git a/packages/oid4vci-common/lib/types/StateManager.types.ts b/packages/oid4vci-common/lib/types/StateManager.types.ts index 1b041d0c..33c2b979 100644 --- a/packages/oid4vci-common/lib/types/StateManager.types.ts +++ b/packages/oid4vci-common/lib/types/StateManager.types.ts @@ -16,6 +16,7 @@ export interface CredentialOfferSession extends StateType { notification_id: string; 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 + authorizationCode?: string } export enum IssueStatus { diff --git a/packages/oid4vci-common/lib/types/v1_0_13.types.ts b/packages/oid4vci-common/lib/types/v1_0_13.types.ts index 3c997a21..9abc31f1 100644 --- a/packages/oid4vci-common/lib/types/v1_0_13.types.ts +++ b/packages/oid4vci-common/lib/types/v1_0_13.types.ts @@ -32,6 +32,7 @@ export interface IssuerMetadataV1_0_13 { credential_response_encryption?: ResponseEncryption; token_endpoint?: string; display?: MetadataDisplay[]; + authorization_challenge_endpoint?: string [x: string]: unknown; } @@ -201,6 +202,7 @@ export interface CredentialIssuerMetadataOptsV1_0_13 { authorization_servers?: string[]; // OPTIONAL. Array of strings that identify the OAuth 2.0 Authorization Servers (as defined in [RFC8414]) the Credential Issuer relies on for authorization. If this element is omitted, the entity providing the Credential Issuer is also acting as the AS, i.e. the Credential Issuer's identifier is used as the OAuth 2.0 Issuer value to obtain the Authorization Server metadata as per [RFC8414]. signed_metadata?: string; // OPTIONAL. String that is a signed JWT. This JWT contains Credential Issuer metadata parameters as claims. display?: MetadataDisplay[]; // An array of objects, where each object contains display properties of a Credential Issuer for a certain language. Below is a non-exhaustive list of valid parameters that MAY be included: + authorization_challenge_endpoint?: string // OPTIONAL URL of the Credential Issuer's Authorization Challange Endpoint. This URL MUST use the https scheme and MAY contain port, path and query parameter components. Described on https://www.ietf.org/archive/id/draft-parecki-oauth-first-party-apps-02.html#name-authorization-challenge-end //todo: these two are not mentioned in the spec token_endpoint?: string; diff --git a/packages/siop-oid4vp/lib/__tests__/IT.spec.ts b/packages/siop-oid4vp/lib/__tests__/IT.spec.ts index 11ff6795..5a2311ba 100644 --- a/packages/siop-oid4vp/lib/__tests__/IT.spec.ts +++ b/packages/siop-oid4vp/lib/__tests__/IT.spec.ts @@ -3,6 +3,8 @@ import { EventEmitter } from 'events' import { SigningAlgo } from '@sphereon/oid4vc-common' import { IPresentationDefinition } from '@sphereon/pex' import { CredentialMapper, IPresentation, IProofType, IVerifiableCredential, W3CVerifiablePresentation } from '@sphereon/ssi-types' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore import nock from 'nock' import { InMemoryRPSessionManager } from '..' diff --git a/packages/siop-oid4vp/lib/authorization-response/Payload.ts b/packages/siop-oid4vp/lib/authorization-response/Payload.ts index bdd7cc80..6dc5a533 100644 --- a/packages/siop-oid4vp/lib/authorization-response/Payload.ts +++ b/packages/siop-oid4vp/lib/authorization-response/Payload.ts @@ -24,6 +24,7 @@ export const createResponsePayload = async ( ...(responseOpts.accessToken && { access_token: responseOpts.accessToken, expires_in: responseOpts.expiresIn || 3600 }), ...(responseOpts.tokenType && { token_type: responseOpts.tokenType }), ...(responseOpts.refreshToken && { refresh_token: responseOpts.refreshToken }), + ...(responseOpts.isFirstParty && { is_first_party: responseOpts.isFirstParty }), state, } diff --git a/packages/siop-oid4vp/lib/authorization-response/types.ts b/packages/siop-oid4vp/lib/authorization-response/types.ts index 8979b649..fe442bfc 100644 --- a/packages/siop-oid4vp/lib/authorization-response/types.ts +++ b/packages/siop-oid4vp/lib/authorization-response/types.ts @@ -41,6 +41,7 @@ export interface AuthorizationResponseOpts { tokenType?: string refreshToken?: string presentationExchange?: PresentationExchangeResponseOpts + isFirstParty?: boolean } export interface PresentationExchangeResponseOpts { diff --git a/packages/siop-oid4vp/lib/op/OP.ts b/packages/siop-oid4vp/lib/op/OP.ts index 0f188ccc..333ca9c9 100644 --- a/packages/siop-oid4vp/lib/op/OP.ts +++ b/packages/siop-oid4vp/lib/op/OP.ts @@ -106,6 +106,7 @@ export class OP { issuer?: ResponseIss | string verification?: Verification presentationExchange?: PresentationExchangeResponseOpts + isFirstParty?: boolean }, ): Promise { if ( diff --git a/packages/siop-oid4vp/lib/rp/RP.ts b/packages/siop-oid4vp/lib/rp/RP.ts index d165dac2..2ead48d0 100644 --- a/packages/siop-oid4vp/lib/rp/RP.ts +++ b/packages/siop-oid4vp/lib/rp/RP.ts @@ -41,7 +41,11 @@ import { VerifiedAuthorizationResponse, } from '../types' -import { createRequestOptsFromBuilderOrExistingOpts, createVerifyResponseOptsFromBuilderOrExistingOpts, isTargetOrNoTargets } from './Opts' +import { + createRequestOptsFromBuilderOrExistingOpts, + createVerifyResponseOptsFromBuilderOrExistingOpts, + isTargetOrNoTargets +} from './Opts' import { RPBuilder } from './RPBuilder' import { IRPSessionManager } from './types' diff --git a/packages/siop-oid4vp/lib/schemas/AuthorizationResponseOpts.schema.ts b/packages/siop-oid4vp/lib/schemas/AuthorizationResponseOpts.schema.ts index ffb4eee2..3c5afdff 100644 --- a/packages/siop-oid4vp/lib/schemas/AuthorizationResponseOpts.schema.ts +++ b/packages/siop-oid4vp/lib/schemas/AuthorizationResponseOpts.schema.ts @@ -52,6 +52,9 @@ export const AuthorizationResponseOptsSchemaObj = { }, "presentationExchange": { "$ref": "#/definitions/PresentationExchangeResponseOpts" + }, + "isFirstParty": { + "type": "boolean" } }, "required": [ diff --git a/packages/siop-oid4vp/lib/types/SIOP.types.ts b/packages/siop-oid4vp/lib/types/SIOP.types.ts index 1bccd68a..3314f5b1 100644 --- a/packages/siop-oid4vp/lib/types/SIOP.types.ts +++ b/packages/siop-oid4vp/lib/types/SIOP.types.ts @@ -179,6 +179,7 @@ export interface AuthorizationResponsePayload { | MdocOid4vpMdocVpToken presentation_submission?: PresentationSubmission verifiedData?: IPresentation | AdditionalClaims + is_first_party?: boolean // eslint-disable-next-line @typescript-eslint/no-explicit-any [x: string]: any } From 5a0dba40364b2c62c2ad3f131c489273fe5dbdf6 Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Thu, 9 Jan 2025 14:16:16 +0100 Subject: [PATCH 02/26] chore: cleanup --- packages/issuer-rest/lib/OID4VCIServer.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/issuer-rest/lib/OID4VCIServer.ts b/packages/issuer-rest/lib/OID4VCIServer.ts index ea2b74a7..ff21e2e0 100644 --- a/packages/issuer-rest/lib/OID4VCIServer.ts +++ b/packages/issuer-rest/lib/OID4VCIServer.ts @@ -86,10 +86,17 @@ export interface IGetIssueStatusEndpointOpts extends ISingleEndpointOpts { } export interface IAuthorizationChallengeEndpointOpts extends ISingleEndpointOpts { - // TODO docs - // optional state so when rest is not used one could sync the state + /** + * Callback used for creating the authorization request uri used for the RP. + * Added an optional state parameter so that when direct calls are used, + * one could set the state value of the RP session to match the state value of the VCI session. + */ createAuthRequestUriCallback: (presentationDefinitionId: string, state?: string) => Promise - verifyAuthResponseCallback: (correlationId: string) => Promise // TODO authorizationResponsePayload: any, -> AuthorizationResponse + /** + * Callback used for verifying the status of the authorization response. + * This is checked by the issuer before issuing an authorization code. + */ + verifyAuthResponseCallback: (correlationId: string) => Promise } export interface IOID4VCIServerOpts extends HasEndpointOpts { From bf1217f41b86d70942aa52f244df13b69bc22931 Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Thu, 9 Jan 2025 14:27:08 +0100 Subject: [PATCH 03/26] chore: cleanup --- packages/issuer-rest/lib/OID4VCIServer.ts | 1 - packages/issuer-rest/lib/oid4vci-api-functions.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/issuer-rest/lib/OID4VCIServer.ts b/packages/issuer-rest/lib/OID4VCIServer.ts index ff21e2e0..11adc1b2 100644 --- a/packages/issuer-rest/lib/OID4VCIServer.ts +++ b/packages/issuer-rest/lib/OID4VCIServer.ts @@ -146,7 +146,6 @@ export class OID4VCIServer { getIssueStatusEndpoint(this.router, this.issuer, { ...opts?.endpointOpts?.getStatusOpts, baseUrl: this.baseUrl }) } if (this.isAuthorizationChallengeEndpointEnabled(opts?.endpointOpts?.authorizationChallengeOpts)) { - // TODO errors or just continue without endpoint if (!opts?.endpointOpts?.authorizationChallengeOpts?.createAuthRequestUriCallback) { throw Error( `Unable to enable authorization challenge endpoint. No createAuthRequestUriCallback present in authorization challenge options`, diff --git a/packages/issuer-rest/lib/oid4vci-api-functions.ts b/packages/issuer-rest/lib/oid4vci-api-functions.ts index d0f72733..874e03d0 100644 --- a/packages/issuer-rest/lib/oid4vci-api-functions.ts +++ b/packages/issuer-rest/lib/oid4vci-api-functions.ts @@ -111,7 +111,7 @@ export function authorizationChallengeEndpoint( const path = determinePath(baseUrl, endpoint, { stripBasePath: true }) LOG.log(`[OID4VCI] authorization challenge endpoint at ${path}`) router.post(path, async (request: Request, response: Response) => { - const authorizationChallengeRequest = request.body as CommonAuthorizationChallengeRequest // TODO we need the request object here + const authorizationChallengeRequest = request.body as CommonAuthorizationChallengeRequest const { client_id, issuer_state, From ce3ef0b76a52a8e74ab69b739327739c9160c278 Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Thu, 9 Jan 2025 14:30:15 +0100 Subject: [PATCH 04/26] chore: typo fix --- packages/oid4vci-common/lib/types/Generic.types.ts | 2 +- packages/oid4vci-common/lib/types/v1_0_13.types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/oid4vci-common/lib/types/Generic.types.ts b/packages/oid4vci-common/lib/types/Generic.types.ts index bd6416f8..78be07a2 100644 --- a/packages/oid4vci-common/lib/types/Generic.types.ts +++ b/packages/oid4vci-common/lib/types/Generic.types.ts @@ -67,7 +67,7 @@ export interface CredentialIssuerMetadataOpts { authorization_server?: string; // OPTIONAL. Identifier of the OAuth 2.0 Authorization Server (as defined in [RFC8414]) the Credential Issuer relies on for authorization. If this element is omitted, the entity providing the Credential Issuer is also acting as the AS, i.e. the Credential Issuer's identifier is used as the OAuth 2.0 Issuer value to obtain the Authorization Server metadata as per [RFC8414]. token_endpoint?: string; notification_endpoint?: string; - authorization_challenge_endpoint?: string // OPTIONAL URL of the Credential Issuer's Authorization Challange Endpoint. This URL MUST use the https scheme and MAY contain port, path and query parameter components. Described on https://www.ietf.org/archive/id/draft-parecki-oauth-first-party-apps-02.html#name-authorization-challenge-end + authorization_challenge_endpoint?: string // OPTIONAL URL of the Credential Issuer's Authorization Challenge Endpoint. This URL MUST use the https scheme and MAY contain port, path and query parameter components. Described on https://www.ietf.org/archive/id/draft-parecki-oauth-first-party-apps-02.html#name-authorization-challenge-end display?: MetadataDisplay[]; // An array of objects, where each object contains display properties of a Credential Issuer for a certain language. Below is a non-exhaustive list of valid parameters that MAY be included: credential_supplier_config?: CredentialSupplierConfig; } diff --git a/packages/oid4vci-common/lib/types/v1_0_13.types.ts b/packages/oid4vci-common/lib/types/v1_0_13.types.ts index 9abc31f1..483a0b66 100644 --- a/packages/oid4vci-common/lib/types/v1_0_13.types.ts +++ b/packages/oid4vci-common/lib/types/v1_0_13.types.ts @@ -202,7 +202,7 @@ export interface CredentialIssuerMetadataOptsV1_0_13 { authorization_servers?: string[]; // OPTIONAL. Array of strings that identify the OAuth 2.0 Authorization Servers (as defined in [RFC8414]) the Credential Issuer relies on for authorization. If this element is omitted, the entity providing the Credential Issuer is also acting as the AS, i.e. the Credential Issuer's identifier is used as the OAuth 2.0 Issuer value to obtain the Authorization Server metadata as per [RFC8414]. signed_metadata?: string; // OPTIONAL. String that is a signed JWT. This JWT contains Credential Issuer metadata parameters as claims. display?: MetadataDisplay[]; // An array of objects, where each object contains display properties of a Credential Issuer for a certain language. Below is a non-exhaustive list of valid parameters that MAY be included: - authorization_challenge_endpoint?: string // OPTIONAL URL of the Credential Issuer's Authorization Challange Endpoint. This URL MUST use the https scheme and MAY contain port, path and query parameter components. Described on https://www.ietf.org/archive/id/draft-parecki-oauth-first-party-apps-02.html#name-authorization-challenge-end + authorization_challenge_endpoint?: string // OPTIONAL URL of the Credential Issuer's Authorization Challenge Endpoint. This URL MUST use the https scheme and MAY contain port, path and query parameter components. Described on https://www.ietf.org/archive/id/draft-parecki-oauth-first-party-apps-02.html#name-authorization-challenge-end //todo: these two are not mentioned in the spec token_endpoint?: string; From 2b4c06992c0b0d09afbd241f3b4d0537b47ef788 Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Thu, 9 Jan 2025 15:40:11 +0100 Subject: [PATCH 05/26] chore: specifically check for true in isAuthorizationChallengeEndpointEnabled as it is an optional endpoint --- packages/issuer-rest/lib/OID4VCIServer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/issuer-rest/lib/OID4VCIServer.ts b/packages/issuer-rest/lib/OID4VCIServer.ts index 11adc1b2..4b936f29 100644 --- a/packages/issuer-rest/lib/OID4VCIServer.ts +++ b/packages/issuer-rest/lib/OID4VCIServer.ts @@ -193,7 +193,7 @@ export class OID4VCIServer { } private isAuthorizationChallengeEndpointEnabled(authorizationChallengeEndpointOpts?: IAuthorizationChallengeEndpointOpts) { - return authorizationChallengeEndpointOpts?.enabled !== false || process.env.AUTHORIZATION_CHALLENGE_ENDPOINT_ENABLED !== 'false' + return authorizationChallengeEndpointOpts?.enabled === true || process.env.AUTHORIZATION_CHALLENGE_ENDPOINT_ENABLED === 'true' } private assertAccessTokenHandling(tokenEndpointOpts?: ITokenEndpointOpts) { From acf2cdc4f149112a5d4ee664cbc74189ac2c1f9a Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Mon, 13 Jan 2025 08:16:20 +0100 Subject: [PATCH 06/26] chore: added acquiring authorization challenge authorization code to vci client using the rest api --- .../client/lib/AuthorizationCodeClient.ts | 63 ++++++++++++++++++- packages/client/lib/OpenID4VCIClient.ts | 32 ++++++++-- .../client/lib/OpenID4VCIClientV1_0_11.ts | 17 ++++- .../client/lib/OpenID4VCIClientV1_0_13.ts | 21 ++++++- .../lib/types/Authorization.types.ts | 11 ++++ 5 files changed, 132 insertions(+), 12 deletions(-) diff --git a/packages/client/lib/AuthorizationCodeClient.ts b/packages/client/lib/AuthorizationCodeClient.ts index 7aaa7b15..37acaffe 100644 --- a/packages/client/lib/AuthorizationCodeClient.ts +++ b/packages/client/lib/AuthorizationCodeClient.ts @@ -1,7 +1,9 @@ import { + AuthorizationChallengeCodeResponse, + AuthorizationChallengeErrorResponse, AuthorizationChallengeRequestOpts, AuthorizationDetails, AuthorizationRequestOpts, - CodeChallengeMethod, + CodeChallengeMethod, CommonAuthorizationChallengeRequest, convertJsonToURI, CreateRequestObjectMode, CredentialConfigurationSupportedV1_0_13, @@ -16,12 +18,13 @@ import { JsonURIMode, Jwt, OpenId4VCIVersion, + OpenIDResponse, PARMode, PKCEOpts, PushedAuthorizationResponse, RequestObjectOpts, - ResponseType, -} from '@sphereon/oid4vci-common'; + ResponseType +} from '@sphereon/oid4vci-common' import Debug from 'debug'; import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder'; @@ -272,3 +275,57 @@ const handleLocations = (endpointMetadata: EndpointMetadataResultV1_0_13, author } return authorizationDetails; }; + +export const acquireAuthorizationChallengeAuthCode = async (opts: AuthorizationChallengeRequestOpts): Promise> => { + return await acquireAuthorizationChallengeAuthCodeUsingRequest({ + authorizationChallengeRequest: await createAuthorizationChallengeRequest(opts) + }); +} + +export const acquireAuthorizationChallengeAuthCodeUsingRequest = async (opts: { authorizationChallengeRequest: CommonAuthorizationChallengeRequest }): Promise> => { + const { authorizationChallengeRequest } = opts + // TODO validate request + const authorizationChallengeCodeUrl = '' // TODO + const response = await sendAuthorizationChallengeRequest( + authorizationChallengeCodeUrl, + authorizationChallengeRequest + ); + + return response +} + +export const createAuthorizationChallengeRequest = async (opts: AuthorizationChallengeRequestOpts): Promise => { + const { + clientId, + issuerState, + authSession, + scope, + definitionId, + codeChallenge, + codeChallengeMethod, + presentationDuringIssuanceSession + } = opts; + + const request: CommonAuthorizationChallengeRequest = { + client_id: clientId, + issuer_state: issuerState, + auth_session: authSession, + scope, + code_challenge: codeChallenge, + code_challenge_method: codeChallengeMethod, + definition_id: definitionId, + presentation_during_issuance_session: presentationDuringIssuanceSession + } + + return request +} + +export const sendAuthorizationChallengeRequest = async ( + authorizationChallengeCodeUrl: string, + authorizationChallengeRequest: CommonAuthorizationChallengeRequest, + opts?: { headers?: Record } +): Promise> => { + return await formPost(authorizationChallengeCodeUrl, convertJsonToURI(authorizationChallengeRequest, { mode: JsonURIMode.X_FORM_WWW_URLENCODED }), { // TODO check encoding + customHeaders: opts?.headers ? opts.headers : undefined, + }); +} diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index 6769ebbd..8dfdaa0b 100644 --- a/packages/client/lib/OpenID4VCIClient.ts +++ b/packages/client/lib/OpenID4VCIClient.ts @@ -3,6 +3,9 @@ import { AccessTokenRequestOpts, AccessTokenResponse, Alg, + AuthorizationChallengeCodeResponse, + AuthorizationChallengeErrorResponse, + AuthorizationChallengeRequestOpts, AuthorizationRequestOpts, AuthorizationResponse, AuthorizationServerOpts, @@ -31,16 +34,20 @@ import { NotificationResponseResult, OID4VCICredentialFormat, OpenId4VCIVersion, + OpenIDResponse, PKCEOpts, ProofOfPossessionCallbacks, - toAuthorizationResponsePayload, -} from '@sphereon/oid4vci-common'; + toAuthorizationResponsePayload +} from '@sphereon/oid4vci-common' import { CredentialFormat } from '@sphereon/ssi-types'; import Debug from 'debug'; import { AccessTokenClient } from './AccessTokenClient'; import { AccessTokenClientV1_0_11 } from './AccessTokenClientV1_0_11'; -import { createAuthorizationRequestUrl } from './AuthorizationCodeClient'; +import { + acquireAuthorizationChallengeAuthCode, + createAuthorizationRequestUrl +} from './AuthorizationCodeClient' import { createAuthorizationRequestUrlV1_0_11 } from './AuthorizationCodeClientV1_0_11'; import { CredentialOfferClient } from './CredentialOfferClient'; import { CredentialRequestOpts } from './CredentialRequestClient'; @@ -270,10 +277,18 @@ export class OpenID4VCIClient { this._state.pkce = generateMissingPKCEOpts({ ...this._state.pkce, ...pkce }); } + public async acquireAuthorizationChallengeCode(opts: AuthorizationChallengeRequestOpts): Promise> { + const response = await acquireAuthorizationChallengeAuthCode({ + clientId: this._state.clientId ?? this._state.authorizationRequestOpts?.clientId, + ...opts + }) + return response + } + public async acquireAccessToken( opts?: Omit & { clientId?: string; - authorizationResponse?: string | AuthorizationResponse; // Pass in an auth response, either as URI/redirect, or object + authorizationResponse?: string | AuthorizationResponse; // Pass in an auth response, either as URI/redirect, or object // TODO we need to add support for the authorization code from the auth challenge additionalRequestParams?: Record; }, ): Promise { @@ -654,6 +669,15 @@ export class OpenID4VCIClient { return this.endpointMetadata ? this.endpointMetadata.credential_endpoint : `${this.getIssuer()}/credential`; } + public getAuthorizationChallengeEndpoint(): string | undefined { + this.assertIssuerData(); + return this.endpointMetadata?.authorization_challenge_endpoint; + } + + public hasAuthorizationChallengeEndpoint(): boolean { + return !!this.getAuthorizationChallengeEndpoint(); + } + public hasDeferredCredentialEndpoint(): boolean { return !!this.getAccessTokenEndpoint(); } diff --git a/packages/client/lib/OpenID4VCIClientV1_0_11.ts b/packages/client/lib/OpenID4VCIClientV1_0_11.ts index 4d06dece..6ea67234 100644 --- a/packages/client/lib/OpenID4VCIClientV1_0_11.ts +++ b/packages/client/lib/OpenID4VCIClientV1_0_11.ts @@ -3,6 +3,9 @@ import { AccessTokenRequestOpts, AccessTokenResponse, Alg, + AuthorizationChallengeCodeResponse, + AuthorizationChallengeErrorResponse, + AuthorizationChallengeRequestOpts, AuthorizationRequestOpts, AuthorizationResponse, AuthorizationServerOpts, @@ -25,10 +28,11 @@ import { KID_JWK_X5C_ERROR, OID4VCICredentialFormat, OpenId4VCIVersion, + OpenIDResponse, PKCEOpts, ProofOfPossessionCallbacks, - toAuthorizationResponsePayload, -} from '@sphereon/oid4vci-common'; + toAuthorizationResponsePayload +} from '@sphereon/oid4vci-common' import { CredentialFormat } from '@sphereon/ssi-types'; import Debug from 'debug'; @@ -39,6 +43,7 @@ import { CredentialRequestClientBuilderV1_0_11 } from './CredentialRequestClient import { MetadataClientV1_0_11 } from './MetadataClientV1_0_11'; import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder'; import { generateMissingPKCEOpts } from './functions'; +import { acquireAuthorizationChallengeAuthCode } from './AuthorizationCodeClient' const debug = Debug('sphereon:oid4vci'); @@ -256,6 +261,14 @@ export class OpenID4VCIClientV1_0_11 { this._state.pkce = generateMissingPKCEOpts({ ...this._state.pkce, ...pkce }); } + public async acquireAuthorizationChallengeCode(opts: AuthorizationChallengeRequestOpts): Promise> { + const response = await acquireAuthorizationChallengeAuthCode({ + clientId: this._state.clientId ?? this._state.authorizationRequestOpts?.clientId, + ...opts + }) + return response + } + public async acquireAccessToken( opts?: Omit & { clientId?: string; diff --git a/packages/client/lib/OpenID4VCIClientV1_0_13.ts b/packages/client/lib/OpenID4VCIClientV1_0_13.ts index 4aedcc9d..e7afaa64 100644 --- a/packages/client/lib/OpenID4VCIClientV1_0_13.ts +++ b/packages/client/lib/OpenID4VCIClientV1_0_13.ts @@ -3,6 +3,9 @@ import { AccessTokenRequestOpts, AccessTokenResponse, Alg, + AuthorizationChallengeCodeResponse, + AuthorizationChallengeErrorResponse, + AuthorizationChallengeRequestOpts, AuthorizationRequestOpts, AuthorizationResponse, AuthorizationServerOpts, @@ -25,15 +28,19 @@ import { NotificationResponseResult, OID4VCICredentialFormat, OpenId4VCIVersion, + OpenIDResponse, PKCEOpts, ProofOfPossessionCallbacks, - toAuthorizationResponsePayload, -} from '@sphereon/oid4vci-common'; + toAuthorizationResponsePayload +} from '@sphereon/oid4vci-common' import { CredentialFormat, DIDDocument } from '@sphereon/ssi-types'; import Debug from 'debug'; import { AccessTokenClient } from './AccessTokenClient'; -import { createAuthorizationRequestUrl } from './AuthorizationCodeClient'; +import { + acquireAuthorizationChallengeAuthCode, + createAuthorizationRequestUrl +} from './AuthorizationCodeClient' import { CredentialOfferClient } from './CredentialOfferClient'; import { CredentialRequestOpts } from './CredentialRequestClient'; import { CredentialRequestClientBuilderV1_0_13 } from './CredentialRequestClientBuilderV1_0_13'; @@ -261,6 +268,14 @@ export class OpenID4VCIClientV1_0_13 { this._state.pkce = generateMissingPKCEOpts({ ...this._state.pkce, ...pkce }); } + public async acquireAuthorizationChallengeCode(opts: AuthorizationChallengeRequestOpts): Promise> { + const response = await acquireAuthorizationChallengeAuthCode({ + clientId: this._state.clientId ?? this._state.authorizationRequestOpts?.clientId, + ...opts + }) + return response + } + public async acquireAccessToken( opts?: Omit & { clientId?: string; diff --git a/packages/oid4vci-common/lib/types/Authorization.types.ts b/packages/oid4vci-common/lib/types/Authorization.types.ts index c13c1b9b..8803eb8e 100644 --- a/packages/oid4vci-common/lib/types/Authorization.types.ts +++ b/packages/oid4vci-common/lib/types/Authorization.types.ts @@ -122,6 +122,17 @@ export interface CommonAuthorizationChallengeRequest { presentation_during_issuance_session?: string; } +export interface AuthorizationChallengeRequestOpts { + clientId?: string; + issuerState?: string + authSession?: string + scope?: string + codeChallenge?: string + codeChallengeMethod?: CodeChallengeMethod + definitionId?: string + presentationDuringIssuanceSession?: string; +} + // https://www.ietf.org/archive/id/draft-parecki-oauth-first-party-apps-02.html#name-error-response export interface AuthorizationChallengeErrorResponse { /** From 58af8fed4dc2643d3964a67c64f95be55fff6cb3 Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Mon, 13 Jan 2025 08:25:59 +0100 Subject: [PATCH 07/26] chore: cleanup --- packages/client/lib/AuthorizationCodeClient.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/client/lib/AuthorizationCodeClient.ts b/packages/client/lib/AuthorizationCodeClient.ts index 37acaffe..0404ea18 100644 --- a/packages/client/lib/AuthorizationCodeClient.ts +++ b/packages/client/lib/AuthorizationCodeClient.ts @@ -1,9 +1,11 @@ import { AuthorizationChallengeCodeResponse, - AuthorizationChallengeErrorResponse, AuthorizationChallengeRequestOpts, + AuthorizationChallengeErrorResponse, + AuthorizationChallengeRequestOpts, AuthorizationDetails, AuthorizationRequestOpts, - CodeChallengeMethod, CommonAuthorizationChallengeRequest, + CodeChallengeMethod, + CommonAuthorizationChallengeRequest, convertJsonToURI, CreateRequestObjectMode, CredentialConfigurationSupportedV1_0_13, From b97ff27b908df23f7fc12067029323f4a38d1692 Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Mon, 13 Jan 2025 08:51:02 +0100 Subject: [PATCH 08/26] chore: make opts optional as client id is default from the client --- packages/client/lib/OpenID4VCIClient.ts | 2 +- packages/client/lib/OpenID4VCIClientV1_0_11.ts | 2 +- packages/client/lib/OpenID4VCIClientV1_0_13.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index 8dfdaa0b..b2bba446 100644 --- a/packages/client/lib/OpenID4VCIClient.ts +++ b/packages/client/lib/OpenID4VCIClient.ts @@ -277,7 +277,7 @@ export class OpenID4VCIClient { this._state.pkce = generateMissingPKCEOpts({ ...this._state.pkce, ...pkce }); } - public async acquireAuthorizationChallengeCode(opts: AuthorizationChallengeRequestOpts): Promise> { + public async acquireAuthorizationChallengeCode(opts?: AuthorizationChallengeRequestOpts): Promise> { const response = await acquireAuthorizationChallengeAuthCode({ clientId: this._state.clientId ?? this._state.authorizationRequestOpts?.clientId, ...opts diff --git a/packages/client/lib/OpenID4VCIClientV1_0_11.ts b/packages/client/lib/OpenID4VCIClientV1_0_11.ts index 6ea67234..ca17f901 100644 --- a/packages/client/lib/OpenID4VCIClientV1_0_11.ts +++ b/packages/client/lib/OpenID4VCIClientV1_0_11.ts @@ -261,7 +261,7 @@ export class OpenID4VCIClientV1_0_11 { this._state.pkce = generateMissingPKCEOpts({ ...this._state.pkce, ...pkce }); } - public async acquireAuthorizationChallengeCode(opts: AuthorizationChallengeRequestOpts): Promise> { + public async acquireAuthorizationChallengeCode(opts?: AuthorizationChallengeRequestOpts): Promise> { const response = await acquireAuthorizationChallengeAuthCode({ clientId: this._state.clientId ?? this._state.authorizationRequestOpts?.clientId, ...opts diff --git a/packages/client/lib/OpenID4VCIClientV1_0_13.ts b/packages/client/lib/OpenID4VCIClientV1_0_13.ts index e7afaa64..926455f9 100644 --- a/packages/client/lib/OpenID4VCIClientV1_0_13.ts +++ b/packages/client/lib/OpenID4VCIClientV1_0_13.ts @@ -268,7 +268,7 @@ export class OpenID4VCIClientV1_0_13 { this._state.pkce = generateMissingPKCEOpts({ ...this._state.pkce, ...pkce }); } - public async acquireAuthorizationChallengeCode(opts: AuthorizationChallengeRequestOpts): Promise> { + public async acquireAuthorizationChallengeCode(opts?: AuthorizationChallengeRequestOpts): Promise> { const response = await acquireAuthorizationChallengeAuthCode({ clientId: this._state.clientId ?? this._state.authorizationRequestOpts?.clientId, ...opts From 3a426b544f5cfd38f7ec41c4619e30ab9eac51e7 Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Mon, 13 Jan 2025 09:15:08 +0100 Subject: [PATCH 09/26] chore: use authorization_code for acquiring the access token --- packages/client/lib/AuthorizationCodeClient.ts | 7 +++---- packages/client/lib/OpenID4VCIClient.ts | 10 +++++++--- packages/client/lib/OpenID4VCIClientV1_0_11.ts | 4 ++-- packages/client/lib/OpenID4VCIClientV1_0_13.ts | 4 ++-- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/client/lib/AuthorizationCodeClient.ts b/packages/client/lib/AuthorizationCodeClient.ts index 0404ea18..5336d165 100644 --- a/packages/client/lib/AuthorizationCodeClient.ts +++ b/packages/client/lib/AuthorizationCodeClient.ts @@ -1,6 +1,5 @@ import { AuthorizationChallengeCodeResponse, - AuthorizationChallengeErrorResponse, AuthorizationChallengeRequestOpts, AuthorizationDetails, AuthorizationRequestOpts, @@ -278,13 +277,13 @@ const handleLocations = (endpointMetadata: EndpointMetadataResultV1_0_13, author return authorizationDetails; }; -export const acquireAuthorizationChallengeAuthCode = async (opts: AuthorizationChallengeRequestOpts): Promise> => { +export const acquireAuthorizationChallengeAuthCode = async (opts: AuthorizationChallengeRequestOpts): Promise> => { //AuthorizationChallengeErrorResponse return await acquireAuthorizationChallengeAuthCodeUsingRequest({ authorizationChallengeRequest: await createAuthorizationChallengeRequest(opts) }); } -export const acquireAuthorizationChallengeAuthCodeUsingRequest = async (opts: { authorizationChallengeRequest: CommonAuthorizationChallengeRequest }): Promise> => { +export const acquireAuthorizationChallengeAuthCodeUsingRequest = async (opts: { authorizationChallengeRequest: CommonAuthorizationChallengeRequest }): Promise> => { //AuthorizationChallengeErrorResponse const { authorizationChallengeRequest } = opts // TODO validate request const authorizationChallengeCodeUrl = '' // TODO @@ -326,7 +325,7 @@ export const sendAuthorizationChallengeRequest = async ( authorizationChallengeCodeUrl: string, authorizationChallengeRequest: CommonAuthorizationChallengeRequest, opts?: { headers?: Record } -): Promise> => { +): Promise> => { //AuthorizationChallengeErrorResponse return await formPost(authorizationChallengeCodeUrl, convertJsonToURI(authorizationChallengeRequest, { mode: JsonURIMode.X_FORM_WWW_URLENCODED }), { // TODO check encoding customHeaders: opts?.headers ? opts.headers : undefined, }); diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index b2bba446..dde5b34e 100644 --- a/packages/client/lib/OpenID4VCIClient.ts +++ b/packages/client/lib/OpenID4VCIClient.ts @@ -4,7 +4,6 @@ import { AccessTokenResponse, Alg, AuthorizationChallengeCodeResponse, - AuthorizationChallengeErrorResponse, AuthorizationChallengeRequestOpts, AuthorizationRequestOpts, AuthorizationResponse, @@ -277,11 +276,16 @@ export class OpenID4VCIClient { this._state.pkce = generateMissingPKCEOpts({ ...this._state.pkce, ...pkce }); } - public async acquireAuthorizationChallengeCode(opts?: AuthorizationChallengeRequestOpts): Promise> { + public async acquireAuthorizationChallengeCode(opts?: AuthorizationChallengeRequestOpts): Promise> { //AuthorizationChallengeErrorResponse const response = await acquireAuthorizationChallengeAuthCode({ clientId: this._state.clientId ?? this._state.authorizationRequestOpts?.clientId, ...opts }) + + if (!this._state.authorizationCodeResponse) { + this._state.authorizationCodeResponse = response.successBody; + } + return response } @@ -299,7 +303,7 @@ export class OpenID4VCIClient { } else if (opts?.code) { this._state.authorizationCodeResponse = { code: opts.code }; } - const code = this._state.authorizationCodeResponse?.code; + const code = (this._state.authorizationCodeResponse as AuthorizationResponse)?.code ?? (this._state.authorizationCodeResponse as AuthorizationChallengeCodeResponse)?.authorization_code; if (opts?.codeVerifier) { this._state.pkce.codeVerifier = opts.codeVerifier; diff --git a/packages/client/lib/OpenID4VCIClientV1_0_11.ts b/packages/client/lib/OpenID4VCIClientV1_0_11.ts index ca17f901..60a2f536 100644 --- a/packages/client/lib/OpenID4VCIClientV1_0_11.ts +++ b/packages/client/lib/OpenID4VCIClientV1_0_11.ts @@ -58,7 +58,7 @@ export interface OpenID4VCIClientStateV1_0_11 { accessTokenResponse?: AccessTokenResponse; dpopResponseParams?: DPoPResponseParams; authorizationRequestOpts?: AuthorizationRequestOpts; - authorizationCodeResponse?: AuthorizationResponse; + authorizationCodeResponse?: AuthorizationResponse | AuthorizationChallengeCodeResponse; pkce: PKCEOpts; accessToken?: string; authorizationURL?: string; @@ -283,7 +283,7 @@ export class OpenID4VCIClientV1_0_11 { } else if (opts?.code) { this._state.authorizationCodeResponse = { code: opts.code }; } - const code = this._state.authorizationCodeResponse?.code; + const code = (this._state.authorizationCodeResponse as AuthorizationResponse)?.code ?? (this._state.authorizationCodeResponse as AuthorizationChallengeCodeResponse)?.authorization_code; if (opts?.codeVerifier) { this._state.pkce.codeVerifier = opts.codeVerifier; diff --git a/packages/client/lib/OpenID4VCIClientV1_0_13.ts b/packages/client/lib/OpenID4VCIClientV1_0_13.ts index 926455f9..a230ed5c 100644 --- a/packages/client/lib/OpenID4VCIClientV1_0_13.ts +++ b/packages/client/lib/OpenID4VCIClientV1_0_13.ts @@ -61,7 +61,7 @@ export interface OpenID4VCIClientStateV1_0_13 { accessTokenResponse?: AccessTokenResponse; dpopResponseParams?: DPoPResponseParams; authorizationRequestOpts?: AuthorizationRequestOpts; - authorizationCodeResponse?: AuthorizationResponse; + authorizationCodeResponse?: AuthorizationResponse | AuthorizationChallengeCodeResponse; pkce: PKCEOpts; accessToken?: string; authorizationURL?: string; @@ -290,7 +290,7 @@ export class OpenID4VCIClientV1_0_13 { } else if (opts?.code) { this._state.authorizationCodeResponse = { code: opts.code }; } - const code = this._state.authorizationCodeResponse?.code; + const code = (this._state.authorizationCodeResponse as AuthorizationResponse)?.code ?? (this._state.authorizationCodeResponse as AuthorizationChallengeCodeResponse)?.authorization_code; if (opts?.codeVerifier) { this._state.pkce.codeVerifier = opts.codeVerifier; From 1825b96189a780e37c6ff50d8c3d16c0d5d6228d Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Mon, 13 Jan 2025 09:42:47 +0100 Subject: [PATCH 10/26] chore: cleanup --- packages/client/lib/OpenID4VCIClient.ts | 2 +- packages/client/lib/OpenID4VCIClientV1_0_11.ts | 2 +- packages/client/lib/OpenID4VCIClientV1_0_13.ts | 2 +- .../oid4vci-common/lib/functions/AuthorizationResponseUtil.ts | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index dde5b34e..51b22f01 100644 --- a/packages/client/lib/OpenID4VCIClient.ts +++ b/packages/client/lib/OpenID4VCIClient.ts @@ -292,7 +292,7 @@ export class OpenID4VCIClient { public async acquireAccessToken( opts?: Omit & { clientId?: string; - authorizationResponse?: string | AuthorizationResponse; // Pass in an auth response, either as URI/redirect, or object // TODO we need to add support for the authorization code from the auth challenge + authorizationResponse?: string | AuthorizationResponse | AuthorizationChallengeCodeResponse; // Pass in an auth response, either as URI/redirect, or object // TODO we need to add support for the authorization code from the auth challenge additionalRequestParams?: Record; }, ): Promise { diff --git a/packages/client/lib/OpenID4VCIClientV1_0_11.ts b/packages/client/lib/OpenID4VCIClientV1_0_11.ts index 60a2f536..66b469e9 100644 --- a/packages/client/lib/OpenID4VCIClientV1_0_11.ts +++ b/packages/client/lib/OpenID4VCIClientV1_0_11.ts @@ -272,7 +272,7 @@ export class OpenID4VCIClientV1_0_11 { public async acquireAccessToken( opts?: Omit & { clientId?: string; - authorizationResponse?: string | AuthorizationResponse; // Pass in an auth response, either as URI/redirect, or object + authorizationResponse?: string | AuthorizationResponse | AuthorizationChallengeCodeResponse; // Pass in an auth response, either as URI/redirect, or object additionalRequestParams?: Record; }, ): Promise { diff --git a/packages/client/lib/OpenID4VCIClientV1_0_13.ts b/packages/client/lib/OpenID4VCIClientV1_0_13.ts index a230ed5c..a316a2de 100644 --- a/packages/client/lib/OpenID4VCIClientV1_0_13.ts +++ b/packages/client/lib/OpenID4VCIClientV1_0_13.ts @@ -279,7 +279,7 @@ export class OpenID4VCIClientV1_0_13 { public async acquireAccessToken( opts?: Omit & { clientId?: string; - authorizationResponse?: string | AuthorizationResponse; // Pass in an auth response, either as URI/redirect, or object + authorizationResponse?: string | AuthorizationResponse | AuthorizationChallengeCodeResponse; // Pass in an auth response, either as URI/redirect, or object additionalRequestParams?: Record; }, ): Promise { diff --git a/packages/oid4vci-common/lib/functions/AuthorizationResponseUtil.ts b/packages/oid4vci-common/lib/functions/AuthorizationResponseUtil.ts index a5438e02..8eb64e6a 100644 --- a/packages/oid4vci-common/lib/functions/AuthorizationResponseUtil.ts +++ b/packages/oid4vci-common/lib/functions/AuthorizationResponseUtil.ts @@ -1,8 +1,8 @@ -import { AuthorizationResponse } from '../types'; +import { AuthorizationChallengeCodeResponse, AuthorizationResponse } from '../types' import { convertURIToJsonObject } from './Encoding'; -export const toAuthorizationResponsePayload = (input: AuthorizationResponse | string): AuthorizationResponse => { +export const toAuthorizationResponsePayload = (input: AuthorizationResponse | AuthorizationChallengeCodeResponse | string): AuthorizationResponse | AuthorizationChallengeCodeResponse => { let response = input; if (typeof input === 'string') { if (input.trim().startsWith('{') && input.trim().endsWith('}')) { From e8dca638ec38f666799899ecf6787daf7bbd6c85 Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Mon, 13 Jan 2025 10:16:28 +0100 Subject: [PATCH 11/26] chore: refactor authorization challenge code error handling --- packages/client/lib/OpenID4VCIClient.ts | 16 ++++++++++------ packages/client/lib/OpenID4VCIClientV1_0_11.ts | 17 +++++++++++++---- packages/client/lib/OpenID4VCIClientV1_0_13.ts | 15 ++++++++++++--- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index 51b22f01..d273a113 100644 --- a/packages/client/lib/OpenID4VCIClient.ts +++ b/packages/client/lib/OpenID4VCIClient.ts @@ -3,7 +3,7 @@ import { AccessTokenRequestOpts, AccessTokenResponse, Alg, - AuthorizationChallengeCodeResponse, + AuthorizationChallengeCodeResponse, AuthorizationChallengeErrorResponse, AuthorizationChallengeRequestOpts, AuthorizationRequestOpts, AuthorizationResponse, @@ -33,7 +33,6 @@ import { NotificationResponseResult, OID4VCICredentialFormat, OpenId4VCIVersion, - OpenIDResponse, PKCEOpts, ProofOfPossessionCallbacks, toAuthorizationResponsePayload @@ -276,17 +275,22 @@ export class OpenID4VCIClient { this._state.pkce = generateMissingPKCEOpts({ ...this._state.pkce, ...pkce }); } - public async acquireAuthorizationChallengeCode(opts?: AuthorizationChallengeRequestOpts): Promise> { //AuthorizationChallengeErrorResponse + public async acquireAuthorizationChallengeCode(opts?: AuthorizationChallengeRequestOpts): Promise { const response = await acquireAuthorizationChallengeAuthCode({ clientId: this._state.clientId ?? this._state.authorizationRequestOpts?.clientId, ...opts }) - if (!this._state.authorizationCodeResponse) { - this._state.authorizationCodeResponse = response.successBody; + if (response.errorBody) { + debug(`Authorization code error:\r\n${JSON.stringify(response.errorBody)}`); + const error = response.errorBody as AuthorizationChallengeErrorResponse + return Promise.reject(error) + } else if (!response.successBody) { + debug(`Authorization code error. No success body`); + return Promise.reject(Error(`Retrieving an authorization code token from ${this._state.endpointMetadata?.authorization_challenge_endpoint} for issuer ${this.getIssuer()} failed as there was no success response body`)) } - return response + return { ...response.successBody } } public async acquireAccessToken( diff --git a/packages/client/lib/OpenID4VCIClientV1_0_11.ts b/packages/client/lib/OpenID4VCIClientV1_0_11.ts index 66b469e9..5d287b79 100644 --- a/packages/client/lib/OpenID4VCIClientV1_0_11.ts +++ b/packages/client/lib/OpenID4VCIClientV1_0_11.ts @@ -28,7 +28,6 @@ import { KID_JWK_X5C_ERROR, OID4VCICredentialFormat, OpenId4VCIVersion, - OpenIDResponse, PKCEOpts, ProofOfPossessionCallbacks, toAuthorizationResponsePayload @@ -37,13 +36,13 @@ import { CredentialFormat } from '@sphereon/ssi-types'; import Debug from 'debug'; import { AccessTokenClientV1_0_11 } from './AccessTokenClientV1_0_11'; +import { acquireAuthorizationChallengeAuthCode } from './AuthorizationCodeClient' import { createAuthorizationRequestUrlV1_0_11 } from './AuthorizationCodeClientV1_0_11'; import { CredentialOfferClientV1_0_11 } from './CredentialOfferClientV1_0_11'; import { CredentialRequestClientBuilderV1_0_11 } from './CredentialRequestClientBuilderV1_0_11'; import { MetadataClientV1_0_11 } from './MetadataClientV1_0_11'; import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder'; import { generateMissingPKCEOpts } from './functions'; -import { acquireAuthorizationChallengeAuthCode } from './AuthorizationCodeClient' const debug = Debug('sphereon:oid4vci'); @@ -261,12 +260,22 @@ export class OpenID4VCIClientV1_0_11 { this._state.pkce = generateMissingPKCEOpts({ ...this._state.pkce, ...pkce }); } - public async acquireAuthorizationChallengeCode(opts?: AuthorizationChallengeRequestOpts): Promise> { + public async acquireAuthorizationChallengeCode(opts?: AuthorizationChallengeRequestOpts): Promise { const response = await acquireAuthorizationChallengeAuthCode({ clientId: this._state.clientId ?? this._state.authorizationRequestOpts?.clientId, ...opts }) - return response + + if (response.errorBody) { + debug(`Authorization code error:\r\n${JSON.stringify(response.errorBody)}`); + const error = response.errorBody as AuthorizationChallengeErrorResponse + return Promise.reject(error) + } else if (!response.successBody) { + debug(`Authorization code error. No success body`); + return Promise.reject(Error(`Retrieving an authorization code token from ${this._state.endpointMetadata?.authorization_challenge_endpoint} for issuer ${this.getIssuer()} failed as there was no success response body`)) + } + + return { ...response.successBody } } public async acquireAccessToken( diff --git a/packages/client/lib/OpenID4VCIClientV1_0_13.ts b/packages/client/lib/OpenID4VCIClientV1_0_13.ts index a316a2de..ea4c8043 100644 --- a/packages/client/lib/OpenID4VCIClientV1_0_13.ts +++ b/packages/client/lib/OpenID4VCIClientV1_0_13.ts @@ -28,7 +28,6 @@ import { NotificationResponseResult, OID4VCICredentialFormat, OpenId4VCIVersion, - OpenIDResponse, PKCEOpts, ProofOfPossessionCallbacks, toAuthorizationResponsePayload @@ -268,12 +267,22 @@ export class OpenID4VCIClientV1_0_13 { this._state.pkce = generateMissingPKCEOpts({ ...this._state.pkce, ...pkce }); } - public async acquireAuthorizationChallengeCode(opts?: AuthorizationChallengeRequestOpts): Promise> { + public async acquireAuthorizationChallengeCode(opts?: AuthorizationChallengeRequestOpts): Promise { const response = await acquireAuthorizationChallengeAuthCode({ clientId: this._state.clientId ?? this._state.authorizationRequestOpts?.clientId, ...opts }) - return response + + if (response.errorBody) { + debug(`Authorization code error:\r\n${JSON.stringify(response.errorBody)}`); + const error = response.errorBody as AuthorizationChallengeErrorResponse + return Promise.reject(error) + } else if (!response.successBody) { + debug(`Authorization code error. No success body`); + return Promise.reject(Error(`Retrieving an authorization code token from ${this._state.endpointMetadata?.authorization_challenge_endpoint} for issuer ${this.getIssuer()} failed as there was no success response body`)) + } + + return { ...response.successBody } } public async acquireAccessToken( From 3e2c8d742eef1521cb6065ad68ad0023dff8f92e Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Wed, 15 Jan 2025 11:14:53 +0100 Subject: [PATCH 12/26] chore: small fixes --- .../client/lib/AuthorizationCodeClient.ts | 8 +++--- packages/client/lib/OpenID4VCIClient.ts | 25 ++++++++++++------- .../client/lib/OpenID4VCIClientV1_0_11.ts | 20 +++++++++------ .../client/lib/OpenID4VCIClientV1_0_13.ts | 20 +++++++++------ packages/issuer-rest/lib/OID4VCIServer.ts | 4 ++- .../issuer-rest/lib/oid4vci-api-functions.ts | 4 +-- 6 files changed, 51 insertions(+), 30 deletions(-) diff --git a/packages/client/lib/AuthorizationCodeClient.ts b/packages/client/lib/AuthorizationCodeClient.ts index 5336d165..2f9d1a2d 100644 --- a/packages/client/lib/AuthorizationCodeClient.ts +++ b/packages/client/lib/AuthorizationCodeClient.ts @@ -277,13 +277,13 @@ const handleLocations = (endpointMetadata: EndpointMetadataResultV1_0_13, author return authorizationDetails; }; -export const acquireAuthorizationChallengeAuthCode = async (opts: AuthorizationChallengeRequestOpts): Promise> => { //AuthorizationChallengeErrorResponse +export const acquireAuthorizationChallengeAuthCode = async (opts: AuthorizationChallengeRequestOpts): Promise> => { return await acquireAuthorizationChallengeAuthCodeUsingRequest({ authorizationChallengeRequest: await createAuthorizationChallengeRequest(opts) }); } -export const acquireAuthorizationChallengeAuthCodeUsingRequest = async (opts: { authorizationChallengeRequest: CommonAuthorizationChallengeRequest }): Promise> => { //AuthorizationChallengeErrorResponse +export const acquireAuthorizationChallengeAuthCodeUsingRequest = async (opts: { authorizationChallengeRequest: CommonAuthorizationChallengeRequest }): Promise> => { const { authorizationChallengeRequest } = opts // TODO validate request const authorizationChallengeCodeUrl = '' // TODO @@ -325,8 +325,8 @@ export const sendAuthorizationChallengeRequest = async ( authorizationChallengeCodeUrl: string, authorizationChallengeRequest: CommonAuthorizationChallengeRequest, opts?: { headers?: Record } -): Promise> => { //AuthorizationChallengeErrorResponse - return await formPost(authorizationChallengeCodeUrl, convertJsonToURI(authorizationChallengeRequest, { mode: JsonURIMode.X_FORM_WWW_URLENCODED }), { // TODO check encoding +): Promise> => { + return await formPost(authorizationChallengeCodeUrl, convertJsonToURI(authorizationChallengeRequest, { mode: JsonURIMode.X_FORM_WWW_URLENCODED }), { customHeaders: opts?.headers ? opts.headers : undefined, }); } diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index d273a113..b19c4e27 100644 --- a/packages/client/lib/OpenID4VCIClient.ts +++ b/packages/client/lib/OpenID4VCIClient.ts @@ -3,7 +3,8 @@ import { AccessTokenRequestOpts, AccessTokenResponse, Alg, - AuthorizationChallengeCodeResponse, AuthorizationChallengeErrorResponse, + AuthorizationChallengeCodeResponse, + AuthorizationChallengeErrorResponse, AuthorizationChallengeRequestOpts, AuthorizationRequestOpts, AuthorizationResponse, @@ -94,7 +95,7 @@ export class OpenID4VCIClient { endpointMetadata?: EndpointMetadataResult; accessTokenResponse?: AccessTokenResponse; authorizationRequestOpts?: AuthorizationRequestOpts; - authorizationCodeResponse?: AuthorizationResponse; + authorizationCodeResponse?: AuthorizationResponse | AuthorizationChallengeCodeResponse; authorizationURL?: string; }) { const issuer = credentialIssuer ?? (credentialOffer ? getIssuerFromCredentialOfferPayload(credentialOffer.credential_offer) : undefined); @@ -296,18 +297,14 @@ export class OpenID4VCIClient { public async acquireAccessToken( opts?: Omit & { clientId?: string; - authorizationResponse?: string | AuthorizationResponse | AuthorizationChallengeCodeResponse; // Pass in an auth response, either as URI/redirect, or object // TODO we need to add support for the authorization code from the auth challenge + authorizationResponse?: string | AuthorizationResponse | AuthorizationChallengeCodeResponse; // Pass in an auth response, either as URI/redirect, or object additionalRequestParams?: Record; }, ): Promise { const { pin, clientId = this._state.clientId ?? this._state.authorizationRequestOpts?.clientId } = opts ?? {}; let { redirectUri } = opts ?? {}; - if (opts?.authorizationResponse) { - this._state.authorizationCodeResponse = { ...toAuthorizationResponsePayload(opts.authorizationResponse) }; - } else if (opts?.code) { - this._state.authorizationCodeResponse = { code: opts.code }; - } - const code = (this._state.authorizationCodeResponse as AuthorizationResponse)?.code ?? (this._state.authorizationCodeResponse as AuthorizationChallengeCodeResponse)?.authorization_code; + + const code = this.getAuthorizationCode(opts?.authorizationResponse, opts?.code) if (opts?.codeVerifier) { this._state.pkce.codeVerifier = opts.codeVerifier; @@ -759,4 +756,14 @@ export class OpenID4VCIClient { authorizationRequestOpts.clientId = clientId; return authorizationRequestOpts; } + + private getAuthorizationCode = (authorizationResponse?: string | AuthorizationResponse | AuthorizationChallengeCodeResponse, code?: string): string | undefined => { + if (authorizationResponse) { + this._state.authorizationCodeResponse = { ...toAuthorizationResponsePayload(authorizationResponse) }; + } else if (code) { + this._state.authorizationCodeResponse = { code }; + } + + return (this._state.authorizationCodeResponse as AuthorizationResponse)?.code ?? (this._state.authorizationCodeResponse as AuthorizationChallengeCodeResponse)?.authorization_code; + } } diff --git a/packages/client/lib/OpenID4VCIClientV1_0_11.ts b/packages/client/lib/OpenID4VCIClientV1_0_11.ts index 5d287b79..151f8648 100644 --- a/packages/client/lib/OpenID4VCIClientV1_0_11.ts +++ b/packages/client/lib/OpenID4VCIClientV1_0_11.ts @@ -92,7 +92,7 @@ export class OpenID4VCIClientV1_0_11 { endpointMetadata?: EndpointMetadataResultV1_0_11; accessTokenResponse?: AccessTokenResponse; authorizationRequestOpts?: AuthorizationRequestOpts; - authorizationCodeResponse?: AuthorizationResponse; + authorizationCodeResponse?: AuthorizationResponse | AuthorizationChallengeCodeResponse; authorizationURL?: string; }) { const issuer = credentialIssuer ?? (credentialOffer ? getIssuerFromCredentialOfferPayload(credentialOffer.credential_offer) : undefined); @@ -287,12 +287,8 @@ export class OpenID4VCIClientV1_0_11 { ): Promise { const { pin, clientId = this._state.clientId ?? this._state.authorizationRequestOpts?.clientId } = opts ?? {}; let { redirectUri } = opts ?? {}; - if (opts?.authorizationResponse) { - this._state.authorizationCodeResponse = { ...toAuthorizationResponsePayload(opts.authorizationResponse) }; - } else if (opts?.code) { - this._state.authorizationCodeResponse = { code: opts.code }; - } - const code = (this._state.authorizationCodeResponse as AuthorizationResponse)?.code ?? (this._state.authorizationCodeResponse as AuthorizationChallengeCodeResponse)?.authorization_code; + + const code = this.getAuthorizationCode(opts?.authorizationResponse, opts?.code) if (opts?.codeVerifier) { this._state.pkce.codeVerifier = opts.codeVerifier; @@ -694,4 +690,14 @@ export class OpenID4VCIClientV1_0_11 { authorizationRequestOpts.clientId = clientId; return authorizationRequestOpts; } + + private getAuthorizationCode = (authorizationResponse?: string | AuthorizationResponse | AuthorizationChallengeCodeResponse, code?: string): string | undefined => { + if (authorizationResponse) { + this._state.authorizationCodeResponse = { ...toAuthorizationResponsePayload(authorizationResponse) }; + } else if (code) { + this._state.authorizationCodeResponse = { code }; + } + + return (this._state.authorizationCodeResponse as AuthorizationResponse)?.code ?? (this._state.authorizationCodeResponse as AuthorizationChallengeCodeResponse)?.authorization_code; + } } diff --git a/packages/client/lib/OpenID4VCIClientV1_0_13.ts b/packages/client/lib/OpenID4VCIClientV1_0_13.ts index ea4c8043..fb0a2b10 100644 --- a/packages/client/lib/OpenID4VCIClientV1_0_13.ts +++ b/packages/client/lib/OpenID4VCIClientV1_0_13.ts @@ -97,7 +97,7 @@ export class OpenID4VCIClientV1_0_13 { endpointMetadata?: EndpointMetadataResultV1_0_13; accessTokenResponse?: AccessTokenResponse; authorizationRequestOpts?: AuthorizationRequestOpts; - authorizationCodeResponse?: AuthorizationResponse; + authorizationCodeResponse?: AuthorizationResponse | AuthorizationChallengeCodeResponse; authorizationURL?: string; }) { const issuer = credentialIssuer ?? (credentialOffer ? getIssuerFromCredentialOfferPayload(credentialOffer.credential_offer) : undefined); @@ -294,12 +294,8 @@ export class OpenID4VCIClientV1_0_13 { ): Promise { const { pin, clientId = this._state.clientId ?? this._state.authorizationRequestOpts?.clientId } = opts ?? {}; let { redirectUri } = opts ?? {}; - if (opts?.authorizationResponse) { - this._state.authorizationCodeResponse = { ...toAuthorizationResponsePayload(opts.authorizationResponse) }; - } else if (opts?.code) { - this._state.authorizationCodeResponse = { code: opts.code }; - } - const code = (this._state.authorizationCodeResponse as AuthorizationResponse)?.code ?? (this._state.authorizationCodeResponse as AuthorizationChallengeCodeResponse)?.authorization_code; + + const code = this.getAuthorizationCode(opts?.authorizationResponse, opts?.code) if (opts?.codeVerifier) { this._state.pkce.codeVerifier = opts.codeVerifier; @@ -797,4 +793,14 @@ export class OpenID4VCIClientV1_0_13 { authorizationRequestOpts.clientId = clientId; return authorizationRequestOpts; } + + private getAuthorizationCode = (authorizationResponse?: string | AuthorizationResponse | AuthorizationChallengeCodeResponse, code?: string): string | undefined => { + if (authorizationResponse) { + this._state.authorizationCodeResponse = { ...toAuthorizationResponsePayload(authorizationResponse) }; + } else if (code) { + this._state.authorizationCodeResponse = { code }; + } + + return (this._state.authorizationCodeResponse as AuthorizationResponse)?.code ?? (this._state.authorizationCodeResponse as AuthorizationChallengeCodeResponse)?.authorization_code; + } } diff --git a/packages/issuer-rest/lib/OID4VCIServer.ts b/packages/issuer-rest/lib/OID4VCIServer.ts index 4b936f29..54aa16eb 100644 --- a/packages/issuer-rest/lib/OID4VCIServer.ts +++ b/packages/issuer-rest/lib/OID4VCIServer.ts @@ -86,6 +86,8 @@ export interface IGetIssueStatusEndpointOpts extends ISingleEndpointOpts { } export interface IAuthorizationChallengeEndpointOpts extends ISingleEndpointOpts { + createAuthRequestUriEndpointPath?: string + verifyAuthResponseEndpointPath?: string /** * Callback used for creating the authorization request uri used for the RP. * Added an optional state parameter so that when direct calls are used, @@ -96,7 +98,7 @@ export interface IAuthorizationChallengeEndpointOpts extends ISingleEndpointOpts * Callback used for verifying the status of the authorization response. * This is checked by the issuer before issuing an authorization code. */ - verifyAuthResponseCallback: (correlationId: string) => Promise + verifyAuthResponseCallback: (presentationDefinitionId: string, correlationId: string) => Promise } export interface IOID4VCIServerOpts extends HasEndpointOpts { diff --git a/packages/issuer-rest/lib/oid4vci-api-functions.ts b/packages/issuer-rest/lib/oid4vci-api-functions.ts index 874e03d0..08db1532 100644 --- a/packages/issuer-rest/lib/oid4vci-api-functions.ts +++ b/packages/issuer-rest/lib/oid4vci-api-functions.ts @@ -153,7 +153,7 @@ export function authorizationChallengeEndpoint( return Promise.reject(authorizationChallengeErrorResponse) } - if (auth_session && presentation_during_issuance_session) { + if (auth_session && presentation_during_issuance_session && definition_id) { const session = await issuer.credentialOfferSessions.get(auth_session) if (!session) { const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = { @@ -162,7 +162,7 @@ export function authorizationChallengeEndpoint( return Promise.reject(authorizationChallengeErrorResponse) } - const verifiedResponse = await opts.verifyAuthResponseCallback(presentation_during_issuance_session) + const verifiedResponse = await opts.verifyAuthResponseCallback(definition_id, presentation_during_issuance_session) if (verifiedResponse) { const authorizationCode = generateRandomString(16, 'base64url') session.authorizationCode = authorizationCode From 084d916a650f4a0e7056f1a35035dfec93a16efa Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Wed, 15 Jan 2025 14:22:14 +0100 Subject: [PATCH 13/26] chore: fixes --- .../client/lib/AuthorizationCodeClient.ts | 41 ++++++++++++++++--- packages/client/lib/OpenID4VCIClient.ts | 2 + .../lib/types/Authorization.types.ts | 2 + 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/packages/client/lib/AuthorizationCodeClient.ts b/packages/client/lib/AuthorizationCodeClient.ts index 2f9d1a2d..0295b8a3 100644 --- a/packages/client/lib/AuthorizationCodeClient.ts +++ b/packages/client/lib/AuthorizationCodeClient.ts @@ -13,8 +13,10 @@ import { CredentialOfferPayloadV1_0_13, CredentialOfferRequestWithBaseUrl, determineSpecVersionFromOffer, + EndpointMetadata, EndpointMetadataResultV1_0_13, formPost, + IssuerOpts, isW3cCredentialSupported, JsonURIMode, Jwt, @@ -28,6 +30,7 @@ import { } from '@sphereon/oid4vci-common' import Debug from 'debug'; +import { MetadataClient } from './MetadataClient' import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder'; const debug = Debug('sphereon:oid4vci'); @@ -278,15 +281,43 @@ const handleLocations = (endpointMetadata: EndpointMetadataResultV1_0_13, author }; export const acquireAuthorizationChallengeAuthCode = async (opts: AuthorizationChallengeRequestOpts): Promise> => { + const { metadata } = opts + + const issuer = opts.credentialIssuer ?? opts?.metadata?.issuer as string + if (!issuer) { + throw Error('Issuer required at this point'); + } + + const issuerOpts = { + issuer, + } + return await acquireAuthorizationChallengeAuthCodeUsingRequest({ - authorizationChallengeRequest: await createAuthorizationChallengeRequest(opts) + authorizationChallengeRequest: await createAuthorizationChallengeRequest(opts), + metadata, + issuerOpts }); } -export const acquireAuthorizationChallengeAuthCodeUsingRequest = async (opts: { authorizationChallengeRequest: CommonAuthorizationChallengeRequest }): Promise> => { - const { authorizationChallengeRequest } = opts - // TODO validate request - const authorizationChallengeCodeUrl = '' // TODO +export const acquireAuthorizationChallengeAuthCodeUsingRequest = async ( + opts: { + authorizationChallengeRequest: CommonAuthorizationChallengeRequest, + metadata?: EndpointMetadata, + issuerOpts?: IssuerOpts + } +): Promise> => { + const { authorizationChallengeRequest, issuerOpts } = opts + const metadata = opts?.metadata + ? opts?.metadata + : issuerOpts?.fetchMetadata + ? await MetadataClient.retrieveAllMetadata(issuerOpts.issuer, { errorOnNotFound: false }) + : undefined + const authorizationChallengeCodeUrl = metadata?.authorization_challenge_endpoint + + if (!authorizationChallengeCodeUrl) { + return Promise.reject(Error('Cannot determine authorization challenge endpoint URL')) + } + const response = await sendAuthorizationChallengeRequest( authorizationChallengeCodeUrl, authorizationChallengeRequest diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index b19c4e27..47231fae 100644 --- a/packages/client/lib/OpenID4VCIClient.ts +++ b/packages/client/lib/OpenID4VCIClient.ts @@ -278,6 +278,8 @@ export class OpenID4VCIClient { public async acquireAuthorizationChallengeCode(opts?: AuthorizationChallengeRequestOpts): Promise { const response = await acquireAuthorizationChallengeAuthCode({ + metadata: this.endpointMetadata, + credentialIssuer: this.getIssuer(), clientId: this._state.clientId ?? this._state.authorizationRequestOpts?.clientId, ...opts }) diff --git a/packages/oid4vci-common/lib/types/Authorization.types.ts b/packages/oid4vci-common/lib/types/Authorization.types.ts index 8803eb8e..cdb0997e 100644 --- a/packages/oid4vci-common/lib/types/Authorization.types.ts +++ b/packages/oid4vci-common/lib/types/Authorization.types.ts @@ -131,6 +131,8 @@ export interface AuthorizationChallengeRequestOpts { codeChallengeMethod?: CodeChallengeMethod definitionId?: string presentationDuringIssuanceSession?: string; + metadata?: EndpointMetadata; + credentialIssuer?: string; } // https://www.ietf.org/archive/id/draft-parecki-oauth-first-party-apps-02.html#name-error-response From ba107cc09a4abe669c217aaab376ee721c48ee07 Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Wed, 15 Jan 2025 14:22:27 +0100 Subject: [PATCH 14/26] chore: added tests --- .../lib/__tests__/OpenID4VCIClient.spec.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/packages/client/lib/__tests__/OpenID4VCIClient.spec.ts b/packages/client/lib/__tests__/OpenID4VCIClient.spec.ts index 53c5a7f3..6383cffc 100644 --- a/packages/client/lib/__tests__/OpenID4VCIClient.spec.ts +++ b/packages/client/lib/__tests__/OpenID4VCIClient.spec.ts @@ -188,6 +188,49 @@ describe('OpenID4VCIClient should', () => { 'https://server.example.com/v1/auth/authorize?response_type=code&code_challenge_method=S256&code_challenge=mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs&authorization_details=%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22ldp_vc%22%2C%22locations%22%3A%5B%22https%3A%2F%2Ftest%2Ecom%22%2C%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%5D%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%7D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&client_id=test-client&scope=openid', ); }); + + it('it should respond with insufficient_authorization when no sessions are provided', async () => { + const url = new URL(`${MOCK_URL}/authorize-challenge`) + const responseBody = { + error: "insufficient_authorization", + auth_session: "123456789", + presentation: "/authorize?client_id=..&request_uri=https://rp.example.com/oidc/request/1234", + }; + (await client.retrieveServerMetadata()).authorization_challenge_endpoint = url.toString() + + nock(url.origin) + .post(url.pathname, { client_id: client.clientId }) + .times(1) + .reply(400, responseBody) + + await expect(client.acquireAuthorizationChallengeCode({ clientId: client.clientId })).rejects.toEqual({ + error: "insufficient_authorization", + auth_session: "123456789", + presentation: "/authorize?client_id=..&request_uri=https://rp.example.com/oidc/request/1234", + }); + }) + + it('it should successfully respond with a authorization code when authorization challenge is used', async () => { + const url = new URL(`${MOCK_URL}/authorize-challenge`) + const responseBody = { + authorization_code: 'test_value', + }; + (await client.retrieveServerMetadata()).authorization_challenge_endpoint = url.toString() + + const authSession = 'test-authSession' + const presentationDuringIssuanceSession = 'test-presentationDuringIssuanceSession' + + nock(url.origin) + .post(url.pathname, { client_id: client.clientId, auth_session: authSession, presentation_during_issuance_session: presentationDuringIssuanceSession }) + .times(1) + .reply(200, responseBody) + + const response = await client.acquireAuthorizationChallengeCode({ clientId: client.clientId, authSession, presentationDuringIssuanceSession }); + + expect(response).toBeDefined(); + expect(response.authorization_code).toBeDefined(); + }) + }); describe('should successfully handle isEbsi function', () => { it('should return true when calling isEbsi function', async () => { From 1eef34bad5cf7900155cd357ab7a9a0501ac7e34 Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Wed, 15 Jan 2025 14:39:25 +0100 Subject: [PATCH 15/26] chore: added tests --- .../client/lib/OpenID4VCIClientV1_0_11.ts | 2 + .../client/lib/OpenID4VCIClientV1_0_13.ts | 2 + .../__tests__/OpenID4VCIClientV1_0_11.spec.ts | 42 +++++++++++++++++++ .../__tests__/OpenID4VCIClientV1_0_13.spec.ts | 42 +++++++++++++++++++ .../issuer-rest/lib/oid4vci-api-functions.ts | 2 +- 5 files changed, 89 insertions(+), 1 deletion(-) diff --git a/packages/client/lib/OpenID4VCIClientV1_0_11.ts b/packages/client/lib/OpenID4VCIClientV1_0_11.ts index 151f8648..b57dfb4c 100644 --- a/packages/client/lib/OpenID4VCIClientV1_0_11.ts +++ b/packages/client/lib/OpenID4VCIClientV1_0_11.ts @@ -262,6 +262,8 @@ export class OpenID4VCIClientV1_0_11 { public async acquireAuthorizationChallengeCode(opts?: AuthorizationChallengeRequestOpts): Promise { const response = await acquireAuthorizationChallengeAuthCode({ + metadata: this.endpointMetadata, + credentialIssuer: this.getIssuer(), clientId: this._state.clientId ?? this._state.authorizationRequestOpts?.clientId, ...opts }) diff --git a/packages/client/lib/OpenID4VCIClientV1_0_13.ts b/packages/client/lib/OpenID4VCIClientV1_0_13.ts index fb0a2b10..3e7355f7 100644 --- a/packages/client/lib/OpenID4VCIClientV1_0_13.ts +++ b/packages/client/lib/OpenID4VCIClientV1_0_13.ts @@ -269,6 +269,8 @@ export class OpenID4VCIClientV1_0_13 { public async acquireAuthorizationChallengeCode(opts?: AuthorizationChallengeRequestOpts): Promise { const response = await acquireAuthorizationChallengeAuthCode({ + metadata: this.endpointMetadata, + credentialIssuer: this.getIssuer(), clientId: this._state.clientId ?? this._state.authorizationRequestOpts?.clientId, ...opts }) diff --git a/packages/client/lib/__tests__/OpenID4VCIClientV1_0_11.spec.ts b/packages/client/lib/__tests__/OpenID4VCIClientV1_0_11.spec.ts index ed89321a..3ada10e4 100644 --- a/packages/client/lib/__tests__/OpenID4VCIClientV1_0_11.spec.ts +++ b/packages/client/lib/__tests__/OpenID4VCIClientV1_0_11.spec.ts @@ -199,6 +199,48 @@ describe('OpenID4VCIClientV1_0_11 should', () => { 'https://server.example.com/v1/auth/authorize?response_type=code&code_challenge_method=S256&code_challenge=mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs&authorization_details=%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22ldp_vc%22%2C%22locations%22%3A%5B%22https%3A%2F%2Ftest%2Ecom%22%2C%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%5D%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%7D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&client_id=test-client&scope=openid', ); }); + + it('it should respond with insufficient_authorization when no sessions are provided', async () => { + const url = new URL(`${MOCK_URL}/authorize-challenge`) + const responseBody = { + error: "insufficient_authorization", + auth_session: "123456789", + presentation: "/authorize?client_id=..&request_uri=https://rp.example.com/oidc/request/1234", + }; + (await client.retrieveServerMetadata()).authorization_challenge_endpoint = url.toString() + + nock(url.origin) + .post(url.pathname, { client_id: client.clientId }) + .times(1) + .reply(400, responseBody) + + await expect(client.acquireAuthorizationChallengeCode({ clientId: client.clientId })).rejects.toEqual({ + error: "insufficient_authorization", + auth_session: "123456789", + presentation: "/authorize?client_id=..&request_uri=https://rp.example.com/oidc/request/1234", + }); + }) + + it('it should successfully respond with a authorization code when authorization challenge is used', async () => { + const url = new URL(`${MOCK_URL}/authorize-challenge`) + const responseBody = { + authorization_code: 'test_value', + }; + (await client.retrieveServerMetadata()).authorization_challenge_endpoint = url.toString() + + const authSession = 'test-authSession' + const presentationDuringIssuanceSession = 'test-presentationDuringIssuanceSession' + + nock(url.origin) + .post(url.pathname, { client_id: client.clientId, auth_session: authSession, presentation_during_issuance_session: presentationDuringIssuanceSession }) + .times(1) + .reply(200, responseBody) + + const response = await client.acquireAuthorizationChallengeCode({ clientId: client.clientId, authSession, presentationDuringIssuanceSession }); + + expect(response).toBeDefined(); + expect(response.authorization_code).toBeDefined(); + }) }); it('should return true when calling isEbsi function', async () => { diff --git a/packages/client/lib/__tests__/OpenID4VCIClientV1_0_13.spec.ts b/packages/client/lib/__tests__/OpenID4VCIClientV1_0_13.spec.ts index 9f7fbc3e..68473dfb 100644 --- a/packages/client/lib/__tests__/OpenID4VCIClientV1_0_13.spec.ts +++ b/packages/client/lib/__tests__/OpenID4VCIClientV1_0_13.spec.ts @@ -180,4 +180,46 @@ describe('OpenID4VCIClientV1_0_13 should', () => { 'https://server.example.com/v1/auth/authorize?response_type=code&code_challenge_method=S256&code_challenge=mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs&authorization_details=%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22ldp_vc%22%2C%22locations%22%3A%5B%22https%3A%2F%2Ftest%2Ecom%22%2C%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%5D%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%7D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&client_id=test-client&scope=openid', ); }); + + it('it should respond with insufficient_authorization when no sessions are provided', async () => { + const url = new URL(`${MOCK_URL}/authorize-challenge`) + const responseBody = { + error: "insufficient_authorization", + auth_session: "123456789", + presentation: "/authorize?client_id=..&request_uri=https://rp.example.com/oidc/request/1234", + }; + (await client.retrieveServerMetadata()).authorization_challenge_endpoint = url.toString() + + nock(url.origin) + .post(url.pathname, { client_id: client.clientId }) + .times(1) + .reply(400, responseBody) + + await expect(client.acquireAuthorizationChallengeCode({ clientId: client.clientId })).rejects.toEqual({ + error: "insufficient_authorization", + auth_session: "123456789", + presentation: "/authorize?client_id=..&request_uri=https://rp.example.com/oidc/request/1234", + }); + }) + + it('it should successfully respond with a authorization code when authorization challenge is used', async () => { + const url = new URL(`${MOCK_URL}/authorize-challenge`) + const responseBody = { + authorization_code: 'test_value', + }; + (await client.retrieveServerMetadata()).authorization_challenge_endpoint = url.toString() + + const authSession = 'test-authSession' + const presentationDuringIssuanceSession = 'test-presentationDuringIssuanceSession' + + nock(url.origin) + .post(url.pathname, { client_id: client.clientId, auth_session: authSession, presentation_during_issuance_session: presentationDuringIssuanceSession }) + .times(1) + .reply(200, responseBody) + + const response = await client.acquireAuthorizationChallengeCode({ clientId: client.clientId, authSession, presentationDuringIssuanceSession }); + + expect(response).toBeDefined(); + expect(response.authorization_code).toBeDefined(); + }) }); diff --git a/packages/issuer-rest/lib/oid4vci-api-functions.ts b/packages/issuer-rest/lib/oid4vci-api-functions.ts index 08db1532..f99da1c0 100644 --- a/packages/issuer-rest/lib/oid4vci-api-functions.ts +++ b/packages/issuer-rest/lib/oid4vci-api-functions.ts @@ -93,10 +93,10 @@ 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 authorizationChallengeEndpoint( router: Router, issuer: VcIssuer, From c6c5df220f9aceeb0b738506ce6da593d097e12f Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Wed, 15 Jan 2025 22:16:40 +0100 Subject: [PATCH 16/26] chore: added tests --- packages/client/lib/MetadataClient.ts | 10 + packages/client/lib/MetadataClientV1_0_11.ts | 10 + packages/client/lib/MetadataClientV1_0_13.ts | 10 + .../lib/__tests__/ClientIssuerIT.spec.ts | 5 + .../authorizationChallengeCodeServer.spec.ts | 193 ++++++++++++++++++ .../issuer-rest/lib/oid4vci-api-functions.ts | 26 ++- .../lib/types/ServerMetadata.ts | 1 + 7 files changed, 244 insertions(+), 11 deletions(-) create mode 100644 packages/issuer-rest/lib/__tests__/authorizationChallengeCodeServer.spec.ts diff --git a/packages/client/lib/MetadataClient.ts b/packages/client/lib/MetadataClient.ts index e76f0c0b..75c49b06 100644 --- a/packages/client/lib/MetadataClient.ts +++ b/packages/client/lib/MetadataClient.ts @@ -70,6 +70,7 @@ export class MetadataClient { let credential_endpoint: string | undefined; let deferred_credential_endpoint: string | undefined; let authorization_endpoint: string | undefined; + let authorization_challenge_endpoint: string | undefined; let authorizationServerType: AuthorizationServerType = 'OID4VCI'; let authorization_servers: string[] | undefined = [issuer]; let authorization_server: string | undefined = undefined; @@ -130,6 +131,14 @@ export class MetadataClient { ); } authorization_endpoint = authMetadata.authorization_endpoint; + if (!authMetadata.authorization_challenge_endpoint) { + throw Error(`Authorization Sever ${authorization_challenge_endpoint} did not provide a authorization_challenge_endpoint`); + } else if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) { + throw Error( + `Credential issuer has a different authorization_challenge_endpoint (${authorization_challenge_endpoint}) from the Authorization Server (${authMetadata.authorization_challenge_endpoint})`, + ); + } + authorization_challenge_endpoint = authMetadata.authorization_challenge_endpoint; if (!authMetadata.token_endpoint) { throw Error(`Authorization Sever ${authorization_servers} did not provide a token_endpoint`); } else if (token_endpoint && authMetadata.token_endpoint !== token_endpoint) { @@ -193,6 +202,7 @@ export class MetadataClient { deferred_credential_endpoint, ...(authorization_server ? { authorization_server } : { authorization_servers: authorization_servers }), authorization_endpoint, + authorization_challenge_endpoint, authorizationServerType, credentialIssuerMetadata: authorization_server ? (credentialIssuerMetadata as IssuerMetadataV1_0_08 & Partial) diff --git a/packages/client/lib/MetadataClientV1_0_11.ts b/packages/client/lib/MetadataClientV1_0_11.ts index ebe9a78e..c0aad97b 100644 --- a/packages/client/lib/MetadataClientV1_0_11.ts +++ b/packages/client/lib/MetadataClientV1_0_11.ts @@ -50,6 +50,7 @@ export class MetadataClientV1_0_11 { let credential_endpoint: string | undefined; let deferred_credential_endpoint: string | undefined; let authorization_endpoint: string | undefined; + let authorization_challenge_endpoint: string | undefined; let authorizationServerType: AuthorizationServerType = 'OID4VCI'; let authorization_server: string = issuer; const oid4vciResponse = await MetadataClientV1_0_11.retrieveOpenID4VCIServerMetadata(issuer, { errorOnNotFound: false }); // We will handle errors later, given we will also try other metadata locations @@ -105,6 +106,14 @@ export class MetadataClientV1_0_11 { ); } authorization_endpoint = authMetadata.authorization_endpoint; + if (!authMetadata.authorization_challenge_endpoint) { + throw Error(`Authorization Sever ${authorization_challenge_endpoint} did not provide a authorization_challenge_endpoint`); + } else if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) { + throw Error( + `Credential issuer has a different authorization_challenge_endpoint (${authorization_challenge_endpoint}) from the Authorization Server (${authMetadata.authorization_challenge_endpoint})`, + ); + } + authorization_challenge_endpoint = authMetadata.authorization_challenge_endpoint; if (!authMetadata.token_endpoint) { throw Error(`Authorization Sever ${authorization_server} did not provide a token_endpoint`); } else if (token_endpoint && authMetadata.token_endpoint !== token_endpoint) { @@ -165,6 +174,7 @@ export class MetadataClientV1_0_11 { deferred_credential_endpoint, authorization_server, authorization_endpoint, + authorization_challenge_endpoint, authorizationServerType, credentialIssuerMetadata: credentialIssuerMetadata as unknown as Partial & IssuerMetadataV1_0_08, authorizationServerMetadata: authMetadata, diff --git a/packages/client/lib/MetadataClientV1_0_13.ts b/packages/client/lib/MetadataClientV1_0_13.ts index 6318e6ec..849c572b 100644 --- a/packages/client/lib/MetadataClientV1_0_13.ts +++ b/packages/client/lib/MetadataClientV1_0_13.ts @@ -50,6 +50,7 @@ export class MetadataClientV1_0_13 { let credential_endpoint: string | undefined; let deferred_credential_endpoint: string | undefined; let authorization_endpoint: string | undefined; + let authorization_challenge_endpoint: string | undefined; let authorizationServerType: AuthorizationServerType = 'OID4VCI'; let authorization_servers: string[] = [issuer]; const oid4vciResponse = await MetadataClientV1_0_13.retrieveOpenID4VCIServerMetadata(issuer, { errorOnNotFound: false }); // We will handle errors later, given we will also try other metadata locations @@ -104,6 +105,14 @@ export class MetadataClientV1_0_13 { ); } authorization_endpoint = authMetadata.authorization_endpoint; + if (!authMetadata.authorization_challenge_endpoint) { + throw Error(`Authorization Sever ${authorization_challenge_endpoint} did not provide a authorization_challenge_endpoint`); + } else if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) { + throw Error( + `Credential issuer has a different authorization_challenge_endpoint (${authorization_challenge_endpoint}) from the Authorization Server (${authMetadata.authorization_challenge_endpoint})`, + ); + } + authorization_challenge_endpoint = authMetadata.authorization_challenge_endpoint; if (!authMetadata.token_endpoint) { throw Error(`Authorization Sever ${authorization_servers} did not provide a token_endpoint`); } else if (token_endpoint && authMetadata.token_endpoint !== token_endpoint) { @@ -164,6 +173,7 @@ export class MetadataClientV1_0_13 { deferred_credential_endpoint, authorization_server: authorization_servers[0], authorization_endpoint, + authorization_challenge_endpoint, authorizationServerType, credentialIssuerMetadata: credentialIssuerMetadata, authorizationServerMetadata: authMetadata, diff --git a/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts b/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts index b209e27b..48c92440 100644 --- a/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts +++ b/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts @@ -60,6 +60,7 @@ describe('VcIssuer', () => { .withCredentialEndpoint('http://localhost:3456/test/credential-endpoint') .withTokenEndpoint('http://localhost:3456/test/token') .withAuthorizationEndpoint('https://token-endpoint.example.com/authorize') + .withAuthorizationChallengeEndpoint('http://localhost:3456/test/authorize-challenge') .withTokenEndpointAuthMethodsSupported(['none', 'client_secret_basic', 'client_secret_jwt', 'client_secret_post']) .withResponseTypesSupported(['code', 'token', 'id_token']) .withScopesSupported(['openid', 'abcdef']) @@ -266,6 +267,7 @@ describe('VcIssuer', () => { it('should retrieve server metadata', async () => { await expect(client.retrieveServerMetadata()).resolves.toEqual({ authorizationServerMetadata: { + authorization_challenge_endpoint: 'http://localhost:3456/test/authorize-challenge', authorization_endpoint: 'https://token-endpoint.example.com/authorize', credential_endpoint: 'http://localhost:3456/test/credential-endpoint', issuer: 'http://localhost:3456/test', @@ -275,6 +277,7 @@ describe('VcIssuer', () => { token_endpoint_auth_methods_supported: ['none', 'client_secret_basic', 'client_secret_jwt', 'client_secret_post'], }, authorizationServerType: 'OID4VCI', + authorization_challenge_endpoint: 'http://localhost:3456/test/authorize-challenge', authorization_endpoint: 'https://token-endpoint.example.com/authorize', deferred_credential_endpoint: undefined, authorization_server: 'http://localhost:3456/test', @@ -316,6 +319,7 @@ describe('VcIssuer', () => { token_endpoint: 'http://localhost:3456/test/token', }) }) + it('should get state on server side', async () => { const preAuthCode = client.credentialOffer!.credential_offer.grants?.[PRE_AUTH_GRANT_LITERAL]?.[PRE_AUTH_CODE_LITERAL] expect(preAuthCode).toBeDefined() @@ -382,4 +386,5 @@ describe('VcIssuer', () => { format: 'jwt_vc_json', }) }) + }) diff --git a/packages/issuer-rest/lib/__tests__/authorizationChallengeCodeServer.spec.ts b/packages/issuer-rest/lib/__tests__/authorizationChallengeCodeServer.spec.ts new file mode 100644 index 00000000..f3b39d0b --- /dev/null +++ b/packages/issuer-rest/lib/__tests__/authorizationChallengeCodeServer.spec.ts @@ -0,0 +1,193 @@ +import { uuidv4 } from '@sphereon/oid4vc-common' +import { + AuthorizationChallengeError, + CNonceState, + CredentialIssuerMetadataOptsV1_0_13, + CredentialOfferSession, + IssueStatus +} from '@sphereon/oid4vci-common' +import { VcIssuer } from '@sphereon/oid4vci-issuer' +import { AuthorizationServerMetadataBuilder } from '@sphereon/oid4vci-issuer' +import { MemoryStates } from '@sphereon/oid4vci-issuer/dist/state-manager' +import { ExpressBuilder, ExpressSupport } from '@sphereon/ssi-express-support' +import { DIDDocument } from 'did-resolver' +import { Express } from 'express' +import requests from 'supertest' + +import { OID4VCIServer } from '../OID4VCIServer' + +const authorizationServerMetadata = new AuthorizationServerMetadataBuilder() + .withIssuer('test-issuer') + .withAuthorizationChallengeEndpoint('http://localhost:3456/test/authorize-challenge') + .withResponseTypesSupported(['code', 'token', 'id_token']) + .build() + +describe('OID4VCIServer', () => { + let app: Express + let expressSupport: ExpressSupport + const sessionId = 'c1413695-8744-4369-845b-c2bd0ee8d5e4' + + beforeAll(async () => { + const credentialOfferState1: CredentialOfferSession = { + // preAuthorizedCode: preAuthorizedCode1, + txCode: '493536', + notification_id: uuidv4(), + createdAt: +new Date(), + lastUpdatedAt: +new Date(), + status: IssueStatus.OFFER_CREATED, + credentialOffer: { + credential_offer: { + credential_issuer: 'test_issuer', + credentials: [ + { + format: 'ldp_vc', + credential_definition: { + '@context': ['test_context'], + types: ['VerifiableCredential'], + credentialSubject: {}, + }, + }, + ], + + // grants: { + // 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { + // tx_code: { + // length: 6, + // input_mode: 'numeric', + // description: 'Please enter the 6 digit code you received on your phone', + // }, + // //'pre-authorized_code': preAuthorizedCode1, + // }, + // }, + }, + }, + } + const credentialOfferSessions = new MemoryStates() + await credentialOfferSessions.set(sessionId, credentialOfferState1) + + const vcIssuer: VcIssuer = new VcIssuer( + { + credential_endpoint: 'http://localhost:9000', + authorization_challenge_endpoint: 'http://localhost:9000/authorize-challenge', + } as CredentialIssuerMetadataOptsV1_0_13, + authorizationServerMetadata, + { + cNonceExpiresIn: 300, + credentialOfferSessions, + cNonces: new MemoryStates(), + }, + ) + + expressSupport = ExpressBuilder.fromServerOpts({ + startListening: false, + port: 9000, + hostname: '0.0.0.0', + }).build({ startListening: false }) + const vcIssuerServer = new OID4VCIServer(expressSupport, { + issuer: vcIssuer, + baseUrl: 'http://localhost:9000', + endpointOpts: { + tokenEndpointOpts: { + tokenEndpointDisabled: true + }, + authorizationChallengeOpts: { + enabled: true, + verifyAuthResponseCallback: async () => true, + createAuthRequestUriCallback: async () => '/authorize?client_id=..&request_uri=https://rp.example.com/oidc/request/1234' + } + }, + }) + expressSupport.start() + app = vcIssuerServer.app + }) + + afterAll(async () => { + if (expressSupport) { + await expressSupport.stop() + } + await new Promise((resolve) => setTimeout((v: void) => resolve(v), 500)) + }) + + it('should return http code 400 with error invalid_request', async () => { + const res = await requests(app) + .post('/authorize-challenge') + .send(`client_id=${uuidv4()}`) + expect(res.statusCode).toEqual(400) + const actual = JSON.parse(res.text) + expect(actual).toEqual({ + error: AuthorizationChallengeError.invalid_request + }) + }) + + it('should return http code 400 with message No client id or auth session present', async () => { + const res = await requests(app) + .post('/authorize-challenge') + .send() + expect(res.statusCode).toEqual(400) + const actual = JSON.parse(res.text) + expect(actual).toEqual({ + error: AuthorizationChallengeError.invalid_request, + error_description: 'No client id or auth session present' + }) + }) + + it('should return http code 400 with message Session is invalid with invalid issuer_state', async () => { + const res = await requests(app) + .post('/authorize-challenge') + .send(`client_id=${uuidv4()}&issuer_state=${uuidv4()}`) + expect(res.statusCode).toEqual(400) + const actual = JSON.parse(res.text) + expect(actual).toEqual({ + error: AuthorizationChallengeError.invalid_session, + error_description: 'Session is invalid' + }) + }) + + it('should return http code 400 with message No definition id present', async () => { + const res = await requests(app) + .post('/authorize-challenge') + .send(`client_id=${uuidv4()}&issuer_state=${sessionId}`) + expect(res.statusCode).toEqual(400) + const actual = JSON.parse(res.text) + expect(actual).toEqual({ + error: AuthorizationChallengeError.invalid_request, + error_description: 'No definition id present' + }) + }) + + it('should return http code 400 with error insufficient_authorization', async () => { + const res = await requests(app) + .post('/authorize-challenge') + .send(`client_id=${uuidv4()}&issuer_state=${sessionId}&definition_id=${'testValue'}`) + expect(res.statusCode).toEqual(400) + const actual = JSON.parse(res.text) + expect(actual).toEqual({ + error: AuthorizationChallengeError.insufficient_authorization, + auth_session: "c1413695-8744-4369-845b-c2bd0ee8d5e4", + presentation: "/authorize?client_id=..&request_uri=https://rp.example.com/oidc/request/1234" + }) + }) + + it('should return http code 400 with message Session is invalid with invalid auth_session', async () => { + const res = await requests(app) + .post('/authorize-challenge') + .send(`auth_session=${uuidv4()}&presentation_during_issuance_session=${uuidv4()}&definition_id=testDefinitionId`) + expect(res.statusCode).toEqual(400) + const actual = JSON.parse(res.text) + expect(actual).toEqual({ + error: AuthorizationChallengeError.invalid_session, + error_description: 'Session is invalid' + }) + }) + + it('should return http code 200 with authorization_code', async () => { + const res = await requests(app) + .post('/authorize-challenge') + .send(`auth_session=${sessionId}&presentation_during_issuance_session=${uuidv4()}&definition_id=testDefinitionId`) + expect(res.statusCode).toEqual(200) + const actual = JSON.parse(res.text) + expect(actual).toBeDefined() + expect(actual.authorization_code).toBeDefined() + }) + +}) diff --git a/packages/issuer-rest/lib/oid4vci-api-functions.ts b/packages/issuer-rest/lib/oid4vci-api-functions.ts index f99da1c0..1a9b757f 100644 --- a/packages/issuer-rest/lib/oid4vci-api-functions.ts +++ b/packages/issuer-rest/lib/oid4vci-api-functions.ts @@ -123,25 +123,28 @@ export function authorizationChallengeEndpoint( try { if (!client_id && !auth_session) { const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = { - error: AuthorizationChallengeError.invalid_request - } - return Promise.reject(authorizationChallengeErrorResponse) + error: AuthorizationChallengeError.invalid_request, + error_description: 'No client id or auth session present' + } as AuthorizationChallengeErrorResponse + throw authorizationChallengeErrorResponse } if (!auth_session && issuer_state) { const session = await issuer.credentialOfferSessions.get(issuer_state) if (!session) { const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = { - error: AuthorizationChallengeError.invalid_session + error: AuthorizationChallengeError.invalid_session, + error_description: 'Session is invalid' } - return Promise.reject(authorizationChallengeErrorResponse) + throw authorizationChallengeErrorResponse } if (!definition_id) { const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = { - error: AuthorizationChallengeError.invalid_request + error: AuthorizationChallengeError.invalid_request, + error_description: 'No definition id present' } - return Promise.reject(authorizationChallengeErrorResponse) + throw authorizationChallengeErrorResponse } const authRequestURI = await opts.createAuthRequestUriCallback(definition_id, issuer_state) @@ -150,16 +153,17 @@ export function authorizationChallengeEndpoint( auth_session: issuer_state, presentation: authRequestURI } - return Promise.reject(authorizationChallengeErrorResponse) + throw authorizationChallengeErrorResponse } if (auth_session && presentation_during_issuance_session && definition_id) { const session = await issuer.credentialOfferSessions.get(auth_session) if (!session) { const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = { - error: AuthorizationChallengeError.invalid_session + error: AuthorizationChallengeError.invalid_session, + error_description: 'Session is invalid' } - return Promise.reject(authorizationChallengeErrorResponse) + throw authorizationChallengeErrorResponse } const verifiedResponse = await opts.verifyAuthResponseCallback(definition_id, presentation_during_issuance_session) @@ -177,7 +181,7 @@ export function authorizationChallengeEndpoint( const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = { error: AuthorizationChallengeError.invalid_request } - return Promise.reject(authorizationChallengeErrorResponse) + throw authorizationChallengeErrorResponse } catch (e) { return sendErrorResponse( response, diff --git a/packages/oid4vci-common/lib/types/ServerMetadata.ts b/packages/oid4vci-common/lib/types/ServerMetadata.ts index 637ac50d..38d8cdaa 100644 --- a/packages/oid4vci-common/lib/types/ServerMetadata.ts +++ b/packages/oid4vci-common/lib/types/ServerMetadata.ts @@ -110,6 +110,7 @@ export interface AuthorizationServerMetadata extends DynamicRegistrationClientMe export const authorizationServerMetadataFieldNames: Array = [ 'issuer', 'authorization_endpoint', + 'authorization_challenge_endpoint', 'token_endpoint', 'jwks_uri', 'registration_endpoint', From a8f712d669a211a06b69ddfbd700dfeee94f4af2 Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Wed, 15 Jan 2025 22:25:51 +0100 Subject: [PATCH 17/26] chore: fixes --- packages/client/lib/MetadataClient.ts | 4 ++-- packages/client/lib/MetadataClientV1_0_11.ts | 4 ++-- packages/client/lib/MetadataClientV1_0_13.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/client/lib/MetadataClient.ts b/packages/client/lib/MetadataClient.ts index 75c49b06..98900d65 100644 --- a/packages/client/lib/MetadataClient.ts +++ b/packages/client/lib/MetadataClient.ts @@ -132,7 +132,7 @@ export class MetadataClient { } authorization_endpoint = authMetadata.authorization_endpoint; if (!authMetadata.authorization_challenge_endpoint) { - throw Error(`Authorization Sever ${authorization_challenge_endpoint} did not provide a authorization_challenge_endpoint`); + console.warn(`Authorization Server ${authorization_challenge_endpoint} did not provide a authorization_challenge_endpoint`); } else if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) { throw Error( `Credential issuer has a different authorization_challenge_endpoint (${authorization_challenge_endpoint}) from the Authorization Server (${authMetadata.authorization_challenge_endpoint})`, @@ -140,7 +140,7 @@ export class MetadataClient { } authorization_challenge_endpoint = authMetadata.authorization_challenge_endpoint; if (!authMetadata.token_endpoint) { - throw Error(`Authorization Sever ${authorization_servers} did not provide a token_endpoint`); + throw Error(`Authorization Server ${authorization_servers} did not provide a token_endpoint`); } else if (token_endpoint && authMetadata.token_endpoint !== token_endpoint) { throw Error( `Credential issuer has a different token_endpoint (${token_endpoint}) from the Authorization Server (${authMetadata.token_endpoint})`, diff --git a/packages/client/lib/MetadataClientV1_0_11.ts b/packages/client/lib/MetadataClientV1_0_11.ts index c0aad97b..d91e5d3d 100644 --- a/packages/client/lib/MetadataClientV1_0_11.ts +++ b/packages/client/lib/MetadataClientV1_0_11.ts @@ -107,7 +107,7 @@ export class MetadataClientV1_0_11 { } authorization_endpoint = authMetadata.authorization_endpoint; if (!authMetadata.authorization_challenge_endpoint) { - throw Error(`Authorization Sever ${authorization_challenge_endpoint} did not provide a authorization_challenge_endpoint`); + console.warn(`Authorization Server ${authorization_challenge_endpoint} did not provide a authorization_challenge_endpoint`); } else if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) { throw Error( `Credential issuer has a different authorization_challenge_endpoint (${authorization_challenge_endpoint}) from the Authorization Server (${authMetadata.authorization_challenge_endpoint})`, @@ -115,7 +115,7 @@ export class MetadataClientV1_0_11 { } authorization_challenge_endpoint = authMetadata.authorization_challenge_endpoint; if (!authMetadata.token_endpoint) { - throw Error(`Authorization Sever ${authorization_server} did not provide a token_endpoint`); + throw Error(`Authorization Server ${authorization_server} did not provide a token_endpoint`); } else if (token_endpoint && authMetadata.token_endpoint !== token_endpoint) { throw Error( `Credential issuer has a different token_endpoint (${token_endpoint}) from the Authorization Server (${authMetadata.token_endpoint})`, diff --git a/packages/client/lib/MetadataClientV1_0_13.ts b/packages/client/lib/MetadataClientV1_0_13.ts index 849c572b..f7d5039a 100644 --- a/packages/client/lib/MetadataClientV1_0_13.ts +++ b/packages/client/lib/MetadataClientV1_0_13.ts @@ -106,7 +106,7 @@ export class MetadataClientV1_0_13 { } authorization_endpoint = authMetadata.authorization_endpoint; if (!authMetadata.authorization_challenge_endpoint) { - throw Error(`Authorization Sever ${authorization_challenge_endpoint} did not provide a authorization_challenge_endpoint`); + console.warn(`Authorization Server ${authorization_challenge_endpoint} did not provide a authorization_challenge_endpoint`); } else if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) { throw Error( `Credential issuer has a different authorization_challenge_endpoint (${authorization_challenge_endpoint}) from the Authorization Server (${authMetadata.authorization_challenge_endpoint})`, @@ -114,7 +114,7 @@ export class MetadataClientV1_0_13 { } authorization_challenge_endpoint = authMetadata.authorization_challenge_endpoint; if (!authMetadata.token_endpoint) { - throw Error(`Authorization Sever ${authorization_servers} did not provide a token_endpoint`); + throw Error(`Authorization Server ${authorization_servers} did not provide a token_endpoint`); } else if (token_endpoint && authMetadata.token_endpoint !== token_endpoint) { throw Error( `Credential issuer has a different token_endpoint (${token_endpoint}) from the Authorization Server (${authMetadata.token_endpoint})`, From 4a7480226db23c5ed0401ddec484b777c269bf88 Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Wed, 15 Jan 2025 22:36:24 +0100 Subject: [PATCH 18/26] chore: fix test --- packages/client/lib/__tests__/MetadataClient.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/lib/__tests__/MetadataClient.spec.ts b/packages/client/lib/__tests__/MetadataClient.spec.ts index e5be8b16..4fe87a2a 100644 --- a/packages/client/lib/__tests__/MetadataClient.spec.ts +++ b/packages/client/lib/__tests__/MetadataClient.spec.ts @@ -95,7 +95,7 @@ describe('MetadataClient with IdentiProof Issuer should', () => { nock(IDENTIPROOF_AS_URL).get(WellKnownEndpoints.OPENID_CONFIGURATION).reply(404); await expect(() => MetadataClient.retrieveAllMetadata(IDENTIPROOF_ISSUER_URL, { errorOnNotFound: true })).rejects.toThrowError( - 'Authorization Sever https://auth.research.identiproof.io did not provide a token_endpoint', + 'Authorization Server https://auth.research.identiproof.io did not provide a token_endpoint', ); }); From 92430bce71d1bf3f14d14bf3764ba21532350665 Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Wed, 15 Jan 2025 22:48:26 +0100 Subject: [PATCH 19/26] chore: cleanup --- .../authorizationChallengeCodeServer.spec.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/packages/issuer-rest/lib/__tests__/authorizationChallengeCodeServer.spec.ts b/packages/issuer-rest/lib/__tests__/authorizationChallengeCodeServer.spec.ts index f3b39d0b..b106dcdc 100644 --- a/packages/issuer-rest/lib/__tests__/authorizationChallengeCodeServer.spec.ts +++ b/packages/issuer-rest/lib/__tests__/authorizationChallengeCodeServer.spec.ts @@ -29,7 +29,6 @@ describe('OID4VCIServer', () => { beforeAll(async () => { const credentialOfferState1: CredentialOfferSession = { - // preAuthorizedCode: preAuthorizedCode1, txCode: '493536', notification_id: uuidv4(), createdAt: +new Date(), @@ -47,18 +46,7 @@ describe('OID4VCIServer', () => { credentialSubject: {}, }, }, - ], - - // grants: { - // 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { - // tx_code: { - // length: 6, - // input_mode: 'numeric', - // description: 'Please enter the 6 digit code you received on your phone', - // }, - // //'pre-authorized_code': preAuthorizedCode1, - // }, - // }, + ] }, }, } From 9b61ea68f72769bb114c3de8c992bfc61c070936 Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Fri, 17 Jan 2025 11:26:13 +0100 Subject: [PATCH 20/26] chore: for first party flow use presentation id from issuer options --- packages/client/lib/AuthorizationCodeClient.ts | 2 -- packages/client/lib/MetadataClient.ts | 2 +- packages/client/lib/MetadataClientV1_0_11.ts | 2 +- packages/client/lib/MetadataClientV1_0_13.ts | 2 +- packages/issuer-rest/lib/OID4VCIServer.ts | 4 ++-- .../authorizationChallengeCodeServer.spec.ts | 18 +++--------------- .../issuer-rest/lib/oid4vci-api-functions.ts | 17 ++++------------- .../lib/types/Authorization.types.ts | 5 ----- 8 files changed, 12 insertions(+), 40 deletions(-) diff --git a/packages/client/lib/AuthorizationCodeClient.ts b/packages/client/lib/AuthorizationCodeClient.ts index 0295b8a3..4510141f 100644 --- a/packages/client/lib/AuthorizationCodeClient.ts +++ b/packages/client/lib/AuthorizationCodeClient.ts @@ -332,7 +332,6 @@ export const createAuthorizationChallengeRequest = async (opts: AuthorizationCha issuerState, authSession, scope, - definitionId, codeChallenge, codeChallengeMethod, presentationDuringIssuanceSession @@ -345,7 +344,6 @@ export const createAuthorizationChallengeRequest = async (opts: AuthorizationCha scope, code_challenge: codeChallenge, code_challenge_method: codeChallengeMethod, - definition_id: definitionId, presentation_during_issuance_session: presentationDuringIssuanceSession } diff --git a/packages/client/lib/MetadataClient.ts b/packages/client/lib/MetadataClient.ts index 98900d65..283f68b3 100644 --- a/packages/client/lib/MetadataClient.ts +++ b/packages/client/lib/MetadataClient.ts @@ -132,7 +132,7 @@ export class MetadataClient { } authorization_endpoint = authMetadata.authorization_endpoint; if (!authMetadata.authorization_challenge_endpoint) { - console.warn(`Authorization Server ${authorization_challenge_endpoint} did not provide a authorization_challenge_endpoint`); + debug(`Authorization Server ${authorization_servers} did not provide a authorization_challenge_endpoint`); } else if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) { throw Error( `Credential issuer has a different authorization_challenge_endpoint (${authorization_challenge_endpoint}) from the Authorization Server (${authMetadata.authorization_challenge_endpoint})`, diff --git a/packages/client/lib/MetadataClientV1_0_11.ts b/packages/client/lib/MetadataClientV1_0_11.ts index d91e5d3d..181028f6 100644 --- a/packages/client/lib/MetadataClientV1_0_11.ts +++ b/packages/client/lib/MetadataClientV1_0_11.ts @@ -107,7 +107,7 @@ export class MetadataClientV1_0_11 { } authorization_endpoint = authMetadata.authorization_endpoint; if (!authMetadata.authorization_challenge_endpoint) { - console.warn(`Authorization Server ${authorization_challenge_endpoint} did not provide a authorization_challenge_endpoint`); + debug(`Authorization Server ${authorization_server} did not provide a authorization_challenge_endpoint`); } else if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) { throw Error( `Credential issuer has a different authorization_challenge_endpoint (${authorization_challenge_endpoint}) from the Authorization Server (${authMetadata.authorization_challenge_endpoint})`, diff --git a/packages/client/lib/MetadataClientV1_0_13.ts b/packages/client/lib/MetadataClientV1_0_13.ts index f7d5039a..9f1e4d83 100644 --- a/packages/client/lib/MetadataClientV1_0_13.ts +++ b/packages/client/lib/MetadataClientV1_0_13.ts @@ -106,7 +106,7 @@ export class MetadataClientV1_0_13 { } authorization_endpoint = authMetadata.authorization_endpoint; if (!authMetadata.authorization_challenge_endpoint) { - console.warn(`Authorization Server ${authorization_challenge_endpoint} did not provide a authorization_challenge_endpoint`); + debug(`Authorization Server ${authorization_servers} did not provide a authorization_challenge_endpoint`); } else if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) { throw Error( `Credential issuer has a different authorization_challenge_endpoint (${authorization_challenge_endpoint}) from the Authorization Server (${authMetadata.authorization_challenge_endpoint})`, diff --git a/packages/issuer-rest/lib/OID4VCIServer.ts b/packages/issuer-rest/lib/OID4VCIServer.ts index 54aa16eb..0ea04021 100644 --- a/packages/issuer-rest/lib/OID4VCIServer.ts +++ b/packages/issuer-rest/lib/OID4VCIServer.ts @@ -93,12 +93,12 @@ export interface IAuthorizationChallengeEndpointOpts extends ISingleEndpointOpts * Added an optional state parameter so that when direct calls are used, * one could set the state value of the RP session to match the state value of the VCI session. */ - createAuthRequestUriCallback: (presentationDefinitionId: string, state?: string) => Promise + createAuthRequestUriCallback: (state?: string) => Promise /** * Callback used for verifying the status of the authorization response. * This is checked by the issuer before issuing an authorization code. */ - verifyAuthResponseCallback: (presentationDefinitionId: string, correlationId: string) => Promise + verifyAuthResponseCallback: (correlationId: string) => Promise } export interface IOID4VCIServerOpts extends HasEndpointOpts { diff --git a/packages/issuer-rest/lib/__tests__/authorizationChallengeCodeServer.spec.ts b/packages/issuer-rest/lib/__tests__/authorizationChallengeCodeServer.spec.ts index b106dcdc..5e1beed8 100644 --- a/packages/issuer-rest/lib/__tests__/authorizationChallengeCodeServer.spec.ts +++ b/packages/issuer-rest/lib/__tests__/authorizationChallengeCodeServer.spec.ts @@ -131,22 +131,10 @@ describe('OID4VCIServer', () => { }) }) - it('should return http code 400 with message No definition id present', async () => { - const res = await requests(app) - .post('/authorize-challenge') - .send(`client_id=${uuidv4()}&issuer_state=${sessionId}`) - expect(res.statusCode).toEqual(400) - const actual = JSON.parse(res.text) - expect(actual).toEqual({ - error: AuthorizationChallengeError.invalid_request, - error_description: 'No definition id present' - }) - }) - it('should return http code 400 with error insufficient_authorization', async () => { const res = await requests(app) .post('/authorize-challenge') - .send(`client_id=${uuidv4()}&issuer_state=${sessionId}&definition_id=${'testValue'}`) + .send(`client_id=${uuidv4()}&issuer_state=${sessionId}`) expect(res.statusCode).toEqual(400) const actual = JSON.parse(res.text) expect(actual).toEqual({ @@ -159,7 +147,7 @@ describe('OID4VCIServer', () => { it('should return http code 400 with message Session is invalid with invalid auth_session', async () => { const res = await requests(app) .post('/authorize-challenge') - .send(`auth_session=${uuidv4()}&presentation_during_issuance_session=${uuidv4()}&definition_id=testDefinitionId`) + .send(`auth_session=${uuidv4()}&presentation_during_issuance_session=${uuidv4()}`) expect(res.statusCode).toEqual(400) const actual = JSON.parse(res.text) expect(actual).toEqual({ @@ -171,7 +159,7 @@ describe('OID4VCIServer', () => { it('should return http code 200 with authorization_code', async () => { const res = await requests(app) .post('/authorize-challenge') - .send(`auth_session=${sessionId}&presentation_during_issuance_session=${uuidv4()}&definition_id=testDefinitionId`) + .send(`auth_session=${sessionId}&presentation_during_issuance_session=${uuidv4()}`) expect(res.statusCode).toEqual(200) const actual = JSON.parse(res.text) expect(actual).toBeDefined() diff --git a/packages/issuer-rest/lib/oid4vci-api-functions.ts b/packages/issuer-rest/lib/oid4vci-api-functions.ts index 1a9b757f..14f387f3 100644 --- a/packages/issuer-rest/lib/oid4vci-api-functions.ts +++ b/packages/issuer-rest/lib/oid4vci-api-functions.ts @@ -116,8 +116,7 @@ export function authorizationChallengeEndpoint( client_id, issuer_state, auth_session, - presentation_during_issuance_session, - definition_id + presentation_during_issuance_session } = authorizationChallengeRequest try { @@ -139,15 +138,7 @@ export function authorizationChallengeEndpoint( throw authorizationChallengeErrorResponse } - if (!definition_id) { - const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = { - error: AuthorizationChallengeError.invalid_request, - error_description: 'No definition id present' - } - throw authorizationChallengeErrorResponse - } - - const authRequestURI = await opts.createAuthRequestUriCallback(definition_id, issuer_state) + const authRequestURI = await opts.createAuthRequestUriCallback(issuer_state) const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = { error: AuthorizationChallengeError.insufficient_authorization, auth_session: issuer_state, @@ -156,7 +147,7 @@ export function authorizationChallengeEndpoint( throw authorizationChallengeErrorResponse } - if (auth_session && presentation_during_issuance_session && definition_id) { + if (auth_session && presentation_during_issuance_session) { const session = await issuer.credentialOfferSessions.get(auth_session) if (!session) { const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = { @@ -166,7 +157,7 @@ export function authorizationChallengeEndpoint( throw authorizationChallengeErrorResponse } - const verifiedResponse = await opts.verifyAuthResponseCallback(definition_id, presentation_during_issuance_session) + const verifiedResponse = await opts.verifyAuthResponseCallback(presentation_during_issuance_session) if (verifiedResponse) { const authorizationCode = generateRandomString(16, 'base64url') session.authorizationCode = authorizationCode diff --git a/packages/oid4vci-common/lib/types/Authorization.types.ts b/packages/oid4vci-common/lib/types/Authorization.types.ts index cdb0997e..6695e319 100644 --- a/packages/oid4vci-common/lib/types/Authorization.types.ts +++ b/packages/oid4vci-common/lib/types/Authorization.types.ts @@ -108,10 +108,6 @@ export interface CommonAuthorizationChallengeRequest { * OPTIONAL. value must be set either to "S256" or a value defined by a cryptographically secure */ code_challenge_method?: CodeChallengeMethod; // TODO what we do with this - /** - * OPTIONAL. A presentation definition id used to create the authorization request uri that will be used to authorize with the RP - */ - definition_id?: string /** * OPTIONAL. String containing information about the session when credential presentation is happening during issuance of another * credential. The content of this parameter is opaque to the wallet. When this parameter is present the Wallet MUST use this parameter in @@ -129,7 +125,6 @@ export interface AuthorizationChallengeRequestOpts { scope?: string codeChallenge?: string codeChallengeMethod?: CodeChallengeMethod - definitionId?: string presentationDuringIssuanceSession?: string; metadata?: EndpointMetadata; credentialIssuer?: string; From 1989813b4455b97ee6c3d5c74d366a76e24fcaf6 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 17 Jan 2025 13:55:59 +0100 Subject: [PATCH 21/26] chore: extract IEndpointOpts for agent project --- packages/issuer-rest/lib/OID4VCIServer.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/issuer-rest/lib/OID4VCIServer.ts b/packages/issuer-rest/lib/OID4VCIServer.ts index 0ea04021..2360bebc 100644 --- a/packages/issuer-rest/lib/OID4VCIServer.ts +++ b/packages/issuer-rest/lib/OID4VCIServer.ts @@ -101,16 +101,18 @@ export interface IAuthorizationChallengeEndpointOpts extends ISingleEndpointOpts verifyAuthResponseCallback: (correlationId: string) => Promise } +export interface IEndpointOpts { + tokenEndpointOpts?: ITokenEndpointOpts + notificationOpts?: ISingleEndpointOpts + createCredentialOfferOpts?: ICreateCredentialOfferEndpointOpts + getCredentialOfferOpts?: IGetCredentialOfferEndpointOpts + getStatusOpts?: IGetIssueStatusEndpointOpts + parOpts?: ISingleEndpointOpts + authorizationChallengeOpts?: IAuthorizationChallengeEndpointOpts +} + export interface IOID4VCIServerOpts extends HasEndpointOpts { - endpointOpts?: { - tokenEndpointOpts?: ITokenEndpointOpts - notificationOpts?: ISingleEndpointOpts - createCredentialOfferOpts?: ICreateCredentialOfferEndpointOpts - getCredentialOfferOpts?: IGetCredentialOfferEndpointOpts - getStatusOpts?: IGetIssueStatusEndpointOpts - parOpts?: ISingleEndpointOpts - authorizationChallengeOpts?: IAuthorizationChallengeEndpointOpts - } + endpointOpts?: IEndpointOpts baseUrl?: string } From 908b2e780b29eb25fef4b86dc0ac51ea5db09cc4 Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Tue, 21 Jan 2025 10:26:07 +0100 Subject: [PATCH 22/26] chore: use auth server endpoint when available --- packages/issuer-rest/lib/oid4vci-api-functions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/issuer-rest/lib/oid4vci-api-functions.ts b/packages/issuer-rest/lib/oid4vci-api-functions.ts index 14f387f3..12989777 100644 --- a/packages/issuer-rest/lib/oid4vci-api-functions.ts +++ b/packages/issuer-rest/lib/oid4vci-api-functions.ts @@ -102,7 +102,7 @@ export function authorizationChallengeEndpoint( issuer: VcIssuer, opts: IAuthorizationChallengeEndpointOpts & { baseUrl: string | URL }, ) { - const endpoint = issuer.issuerMetadata.authorization_challenge_endpoint + const endpoint = issuer.authorizationServerMetadata.authorization_challenge_endpoint ?? issuer.issuerMetadata.authorization_challenge_endpoint const baseUrl = getBaseUrl(opts.baseUrl) if (!endpoint) { LOG.warning('authorization challenge endpoint disabled as no "authorization_challenge_endpoint" has been configured in issuer metadata') From 5c4b66ebcf2f25315a5eec659b515f178759724d Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Wed, 22 Jan 2025 13:20:14 +0100 Subject: [PATCH 23/26] chore: addressing PR comments --- packages/client/lib/MetadataClient.ts | 5 ++--- packages/client/lib/MetadataClientV1_0_11.ts | 5 ++--- packages/client/lib/MetadataClientV1_0_13.ts | 5 ++--- packages/client/lib/__tests__/OpenID4VCIClient.spec.ts | 4 ++-- .../client/lib/__tests__/OpenID4VCIClientV1_0_11.spec.ts | 4 ++-- .../client/lib/__tests__/OpenID4VCIClientV1_0_13.spec.ts | 4 ++-- packages/issuer-rest/lib/OID4VCIServer.ts | 4 ++-- packages/issuer-rest/lib/oid4vci-api-functions.ts | 6 +++--- 8 files changed, 17 insertions(+), 20 deletions(-) diff --git a/packages/client/lib/MetadataClient.ts b/packages/client/lib/MetadataClient.ts index 283f68b3..457e7c1c 100644 --- a/packages/client/lib/MetadataClient.ts +++ b/packages/client/lib/MetadataClient.ts @@ -85,6 +85,7 @@ export class MetadataClient { if (credentialIssuerMetadata.token_endpoint) { token_endpoint = credentialIssuerMetadata.token_endpoint; } + authorization_challenge_endpoint = credentialIssuerMetadata.authorization_challenge_endpoint if (credentialIssuerMetadata.authorization_servers) { authorization_servers = credentialIssuerMetadata.authorization_servers as string[]; } else if (credentialIssuerMetadata.authorization_server) { @@ -131,9 +132,7 @@ export class MetadataClient { ); } authorization_endpoint = authMetadata.authorization_endpoint; - if (!authMetadata.authorization_challenge_endpoint) { - debug(`Authorization Server ${authorization_servers} did not provide a authorization_challenge_endpoint`); - } else if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) { + if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) { throw Error( `Credential issuer has a different authorization_challenge_endpoint (${authorization_challenge_endpoint}) from the Authorization Server (${authMetadata.authorization_challenge_endpoint})`, ); diff --git a/packages/client/lib/MetadataClientV1_0_11.ts b/packages/client/lib/MetadataClientV1_0_11.ts index 181028f6..3e18ac8e 100644 --- a/packages/client/lib/MetadataClientV1_0_11.ts +++ b/packages/client/lib/MetadataClientV1_0_11.ts @@ -62,6 +62,7 @@ export class MetadataClientV1_0_11 { if (credentialIssuerMetadata.token_endpoint) { token_endpoint = credentialIssuerMetadata.token_endpoint; } + authorization_challenge_endpoint = credentialIssuerMetadata.authorization_challenge_endpoint if (credentialIssuerMetadata.authorization_server) { authorization_server = credentialIssuerMetadata.authorization_server; } @@ -106,9 +107,7 @@ export class MetadataClientV1_0_11 { ); } authorization_endpoint = authMetadata.authorization_endpoint; - if (!authMetadata.authorization_challenge_endpoint) { - debug(`Authorization Server ${authorization_server} did not provide a authorization_challenge_endpoint`); - } else if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) { + if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) { throw Error( `Credential issuer has a different authorization_challenge_endpoint (${authorization_challenge_endpoint}) from the Authorization Server (${authMetadata.authorization_challenge_endpoint})`, ); diff --git a/packages/client/lib/MetadataClientV1_0_13.ts b/packages/client/lib/MetadataClientV1_0_13.ts index 9f1e4d83..d9f11100 100644 --- a/packages/client/lib/MetadataClientV1_0_13.ts +++ b/packages/client/lib/MetadataClientV1_0_13.ts @@ -62,6 +62,7 @@ export class MetadataClientV1_0_13 { if (credentialIssuerMetadata.token_endpoint) { token_endpoint = credentialIssuerMetadata.token_endpoint; } + authorization_challenge_endpoint = credentialIssuerMetadata.authorization_challenge_endpoint if (credentialIssuerMetadata.authorization_servers) { authorization_servers = credentialIssuerMetadata.authorization_servers; } @@ -105,9 +106,7 @@ export class MetadataClientV1_0_13 { ); } authorization_endpoint = authMetadata.authorization_endpoint; - if (!authMetadata.authorization_challenge_endpoint) { - debug(`Authorization Server ${authorization_servers} did not provide a authorization_challenge_endpoint`); - } else if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) { + if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) { throw Error( `Credential issuer has a different authorization_challenge_endpoint (${authorization_challenge_endpoint}) from the Authorization Server (${authMetadata.authorization_challenge_endpoint})`, ); diff --git a/packages/client/lib/__tests__/OpenID4VCIClient.spec.ts b/packages/client/lib/__tests__/OpenID4VCIClient.spec.ts index 6383cffc..a41d7ddc 100644 --- a/packages/client/lib/__tests__/OpenID4VCIClient.spec.ts +++ b/packages/client/lib/__tests__/OpenID4VCIClient.spec.ts @@ -213,7 +213,7 @@ describe('OpenID4VCIClient should', () => { it('it should successfully respond with a authorization code when authorization challenge is used', async () => { const url = new URL(`${MOCK_URL}/authorize-challenge`) const responseBody = { - authorization_code: 'test_value', + authorization_code: 'test_authorization_code', }; (await client.retrieveServerMetadata()).authorization_challenge_endpoint = url.toString() @@ -228,7 +228,7 @@ describe('OpenID4VCIClient should', () => { const response = await client.acquireAuthorizationChallengeCode({ clientId: client.clientId, authSession, presentationDuringIssuanceSession }); expect(response).toBeDefined(); - expect(response.authorization_code).toBeDefined(); + expect(response.authorization_code).toEqual(responseBody.authorization_code); }) }); diff --git a/packages/client/lib/__tests__/OpenID4VCIClientV1_0_11.spec.ts b/packages/client/lib/__tests__/OpenID4VCIClientV1_0_11.spec.ts index 3ada10e4..7c4832d4 100644 --- a/packages/client/lib/__tests__/OpenID4VCIClientV1_0_11.spec.ts +++ b/packages/client/lib/__tests__/OpenID4VCIClientV1_0_11.spec.ts @@ -224,7 +224,7 @@ describe('OpenID4VCIClientV1_0_11 should', () => { it('it should successfully respond with a authorization code when authorization challenge is used', async () => { const url = new URL(`${MOCK_URL}/authorize-challenge`) const responseBody = { - authorization_code: 'test_value', + authorization_code: 'test_authorization_code', }; (await client.retrieveServerMetadata()).authorization_challenge_endpoint = url.toString() @@ -239,7 +239,7 @@ describe('OpenID4VCIClientV1_0_11 should', () => { const response = await client.acquireAuthorizationChallengeCode({ clientId: client.clientId, authSession, presentationDuringIssuanceSession }); expect(response).toBeDefined(); - expect(response.authorization_code).toBeDefined(); + expect(response.authorization_code).toEqual(responseBody.authorization_code); }) }); diff --git a/packages/client/lib/__tests__/OpenID4VCIClientV1_0_13.spec.ts b/packages/client/lib/__tests__/OpenID4VCIClientV1_0_13.spec.ts index 68473dfb..c868a1c4 100644 --- a/packages/client/lib/__tests__/OpenID4VCIClientV1_0_13.spec.ts +++ b/packages/client/lib/__tests__/OpenID4VCIClientV1_0_13.spec.ts @@ -205,7 +205,7 @@ describe('OpenID4VCIClientV1_0_13 should', () => { it('it should successfully respond with a authorization code when authorization challenge is used', async () => { const url = new URL(`${MOCK_URL}/authorize-challenge`) const responseBody = { - authorization_code: 'test_value', + authorization_code: 'test_authorization_code', }; (await client.retrieveServerMetadata()).authorization_challenge_endpoint = url.toString() @@ -220,6 +220,6 @@ describe('OpenID4VCIClientV1_0_13 should', () => { const response = await client.acquireAuthorizationChallengeCode({ clientId: client.clientId, authSession, presentationDuringIssuanceSession }); expect(response).toBeDefined(); - expect(response.authorization_code).toBeDefined(); + expect(response.authorization_code).toEqual(responseBody.authorization_code); }) }); diff --git a/packages/issuer-rest/lib/OID4VCIServer.ts b/packages/issuer-rest/lib/OID4VCIServer.ts index 2360bebc..0d2e19c6 100644 --- a/packages/issuer-rest/lib/OID4VCIServer.ts +++ b/packages/issuer-rest/lib/OID4VCIServer.ts @@ -101,7 +101,7 @@ export interface IAuthorizationChallengeEndpointOpts extends ISingleEndpointOpts verifyAuthResponseCallback: (correlationId: string) => Promise } -export interface IEndpointOpts { +export interface IOID4VCIEndpointOpts { tokenEndpointOpts?: ITokenEndpointOpts notificationOpts?: ISingleEndpointOpts createCredentialOfferOpts?: ICreateCredentialOfferEndpointOpts @@ -112,7 +112,7 @@ export interface IEndpointOpts { } export interface IOID4VCIServerOpts extends HasEndpointOpts { - endpointOpts?: IEndpointOpts + endpointOpts?: IOID4VCIEndpointOpts baseUrl?: string } diff --git a/packages/issuer-rest/lib/oid4vci-api-functions.ts b/packages/issuer-rest/lib/oid4vci-api-functions.ts index 12989777..e4476bc5 100644 --- a/packages/issuer-rest/lib/oid4vci-api-functions.ts +++ b/packages/issuer-rest/lib/oid4vci-api-functions.ts @@ -105,7 +105,7 @@ export function authorizationChallengeEndpoint( const endpoint = issuer.authorizationServerMetadata.authorization_challenge_endpoint ?? issuer.issuerMetadata.authorization_challenge_endpoint const baseUrl = getBaseUrl(opts.baseUrl) if (!endpoint) { - LOG.warning('authorization challenge endpoint disabled as no "authorization_challenge_endpoint" has been configured in issuer metadata') + LOG.info('authorization challenge endpoint disabled as no "authorization_challenge_endpoint" has been configured in issuer metadata') return } const path = determinePath(baseUrl, endpoint, { stripBasePath: true }) @@ -138,7 +138,7 @@ export function authorizationChallengeEndpoint( throw authorizationChallengeErrorResponse } - const authRequestURI = await opts.createAuthRequestUriCallback(issuer_state) + const authRequestURI = await opts.createAuthRequestUriCallback(issuer_state) // TODO generate some error const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = { error: AuthorizationChallengeError.insufficient_authorization, auth_session: issuer_state, @@ -157,7 +157,7 @@ export function authorizationChallengeEndpoint( throw authorizationChallengeErrorResponse } - const verifiedResponse = await opts.verifyAuthResponseCallback(presentation_during_issuance_session) + const verifiedResponse = await opts.verifyAuthResponseCallback(presentation_during_issuance_session) // TODO generate some error if (verifiedResponse) { const authorizationCode = generateRandomString(16, 'base64url') session.authorizationCode = authorizationCode From d416e00689dd9662de8f67e1e903ddb6568c9bfb Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Wed, 22 Jan 2025 14:12:43 +0100 Subject: [PATCH 24/26] chore: fix test url --- .../lib/__tests__/authorizationChallengeCodeServer.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/issuer-rest/lib/__tests__/authorizationChallengeCodeServer.spec.ts b/packages/issuer-rest/lib/__tests__/authorizationChallengeCodeServer.spec.ts index 5e1beed8..0b66e384 100644 --- a/packages/issuer-rest/lib/__tests__/authorizationChallengeCodeServer.spec.ts +++ b/packages/issuer-rest/lib/__tests__/authorizationChallengeCodeServer.spec.ts @@ -18,7 +18,7 @@ import { OID4VCIServer } from '../OID4VCIServer' const authorizationServerMetadata = new AuthorizationServerMetadataBuilder() .withIssuer('test-issuer') - .withAuthorizationChallengeEndpoint('http://localhost:3456/test/authorize-challenge') + .withAuthorizationChallengeEndpoint('http://localhost:9000/authorize-challenge') .withResponseTypesSupported(['code', 'token', 'id_token']) .build() @@ -98,8 +98,8 @@ describe('OID4VCIServer', () => { it('should return http code 400 with error invalid_request', async () => { const res = await requests(app) - .post('/authorize-challenge') - .send(`client_id=${uuidv4()}`) + .post('/authorize-challenge') + .send(`client_id=${uuidv4()}`) expect(res.statusCode).toEqual(400) const actual = JSON.parse(res.text) expect(actual).toEqual({ From 8eca88038f7be99a2fec6863db22ec7c05da4159 Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Wed, 22 Jan 2025 14:54:24 +0100 Subject: [PATCH 25/26] chore: removed first party flag --- packages/siop-oid4vp/lib/authorization-response/Payload.ts | 1 - packages/siop-oid4vp/lib/authorization-response/types.ts | 1 - packages/siop-oid4vp/lib/op/OP.ts | 1 - .../lib/schemas/AuthorizationResponseOpts.schema.ts | 3 --- packages/siop-oid4vp/lib/types/SIOP.types.ts | 1 - 5 files changed, 7 deletions(-) diff --git a/packages/siop-oid4vp/lib/authorization-response/Payload.ts b/packages/siop-oid4vp/lib/authorization-response/Payload.ts index 6dc5a533..bdd7cc80 100644 --- a/packages/siop-oid4vp/lib/authorization-response/Payload.ts +++ b/packages/siop-oid4vp/lib/authorization-response/Payload.ts @@ -24,7 +24,6 @@ export const createResponsePayload = async ( ...(responseOpts.accessToken && { access_token: responseOpts.accessToken, expires_in: responseOpts.expiresIn || 3600 }), ...(responseOpts.tokenType && { token_type: responseOpts.tokenType }), ...(responseOpts.refreshToken && { refresh_token: responseOpts.refreshToken }), - ...(responseOpts.isFirstParty && { is_first_party: responseOpts.isFirstParty }), state, } diff --git a/packages/siop-oid4vp/lib/authorization-response/types.ts b/packages/siop-oid4vp/lib/authorization-response/types.ts index fe442bfc..8979b649 100644 --- a/packages/siop-oid4vp/lib/authorization-response/types.ts +++ b/packages/siop-oid4vp/lib/authorization-response/types.ts @@ -41,7 +41,6 @@ export interface AuthorizationResponseOpts { tokenType?: string refreshToken?: string presentationExchange?: PresentationExchangeResponseOpts - isFirstParty?: boolean } export interface PresentationExchangeResponseOpts { diff --git a/packages/siop-oid4vp/lib/op/OP.ts b/packages/siop-oid4vp/lib/op/OP.ts index 041e2637..6ec4474d 100644 --- a/packages/siop-oid4vp/lib/op/OP.ts +++ b/packages/siop-oid4vp/lib/op/OP.ts @@ -106,7 +106,6 @@ export class OP { issuer?: ResponseIss | string verification?: Verification presentationExchange?: PresentationExchangeResponseOpts - isFirstParty?: boolean }, ): Promise { if ( diff --git a/packages/siop-oid4vp/lib/schemas/AuthorizationResponseOpts.schema.ts b/packages/siop-oid4vp/lib/schemas/AuthorizationResponseOpts.schema.ts index 3c5afdff..ffb4eee2 100644 --- a/packages/siop-oid4vp/lib/schemas/AuthorizationResponseOpts.schema.ts +++ b/packages/siop-oid4vp/lib/schemas/AuthorizationResponseOpts.schema.ts @@ -52,9 +52,6 @@ export const AuthorizationResponseOptsSchemaObj = { }, "presentationExchange": { "$ref": "#/definitions/PresentationExchangeResponseOpts" - }, - "isFirstParty": { - "type": "boolean" } }, "required": [ diff --git a/packages/siop-oid4vp/lib/types/SIOP.types.ts b/packages/siop-oid4vp/lib/types/SIOP.types.ts index 3314f5b1..1bccd68a 100644 --- a/packages/siop-oid4vp/lib/types/SIOP.types.ts +++ b/packages/siop-oid4vp/lib/types/SIOP.types.ts @@ -179,7 +179,6 @@ export interface AuthorizationResponsePayload { | MdocOid4vpMdocVpToken presentation_submission?: PresentationSubmission verifiedData?: IPresentation | AdditionalClaims - is_first_party?: boolean // eslint-disable-next-line @typescript-eslint/no-explicit-any [x: string]: any } From fa1663453103770a8bbb3cbee0532fd0432c287f Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Wed, 22 Jan 2025 16:39:44 +0100 Subject: [PATCH 26/26] chore: revert isFirstParty flag --- packages/siop-oid4vp/lib/authorization-response/Payload.ts | 1 + packages/siop-oid4vp/lib/authorization-response/types.ts | 1 + packages/siop-oid4vp/lib/op/OP.ts | 1 + .../lib/schemas/AuthorizationResponseOpts.schema.ts | 3 +++ packages/siop-oid4vp/lib/types/SIOP.types.ts | 1 + 5 files changed, 7 insertions(+) diff --git a/packages/siop-oid4vp/lib/authorization-response/Payload.ts b/packages/siop-oid4vp/lib/authorization-response/Payload.ts index bdd7cc80..6dc5a533 100644 --- a/packages/siop-oid4vp/lib/authorization-response/Payload.ts +++ b/packages/siop-oid4vp/lib/authorization-response/Payload.ts @@ -24,6 +24,7 @@ export const createResponsePayload = async ( ...(responseOpts.accessToken && { access_token: responseOpts.accessToken, expires_in: responseOpts.expiresIn || 3600 }), ...(responseOpts.tokenType && { token_type: responseOpts.tokenType }), ...(responseOpts.refreshToken && { refresh_token: responseOpts.refreshToken }), + ...(responseOpts.isFirstParty && { is_first_party: responseOpts.isFirstParty }), state, } diff --git a/packages/siop-oid4vp/lib/authorization-response/types.ts b/packages/siop-oid4vp/lib/authorization-response/types.ts index 8979b649..fe442bfc 100644 --- a/packages/siop-oid4vp/lib/authorization-response/types.ts +++ b/packages/siop-oid4vp/lib/authorization-response/types.ts @@ -41,6 +41,7 @@ export interface AuthorizationResponseOpts { tokenType?: string refreshToken?: string presentationExchange?: PresentationExchangeResponseOpts + isFirstParty?: boolean } export interface PresentationExchangeResponseOpts { diff --git a/packages/siop-oid4vp/lib/op/OP.ts b/packages/siop-oid4vp/lib/op/OP.ts index 6ec4474d..041e2637 100644 --- a/packages/siop-oid4vp/lib/op/OP.ts +++ b/packages/siop-oid4vp/lib/op/OP.ts @@ -106,6 +106,7 @@ export class OP { issuer?: ResponseIss | string verification?: Verification presentationExchange?: PresentationExchangeResponseOpts + isFirstParty?: boolean }, ): Promise { if ( diff --git a/packages/siop-oid4vp/lib/schemas/AuthorizationResponseOpts.schema.ts b/packages/siop-oid4vp/lib/schemas/AuthorizationResponseOpts.schema.ts index ffb4eee2..3c5afdff 100644 --- a/packages/siop-oid4vp/lib/schemas/AuthorizationResponseOpts.schema.ts +++ b/packages/siop-oid4vp/lib/schemas/AuthorizationResponseOpts.schema.ts @@ -52,6 +52,9 @@ export const AuthorizationResponseOptsSchemaObj = { }, "presentationExchange": { "$ref": "#/definitions/PresentationExchangeResponseOpts" + }, + "isFirstParty": { + "type": "boolean" } }, "required": [ diff --git a/packages/siop-oid4vp/lib/types/SIOP.types.ts b/packages/siop-oid4vp/lib/types/SIOP.types.ts index 1bccd68a..3314f5b1 100644 --- a/packages/siop-oid4vp/lib/types/SIOP.types.ts +++ b/packages/siop-oid4vp/lib/types/SIOP.types.ts @@ -179,6 +179,7 @@ export interface AuthorizationResponsePayload { | MdocOid4vpMdocVpToken presentation_submission?: PresentationSubmission verifiedData?: IPresentation | AdditionalClaims + is_first_party?: boolean // eslint-disable-next-line @typescript-eslint/no-explicit-any [x: string]: any }