Skip to content

Commit c24a898

Browse files
committed
fix: add some dpop unit tests
1 parent 1a54e69 commit c24a898

3 files changed

Lines changed: 131 additions & 4 deletions

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { createDPoP, getCreateDPoPOptions, verifyDPoP } from '../dpop';
2+
3+
describe('dpop', () => {
4+
const alg = 'HS256';
5+
const jwk = { kty: 'Ed25519', crv: 'Ed25519', x: '123', y: '123' };
6+
const jwtIssuer = { alg, jwk };
7+
const htm = 'POST';
8+
const htu = 'https://example.com/token';
9+
const nonce = 'nonce';
10+
const jwtPayloadProps = { htm, htu, nonce } as const;
11+
const jwtHeaderProps = { alg, jwk, typ: 'dpop+jwt' };
12+
const unsignedDpop =
13+
'eyJhbGciOiJIUzI1NiIsImp3ayI6eyJrdHkiOiJFZDI1NTE5IiwiY3J2IjoiRWQyNTUxOSIsIngiOiIxMjMiLCJ5IjoiMTIzIn0sInR5cCI6ImRwb3Arand0In0.eyJodG0iOiJQT1NUIiwiaHR1IjoiaHR0cHM6Ly9leGFtcGxlLmNvbS90b2tlbiIsIm5vbmNlIjoibm9uY2UiLCJpYXQiOjE3MjIzMjcxOTQsImp0aSI6Ijk4OWNiZTc4LWI1ZTYtNDViYS1iYjMzLWQ0MGE4ZGEwZjFhYSJ9';
14+
15+
it('should create a dpop with valid options', async () => {
16+
const dpop = await createDPoP({
17+
jwtIssuer,
18+
jwtPayloadProps,
19+
createJwtCallback: async (dpopJwtIssuerWithContext, jwt) => {
20+
expect(dpopJwtIssuerWithContext.alg).toEqual(alg);
21+
expect(dpopJwtIssuerWithContext.jwk).toEqual(jwk);
22+
expect(dpopJwtIssuerWithContext.dPoPSigningAlgValuesSupported).toBeUndefined();
23+
expect(dpopJwtIssuerWithContext.type).toEqual('dpop');
24+
25+
expect(jwt.header).toStrictEqual(jwtHeaderProps);
26+
expect(jwt.payload).toStrictEqual({
27+
...jwtPayloadProps,
28+
iat: expect.any(Number),
29+
jti: expect.any(String),
30+
});
31+
32+
return unsignedDpop;
33+
},
34+
});
35+
36+
expect(unsignedDpop).toEqual(dpop);
37+
expect.assertions(7);
38+
});
39+
40+
it('should create a dpop with valid createDPoPOptions', async () => {
41+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
42+
const { htm, htu, ...rest } = jwtPayloadProps;
43+
const options = getCreateDPoPOptions(
44+
{
45+
jwtIssuer,
46+
jwtPayloadProps: rest,
47+
createJwtCallback: async (dpopJwtIssuerWithContext, jwt) => {
48+
expect(dpopJwtIssuerWithContext.alg).toEqual(alg);
49+
expect(dpopJwtIssuerWithContext.jwk).toEqual(jwk);
50+
expect(dpopJwtIssuerWithContext.dPoPSigningAlgValuesSupported).toBeUndefined();
51+
expect(dpopJwtIssuerWithContext.type).toEqual('dpop');
52+
53+
expect(jwt.header).toStrictEqual(jwtHeaderProps);
54+
expect(jwt.payload).toStrictEqual({
55+
...jwtPayloadProps,
56+
iat: expect.any(Number),
57+
jti: expect.any(String),
58+
});
59+
60+
return unsignedDpop;
61+
},
62+
},
63+
htu + '?123412341#xyaksdjfaksdjf',
64+
);
65+
66+
const dpop = await createDPoP(options);
67+
68+
expect(unsignedDpop).toEqual(dpop);
69+
expect.assertions(7);
70+
});
71+
72+
it('verify dpop fails if jwtVerifyCallback throws an error', async () => {
73+
await expect(
74+
verifyDPoP(
75+
{
76+
headers: { dpop: unsignedDpop },
77+
fullUrl: htu + '?123412341#xyaksdjfaksdjf',
78+
method: 'POST',
79+
},
80+
{
81+
jwtVerifyCallback: async () => {
82+
throw new Error('jwtVerifyCallback');
83+
},
84+
expectedNonce: 'nonce',
85+
expectAccessToken: false,
86+
now: 1722327194,
87+
},
88+
),
89+
).rejects.toThrow();
90+
});
91+
92+
it('should verify a dpop with valid options', async () => {
93+
const dpop = await verifyDPoP(
94+
{
95+
headers: { dpop: unsignedDpop },
96+
fullUrl: htu + '?123412341#xyaksdjfaksdjf',
97+
method: 'POST',
98+
},
99+
{
100+
jwtVerifyCallback: async (jwtVerifier, jwt) => {
101+
expect(jwtVerifier.method).toEqual('jwk');
102+
expect(jwtVerifier.jwk).toEqual(jwk);
103+
expect(jwtVerifier.type).toEqual('dpop');
104+
expect(jwtVerifier.alg).toEqual(alg);
105+
106+
expect(jwt.header).toStrictEqual(jwtHeaderProps);
107+
expect(jwt.payload).toStrictEqual({
108+
...jwtPayloadProps,
109+
iat: expect.any(Number),
110+
jti: expect.any(String),
111+
});
112+
expect(jwt.raw).toEqual(unsignedDpop);
113+
114+
return true;
115+
},
116+
expectAccessToken: false,
117+
expectedNonce: 'nonce',
118+
now: 1722327194,
119+
},
120+
);
121+
expect(dpop).toStrictEqual(jwk);
122+
expect.assertions(6);
123+
});
124+
});

packages/common/lib/dpop/DPoP.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export interface DPoPVerifyOptions {
8989
maxIatAgeInSeconds?: number;
9090
expectAccessToken?: boolean;
9191
jwtVerifyCallback: DPoPVerifyJwtCallback;
92+
now?: number;
9293
}
9394

9495
export async function verifyDPoP(
@@ -164,10 +165,12 @@ export async function verifyDPoP(
164165
}
165166

166167
// Validate iat claim
167-
const { nowSkewedPast, nowSkewedFuture } = getNowSkewed();
168+
const { nowSkewedPast, nowSkewedFuture } = getNowSkewed(options.now);
168169
if (
169-
dPoPPayload.iat > nowSkewedFuture + (options.maxIatAgeInSeconds ?? 300) ||
170-
dPoPPayload.iat < nowSkewedPast - (options.maxIatAgeInSeconds ?? 300)
170+
// iat claim is to far in the future
171+
nowSkewedPast - (options.maxIatAgeInSeconds ?? 300) > dPoPPayload.iat ||
172+
// iat claim is too old
173+
nowSkewedFuture + (options.maxIatAgeInSeconds ?? 300) < dPoPPayload.iat
171174
) {
172175
// 5 minute window
173176
throw new Error('invalid_dpop_proof. Invalid iat claim');

packages/common/lib/jwt/Jwt.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export type JwtHeader = jwtDecodeJwtHeader & {
55
alg?: string;
66
x5c?: string[];
77
kid?: string;
8-
jwk?: JWK & { kty: string };
8+
jwk?: JWK;
99
jwt?: string;
1010
} & Record<string, unknown>;
1111

0 commit comments

Comments
 (0)