Skip to content

Commit c7c6af4

Browse files
committed
feat: incorporate feedback and fix tests
1 parent b696dba commit c7c6af4

10 files changed

Lines changed: 55 additions & 57 deletions

File tree

packages/client/lib/AccessTokenClient.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { ObjectUtils } from '@sphereon/ssi-types';
2626

2727
import { MetadataClientV1_0_13 } from './MetadataClientV1_0_13';
2828
import { createJwtBearerClientAssertion } from './functions';
29-
import { dPoPShouldRetryRequestWithNonce } from './functions/dpopUtil';
29+
import { shouldRetryTokenRequestWithDPoPNonce } from './functions/dpopUtil';
3030
import { LOG } from './types';
3131

3232
export class AccessTokenClient {
@@ -99,7 +99,7 @@ export class AccessTokenClient {
9999
let response = await this.sendAuthCode(requestTokenURL, accessTokenRequest, dPoP ? { headers: { dpop: dPoP } } : undefined);
100100

101101
let nextDPoPNonce = createDPoPOpts?.jwtPayloadProps.nonce;
102-
const retryWithNonce = dPoPShouldRetryRequestWithNonce(response);
102+
const retryWithNonce = shouldRetryTokenRequestWithDPoPNonce(response);
103103
if (retryWithNonce.ok && createDPoPOpts) {
104104
createDPoPOpts.jwtPayloadProps.nonce = retryWithNonce.dpopNonce;
105105

@@ -110,7 +110,7 @@ export class AccessTokenClient {
110110
nextDPoPNonce = successDPoPNonce ?? retryWithNonce.dpopNonce;
111111
}
112112

113-
if (response.successBody && createDPoPOpts && createDPoPOpts && response.successBody.token_type !== 'DPoP') {
113+
if (response.successBody && createDPoPOpts && response.successBody.token_type !== 'DPoP') {
114114
throw new Error('Invalid token type returned. Expected DPoP. Received: ' + response.successBody.token_type);
115115
}
116116

packages/client/lib/AccessTokenClientV1_0_11.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import Debug from 'debug';
2929

3030
import { MetadataClientV1_0_13 } from './MetadataClientV1_0_13';
3131
import { createJwtBearerClientAssertion } from './functions';
32-
import { dPoPShouldRetryRequestWithNonce } from './functions/dpopUtil';
32+
import { shouldRetryTokenRequestWithDPoPNonce } from './functions/dpopUtil';
3333

3434
const debug = Debug('sphereon:oid4vci:token');
3535

@@ -103,7 +103,7 @@ export class AccessTokenClientV1_0_11 {
103103
let response = await this.sendAuthCode(requestTokenURL, accessTokenRequest, dPoP ? { headers: { dpop: dPoP } } : undefined);
104104

105105
let nextDPoPNonce = createDPoPOpts?.jwtPayloadProps.nonce;
106-
const retryWithNonce = dPoPShouldRetryRequestWithNonce(response);
106+
const retryWithNonce = shouldRetryTokenRequestWithDPoPNonce(response);
107107
if (retryWithNonce.ok && createDPoPOpts) {
108108
createDPoPOpts.jwtPayloadProps.nonce = retryWithNonce.dpopNonce;
109109

@@ -114,9 +114,10 @@ export class AccessTokenClientV1_0_11 {
114114
nextDPoPNonce = successDPoPNonce ?? retryWithNonce.dpopNonce;
115115
}
116116

117-
if (response.successBody && createDPoPOpts && createDPoPOpts && response.successBody.token_type !== 'DPoP') {
117+
if (response.successBody && createDPoPOpts && response.successBody.token_type !== 'DPoP') {
118118
throw new Error('Invalid token type returned. Expected DPoP. Received: ' + response.successBody.token_type);
119119
}
120+
120121
return {
121122
...response,
122123
params: { ...(nextDPoPNonce && { dpop: { dpopNonce: nextDPoPNonce } }) },

packages/client/lib/CredentialRequestClient.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import Debug from 'debug';
2323
import { CredentialRequestClientBuilderV1_0_11 } from './CredentialRequestClientBuilderV1_0_11';
2424
import { CredentialRequestClientBuilderV1_0_13 } from './CredentialRequestClientBuilderV1_0_13';
2525
import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder';
26-
import { dPoPShouldRetryResourceRequestWithNonce } from './functions/dpopUtil';
26+
import { shouldRetryResourceRequestWithDPoPNonce } from './functions/dpopUtil';
2727

2828
const debug = Debug('sphereon:oid4vci:credential');
2929

@@ -135,7 +135,7 @@ export class CredentialRequestClient {
135135
};
136136

137137
let nextDPoPNonce = createDPoPOpts?.jwtPayloadProps.nonce;
138-
const retryWithNonce = dPoPShouldRetryResourceRequestWithNonce(response);
138+
const retryWithNonce = shouldRetryResourceRequestWithDPoPNonce(response);
139139
if (retryWithNonce.ok && createDPoPOpts) {
140140
createDPoPOpts.jwtPayloadProps.nonce = retryWithNonce.dpopNonce;
141141
dPoP = await createDPoP(getCreateDPoPOptions(createDPoPOpts, credentialEndpoint, { accessToken: requestToken }));

packages/client/lib/CredentialRequestClientV1_0_11.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import Debug from 'debug';
2222
import { buildProof } from './CredentialRequestClient';
2323
import { CredentialRequestClientBuilderV1_0_11 } from './CredentialRequestClientBuilderV1_0_11';
2424
import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder';
25-
import { dPoPShouldRetryResourceRequestWithNonce } from './functions/dpopUtil';
25+
import { shouldRetryResourceRequestWithDPoPNonce } from './functions/dpopUtil';
2626

2727
const debug = Debug('sphereon:oid4vci:credential');
2828

@@ -99,7 +99,7 @@ export class CredentialRequestClientV1_0_11 {
9999
};
100100

101101
let nextDPoPNonce = createDPoPOpts?.jwtPayloadProps.nonce;
102-
const retryWithNonce = dPoPShouldRetryResourceRequestWithNonce(response);
102+
const retryWithNonce = shouldRetryResourceRequestWithDPoPNonce(response);
103103
if (retryWithNonce.ok && createDPoPOpts) {
104104
createDPoPOpts.jwtPayloadProps.nonce = retryWithNonce.dpopNonce;
105105
dPoP = await createDPoP(getCreateDPoPOptions(createDPoPOpts, credentialEndpoint, { accessToken: requestToken }));

packages/client/lib/__tests__/AccessTokenClient.spec.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,4 @@
1-
import {
2-
AccessTokenRequest,
3-
AccessTokenResponse,
4-
GrantTypes,
5-
OpenIDResponse,
6-
PRE_AUTH_CODE_LITERAL,
7-
WellKnownEndpoints,
8-
} from '@sphereon/oid4vci-common';
1+
import { AccessTokenRequest, AccessTokenResponse, GrantTypes, PRE_AUTH_CODE_LITERAL, WellKnownEndpoints } from '@sphereon/oid4vci-common';
92
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
103
// @ts-ignore
114
import nock from 'nock';
@@ -50,7 +43,7 @@ describe('AccessTokenClient should', () => {
5043
};
5144
nock(MOCK_URL).post(/.*/).reply(200, JSON.stringify(body));
5245

53-
const accessTokenResponse: OpenIDResponse<AccessTokenResponse> = await accessTokenClient.acquireAccessTokenUsingRequest({
46+
const accessTokenResponse = await accessTokenClient.acquireAccessTokenUsingRequest({
5447
accessTokenRequest,
5548
pinMetadata: {
5649
isPinRequired: true,
@@ -88,7 +81,7 @@ describe('AccessTokenClient should', () => {
8881
};
8982
nock(MOCK_URL).post(/.*/).reply(200, JSON.stringify(body));
9083

91-
const accessTokenResponse: OpenIDResponse<AccessTokenResponse> = await accessTokenClient.acquireAccessTokenUsingRequest({
84+
const accessTokenResponse = await accessTokenClient.acquireAccessTokenUsingRequest({
9285
accessTokenRequest,
9386
asOpts: { as: MOCK_URL },
9487
});
@@ -227,7 +220,7 @@ describe('AccessTokenClient should', () => {
227220
.post(/.*/)
228221
.reply(200, {});
229222

230-
const response: OpenIDResponse<AccessTokenResponse> = await accessTokenClient.acquireAccessToken({
223+
const response = await accessTokenClient.acquireAccessToken({
231224
credentialOffer: INITIATION_TEST,
232225
pin: '1234',
233226
});
Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,35 @@
1-
import { dpopResourceAuthenticateError, dpopTokenRequestNonceError } from '@sphereon/oid4vc-common';
1+
import { dpopTokenRequestNonceError } from '@sphereon/oid4vc-common';
22
import { OpenIDResponse } from 'oid4vci-common';
33

4-
export function dPoPShouldRetryRequestWithNonce(response: OpenIDResponse<unknown, unknown>) {
5-
if (response.errorBody && response.errorBody.error === dpopTokenRequestNonceError) {
6-
const dPoPNonce = response.errorBody.headers.get('DPoP-Nonce');
7-
if (!dPoPNonce) {
8-
throw new Error('The DPoP nonce was not returned');
9-
}
4+
export type RetryRequestWithDPoPNonce = { ok: true; dpopNonce: string } | { ok: false };
105

11-
return { ok: true, dpopNonce: dPoPNonce } as const;
6+
export function shouldRetryTokenRequestWithDPoPNonce(response: OpenIDResponse<unknown, unknown>): RetryRequestWithDPoPNonce {
7+
if (!response.errorBody || response.errorBody.error !== dpopTokenRequestNonceError) {
8+
return { ok: false };
129
}
1310

14-
return { ok: false } as const;
11+
const dPoPNonce = response.errorBody.headers.get('DPoP-Nonce');
12+
if (!dPoPNonce) {
13+
throw new Error('Missing required DPoP-Nonce header.');
14+
}
15+
16+
return { ok: true, dpopNonce: dPoPNonce };
1517
}
1618

17-
export function dPoPShouldRetryResourceRequestWithNonce(response: OpenIDResponse<unknown, unknown>) {
18-
if (response.errorBody && response.origResponse.status === 401) {
19-
const wwwAuthenticateHeader = response.errorBody.headers?.get('WWW-Authenticate');
20-
if (!wwwAuthenticateHeader?.includes(dpopResourceAuthenticateError)) {
21-
return { ok: false } as const;
22-
}
19+
export function shouldRetryResourceRequestWithDPoPNonce(response: OpenIDResponse<unknown, unknown>): RetryRequestWithDPoPNonce {
20+
if (!response.errorBody || response.origResponse.status !== 401) {
21+
return { ok: false };
22+
}
2323

24-
const dPoPNonce = response.errorBody.headers.get('DPoP-Nonce');
25-
if (!dPoPNonce) {
26-
throw new Error('The DPoP nonce was not returned');
27-
}
24+
const wwwAuthenticateHeader = response.errorBody.headers?.get('WWW-Authenticate');
25+
if (!wwwAuthenticateHeader?.includes(dpopTokenRequestNonceError)) {
26+
return { ok: false };
27+
}
2828

29-
return { ok: true, dpopNonce: dPoPNonce } as const;
29+
const dPoPNonce = response.errorBody.headers.get('DPoP-Nonce');
30+
if (!dPoPNonce) {
31+
throw new Error('Missing required DPoP-Nonce header.');
3032
}
3133

32-
return { ok: false } as const;
34+
return { ok: true, dpopNonce: dPoPNonce };
3335
}

packages/common/lib/dpop/DPoP.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {
1818
} from './../jwt';
1919

2020
export const dpopTokenRequestNonceError = 'use_dpop_nonce';
21-
export const dpopResourceAuthenticateError = 'DPoP error="use_dpop_nonce", error_description="Resource server requires nonce in DPoP proof"';
2221

2322
export interface DPoPJwtIssuerWithContext extends JwtIssuerJwk {
2423
type: 'dpop';
@@ -170,10 +169,10 @@ export async function verifyDPoP(
170169
// Validate iat claim
171170
const { nowSkewedPast, nowSkewedFuture } = getNowSkewed(options.now);
172171
if (
173-
// iat claim is to far in the future
174-
nowSkewedPast - (options.maxIatAgeInSeconds ?? 300) > dPoPPayload.iat ||
172+
// iat claim is too far in the future
173+
nowSkewedPast - (options.maxIatAgeInSeconds ?? 60) > dPoPPayload.iat ||
175174
// iat claim is too old
176-
nowSkewedFuture + (options.maxIatAgeInSeconds ?? 300) < dPoPPayload.iat
175+
nowSkewedFuture + (options.maxIatAgeInSeconds ?? 60) < dPoPPayload.iat
177176
) {
178177
// 5 minute window
179178
throw new Error('invalid_dpop_proof. Invalid iat claim');

packages/common/lib/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Loggers } from '@sphereon/ssi-types';
22

33
export const VCI_LOGGERS = Loggers.DEFAULT;
4-
export const VCI_LOG_COMMON = VCI_LOGGERS.get('sphereon:common');
4+
export const VCI_LOG_COMMON = VCI_LOGGERS.get('sphereon:oid4vci:common');
55

66
export * from './jwt';
77
export * from './dpop';

packages/common/lib/jwt/jwtUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function parseJWT<Header = JwtHeader, Payload = JwtPayload>(jwt: string)
2323
*
2424
* See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5
2525
*/
26-
const DEFAULT_SKEW_TIME = 300;
26+
const DEFAULT_SKEW_TIME = 60;
2727

2828
export function getNowSkewed(now?: number, skewTime?: number) {
2929
const _now = now ? now : epochTime();

packages/issuer-rest/lib/IssuerTokenEndpoint.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@ export const handleTokenRequest = <T extends object>({
2121
cNonceExpiresIn, // expiration in seconds
2222
issuer,
2323
interval,
24-
dPoPVerifyJwtCallback,
25-
requireDPoP,
24+
dpop,
2625
}: Required<Pick<ITokenEndpointOpts, 'accessTokenIssuer' | 'cNonceExpiresIn' | 'interval' | 'accessTokenSignerCallback' | 'tokenExpiresIn'>> & {
2726
issuer: VcIssuer<T>
28-
dPoPVerifyJwtCallback?: DPoPVerifyJwtCallback
29-
requireDPoP?: boolean
27+
dpop?: {
28+
requireDPoP?: boolean
29+
dPoPVerifyJwtCallback: DPoPVerifyJwtCallback
30+
}
3031
// The full URL of the access token endpoint
3132
accessTokenEndpoint?: string
3233
}) => {
@@ -52,18 +53,20 @@ export const handleTokenRequest = <T extends object>({
5253
}
5354

5455
let dPoPJwk: JWK | undefined
55-
if (requireDPoP && !request.headers.dpop) {
56+
if (dpop?.requireDPoP && !request.headers.dpop) {
5657
return sendErrorResponse(response, 400, {
5758
error: TokenErrorResponse.invalid_request,
58-
error_description: 'DPoP is required for requesting access tokens',
59+
error_description: 'DPoP is required for requesting access tokens.',
5960
})
6061
}
6162

6263
if (request.headers.dpop) {
63-
if (!dPoPVerifyJwtCallback) {
64+
if (!dpop) {
65+
console.error('Received unsupported DPoP header. The issuer is not configured to work with DPoP. Provide DPoP options for it to work.')
66+
6467
return sendErrorResponse(response, 400, {
6568
error: TokenErrorResponse.invalid_request,
66-
error_description: 'DPOP is not supported',
69+
error_description: 'Received unsupported DPoP header.',
6770
})
6871
}
6972

@@ -72,7 +75,7 @@ export const handleTokenRequest = <T extends object>({
7275
dPoPJwk = await verifyDPoP(
7376
{ method: request.method, headers: request.headers, fullUrl },
7477
{
75-
jwtVerifyCallback: dPoPVerifyJwtCallback,
78+
jwtVerifyCallback: dpop.dPoPVerifyJwtCallback,
7679
expectAccessToken: false,
7780
maxIatAgeInSeconds: undefined,
7881
},

0 commit comments

Comments
 (0)