diff --git a/packages/client/lib/AccessTokenClient.ts b/packages/client/lib/AccessTokenClient.ts index fab8186d..9e3e235a 100644 --- a/packages/client/lib/AccessTokenClient.ts +++ b/packages/client/lib/AccessTokenClient.ts @@ -1,4 +1,4 @@ -import { createDPoP, CreateDPoPClientOpts } from '@sphereon/common'; +import { createDPoP, CreateDPoPClientOpts, getCreateDPoPOptions } from '@sphereon/common'; import { AccessTokenRequest, AccessTokenRequestOpts, @@ -93,12 +93,9 @@ export class AccessTokenClient { let dPoP: string | undefined; if (createDPoPOpts?.dPoPSigningAlgValuesSupported && createDPoPOpts.dPoPSigningAlgValuesSupported.length > 0) { - const htu = requestTokenURL.split('?')[0].split('#')[0]; - dPoP = createDPoPOpts - ? await createDPoP({ ...createDPoPOpts, jwtPayloadProps: { ...createDPoPOpts.jwtPayloadProps, htu, htm: 'POST' } }) - : undefined; + dPoP = createDPoPOpts ? await createDPoP(getCreateDPoPOptions(createDPoPOpts, requestTokenURL)) : undefined; } - return this.sendAuthCode(requestTokenURL, accessTokenRequest, { dPoP }); + return this.sendAuthCode(requestTokenURL, accessTokenRequest, dPoP ? { headers: { dPoP } } : undefined); } public async createAccessTokenRequest(opts: Omit): Promise { @@ -235,10 +232,10 @@ export class AccessTokenClient { private async sendAuthCode( requestTokenURL: string, accessTokenRequest: AccessTokenRequest, - opts?: { dPoP?: string }, + opts?: { headers?: Record }, ): Promise> { return await formPost(requestTokenURL, convertJsonToURI(accessTokenRequest, { mode: JsonURIMode.X_FORM_WWW_URLENCODED }), { - customHeaders: { ...(opts?.dPoP && { dpop: opts.dPoP }) }, + customHeaders: opts?.headers ? opts.headers : undefined, }); } diff --git a/packages/client/lib/AccessTokenClientV1_0_11.ts b/packages/client/lib/AccessTokenClientV1_0_11.ts index 6a5be3e1..cf6fec02 100644 --- a/packages/client/lib/AccessTokenClientV1_0_11.ts +++ b/packages/client/lib/AccessTokenClientV1_0_11.ts @@ -1,4 +1,4 @@ -import { createDPoP, CreateDPoPClientOpts } from '@sphereon/common'; +import { createDPoP, CreateDPoPClientOpts, getCreateDPoPOptions } from '@sphereon/common'; import { AccessTokenRequest, AccessTokenRequestOpts, @@ -97,13 +97,10 @@ export class AccessTokenClientV1_0_11 { let dPoP: string | undefined; if (createDPoPOpts?.dPoPSigningAlgValuesSupported && createDPoPOpts.dPoPSigningAlgValuesSupported.length > 0) { - const htu = requestTokenURL.split('?')[0].split('#')[0]; - dPoP = createDPoPOpts - ? await createDPoP({ ...createDPoPOpts, jwtPayloadProps: { ...createDPoPOpts.jwtPayloadProps, htu, htm: 'POST' } }) - : undefined; + dPoP = createDPoPOpts ? await createDPoP(getCreateDPoPOptions(createDPoPOpts, requestTokenURL)) : undefined; } - return this.sendAuthCode(requestTokenURL, accessTokenRequest, { dPoP }); + return this.sendAuthCode(requestTokenURL, accessTokenRequest, dPoP ? { headers: { dPoP } } : undefined); } public async createAccessTokenRequest(opts: Omit): Promise { @@ -219,10 +216,10 @@ export class AccessTokenClientV1_0_11 { private async sendAuthCode( requestTokenURL: string, accessTokenRequest: AccessTokenRequest, - opts?: { dPoP?: string }, + opts?: { headers?: Record }, ): Promise> { return await formPost(requestTokenURL, convertJsonToURI(accessTokenRequest, { mode: JsonURIMode.X_FORM_WWW_URLENCODED }), { - customHeaders: { ...(opts?.dPoP && { dpop: opts.dPoP }) }, + customHeaders: opts?.headers ? opts.headers : undefined, }); } diff --git a/packages/client/lib/CredentialRequestClient.ts b/packages/client/lib/CredentialRequestClient.ts index 75a58df7..d3168016 100644 --- a/packages/client/lib/CredentialRequestClient.ts +++ b/packages/client/lib/CredentialRequestClient.ts @@ -1,4 +1,4 @@ -import { createDPoP, CreateDPoPClientOpts } from '@sphereon/common'; +import { createDPoP, CreateDPoPClientOpts, getCreateDPoPOptions } from '@sphereon/common'; import { acquireDeferredCredential, CredentialRequestV1_0_13, @@ -108,7 +108,7 @@ export class CredentialRequestClient { public async acquireCredentialsUsingRequest( uniformRequest: UniformCredentialRequest, - createDPoPOps?: CreateDPoPClientOpts, + createDPoPOpts?: CreateDPoPClientOpts, ): Promise & { access_token: string }> { if (this.version() < OpenId4VCIVersion.VER_1_0_13) { throw new Error('Versions below v1.0.13 (draft 13) are not supported by the V13 credential request client.'); @@ -124,19 +124,13 @@ export class CredentialRequestClient { const requestToken: string = this.credentialRequestOpts.token; let dPoP: string | undefined; - if (createDPoPOps) { - const htu = credentialEndpoint.split('?')[0].split('#')[0]; - dPoP = createDPoPOps - ? await createDPoP({ - ...createDPoPOps, - jwtPayloadProps: { ...createDPoPOps.jwtPayloadProps, htu, htm: 'POST', accessToken: requestToken }, - }) - : undefined; + if (createDPoPOpts) { + dPoP = createDPoPOpts ? await createDPoP(getCreateDPoPOptions(createDPoPOpts, credentialEndpoint, { accessToken: requestToken })) : undefined; } let response = (await post(credentialEndpoint, JSON.stringify(request), { bearerToken: requestToken, - customHeaders: { ...(createDPoPOps && { dpop: dPoP }) }, + customHeaders: { ...(createDPoPOpts && { dpop: dPoP }) }, })) as OpenIDResponse & { access_token: string; }; diff --git a/packages/client/lib/CredentialRequestClientV1_0_11.ts b/packages/client/lib/CredentialRequestClientV1_0_11.ts index e05debf9..03dbc46d 100644 --- a/packages/client/lib/CredentialRequestClientV1_0_11.ts +++ b/packages/client/lib/CredentialRequestClientV1_0_11.ts @@ -1,4 +1,4 @@ -import { createDPoP, CreateDPoPClientOpts } from '@sphereon/common'; +import { createDPoP, CreateDPoPClientOpts, getCreateDPoPOptions } from '@sphereon/common'; import { acquireDeferredCredential, CredentialResponse, @@ -65,17 +65,17 @@ export class CredentialRequestClientV1_0_11 { credentialTypes?: string | string[]; context?: string[]; format?: CredentialFormat | OID4VCICredentialFormat; - createDPoPOptions?: CreateDPoPClientOpts; + createDPoPOpts?: CreateDPoPClientOpts; }): Promise & { access_token: string }> { const { credentialTypes, proofInput, format, context } = opts; const request = await this.createCredentialRequest({ proofInput, credentialTypes, context, format, version: this.version() }); - return await this.acquireCredentialsUsingRequest(request, opts.createDPoPOptions); + return await this.acquireCredentialsUsingRequest(request, opts.createDPoPOpts); } public async acquireCredentialsUsingRequest( uniformRequest: UniformCredentialRequest, - createDPoPOptions?: CreateDPoPClientOpts, + createDPoPOpts?: CreateDPoPClientOpts, ): Promise & { access_token: string }> { const request = getCredentialRequestForVersion(uniformRequest, this.version()); const credentialEndpoint: string = this.credentialRequestOpts.credentialEndpoint; @@ -88,19 +88,13 @@ export class CredentialRequestClientV1_0_11 { const requestToken: string = this.credentialRequestOpts.token; let dPoP: string | undefined; - if (createDPoPOptions) { - const htu = credentialEndpoint.split('?')[0].split('#')[0]; - dPoP = createDPoPOptions - ? await createDPoP({ - ...createDPoPOptions, - jwtPayloadProps: { ...createDPoPOptions.jwtPayloadProps, htu, htm: 'POST', accessToken: requestToken }, - }) - : undefined; + if (createDPoPOpts) { + dPoP = createDPoPOpts ? await createDPoP(getCreateDPoPOptions(createDPoPOpts, credentialEndpoint, { accessToken: requestToken })) : undefined; } let response = (await post(credentialEndpoint, JSON.stringify(request), { bearerToken: requestToken, - customHeaders: { ...(createDPoPOptions && { dpop: dPoP }) }, + customHeaders: { ...(createDPoPOpts && { dpop: dPoP }) }, })) as OpenIDResponse & { access_token: string; }; diff --git a/packages/common/lib/dpop/DPoP.ts b/packages/common/lib/dpop/DPoP.ts index 63fe2e10..2b174ed3 100644 --- a/packages/common/lib/dpop/DPoP.ts +++ b/packages/common/lib/dpop/DPoP.ts @@ -66,6 +66,23 @@ export interface CreateDPoPOpts { export type CreateDPoPClientOpts = CreateDPoPOpts>; +export function getCreateDPoPOptions( + createDPoPClientOpts: CreateDPoPClientOpts, + endPointUrl: string, + resourceRequestOpts?: { accessToken: string }, +): CreateDPoPOpts { + const htu = endPointUrl.split('?')[0].split('#')[0]; + return { + ...createDPoPClientOpts, + jwtPayloadProps: { + ...createDPoPClientOpts.jwtPayloadProps, + htu, + htm: 'POST', + ...(resourceRequestOpts && { accessToken: resourceRequestOpts.accessToken }), + }, + }; +} + export async function createDPoP(options: CreateDPoPOpts): Promise { const { createJwtCallback, jwtIssuer, jwtPayloadProps, dPoPSigningAlgValuesSupported } = options; diff --git a/packages/issuer-rest/lib/IssuerTokenEndpoint.ts b/packages/issuer-rest/lib/IssuerTokenEndpoint.ts index 92b92893..005e5657 100644 --- a/packages/issuer-rest/lib/IssuerTokenEndpoint.ts +++ b/packages/issuer-rest/lib/IssuerTokenEndpoint.ts @@ -15,15 +15,20 @@ import { NextFunction, Request, Response } from 'express' */ export const handleTokenRequest = ({ tokenExpiresIn, // expiration in seconds + accessTokenEndpoint, accessTokenSignerCallback, accessTokenIssuer, cNonceExpiresIn, // expiration in seconds issuer, interval, dPoPVerifyJwtCallback, + requireDPoP, }: Required> & { issuer: VcIssuer dPoPVerifyJwtCallback?: DPoPVerifyJwtCallback + requireDPoP?: boolean + // The full URL of the access token endpoint + accessTokenEndpoint?: string }) => { return async (request: Request, response: Response) => { response.set({ @@ -47,6 +52,13 @@ export const handleTokenRequest = ({ } let dPoPJwk: JWK | undefined + if (requireDPoP && !request.headers.dpop) { + return sendErrorResponse(response, 400, { + error: TokenErrorResponse.invalid_request, + error_description: 'DPoP is required for requesting access tokens', + }) + } + if (request.headers.dpop) { if (!dPoPVerifyJwtCallback) { return sendErrorResponse(response, 400, { @@ -56,7 +68,7 @@ export const handleTokenRequest = ({ } try { - const fullUrl = request.protocol + '://' + request.get('host') + request.originalUrl + const fullUrl = accessTokenEndpoint ?? request.protocol + '://' + request.get('host') + request.originalUrl dPoPJwk = await verifyDPoP( { method: request.method, headers: request.headers, fullUrl }, {