Skip to content

Commit

Permalink
feat: incorporate feedback part1
Browse files Browse the repository at this point in the history
  • Loading branch information
auer-martin committed Jul 29, 2024
1 parent 5034468 commit f30475a
Show file tree
Hide file tree
Showing 12 changed files with 209 additions and 142 deletions.
22 changes: 11 additions & 11 deletions packages/client/lib/AccessTokenClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
AuthzFlowType,
convertJsonToURI,
createDPoP,
CreateDPoPClientOptions,
CreateDPoPClientOpts,
EndpointMetadata,
formPost,
getIssuerFromCredentialOfferPayload,
Expand All @@ -30,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, createDPoPOptions } = opts;
const { asOpts, pin, codeVerifier, code, redirectUri, metadata, createDPoPOpts } = opts;

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

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

Expand All @@ -93,16 +93,16 @@ export class AccessTokenClient {
});

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

public async createAccessTokenRequest(opts: Omit<AccessTokenRequestOpts, 'createDPoPOptions'>): Promise<AccessTokenRequest> {
public async createAccessTokenRequest(opts: Omit<AccessTokenRequestOpts, 'createDPoPOpts'>): Promise<AccessTokenRequest> {
const { asOpts, pin, codeVerifier, code, redirectUri } = opts;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Expand Down Expand Up @@ -236,10 +236,10 @@ export class AccessTokenClient {
private async sendAuthCode(
requestTokenURL: string,
accessTokenRequest: AccessTokenRequest,
options?: { dPoP?: string },
opts?: { dPoP?: string },
): Promise<OpenIDResponse<AccessTokenResponse>> {
return await formPost(requestTokenURL, convertJsonToURI(accessTokenRequest, { mode: JsonURIMode.X_FORM_WWW_URLENCODED }), {
customHeaders: { ...(options?.dPoP && { dpop: options.dPoP }) },
customHeaders: { ...(opts?.dPoP && { dpop: opts.dPoP }) },
});
}

Expand Down
22 changes: 11 additions & 11 deletions packages/client/lib/AccessTokenClientV1_0_11.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
AuthzFlowType,
convertJsonToURI,
createDPoP,
CreateDPoPClientOptions,
CreateDPoPClientOpts,
CredentialOfferV1_0_11,
CredentialOfferV1_0_13,
EndpointMetadata,
Expand All @@ -34,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, createDPoPOptions } = opts;
const { asOpts, pin, codeVerifier, code, redirectUri, metadata, createDPoPOpts } = opts;

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

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

Expand All @@ -97,17 +97,17 @@ export class AccessTokenClientV1_0_11 {
});

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

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

public async createAccessTokenRequest(opts: Omit<AccessTokenRequestOpts, 'createDPoPOptions'>): Promise<AccessTokenRequest> {
public async createAccessTokenRequest(opts: Omit<AccessTokenRequestOpts, 'createDPoPOpts'>): 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 @@ -220,10 +220,10 @@ export class AccessTokenClientV1_0_11 {
private async sendAuthCode(
requestTokenURL: string,
accessTokenRequest: AccessTokenRequest,
options?: { dPoP?: string },
opts?: { dPoP?: string },
): Promise<OpenIDResponse<AccessTokenResponse>> {
return await formPost(requestTokenURL, convertJsonToURI(accessTokenRequest, { mode: JsonURIMode.X_FORM_WWW_URLENCODED }), {
customHeaders: { ...(options?.dPoP && { dpop: options.dPoP }) },
customHeaders: { ...(opts?.dPoP && { dpop: opts.dPoP }) },
});
}

Expand Down
18 changes: 9 additions & 9 deletions packages/client/lib/CredentialRequestClient.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
acquireDeferredCredential,
createDPoP,
CreateDPoPClientOptions,
CreateDPoPClientOpts,
CredentialRequestV1_0_13,
CredentialResponse,
getCredentialRequestForVersion,
Expand Down Expand Up @@ -91,7 +91,7 @@ export class CredentialRequestClient {
context?: string[];
format?: CredentialFormat | OID4VCICredentialFormat;
subjectIssuance?: ExperimentalSubjectIssuance;
createDPoPOptions?: CreateDPoPClientOptions;
createDPoPOps?: CreateDPoPClientOpts;
}): Promise<OpenIDResponse<CredentialResponse> & { access_token: string }> {
const { credentialIdentifier, credentialTypes, proofInput, format, context, subjectIssuance } = opts;

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

public async acquireCredentialsUsingRequest(
uniformRequest: UniformCredentialRequest,
createDPoPOptions?: CreateDPoPClientOptions,
createDPoPOps?: CreateDPoPClientOpts,
): 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 @@ -125,19 +125,19 @@ export class CredentialRequestClient {
const requestToken: string = this.credentialRequestOpts.token;

let dPoP: string | undefined;
if (createDPoPOptions) {
if (createDPoPOps) {
const htu = credentialEndpoint.split('?')[0].split('#')[0];
dPoP = createDPoPOptions
dPoP = createDPoPOps
? await createDPoP({
...createDPoPOptions,
jwtPayloadProps: { ...createDPoPOptions.jwtPayloadProps, htu, htm: 'POST', accessToken: requestToken },
...createDPoPOps,
jwtPayloadProps: { ...createDPoPOps.jwtPayloadProps, htu, htm: 'POST', accessToken: requestToken },
})
: undefined;
}

let response = (await post(credentialEndpoint, JSON.stringify(request), {
bearerToken: requestToken,
customHeaders: { ...(createDPoPOptions && { dpop: dPoP }) },
customHeaders: { ...(createDPoPOps && { dpop: dPoP }) },
})) as OpenIDResponse<CredentialResponse> & {
access_token: string;
};
Expand Down
6 changes: 3 additions & 3 deletions packages/client/lib/CredentialRequestClientV1_0_11.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
acquireDeferredCredential,
createDPoP,
CreateDPoPClientOptions,
CreateDPoPClientOpts,
CredentialResponse,
getCredentialRequestForVersion,
getUniformFormat,
Expand Down Expand Up @@ -66,7 +66,7 @@ export class CredentialRequestClientV1_0_11 {
credentialTypes?: string | string[];
context?: string[];
format?: CredentialFormat | OID4VCICredentialFormat;
createDPoPOptions?: CreateDPoPClientOptions;
createDPoPOptions?: CreateDPoPClientOpts;
}): Promise<OpenIDResponse<CredentialResponse> & { access_token: string }> {
const { credentialTypes, proofInput, format, context } = opts;

Expand All @@ -76,7 +76,7 @@ export class CredentialRequestClientV1_0_11 {

public async acquireCredentialsUsingRequest(
uniformRequest: UniformCredentialRequest,
createDPoPOptions?: CreateDPoPClientOptions,
createDPoPOptions?: CreateDPoPClientOpts,
): Promise<OpenIDResponse<CredentialResponse> & { access_token: string }> {
const request = getCredentialRequestForVersion(uniformRequest, this.version());
const credentialEndpoint: string = this.credentialRequestOpts.credentialEndpoint;
Expand Down
45 changes: 33 additions & 12 deletions packages/common/lib/functions/DPoP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,32 @@ import { JWK } from '../types/CredentialIssuance.types';
import { calculateJwkThumbprint } from './JwkThumbprint';
import { CreateJwtCallback, JwtIssuerJwk } from './JwtIssuer';
import { VerifyJwtCallbackBase } from './JwtVerifier';

import { parseJWT } from './jwtUtils';

export interface DPoPJwtIssuerWithContext extends JwtIssuerJwk {
type: 'dpop';
dPoPSigningAlgValuesSupported?: string[];
}

/**
* The maximum allowed clock skew time in seconds. If an time based validation
* is performed against current time (`now`), the validation can be of by the skew
* time.
*
* See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5
*/
const DEFAULT_SKEW_TIME = 300;

function getNowSkewed(now?: number, skewTime?: number) {
const _now = now ? now : epochTime();
const _skewTime = skewTime ? skewTime : DEFAULT_SKEW_TIME;

return {
nowSkewedPast: _now - _skewTime,
nowSkewedFuture: _now + _skewTime,
};
}

/**
* Returns the current unix timestamp in seconds.
*/
Expand All @@ -34,27 +53,27 @@ export type DPoPJwtPayloadProps = {
export type DPoPJwtHeaderProps = { typ: 'dpop+jwt'; alg: SigningAlgo; jwk: JWK };
export type CreateDPoPJwtPayloadProps = Omit<DPoPJwtPayloadProps, 'iat' | 'jti' | 'ath'> & { accessToken?: string };

export interface CreateDPoPOptions<JwtPayloadProps = CreateDPoPJwtPayloadProps> {
export interface CreateDPoPOpts<JwtPayloadProps = CreateDPoPJwtPayloadProps> {
createJwtCallback: CreateJwtCallback<DPoPJwtIssuerWithContext>;
jwtIssuer: Omit<JwtIssuerJwk, 'method' | 'type'>;
jwtPayloadProps: Record<string, unknown> & JwtPayloadProps;
dPoPSigningAlgValuesSupported?: string[];
dPoPSigningAlgValuesSupported?: (string | SigningAlgo)[];
}

export type CreateDPoPClientOptions = CreateDPoPOptions<Omit<CreateDPoPJwtPayloadProps, 'htm' | 'htu'>>;
export type CreateDPoPClientOpts = CreateDPoPOpts<Omit<CreateDPoPJwtPayloadProps, 'htm' | 'htu'>>;

export async function createDPoP(options: CreateDPoPOptions): Promise<string> {
export async function createDPoP(options: CreateDPoPOpts): Promise<string> {
const { createJwtCallback, jwtIssuer, jwtPayloadProps, dPoPSigningAlgValuesSupported } = options;

if (jwtPayloadProps.accessToken && (jwtPayloadProps.accessToken?.startsWith('DPoP ') || jwtPayloadProps.accessToken?.startsWith('Bearer '))) {
throw new Error('expected accessToken without scheme');
throw new Error('expected access token without scheme');
}

const ath = jwtPayloadProps.accessToken ? u8a.toString(SHA('sha256').update(jwtPayloadProps.accessToken).digest(), 'base64url') : undefined;
return createJwtCallback(
{ method: 'jwk', type: 'dpop', alg: jwtIssuer.alg, jwk: jwtIssuer.jwk, dPoPSigningAlgValuesSupported },
{
header: { ...jwtIssuer, typ: 'dpop+jwt', alg: jwtIssuer.alg, jwk: jwtIssuer.jwk, },
header: { ...jwtIssuer, typ: 'dpop+jwt', alg: jwtIssuer.alg, jwk: jwtIssuer.jwk },
payload: {
...jwtPayloadProps,
iat: epochTime(),
Expand All @@ -68,7 +87,7 @@ export async function createDPoP(options: CreateDPoPOptions): Promise<string> {
export type DPoPVerifyJwtCallback = VerifyJwtCallbackBase<JwtIssuerJwk & { type: 'dpop' }>;
export interface DPoPVerifyOptions {
expectedNonce?: string;
acceptedAlgorithms?: SigningAlgo[];
acceptedAlgorithms?: (string | SigningAlgo)[];
// defaults to 300 seconds (5 minutes)
maxIatAgeInSeconds?: number;
expectAccessToken?: boolean;
Expand All @@ -86,8 +105,7 @@ export async function verifyDPoP(
}

// The DPoP HTTP request header field value is a single and well-formed JWT.
const dPoPHeader = jwtDecode<JwtHeader & Partial<DPoPJwtHeaderProps>>(dpop, { header: true });
const dPoPPayload = jwtDecode<JwtPayload & Partial<DPoPJwtPayloadProps>>(dpop, { header: false });
const { header: dPoPHeader, payload: dPoPPayload } = parseJWT<JwtHeader, JwtPayload & Partial<DPoPJwtPayloadProps>>(dpop);

// Ensure all required header claims are present
if (dPoPHeader.typ !== 'dpop+jwt' || !dPoPHeader.alg || !dPoPHeader.jwk || typeof dPoPHeader.jwk !== 'object' || dPoPHeader.jwk.d) {
Expand Down Expand Up @@ -149,8 +167,11 @@ export async function verifyDPoP(
}

// Validate iat claim
const now = epochTime();
if (dPoPPayload.iat > now + (options.maxIatAgeInSeconds ?? 300) || dPoPPayload.iat < now - (options.maxIatAgeInSeconds ?? 300)) {
const { nowSkewedPast, nowSkewedFuture } = getNowSkewed();
if (
dPoPPayload.iat > nowSkewedFuture + (options.maxIatAgeInSeconds ?? 300) ||
dPoPPayload.iat < nowSkewedPast - (options.maxIatAgeInSeconds ?? 300)
) {
// 5 minute window
throw new Error('invalid_dpop_proof. Invalid iat claim');
}
Expand Down
10 changes: 5 additions & 5 deletions packages/common/lib/functions/JwtIssuer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { JwtHeader, JwtPayload, JwtProtectionMethod, SigningAlgo } from '..';
import { JWK, JwtHeader, JwtPayload, JwtProtectionMethod, SigningAlgo } from '..';

export interface JwtIssuerBase {
method: JwtProtectionMethod;
Expand All @@ -11,12 +11,12 @@ export interface JwtIssuerBase {
export interface JwtIssuerDid extends JwtIssuerBase {
method: 'did';
didUrl: string;
alg: SigningAlgo;
alg: SigningAlgo | string;
}

export interface JwtIssuerX5c extends JwtIssuerBase {
method: 'x5c';
alg: SigningAlgo;
alg: SigningAlgo | string;

/**
*
Expand All @@ -42,8 +42,8 @@ export interface JwtIssuerX5c extends JwtIssuerBase {

export interface JwtIssuerJwk extends JwtIssuerBase {
method: 'jwk';
alg: SigningAlgo;
jwk: JsonWebKey;
alg: SigningAlgo | string;
jwk: JWK;
}

export interface JwtIssuerCustom extends JwtIssuerBase {
Expand Down
Loading

0 comments on commit f30475a

Please sign in to comment.