Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: dpop support #131

Merged
merged 21 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import {
OpenId4VCIVersion,
ProofOfPossession,
} from '@sphereon/oid4vci-common'
import { uuidv4 } from '@sphereon/oid4vci-common'
import { CredentialOfferSession } from '@sphereon/oid4vci-common/dist'
import { CredentialSupportedBuilderV1_13, VcIssuer, VcIssuerBuilder } from '@sphereon/oid4vci-issuer'
import { MemoryStates } from '@sphereon/oid4vci-issuer'
import { CredentialDataSupplierResult } from '@sphereon/oid4vci-issuer/dist/types'
import { ICredential, IProofPurpose, IProofType, W3CVerifiableCredential } from '@sphereon/ssi-types'
import { DIDDocument } from 'did-resolver'
import * as jose from 'jose'
import { v4 } from 'uuid'

import { generateDid, getIssuerCallbackV1_0_11, getIssuerCallbackV1_0_13, verifyCredential } from '../IssuerCallback'

Expand Down Expand Up @@ -118,7 +118,7 @@ describe('issuerCallback', () => {
createdAt: +new Date(),
lastUpdatedAt: +new Date(),
status: IssueStatus.OFFER_CREATED,
notification_id: v4(),
notification_id: uuidv4(),
txCode: '123456',
credentialOffer: {
credential_offer: {
Expand Down
1 change: 0 additions & 1 deletion packages/callback-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
"@babel/preset-env": "^7.21.4",
"@types/jest": "^29.5.0",
"@types/node": "^18.15.3",
"@types/uuid": "^9.0.1",
"did-resolver": "^4.1.0",
"expo": "^48.0.11",
"react": "^18.2.0",
Expand Down
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (createDPoPOptions?.dPoPSigningAlgValuesSupported && createDPoPOptions?.dPoPSigningAlgValuesSupported.length > 0) {
if (createDPoPOptions?.dPoPSigningAlgValuesSupported && createDPoPOptions.dPoPSigningAlgValuesSupported.length > 0) {

const htu = requestTokenURL.split('?')[0].split('#')[0];
nklomp marked this conversation as resolved.
Show resolved Hide resolved
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 },
nklomp marked this conversation as resolved.
Show resolved Hide resolved
): 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;
nklomp marked this conversation as resolved.
Show resolved Hide resolved
}): 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];
nklomp marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we extract this into an util?

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 },
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we allow passing customHeaders here as well? So if other extensions are needed we can add it over time without needing to add them? Not sure just thinking here

): 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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is inside an opts object, would just dpop as a key make more sense here / simpler?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO not the dpop to me is the proof itself not to options for creating it.

}): 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;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this repeated quite often. Maybe we can do the htu extracting in the createDpop method. That way we can do:

const dpop = createDPoPOptions ? createDPoP() : 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
2 changes: 1 addition & 1 deletion packages/client/lib/OpenID4VCIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,7 @@ export class OpenID4VCIClient {
issuerSupportedFlowTypes(): AuthzFlowType[] {
return (
this.credentialOffer?.supportedFlows ??
(this._state.endpointMetadata?.credentialIssuerMetadata?.authorization_endpoint ?? this._state.endpointMetadata?.authorization_server
((this._state.endpointMetadata?.credentialIssuerMetadata?.authorization_endpoint ?? this._state.endpointMetadata?.authorization_server)
? [AuthzFlowType.AUTHORIZATION_CODE_FLOW]
: [])
);
Expand Down
7 changes: 3 additions & 4 deletions packages/client/lib/__tests__/SphereonE2E.spec.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import * as crypto from 'crypto';

import { Alg, Jwt, ProofOfPossessionCallbacks } from '@sphereon/oid4vci-common';
import { Alg, Jwt, ProofOfPossessionCallbacks, uuidv4 } from '@sphereon/oid4vci-common';
import { CredentialMapper } from '@sphereon/ssi-types';
import * as didts from '@transmute/did-key.js';
import { fetch } from 'cross-fetch';
import debug from 'debug';
import { importJWK, JWK, SignJWT } from 'jose';
import { v4 } from 'uuid';

import { OpenID4VCIClientV1_0_11 } from '..';

Expand Down Expand Up @@ -94,7 +93,7 @@ async function getCredentialOffer(format: 'ldp_vc' | 'jwt_vc_json'): Promise<Cre
credentials: ['GuestCredential'],
grants: {
'urn:ietf:params:oauth:grant-type:pre-authorized_code': {
'pre-authorized_code': v4().substring(0, 10),
'pre-authorized_code': uuidv4().substring(0, 10),
user_pin_required: false,
},
},
Expand Down Expand Up @@ -165,7 +164,7 @@ describe('ismapolis bug report #63, https://github.com/Sphereon-Opensource/OID4V
format: 'jwt_vc_json',
alg: Alg.ES256K,
kid: didDocument.verificationMethod[0].id,
jti: v4(),
jti: uuidv4(),
});
console.log(JSON.stringify(credentialResponse.credential));
});
Expand Down
5 changes: 2 additions & 3 deletions packages/client/lib/functions/AccessTokenUtil.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { AccessTokenRequest, AccessTokenRequestOpts, Jwt, OpenId4VCIVersion } from '@sphereon/oid4vci-common';
import { v4 } from 'uuid';
import { AccessTokenRequest, AccessTokenRequestOpts, Jwt, OpenId4VCIVersion, uuidv4 } from '@sphereon/oid4vci-common';

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

Expand Down Expand Up @@ -35,7 +34,7 @@ export const createJwtBearerClientAssertion = async (
iss: clientId,
sub: clientId,
aud: credentialIssuer,
jti: v4(),
jti: uuidv4(),
exp: Date.now() / 1000 + 60,
iat: Date.now() / 1000 - 60,
},
Expand Down
Loading