Skip to content

Commit

Permalink
feat: Add support for jwt-bearer client assertions in access token
Browse files Browse the repository at this point in the history
  • Loading branch information
nklomp committed Jun 27, 2024
1 parent 1a469f9 commit ab4905c
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 11 deletions.
7 changes: 4 additions & 3 deletions packages/client/lib/AccessTokenClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import { ObjectUtils } from '@sphereon/ssi-types';

import { MetadataClientV1_0_13 } from './MetadataClientV1_0_13';
import { createJwtBearerClientAssertion } from './functions';
import { LOG } from './types';

export class AccessTokenClient {
Expand Down Expand Up @@ -91,10 +92,10 @@ export class AccessTokenClient {
// @ts-ignore
const credentialOfferRequest = opts.credentialOffer ? await toUniformCredentialOfferRequest(opts.credentialOffer) : undefined;
const request: Partial<AccessTokenRequest> = { ...opts.additionalParams };

if (asOpts?.clientId) {
request.client_id = asOpts.clientId;
if (asOpts?.clientOpts?.clientId) {
request.client_id = asOpts.clientOpts.clientId;
}
await createJwtBearerClientAssertion(request, opts);

if (credentialOfferRequest?.supportedFlows.includes(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW)) {
this.assertAlphanumericPin(opts.pinMetadata, pin);
Expand Down
7 changes: 5 additions & 2 deletions packages/client/lib/AccessTokenClientV1_0_11.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
GrantTypes,
IssuerOpts,
JsonURIMode,
OpenId4VCIVersion,
OpenIDResponse,
PRE_AUTH_CODE_LITERAL,
TokenErrorResponse,
Expand All @@ -24,6 +25,7 @@ import { ObjectUtils } from '@sphereon/ssi-types';
import Debug from 'debug';

import { MetadataClientV1_0_13 } from './MetadataClientV1_0_13';
import { createJwtBearerClientAssertion } from './functions';

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

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

if (asOpts?.clientId) {
request.client_id = asOpts.clientId;
if (asOpts?.clientOpts?.clientId) {
request.client_id = asOpts.clientOpts.clientId;
}
await createJwtBearerClientAssertion(request, { ...opts, version: OpenId4VCIVersion.VER_1_0_11 });

if (credentialOfferRequest?.supportedFlows.includes(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW)) {
this.assertNumericPin(this.isPinRequiredValue(credentialOfferRequest.credential_offer), pin);
Expand Down
23 changes: 21 additions & 2 deletions packages/client/lib/OpenID4VCIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Alg,
AuthorizationRequestOpts,
AuthorizationResponse,
AuthorizationServerOpts,
AuthzFlowType,
CodeChallengeMethod,
CredentialConfigurationSupported,
Expand Down Expand Up @@ -274,8 +275,9 @@ export class OpenID4VCIClient {
code?: string; // Directly pass in a code from an auth response
redirectUri?: string;
additionalRequestParams?: Record<string, any>;
asOpts?: AuthorizationServerOpts;
}): Promise<AccessTokenResponse> {
const { pin, clientId } = opts ?? {};
const { pin, clientId = this._state.clientId ?? this._state.authorizationRequestOpts?.clientId } = opts ?? {};
let { redirectUri } = opts ?? {};
if (opts?.authorizationResponse) {
this._state.authorizationCodeResponse = { ...toAuthorizationResponsePayload(opts.authorizationResponse) };
Expand All @@ -289,6 +291,23 @@ export class OpenID4VCIClient {
}
this.assertIssuerData();

const asOpts: AuthorizationServerOpts = { ...opts?.asOpts };
const kid = asOpts.clientOpts?.kid ?? this._state.kid ?? this._state.authorizationRequestOpts?.requestObjectOpts?.kid;
const clientAssertionType =
asOpts.clientOpts?.clientAssertionType ??
(kid && clientId && typeof asOpts.clientOpts?.signCallbacks === 'function'
? 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
: undefined);
if (clientId) {
asOpts.clientOpts = {
...asOpts.clientOpts,
clientId,
...(kid && { kid }),
...(clientAssertionType && { clientAssertionType }),
signCallbacks: asOpts.clientOpts?.signCallbacks ?? this._state.authorizationRequestOpts?.requestObjectOpts?.signCallbacks,
};
}

if (clientId) {
this._state.clientId = clientId;
}
Expand All @@ -312,7 +331,7 @@ export class OpenID4VCIClient {
...(!this._state.pkce.disabled && { codeVerifier: this._state.pkce.codeVerifier }),
code,
redirectUri,
asOpts: { clientId: this.clientId },
asOpts,
...(opts?.additionalRequestParams && { additionalParams: opts.additionalRequestParams }),
});

Expand Down
20 changes: 19 additions & 1 deletion packages/client/lib/OpenID4VCIClientV1_0_11.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Alg,
AuthorizationRequestOpts,
AuthorizationResponse,
AuthorizationServerOpts,
AuthzFlowType,
CodeChallengeMethod,
CredentialConfigurationSupported,
Expand Down Expand Up @@ -259,6 +260,7 @@ export class OpenID4VCIClientV1_0_11 {
authorizationResponse?: string | AuthorizationResponse; // Pass in an auth response, either as URI/redirect, or object
code?: string; // Directly pass in a code from an auth response
redirectUri?: string;
asOpts?: AuthorizationServerOpts;
}): Promise<AccessTokenResponse> {
const { pin, clientId } = opts ?? {};
let { redirectUri } = opts ?? {};
Expand Down Expand Up @@ -288,6 +290,22 @@ export class OpenID4VCIClientV1_0_11 {
if (this._state.authorizationRequestOpts?.redirectUri && !redirectUri) {
redirectUri = this._state.authorizationRequestOpts.redirectUri;
}
const asOpts: AuthorizationServerOpts = { ...opts?.asOpts };
const kid = asOpts.clientOpts?.kid ?? this._state.kid ?? this._state.authorizationRequestOpts?.requestObjectOpts?.kid;
const clientAssertionType =
asOpts.clientOpts?.clientAssertionType ??
(kid && clientId && typeof asOpts.clientOpts?.signCallbacks === 'function'
? 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
: undefined);
if (clientId) {
asOpts.clientOpts = {
...asOpts.clientOpts,
clientId,
...(kid && { kid }),
...(clientAssertionType && { clientAssertionType }),
signCallbacks: asOpts.clientOpts?.signCallbacks ?? this._state.authorizationRequestOpts?.requestObjectOpts?.signCallbacks,
};
}

const response = await accessTokenClient.acquireAccessToken({
credentialOffer: this.credentialOffer,
Expand All @@ -297,7 +315,7 @@ export class OpenID4VCIClientV1_0_11 {
...(!this._state.pkce.disabled && { codeVerifier: this._state.pkce.codeVerifier }),
code,
redirectUri,
asOpts: { clientId: this.clientId },
asOpts,
});

if (response.errorBody) {
Expand Down
20 changes: 19 additions & 1 deletion packages/client/lib/OpenID4VCIClientV1_0_13.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Alg,
AuthorizationRequestOpts,
AuthorizationResponse,
AuthorizationServerOpts,
AuthzFlowType,
CodeChallengeMethod,
CredentialConfigurationSupportedV1_0_13,
Expand Down Expand Up @@ -265,6 +266,7 @@ export class OpenID4VCIClientV1_0_13 {
authorizationResponse?: string | AuthorizationResponse; // Pass in an auth response, either as URI/redirect, or object
code?: string; // Directly pass in a code from an auth response
redirectUri?: string;
asOpts?: AuthorizationServerOpts;
}): Promise<AccessTokenResponse> {
const { pin, clientId } = opts ?? {};
let { redirectUri } = opts ?? {};
Expand All @@ -279,6 +281,22 @@ export class OpenID4VCIClientV1_0_13 {
this._state.pkce.codeVerifier = opts.codeVerifier;
}
this.assertIssuerData();
const asOpts: AuthorizationServerOpts = { ...opts?.asOpts };
const kid = asOpts.clientOpts?.kid ?? this._state.kid ?? this._state.authorizationRequestOpts?.requestObjectOpts?.kid;
const clientAssertionType =
asOpts.clientOpts?.clientAssertionType ??
(kid && clientId && typeof asOpts.clientOpts?.signCallbacks === 'function'
? 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
: undefined);
if (clientId) {
asOpts.clientOpts = {
...asOpts.clientOpts,
clientId,
...(kid && { kid }),
...(clientAssertionType && { clientAssertionType }),
signCallbacks: asOpts.clientOpts?.signCallbacks ?? this._state.authorizationRequestOpts?.requestObjectOpts?.signCallbacks,
};
}

if (clientId) {
this._state.clientId = clientId;
Expand All @@ -302,7 +320,7 @@ export class OpenID4VCIClientV1_0_13 {
...(!this._state.pkce.disabled && { codeVerifier: this._state.pkce.codeVerifier }),
code,
redirectUri,
asOpts: { clientId: this.clientId },
asOpts,
});

if (response.errorBody) {
Expand Down
45 changes: 45 additions & 0 deletions packages/client/lib/functions/AccessTokenUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { AccessTokenRequest, AccessTokenRequestOpts, Jwt, OpenId4VCIVersion } from '@sphereon/oid4vci-common';
import { v4 } from 'uuid';

import { ProofOfPossessionBuilder } from '../ProofOfPossessionBuilder';

export const createJwtBearerClientAssertion = async (
request: Partial<AccessTokenRequest>,
opts: AccessTokenRequestOpts & {
version?: OpenId4VCIVersion;
},
): Promise<void> => {
const { asOpts } = opts;
if (asOpts?.clientOpts?.clientAssertionType === 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer') {
if (!request.client_id) {
throw Error(`Not client_id supplied, but client-assertion jwt-bearer requested.`);
} else if (!asOpts.clientOpts.kid) {
throw Error(`No kid supplied, but client-assertion jwt-bearer requested.`);
} else if (!asOpts.clientOpts.signCallbacks) {
throw Error(`No sign callback supplied, but client-assertion jwt-bearer requested.`);
}
const jwt: Jwt = {
header: {
typ: 'JWT',
kid: asOpts.clientOpts.kid,
alg: asOpts.clientOpts.alg ?? 'ES256',
},
payload: {
iss: request.client_id,
sub: request.client_id,
aud: opts.credentialIssuer,
jti: v4(),
exp: Date.now() / 1000 + 60,
iat: Date.now() / 1000 - 60,
},
};
const pop = await ProofOfPossessionBuilder.fromJwt({
jwt,
callbacks: asOpts.clientOpts.signCallbacks,
version: opts.version ?? OpenId4VCIVersion.VER_1_0_13,
mode: 'jwt',
}).build();
request.client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
request.client_assertion = pop.jwt;
}
};
2 changes: 2 additions & 0 deletions packages/client/lib/functions/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './AuthorizationUtil';
export * from './notifications';
export * from './OpenIDUtils';
export * from './AccessTokenUtil';
12 changes: 10 additions & 2 deletions packages/common/lib/types/Authorization.types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CredentialOfferPayload, ProofOfPossessionCallbacks, UniformCredentialOffer } from './CredentialIssuance.types';
import { Alg, CredentialOfferPayload, ProofOfPossessionCallbacks, UniformCredentialOffer } from './CredentialIssuance.types';
import {
ErrorResponse,
IssuerCredentialSubject,
Expand Down Expand Up @@ -186,9 +186,17 @@ export interface AuthorizationServerOpts {
allowInsecureEndpoints?: boolean;
as?: string; // If not provided the issuer hostname will be used!
tokenEndpoint?: string; // Allows to override the default '/token' endpoint
clientId?: string;
clientOpts?: AuthorizationServerClientOpts;
}

export type AuthorizationServerClientOpts = {
clientId: string;
clientAssertionType?: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
kid?: string;
alg?: Alg;
signCallbacks?: ProofOfPossessionCallbacks<never>;
};

export interface IssuerOpts {
issuer: string;
tokenEndpoint?: string;
Expand Down

0 comments on commit ab4905c

Please sign in to comment.