Skip to content

Commit

Permalink
feat: dpop support
Browse files Browse the repository at this point in the history
  • Loading branch information
auer-martin committed Jul 25, 2024
1 parent 981e262 commit 9202667
Show file tree
Hide file tree
Showing 12 changed files with 1,881 additions and 1,502 deletions.
28 changes: 23 additions & 5 deletions packages/client/lib/AccessTokenClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
AuthorizationServerOpts,
AuthzFlowType,
convertJsonToURI,
createDPoP,
CreateDPoPClientOptions,
EndpointMetadata,
formPost,
getIssuerFromCredentialOfferPayload,
Expand All @@ -28,7 +30,7 @@ import { LOG } from './types';

export class AccessTokenClient {
public async acquireAccessToken(opts: AccessTokenRequestOpts): Promise<OpenIDResponse<AccessTokenResponse>> {
const { asOpts, pin, codeVerifier, code, redirectUri, metadata } = opts;
const { asOpts, pin, codeVerifier, code, redirectUri, metadata, createDPoPOptions } = opts;

const credentialOffer = opts.credentialOffer ? await assertedUniformCredentialOffer(opts.credentialOffer) : undefined;
const pinMetadata: TxCodeAndPinRequired | undefined = credentialOffer && this.getPinMetadata(credentialOffer.credential_offer);
Expand Down Expand Up @@ -59,6 +61,7 @@ export class AccessTokenClient {
metadata,
asOpts,
issuerOpts,
createDPoPOptions,
});
}

Expand All @@ -68,12 +71,14 @@ export class AccessTokenClient {
metadata,
asOpts,
issuerOpts,
createDPoPOptions,
}: {
accessTokenRequest: AccessTokenRequest;
pinMetadata?: TxCodeAndPinRequired;
metadata?: EndpointMetadata;
asOpts?: AuthorizationServerOpts;
issuerOpts?: IssuerOpts;
createDPoPOptions?: CreateDPoPClientOptions;
}): Promise<OpenIDResponse<AccessTokenResponse>> {
this.validate(accessTokenRequest, pinMetadata);

Expand All @@ -87,10 +92,17 @@ export class AccessTokenClient {
: undefined,
});

return this.sendAuthCode(requestTokenURL, accessTokenRequest);
let dPoP: string | undefined;
if (createDPoPOptions?.dPoPSigningAlgValuesSupported && createDPoPOptions?.dPoPSigningAlgValuesSupported.length > 0) {
const htu = requestTokenURL.split('?')[0].split('#')[0];
dPoP = createDPoPOptions
? await createDPoP({ ...createDPoPOptions, jwtPayloadProps: { ...createDPoPOptions.jwtPayloadProps, htu, htm: 'POST' } })
: undefined;
}
return this.sendAuthCode(requestTokenURL, accessTokenRequest, { dPoP });
}

public async createAccessTokenRequest(opts: AccessTokenRequestOpts): Promise<AccessTokenRequest> {
public async createAccessTokenRequest(opts: Omit<AccessTokenRequestOpts, 'createDPoPOptions'>): Promise<AccessTokenRequest> {
const { asOpts, pin, codeVerifier, code, redirectUri } = opts;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Expand Down Expand Up @@ -221,8 +233,14 @@ export class AccessTokenClient {
}
}

private async sendAuthCode(requestTokenURL: string, accessTokenRequest: AccessTokenRequest): Promise<OpenIDResponse<AccessTokenResponse>> {
return await formPost(requestTokenURL, convertJsonToURI(accessTokenRequest, { mode: JsonURIMode.X_FORM_WWW_URLENCODED }));
private async sendAuthCode(
requestTokenURL: string,
accessTokenRequest: AccessTokenRequest,
options?: { dPoP?: string },
): Promise<OpenIDResponse<AccessTokenResponse>> {
return await formPost(requestTokenURL, convertJsonToURI(accessTokenRequest, { mode: JsonURIMode.X_FORM_WWW_URLENCODED }), {
customHeaders: { ...(options?.dPoP && { dpop: options.dPoP }) },
});
}

public static determineTokenURL({
Expand Down
29 changes: 24 additions & 5 deletions packages/client/lib/AccessTokenClientV1_0_11.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
AuthorizationServerOpts,
AuthzFlowType,
convertJsonToURI,
createDPoP,
CreateDPoPClientOptions,
CredentialOfferV1_0_11,
CredentialOfferV1_0_13,
EndpointMetadata,
Expand All @@ -32,7 +34,7 @@ const debug = Debug('sphereon:oid4vci:token');

export class AccessTokenClientV1_0_11 {
public async acquireAccessToken(opts: AccessTokenRequestOpts): Promise<OpenIDResponse<AccessTokenResponse>> {
const { asOpts, pin, codeVerifier, code, redirectUri, metadata } = opts;
const { asOpts, pin, codeVerifier, code, redirectUri, metadata, createDPoPOptions } = opts;

const credentialOffer = opts.credentialOffer ? await assertedUniformCredentialOffer(opts.credentialOffer) : undefined;
const isPinRequired = credentialOffer && this.isPinRequiredValue(credentialOffer.credential_offer);
Expand Down Expand Up @@ -63,6 +65,7 @@ export class AccessTokenClientV1_0_11 {
metadata,
asOpts,
issuerOpts,
createDPoPOptions,
});
}

Expand All @@ -71,13 +74,15 @@ export class AccessTokenClientV1_0_11 {
isPinRequired,
metadata,
asOpts,
createDPoPOptions,
issuerOpts,
}: {
accessTokenRequest: AccessTokenRequest;
isPinRequired?: boolean;
metadata?: EndpointMetadata;
asOpts?: AuthorizationServerOpts;
issuerOpts?: IssuerOpts;
createDPoPOptions?: CreateDPoPClientOptions;
}): Promise<OpenIDResponse<AccessTokenResponse>> {
this.validate(accessTokenRequest, isPinRequired);

Expand All @@ -91,10 +96,18 @@ export class AccessTokenClientV1_0_11 {
: undefined,
});

return this.sendAuthCode(requestTokenURL, accessTokenRequest);
let dPoP: string | undefined;
if (createDPoPOptions?.dPoPSigningAlgValuesSupported && createDPoPOptions.dPoPSigningAlgValuesSupported.length > 0) {
const htu = requestTokenURL.split('?')[0].split('#')[0];
dPoP = createDPoPOptions
? await createDPoP({ ...createDPoPOptions, jwtPayloadProps: { ...createDPoPOptions.jwtPayloadProps, htu, htm: 'POST' } })
: undefined;
}

return this.sendAuthCode(requestTokenURL, accessTokenRequest, { dPoP });
}

public async createAccessTokenRequest(opts: AccessTokenRequestOpts): Promise<AccessTokenRequest> {
public async createAccessTokenRequest(opts: Omit<AccessTokenRequestOpts, 'createDPoPOptions'>): Promise<AccessTokenRequest> {
const { asOpts, pin, codeVerifier, code, redirectUri } = opts;
const credentialOfferRequest = opts.credentialOffer
? await toUniformCredentialOfferRequest(opts.credentialOffer as CredentialOfferV1_0_11 | CredentialOfferV1_0_13)
Expand Down Expand Up @@ -204,8 +217,14 @@ export class AccessTokenClientV1_0_11 {
}
}

private async sendAuthCode(requestTokenURL: string, accessTokenRequest: AccessTokenRequest): Promise<OpenIDResponse<AccessTokenResponse>> {
return await formPost(requestTokenURL, convertJsonToURI(accessTokenRequest, { mode: JsonURIMode.X_FORM_WWW_URLENCODED }));
private async sendAuthCode(
requestTokenURL: string,
accessTokenRequest: AccessTokenRequest,
options?: { dPoP?: string },
): Promise<OpenIDResponse<AccessTokenResponse>> {
return await formPost(requestTokenURL, convertJsonToURI(accessTokenRequest, { mode: JsonURIMode.X_FORM_WWW_URLENCODED }), {
customHeaders: { ...(options?.dPoP && { dpop: options.dPoP }) },
});
}

public static determineTokenURL({
Expand Down
2 changes: 1 addition & 1 deletion packages/client/lib/AuthorizationCodeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export const createAuthorizationRequestUrl = async ({
let { scope, authorizationDetails } = authorizationRequest;
const parMode = endpointMetadata?.credentialIssuerMetadata?.require_pushed_authorization_requests
? PARMode.REQUIRE
: authorizationRequest.parMode ?? (client_id ? PARMode.AUTO : PARMode.NEVER);
: (authorizationRequest.parMode ?? (client_id ? PARMode.AUTO : PARMode.NEVER));
// Scope and authorization_details can be used in the same authorization request
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param
if (!scope && !authorizationDetails) {
Expand Down
2 changes: 1 addition & 1 deletion packages/client/lib/AuthorizationCodeClientV1_0_11.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const createAuthorizationRequestUrlV1_0_11 = async ({

const parMode = endpointMetadata?.credentialIssuerMetadata?.require_pushed_authorization_requests
? PARMode.REQUIRE
: authorizationRequest.parMode ?? PARMode.AUTO;
: (authorizationRequest.parMode ?? PARMode.AUTO);
// Scope and authorization_details can be used in the same authorization request
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param
if (!scope && !authorizationDetails) {
Expand Down
23 changes: 21 additions & 2 deletions packages/client/lib/CredentialRequestClient.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
acquireDeferredCredential,
createDPoP,
CreateDPoPClientOptions,
CredentialRequestV1_0_13,
CredentialResponse,
getCredentialRequestForVersion,
Expand Down Expand Up @@ -89,6 +91,7 @@ export class CredentialRequestClient {
context?: string[];
format?: CredentialFormat | OID4VCICredentialFormat;
subjectIssuance?: ExperimentalSubjectIssuance;
createDPoPOptions?: CreateDPoPClientOptions;
}): Promise<OpenIDResponse<CredentialResponse> & { access_token: string }> {
const { credentialIdentifier, credentialTypes, proofInput, format, context, subjectIssuance } = opts;

Expand All @@ -101,11 +104,12 @@ export class CredentialRequestClient {
credentialIdentifier,
subjectIssuance,
});
return await this.acquireCredentialsUsingRequest(request);
return await this.acquireCredentialsUsingRequest(request, opts.createDPoPOptions);
}

public async acquireCredentialsUsingRequest(
uniformRequest: UniformCredentialRequest,
createDPoPOptions?: CreateDPoPClientOptions,
): Promise<OpenIDResponse<CredentialResponse> & { 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.');
Expand All @@ -119,7 +123,22 @@ export class CredentialRequestClient {
debug(`Acquiring credential(s) from: ${credentialEndpoint}`);
debug(`request\n: ${JSON.stringify(request, null, 2)}`);
const requestToken: string = this.credentialRequestOpts.token;
let response = (await post(credentialEndpoint, JSON.stringify(request), { bearerToken: requestToken })) as OpenIDResponse<CredentialResponse> & {

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;
}

let response = (await post(credentialEndpoint, JSON.stringify(request), {
bearerToken: requestToken,
customHeaders: { ...(createDPoPOptions && { dpop: dPoP }) },
})) as OpenIDResponse<CredentialResponse> & {
access_token: string;
};
this._isDeferred = isDeferredCredentialResponse(response);
Expand Down
23 changes: 21 additions & 2 deletions packages/client/lib/CredentialRequestClientV1_0_11.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
acquireDeferredCredential,
createDPoP,
CreateDPoPClientOptions,
CredentialResponse,
getCredentialRequestForVersion,
getUniformFormat,
Expand Down Expand Up @@ -64,15 +66,17 @@ export class CredentialRequestClientV1_0_11 {
credentialTypes?: string | string[];
context?: string[];
format?: CredentialFormat | OID4VCICredentialFormat;
createDPoPOptions?: CreateDPoPClientOptions;
}): Promise<OpenIDResponse<CredentialResponse> & { 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);
return await this.acquireCredentialsUsingRequest(request, opts.createDPoPOptions);
}

public async acquireCredentialsUsingRequest(
uniformRequest: UniformCredentialRequest,
createDPoPOptions?: CreateDPoPClientOptions,
): Promise<OpenIDResponse<CredentialResponse> & { access_token: string }> {
const request = getCredentialRequestForVersion(uniformRequest, this.version());
const credentialEndpoint: string = this.credentialRequestOpts.credentialEndpoint;
Expand All @@ -83,7 +87,22 @@ export class CredentialRequestClientV1_0_11 {
debug(`Acquiring credential(s) from: ${credentialEndpoint}`);
debug(`request\n: ${JSON.stringify(request, null, 2)}`);
const requestToken: string = this.credentialRequestOpts.token;
let response = (await post(credentialEndpoint, JSON.stringify(request), { bearerToken: requestToken })) as OpenIDResponse<CredentialResponse> & {

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;
}

let response = (await post(credentialEndpoint, JSON.stringify(request), {
bearerToken: requestToken,
customHeaders: { ...(createDPoPOptions && { dpop: dPoP }) },
})) as OpenIDResponse<CredentialResponse> & {
access_token: string;
};
this._isDeferred = isDeferredCredentialResponse(response);
Expand Down
Loading

0 comments on commit 9202667

Please sign in to comment.