Skip to content

Commit ab4905c

Browse files
committed
feat: Add support for jwt-bearer client assertions in access token
1 parent 1a469f9 commit ab4905c

8 files changed

Lines changed: 125 additions & 11 deletions

File tree

packages/client/lib/AccessTokenClient.ts

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

2424
import { MetadataClientV1_0_13 } from './MetadataClientV1_0_13';
25+
import { createJwtBearerClientAssertion } from './functions';
2526
import { LOG } from './types';
2627

2728
export class AccessTokenClient {
@@ -91,10 +92,10 @@ export class AccessTokenClient {
9192
// @ts-ignore
9293
const credentialOfferRequest = opts.credentialOffer ? await toUniformCredentialOfferRequest(opts.credentialOffer) : undefined;
9394
const request: Partial<AccessTokenRequest> = { ...opts.additionalParams };
94-
95-
if (asOpts?.clientId) {
96-
request.client_id = asOpts.clientId;
95+
if (asOpts?.clientOpts?.clientId) {
96+
request.client_id = asOpts.clientOpts.clientId;
9797
}
98+
await createJwtBearerClientAssertion(request, opts);
9899

99100
if (credentialOfferRequest?.supportedFlows.includes(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW)) {
100101
this.assertAlphanumericPin(opts.pinMetadata, pin);

packages/client/lib/AccessTokenClientV1_0_11.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
GrantTypes,
1515
IssuerOpts,
1616
JsonURIMode,
17+
OpenId4VCIVersion,
1718
OpenIDResponse,
1819
PRE_AUTH_CODE_LITERAL,
1920
TokenErrorResponse,
@@ -24,6 +25,7 @@ import { ObjectUtils } from '@sphereon/ssi-types';
2425
import Debug from 'debug';
2526

2627
import { MetadataClientV1_0_13 } from './MetadataClientV1_0_13';
28+
import { createJwtBearerClientAssertion } from './functions';
2729

2830
const debug = Debug('sphereon:oid4vci:token');
2931

@@ -94,9 +96,10 @@ export class AccessTokenClientV1_0_11 {
9496
: undefined;
9597
const request: Partial<AccessTokenRequest> = { ...opts.additionalParams };
9698

97-
if (asOpts?.clientId) {
98-
request.client_id = asOpts.clientId;
99+
if (asOpts?.clientOpts?.clientId) {
100+
request.client_id = asOpts.clientOpts.clientId;
99101
}
102+
await createJwtBearerClientAssertion(request, { ...opts, version: OpenId4VCIVersion.VER_1_0_11 });
100103

101104
if (credentialOfferRequest?.supportedFlows.includes(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW)) {
102105
this.assertNumericPin(this.isPinRequiredValue(credentialOfferRequest.credential_offer), pin);

packages/client/lib/OpenID4VCIClient.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
Alg,
44
AuthorizationRequestOpts,
55
AuthorizationResponse,
6+
AuthorizationServerOpts,
67
AuthzFlowType,
78
CodeChallengeMethod,
89
CredentialConfigurationSupported,
@@ -274,8 +275,9 @@ export class OpenID4VCIClient {
274275
code?: string; // Directly pass in a code from an auth response
275276
redirectUri?: string;
276277
additionalRequestParams?: Record<string, any>;
278+
asOpts?: AuthorizationServerOpts;
277279
}): Promise<AccessTokenResponse> {
278-
const { pin, clientId } = opts ?? {};
280+
const { pin, clientId = this._state.clientId ?? this._state.authorizationRequestOpts?.clientId } = opts ?? {};
279281
let { redirectUri } = opts ?? {};
280282
if (opts?.authorizationResponse) {
281283
this._state.authorizationCodeResponse = { ...toAuthorizationResponsePayload(opts.authorizationResponse) };
@@ -289,6 +291,23 @@ export class OpenID4VCIClient {
289291
}
290292
this.assertIssuerData();
291293

294+
const asOpts: AuthorizationServerOpts = { ...opts?.asOpts };
295+
const kid = asOpts.clientOpts?.kid ?? this._state.kid ?? this._state.authorizationRequestOpts?.requestObjectOpts?.kid;
296+
const clientAssertionType =
297+
asOpts.clientOpts?.clientAssertionType ??
298+
(kid && clientId && typeof asOpts.clientOpts?.signCallbacks === 'function'
299+
? 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
300+
: undefined);
301+
if (clientId) {
302+
asOpts.clientOpts = {
303+
...asOpts.clientOpts,
304+
clientId,
305+
...(kid && { kid }),
306+
...(clientAssertionType && { clientAssertionType }),
307+
signCallbacks: asOpts.clientOpts?.signCallbacks ?? this._state.authorizationRequestOpts?.requestObjectOpts?.signCallbacks,
308+
};
309+
}
310+
292311
if (clientId) {
293312
this._state.clientId = clientId;
294313
}
@@ -312,7 +331,7 @@ export class OpenID4VCIClient {
312331
...(!this._state.pkce.disabled && { codeVerifier: this._state.pkce.codeVerifier }),
313332
code,
314333
redirectUri,
315-
asOpts: { clientId: this.clientId },
334+
asOpts,
316335
...(opts?.additionalRequestParams && { additionalParams: opts.additionalRequestParams }),
317336
});
318337

packages/client/lib/OpenID4VCIClientV1_0_11.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
Alg,
44
AuthorizationRequestOpts,
55
AuthorizationResponse,
6+
AuthorizationServerOpts,
67
AuthzFlowType,
78
CodeChallengeMethod,
89
CredentialConfigurationSupported,
@@ -259,6 +260,7 @@ export class OpenID4VCIClientV1_0_11 {
259260
authorizationResponse?: string | AuthorizationResponse; // Pass in an auth response, either as URI/redirect, or object
260261
code?: string; // Directly pass in a code from an auth response
261262
redirectUri?: string;
263+
asOpts?: AuthorizationServerOpts;
262264
}): Promise<AccessTokenResponse> {
263265
const { pin, clientId } = opts ?? {};
264266
let { redirectUri } = opts ?? {};
@@ -288,6 +290,22 @@ export class OpenID4VCIClientV1_0_11 {
288290
if (this._state.authorizationRequestOpts?.redirectUri && !redirectUri) {
289291
redirectUri = this._state.authorizationRequestOpts.redirectUri;
290292
}
293+
const asOpts: AuthorizationServerOpts = { ...opts?.asOpts };
294+
const kid = asOpts.clientOpts?.kid ?? this._state.kid ?? this._state.authorizationRequestOpts?.requestObjectOpts?.kid;
295+
const clientAssertionType =
296+
asOpts.clientOpts?.clientAssertionType ??
297+
(kid && clientId && typeof asOpts.clientOpts?.signCallbacks === 'function'
298+
? 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
299+
: undefined);
300+
if (clientId) {
301+
asOpts.clientOpts = {
302+
...asOpts.clientOpts,
303+
clientId,
304+
...(kid && { kid }),
305+
...(clientAssertionType && { clientAssertionType }),
306+
signCallbacks: asOpts.clientOpts?.signCallbacks ?? this._state.authorizationRequestOpts?.requestObjectOpts?.signCallbacks,
307+
};
308+
}
291309

292310
const response = await accessTokenClient.acquireAccessToken({
293311
credentialOffer: this.credentialOffer,
@@ -297,7 +315,7 @@ export class OpenID4VCIClientV1_0_11 {
297315
...(!this._state.pkce.disabled && { codeVerifier: this._state.pkce.codeVerifier }),
298316
code,
299317
redirectUri,
300-
asOpts: { clientId: this.clientId },
318+
asOpts,
301319
});
302320

303321
if (response.errorBody) {

packages/client/lib/OpenID4VCIClientV1_0_13.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
Alg,
44
AuthorizationRequestOpts,
55
AuthorizationResponse,
6+
AuthorizationServerOpts,
67
AuthzFlowType,
78
CodeChallengeMethod,
89
CredentialConfigurationSupportedV1_0_13,
@@ -265,6 +266,7 @@ export class OpenID4VCIClientV1_0_13 {
265266
authorizationResponse?: string | AuthorizationResponse; // Pass in an auth response, either as URI/redirect, or object
266267
code?: string; // Directly pass in a code from an auth response
267268
redirectUri?: string;
269+
asOpts?: AuthorizationServerOpts;
268270
}): Promise<AccessTokenResponse> {
269271
const { pin, clientId } = opts ?? {};
270272
let { redirectUri } = opts ?? {};
@@ -279,6 +281,22 @@ export class OpenID4VCIClientV1_0_13 {
279281
this._state.pkce.codeVerifier = opts.codeVerifier;
280282
}
281283
this.assertIssuerData();
284+
const asOpts: AuthorizationServerOpts = { ...opts?.asOpts };
285+
const kid = asOpts.clientOpts?.kid ?? this._state.kid ?? this._state.authorizationRequestOpts?.requestObjectOpts?.kid;
286+
const clientAssertionType =
287+
asOpts.clientOpts?.clientAssertionType ??
288+
(kid && clientId && typeof asOpts.clientOpts?.signCallbacks === 'function'
289+
? 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
290+
: undefined);
291+
if (clientId) {
292+
asOpts.clientOpts = {
293+
...asOpts.clientOpts,
294+
clientId,
295+
...(kid && { kid }),
296+
...(clientAssertionType && { clientAssertionType }),
297+
signCallbacks: asOpts.clientOpts?.signCallbacks ?? this._state.authorizationRequestOpts?.requestObjectOpts?.signCallbacks,
298+
};
299+
}
282300

283301
if (clientId) {
284302
this._state.clientId = clientId;
@@ -302,7 +320,7 @@ export class OpenID4VCIClientV1_0_13 {
302320
...(!this._state.pkce.disabled && { codeVerifier: this._state.pkce.codeVerifier }),
303321
code,
304322
redirectUri,
305-
asOpts: { clientId: this.clientId },
323+
asOpts,
306324
});
307325

308326
if (response.errorBody) {
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { AccessTokenRequest, AccessTokenRequestOpts, Jwt, OpenId4VCIVersion } from '@sphereon/oid4vci-common';
2+
import { v4 } from 'uuid';
3+
4+
import { ProofOfPossessionBuilder } from '../ProofOfPossessionBuilder';
5+
6+
export const createJwtBearerClientAssertion = async (
7+
request: Partial<AccessTokenRequest>,
8+
opts: AccessTokenRequestOpts & {
9+
version?: OpenId4VCIVersion;
10+
},
11+
): Promise<void> => {
12+
const { asOpts } = opts;
13+
if (asOpts?.clientOpts?.clientAssertionType === 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer') {
14+
if (!request.client_id) {
15+
throw Error(`Not client_id supplied, but client-assertion jwt-bearer requested.`);
16+
} else if (!asOpts.clientOpts.kid) {
17+
throw Error(`No kid supplied, but client-assertion jwt-bearer requested.`);
18+
} else if (!asOpts.clientOpts.signCallbacks) {
19+
throw Error(`No sign callback supplied, but client-assertion jwt-bearer requested.`);
20+
}
21+
const jwt: Jwt = {
22+
header: {
23+
typ: 'JWT',
24+
kid: asOpts.clientOpts.kid,
25+
alg: asOpts.clientOpts.alg ?? 'ES256',
26+
},
27+
payload: {
28+
iss: request.client_id,
29+
sub: request.client_id,
30+
aud: opts.credentialIssuer,
31+
jti: v4(),
32+
exp: Date.now() / 1000 + 60,
33+
iat: Date.now() / 1000 - 60,
34+
},
35+
};
36+
const pop = await ProofOfPossessionBuilder.fromJwt({
37+
jwt,
38+
callbacks: asOpts.clientOpts.signCallbacks,
39+
version: opts.version ?? OpenId4VCIVersion.VER_1_0_13,
40+
mode: 'jwt',
41+
}).build();
42+
request.client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
43+
request.client_assertion = pop.jwt;
44+
}
45+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
export * from './AuthorizationUtil';
22
export * from './notifications';
3+
export * from './OpenIDUtils';
4+
export * from './AccessTokenUtil';

packages/common/lib/types/Authorization.types.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CredentialOfferPayload, ProofOfPossessionCallbacks, UniformCredentialOffer } from './CredentialIssuance.types';
1+
import { Alg, CredentialOfferPayload, ProofOfPossessionCallbacks, UniformCredentialOffer } from './CredentialIssuance.types';
22
import {
33
ErrorResponse,
44
IssuerCredentialSubject,
@@ -186,9 +186,17 @@ export interface AuthorizationServerOpts {
186186
allowInsecureEndpoints?: boolean;
187187
as?: string; // If not provided the issuer hostname will be used!
188188
tokenEndpoint?: string; // Allows to override the default '/token' endpoint
189-
clientId?: string;
189+
clientOpts?: AuthorizationServerClientOpts;
190190
}
191191

192+
export type AuthorizationServerClientOpts = {
193+
clientId: string;
194+
clientAssertionType?: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
195+
kid?: string;
196+
alg?: Alg;
197+
signCallbacks?: ProofOfPossessionCallbacks<never>;
198+
};
199+
192200
export interface IssuerOpts {
193201
issuer: string;
194202
tokenEndpoint?: string;

0 commit comments

Comments
 (0)