Skip to content
This repository has been archived by the owner on Oct 2, 2024. It is now read-only.

Commit

Permalink
fix: vp_token handling and nonce management incorrect in certain case…
Browse files Browse the repository at this point in the history
…s (for instance when no id token is used)
  • Loading branch information
nklomp committed Jan 22, 2024
1 parent 323a84f commit 25a61ca
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 85 deletions.
20 changes: 9 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sphereon/did-auth-siop",
"version": "0.6.0-unstable.0",
"version": "0.6.0-unstable.2",
"source": "src/index.ts",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down Expand Up @@ -28,25 +28,24 @@
"node": ">=18"
},
"dependencies": {
"@astronautlabs/jsonpath": "^1.1.2",
"@sphereon/did-uni-client": "^0.6.1",
"qs": "^6.11.2",
"@sphereon/pex": "^3.0.0",
"@sphereon/pex": "^3.0.1",
"@sphereon/pex-models": "^2.1.5",
"@sphereon/ssi-types": "^0.18.0",
"@sphereon/ssi-types": "0.18.1",
"@sphereon/wellknown-dids-client": "^0.1.3",
"@astronautlabs/jsonpath": "^1.1.2",
"sha.js": "^2.4.11",
"cross-fetch": "^4.0.0",
"did-jwt": "6.11.6",
"did-resolver": "^4.1.0",
"events": "^3.3.0",
"language-tags": "^1.0.9",
"multiformats": "^11.0.2",
"qs": "^6.11.2",
"sha.js": "^2.4.11",
"uint8arrays": "^3.1.1",
"uuid": "^9.0.0"
},
"devDependencies": {
"@types/qs": "^6.9.11",
"@digitalcredentials/did-method-key": "^2.0.3",
"@digitalcredentials/ed25519-signature-2020": "^3.0.2",
"@digitalcredentials/jsonld-signatures": "^9.3.2",
Expand All @@ -57,6 +56,8 @@
"did-resolver": "^4.1.0",
"@types/jest": "^29.5.11",
"@types/language-tags": "^1.0.4",
"@types/qs": "^6.9.11",
"@types/sha.js": "^2.4.4",
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^5.52.0",
"@typescript-eslint/parser": "^5.52.0",
Expand Down Expand Up @@ -87,10 +88,7 @@
},
"resolutions": {
"isomorphic-webcrypto": "npm:@sphereon/isomorphic-webcrypto@^2.4.0-unstable.4",
"esline/**/strip-ansi": "6.0.1",
"@sd-jwt/utils": "0.1.2-alpha.2",
"@sd-jwt/decode": "0.1.2-alpha.1",
"@sd-jwt/types": "0.1.2-alpha.3"
"esline/**/strip-ansi": "6.0.1"
},
"files": [
"dist"
Expand Down
76 changes: 55 additions & 21 deletions src/authorization-response/AuthorizationResponse.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Hasher } from '@sphereon/ssi-types';

import { AuthorizationRequest, VerifyAuthorizationRequestOpts } from '../authorization-request';
import { assertValidVerifyAuthorizationRequestOpts } from '../authorization-request/Opts';
import { IDToken } from '../id-token';
Expand All @@ -16,14 +18,14 @@ export class AuthorizationResponse {

private readonly _options?: AuthorizationResponseOpts;

constructor({
private constructor({
authorizationResponsePayload,
idToken,
responseOpts,
authorizationRequest,
}: {
authorizationResponsePayload: AuthorizationResponsePayload;
idToken: IDToken;
idToken?: IDToken;
responseOpts?: AuthorizationResponseOpts;
authorizationRequest?: AuthorizationRequest;
}) {
Expand Down Expand Up @@ -64,7 +66,7 @@ export class AuthorizationResponse {
if (responseOpts) {
assertValidResponseOpts(responseOpts);
}
const idToken = await IDToken.fromIDToken(authorizationResponsePayload.id_token);
const idToken = authorizationResponsePayload.id_token ? await IDToken.fromIDToken(authorizationResponsePayload.id_token) : undefined;
return new AuthorizationResponse({ authorizationResponsePayload, idToken, responseOpts });
}

Expand Down Expand Up @@ -99,10 +101,10 @@ export class AuthorizationResponse {
JSON.stringify(verifiedAuthorizationRequest.presentationDefinitions)
) as PresentationDefinitionWithLocation[];
const wantsIdToken = await authorizationRequest.containsResponseType(ResponseType.ID_TOKEN);
// const hasVpToken = await authorizationRequest.containsResponseType(ResponseType.VP_TOKEN);
const hasVpToken = await authorizationRequest.containsResponseType(ResponseType.VP_TOKEN);

const idToken = wantsIdToken ? await IDToken.fromVerifiedAuthorizationRequest(verifiedAuthorizationRequest, responseOpts) : undefined;
const idTokenPayload = wantsIdToken ? await idToken.payload() : undefined;
const idTokenPayload = idToken ? await idToken.payload() : undefined;
const authorizationResponsePayload = await createResponsePayload(authorizationRequest, responseOpts, idTokenPayload);
const response = new AuthorizationResponse({
authorizationResponsePayload,
Expand All @@ -111,34 +113,54 @@ export class AuthorizationResponse {
authorizationRequest,
});

const wrappedPresentations = await extractPresentationsFromAuthorizationResponse(response, { hasher: verifyOpts.hasher });
/*let nonce = idTokenPayload?.nonce
const state = response._payload.state
*/

await assertValidVerifiablePresentations({
presentationDefinitions,
presentations: wrappedPresentations,
verificationCallback: verifyOpts.verification.presentationVerificationCallback,
opts: { ...responseOpts.presentationExchange, hasher: verifyOpts.hasher },
});
if (hasVpToken) {
const wrappedPresentations = await extractPresentationsFromAuthorizationResponse(response, { hasher: verifyOpts.hasher });

await assertValidVerifiablePresentations({
presentationDefinitions,
presentations: wrappedPresentations,
verificationCallback: verifyOpts.verification.presentationVerificationCallback,
opts: { ...responseOpts.presentationExchange, hasher: verifyOpts.hasher },
});
/*if (!nonce) {
nonce = wrappedPresentations[0].decoded.nonce
}*/
}

return response;
}

public async verify(verifyOpts: VerifyAuthorizationResponseOpts): Promise<VerifiedAuthorizationResponse> {
// Merge payloads checks for inconsistencies in properties which are present in both the auth request and request object
const merged = await this.mergedPayloads(true);
const merged = await this.mergedPayloads({ consistencyCheck: true, hasher: verifyOpts.hasher });
if (verifyOpts.state && merged.state !== verifyOpts.state) {
throw Error(SIOPErrors.BAD_STATE);
}

const verifiedIdToken = await this.idToken?.verify(verifyOpts);
const oid4vp = await verifyPresentations(this, verifyOpts);

const nonce = merged.nonce ?? verifiedIdToken.payload.nonce ?? oid4vp.nonce;
const state = merged.state ?? verifiedIdToken.payload.state;

if (!state) {
throw Error(`State is required`);
} else if (oid4vp.presentationDefinitions.length > 0 && !nonce) {
throw Error('Nonce is required when using OID4VP');
}

return {
authorizationResponse: this,
verifyOpts,
nonce,
state,
correlationId: verifyOpts.correlationId,
...(this.idToken ? { idToken: verifiedIdToken } : {}),
...(oid4vp ? { oid4vpSubmission: oid4vp } : {}),
...(this.idToken && { idToken: verifiedIdToken }),
...(oid4vp && { oid4vpSubmission: oid4vp }),
};
}

Expand All @@ -154,24 +176,36 @@ export class AuthorizationResponse {
return this._options;
}

get idToken(): IDToken {
get idToken(): IDToken | undefined {
return this._idToken;
}

public async getMergedProperty<T>(key: string, consistencyCheck?: boolean): Promise<T | undefined> {
const merged = await this.mergedPayloads(consistencyCheck);
public async getMergedProperty<T>(key: string, opts?: { consistencyCheck?: boolean; hasher?: Hasher }): Promise<T | undefined> {
const merged = await this.mergedPayloads(opts);
return merged[key] as T;
}

public async mergedPayloads(consistencyCheck?: boolean): Promise<AuthorizationResponsePayload> {
public async mergedPayloads(opts?: { consistencyCheck?: boolean; hasher?: Hasher }): Promise<AuthorizationResponsePayload> {
let nonce: string | undefined = this._payload.nonce;
if (this._payload?.vp_token) {
const presentations = await extractPresentationsFromAuthorizationResponse(this, opts);
// We do not verify them, as that is done elsewhere. So we simply can take the first nonce
if (!nonce) {
nonce = presentations[0].decoded.nonce;
}
}
const idTokenPayload = await this.idToken?.payload();
if (consistencyCheck !== false && idTokenPayload) {
if (opts?.consistencyCheck !== false && idTokenPayload) {
Object.entries(idTokenPayload).forEach((entry) => {
if (typeof entry[0] === 'string' && this.payload[entry[0]] && this.payload[entry[0]] !== entry[1]) {
throw Error(`Mismatch in Authorization Request and Request object value for ${entry[0]}`);
}
});
}
return { ...this.payload, ...idTokenPayload };
if (!nonce && this._idToken) {
nonce = (await this._idToken.payload()).nonce;
}

return { ...this.payload, ...idTokenPayload, nonce };
}
}
14 changes: 10 additions & 4 deletions src/authorization-response/OpenID4VP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,8 @@ export const verifyPresentations = async (
idPayload = await authorizationResponse.idToken.payload();
}
// todo: Probably wise to check against request for the location of the submission_data
const presentationSubmission = authorizationResponse.payload.presentation_submission
? authorizationResponse.payload.presentation_submission
: idPayload?._vp_token?.presentation_submission;
const presentationSubmission = idPayload?._vp_token?.presentation_submission ?? authorizationResponse.payload.presentation_submission;

await assertValidVerifiablePresentations({
presentationDefinitions,
presentations,
Expand All @@ -54,6 +53,13 @@ export const verifyPresentations = async (
},
});

const nonces: Set<string> = new Set(presentations.map((presentation) => presentation.decoded.nonce));
if (presentations.length > 0 && nonces.size !== 1) {
throw Error(`${nonces.size} nonce values found for ${presentations.length}. Should be 1`);
}

const nonce = nonces[0];

const revocationVerification = verifyOpts.verification?.revocationOpts
? verifyOpts.verification.revocationOpts.revocationVerification
: RevocationVerification.IF_PRESENT;
Expand All @@ -65,7 +71,7 @@ export const verifyPresentations = async (
await verifyRevocation(vp, verifyOpts.verification.revocationOpts.revocationVerificationCallback, revocationVerification);
}
}
return { presentations, presentationDefinitions, submissionData: presentationSubmission };
return { nonce, presentations, presentationDefinitions, submissionData: presentationSubmission };
};

export const extractPresentationsFromAuthorizationResponse = async (
Expand Down
2 changes: 1 addition & 1 deletion src/id-token/Payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const createIDTokenPayload = async (
const rpSupportedVersions = authorizationRequestVersionDiscovery(payload);
const maxRPVersion = rpSupportedVersions.reduce(
(previous, current) => (current.valueOf() > previous.valueOf() ? current : previous),
SupportedVersion.SIOPv2_ID1
SupportedVersion.SIOPv2_D12_OID4VP_D18
);
if (responseOpts.version && rpSupportedVersions.length > 0 && !rpSupportedVersions.includes(responseOpts.version)) {
throw Error(`RP does not support spec version ${responseOpts.version}, supported versions: ${rpSupportedVersions.toString()}`);
Expand Down
2 changes: 1 addition & 1 deletion src/op/OP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export class OP {
SupportedVersion.SIOPv2_ID1
);
}
const correlationId = responseOpts?.correlationId || verifiedAuthorizationRequest.correlationId || uuidv4();
const correlationId = responseOpts?.correlationId ?? verifiedAuthorizationRequest.correlationId ?? uuidv4();
try {
// IF using DIRECT_POST, the response_uri takes precedence over the redirect_uri
let responseUri = verifiedAuthorizationRequest.responseURI;
Expand Down
17 changes: 14 additions & 3 deletions src/rp/RP.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { EventEmitter } from 'events';

import { Hasher } from '@sphereon/ssi-types';
import { v4 as uuidv4 } from 'uuid';

import {
Expand Down Expand Up @@ -136,6 +137,7 @@ export class RP {
authorizationResponsePayload: AuthorizationResponsePayload,
opts?: {
correlationId?: string;
hasher?: Hasher;
audience?: string;
state?: string;
nonce?: string;
Expand Down Expand Up @@ -296,6 +298,7 @@ export class RP {
authorizationResponse: AuthorizationResponse,
opts: {
correlationId: string;
hasher?: Hasher;
state?: string;
nonce?: string;
verification?: InternalVerification | ExternalVerification;
Expand All @@ -308,9 +311,17 @@ export class RP {
let state = opts?.state ?? this._verifyResponseOptions.state;
let nonce = opts?.nonce ?? this._verifyResponseOptions.nonce;
if (this.sessionManager) {
const resNonce = (await authorizationResponse.getMergedProperty('nonce', false)) as string;
const resState = (await authorizationResponse.getMergedProperty('state', false)) as string;
correlationId = await this.sessionManager.getCorrelationIdByNonce(resNonce, false);
const resNonce = (await authorizationResponse.getMergedProperty('nonce', {
consistencyCheck: false,
hasher: opts.hasher ?? this._verifyResponseOptions.hasher,
})) as string;
const resState = (await authorizationResponse.getMergedProperty('state', {
consistencyCheck: false,
hasher: opts.hasher ?? this._verifyResponseOptions.hasher,
})) as string;
if (resNonce && !correlationId) {
correlationId = await this.sessionManager.getCorrelationIdByNonce(resNonce, false);
}
if (!correlationId) {
correlationId = await this.sessionManager.getCorrelationIdByState(resState, false);
}
Expand Down
17 changes: 15 additions & 2 deletions src/schemas/validation/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,15 @@
import { AuthorizationRequestPayloadVD11Schema, AuthorizationRequestPayloadVID1Schema, AuthorizationResponseOptsSchema, /*CreateAuthorizationRequestOptsSchema, */RPRegistrationMetadataPayloadSchema } from './schemaValidation.js';
export { AuthorizationRequestPayloadVID1Schema, AuthorizationRequestPayloadVD11Schema, RPRegistrationMetadataPayloadSchema, /*CreateAuthorizationRequestOptsSchema, */AuthorizationResponseOptsSchema }
import {
AuthorizationRequestPayloadVD11Schema,
AuthorizationRequestPayloadVID1Schema,
AuthorizationResponseOptsSchema,
RPRegistrationMetadataPayloadSchema
/*CreateAuthorizationRequestOptsSchema, */
} from './schemaValidation.js';

export {
AuthorizationRequestPayloadVID1Schema,
AuthorizationRequestPayloadVD11Schema,
RPRegistrationMetadataPayloadSchema,
AuthorizationResponseOptsSchema
/*CreateAuthorizationRequestOptsSchema, */
};
4 changes: 4 additions & 0 deletions src/types/SIOP.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ export interface VerifiedOpenID4VPSubmission {
submissionData: PresentationSubmission;
presentationDefinitions: PresentationDefinitionWithLocation[];
presentations: WrappedVerifiablePresentation[];
nonce: string;
}

export interface VerifiedAuthorizationResponse {
Expand All @@ -580,6 +581,9 @@ export interface VerifiedAuthorizationResponse {

oid4vpSubmission?: VerifiedOpenID4VPSubmission;

nonce?: string;
state: string;

idToken?: VerifiedIDToken;
verifyOpts?: VerifyAuthorizationResponseOpts;
}
Expand Down
4 changes: 2 additions & 2 deletions test/IT.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ describe('RP and OP interaction should', () => {
const rp = RP.builder({ requestVersion: SupportedVersion.SIOPv2_ID1 })
.withClientId(WELL_KNOWN_OPENID_FEDERATION)
.withScope('test')
.withResponseType(ResponseType.ID_TOKEN)
.withResponseType([ResponseType.ID_TOKEN, ResponseType.VP_TOKEN])
.withRedirectUri(EXAMPLE_REDIRECT_URL)
.withWellknownDIDVerifyCallback(verifyCallback)
.withPresentationVerification(presentationVerificationCallback)
Expand All @@ -377,7 +377,7 @@ describe('RP and OP interaction should', () => {
client_id: rpMockEntity.did,
idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
responseTypesSupported: [ResponseType.ID_TOKEN],
responseTypesSupported: [ResponseType.ID_TOKEN, ResponseType.VP_TOKEN],
vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
subjectTypesSupported: [SubjectType.PAIRWISE],
Expand Down
Loading

0 comments on commit 25a61ca

Please sign in to comment.