From 7a221bb18cd3b4f28f7b1a333c65108ba86e8161 Mon Sep 17 00:00:00 2001 From: Niels Klomp Date: Sun, 3 Mar 2024 02:29:26 +0100 Subject: [PATCH 1/4] chore: Properly log accesstoken response in case of error --- packages/client/lib/OpenID4VCIClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index dec16797..c367f97e 100644 --- a/packages/client/lib/OpenID4VCIClient.ts +++ b/packages/client/lib/OpenID4VCIClient.ts @@ -288,7 +288,7 @@ export class OpenID4VCIClient { }); if (response.errorBody) { - debug(`Access token error:\r\n${response.errorBody}`); + debug(`Access token error:\r\n${JSON.stringify(response.errorBody)}`); throw Error( `Retrieving an access token from ${this._state.endpointMetadata?.token_endpoint} for issuer ${this.getIssuer()} failed with status: ${ response.origResponse.status From dbbe44784f60234897c1b9ccdac09259a1226066 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 11 Mar 2024 12:17:23 +0100 Subject: [PATCH 2/4] fix: type for cred request ldp Signed-off-by: Timo Glastra --- packages/common/lib/functions/CredentialRequestUtil.ts | 8 +++----- packages/common/lib/types/v1_0_11.types.ts | 9 ++------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/common/lib/functions/CredentialRequestUtil.ts b/packages/common/lib/functions/CredentialRequestUtil.ts index 55f6c137..b827e1e3 100644 --- a/packages/common/lib/functions/CredentialRequestUtil.ts +++ b/packages/common/lib/functions/CredentialRequestUtil.ts @@ -7,14 +7,12 @@ export function getTypesFromRequest(credentialRequest: UniformCredentialRequest, if (credentialRequest.format === 'jwt_vc_json' || credentialRequest.format === 'jwt_vc') { types = credentialRequest.types; } else if (credentialRequest.format === 'jwt_vc_json-ld' || credentialRequest.format === 'ldp_vc') { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore types = 'credential_definition' in credentialRequest && credentialRequest.credential_definition - ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment + ? credentialRequest.credential_definition.types + : // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - credentialRequest.credential_definition.types - : credentialRequest.types; + credentialRequest.types; } else if (credentialRequest.format === 'vc+sd-jwt') { types = [credentialRequest.vct]; } diff --git a/packages/common/lib/types/v1_0_11.types.ts b/packages/common/lib/types/v1_0_11.types.ts index 1411641d..8500da8a 100644 --- a/packages/common/lib/types/v1_0_11.types.ts +++ b/packages/common/lib/types/v1_0_11.types.ts @@ -5,9 +5,9 @@ import { CredentialIssuerMetadataOpts, CredentialOfferFormat, CredentialRequestJwtVcJson, + CredentialRequestJwtVcJsonLdAndLdpVc, CredentialRequestSdJwtVc, Grant, - JsonLdIssuerCredentialDefinition, } from './Generic.types'; import { QRCodeOpts } from './QRCode.types'; import { AuthorizationServerMetadata } from './ServerMetadata'; @@ -58,13 +58,8 @@ export interface CredentialOfferPayloadV1_0_11 { } export type CredentialRequestV1_0_11 = CommonCredentialRequest & - (CredentialRequestJwtVcJson | CredentialRequestJwtVcJsonLdAndLdpVcV1_0_11 | CredentialRequestSdJwtVc); + (CredentialRequestJwtVcJson | CredentialRequestJwtVcJsonLdAndLdpVc | CredentialRequestSdJwtVc); -export interface CredentialRequestJwtVcJsonLdAndLdpVcV1_0_11 - extends CommonCredentialRequest, - Pick { - format: 'ldp_vc' | 'jwt_vc_json-ld'; -} export interface CredentialIssuerMetadataV1_0_11 extends CredentialIssuerMetadataOpts, Partial { credential_endpoint: string; // REQUIRED. URL of the Credential Issuer's Credential Endpoint. This URL MUST use the https scheme and MAY contain port, path and query parameter components. authorization_server?: string; From 963fb88201af15ccfce189bb3ac7eedc846833d0 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 11 Mar 2024 16:53:25 +0100 Subject: [PATCH 3/4] fix: await session state updates Signed-off-by: Timo Glastra --- .../lib/__tests__/issuerCallback.spec.ts | 2 +- packages/issuer/lib/VcIssuer.ts | 19 +++++++------------ .../lib/state-manager/LookupStateManager.ts | 8 ++++---- packages/issuer/lib/tokens/index.ts | 4 ++-- 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/packages/callback-example/lib/__tests__/issuerCallback.spec.ts b/packages/callback-example/lib/__tests__/issuerCallback.spec.ts index 9ec8f93a..be47ffa6 100644 --- a/packages/callback-example/lib/__tests__/issuerCallback.spec.ts +++ b/packages/callback-example/lib/__tests__/issuerCallback.spec.ts @@ -139,7 +139,7 @@ describe('issuerCallback', () => { }) const nonces = new MemoryStates() - nonces.set('test_value', { cNonce: 'test_value', createdAt: +new Date(), issuerState: 'existing-state' }) + await nonces.set('test_value', { cNonce: 'test_value', createdAt: +new Date(), issuerState: 'existing-state' }) vcIssuer = new VcIssuerBuilder() .withAuthorizationServer('https://authorization-server') .withCredentialEndpoint('https://credential-endpoint') diff --git a/packages/issuer/lib/VcIssuer.ts b/packages/issuer/lib/VcIssuer.ts index 676b0be6..66ffbf8b 100644 --- a/packages/issuer/lib/VcIssuer.ts +++ b/packages/issuer/lib/VcIssuer.ts @@ -32,19 +32,14 @@ import { toUniformCredentialOfferRequest, TYP_ERROR, UniformCredentialRequest, - URIState + URIState, } from '@sphereon/oid4vci-common' import { CompactSdJwtVc, CredentialMapper, W3CVerifiableCredential } from '@sphereon/ssi-types' import { v4 } from 'uuid' import { assertValidPinNumber, createCredentialOfferObject, createCredentialOfferURIFromObject } from './functions' import { LookupStateManager } from './state-manager' -import { - CredentialDataSupplier, - CredentialDataSupplierArgs, - CredentialIssuanceInput, - CredentialSignerCallback -} from './types' +import { CredentialDataSupplier, CredentialDataSupplierArgs, CredentialIssuanceInput, CredentialSignerCallback } from './types' const SECOND = 1000 @@ -350,17 +345,17 @@ export class VcIssuer { throw new Error(CREDENTIAL_MISSING_ERROR) } // remove the previous nonce - this.cNonces.delete(cNonceState.cNonce) + await this.cNonces.delete(cNonceState.cNonce) if (preAuthorizedCode && preAuthSession) { preAuthSession.lastUpdatedAt = +new Date() preAuthSession.status = IssueStatus.CREDENTIAL_ISSUED - this._credentialOfferSessions.set(preAuthorizedCode, preAuthSession) + await this._credentialOfferSessions.set(preAuthorizedCode, preAuthSession) } else if (issuerState && authSession) { // If both were set we used the pre auth flow above as well, hence the else if authSession.lastUpdatedAt = +new Date() authSession.status = IssueStatus.CREDENTIAL_ISSUED - this._credentialOfferSessions.set(issuerState, authSession) + await this._credentialOfferSessions.set(issuerState, authSession) } return { @@ -390,7 +385,7 @@ export class VcIssuer { preAuthSession.lastUpdatedAt = +new Date() preAuthSession.status = IssueStatus.ERROR preAuthSession.error = error instanceof Error ? error.message : error?.toString() - this._credentialOfferSessions.set(preAuthorizedCode, preAuthSession) + await this._credentialOfferSessions.set(preAuthorizedCode, preAuthSession) } } if (issuerState) { @@ -399,7 +394,7 @@ export class VcIssuer { authSession.lastUpdatedAt = +new Date() authSession.status = IssueStatus.ERROR authSession.error = error instanceof Error ? error.message : error?.toString() - this._credentialOfferSessions.set(issuerState, authSession) + await this._credentialOfferSessions.set(issuerState, authSession) } } } diff --git a/packages/issuer/lib/state-manager/LookupStateManager.ts b/packages/issuer/lib/state-manager/LookupStateManager.ts index 975caf97..b1d3adc5 100644 --- a/packages/issuer/lib/state-manager/LookupStateManager.ts +++ b/packages/issuer/lib/state-manager/LookupStateManager.ts @@ -48,8 +48,8 @@ export class LookupStateManager implem } async delete(id: string): Promise { - return await this.assertedValueId(id).then((value) => { - this.keyValueMapper.delete(id) + return await this.assertedValueId(id).then(async (value) => { + await this.keyValueMapper.delete(id) return this.valueStateManager.delete(value) }) } @@ -68,8 +68,8 @@ export class LookupStateManager implem } async setMapped(id: string, keyValue: K, stateValue: V): Promise { - this.keyValueMapper.set(id, keyValue) - this.valueStateManager.set(id, stateValue) + await this.keyValueMapper.set(id, keyValue) + await this.valueStateManager.set(id, stateValue) } async getAsserted(id: string): Promise { diff --git a/packages/issuer/lib/tokens/index.ts b/packages/issuer/lib/tokens/index.ts index 4d9c3cc6..06222703 100644 --- a/packages/issuer/lib/tokens/index.ts +++ b/packages/issuer/lib/tokens/index.ts @@ -88,7 +88,7 @@ export const assertValidAccessTokenRequest = async ( const credentialOfferSession = await credentialOfferSessions.getAsserted(request[PRE_AUTH_CODE_LITERAL]) credentialOfferSession.status = IssueStatus.ACCESS_TOKEN_REQUESTED credentialOfferSession.lastUpdatedAt = +new Date() - credentialOfferSessions.set(request[PRE_AUTH_CODE_LITERAL], credentialOfferSession) + await credentialOfferSessions.set(request[PRE_AUTH_CODE_LITERAL], credentialOfferSession) if (!isValidGrant(credentialOfferSession, request.grant_type)) { throw new TokenError(400, TokenErrorResponse.invalid_grant, UNSUPPORTED_GRANT_TYPE_ERROR) } @@ -165,6 +165,6 @@ export const createAccessTokenResponse = async ( const credentialOfferSession = await credentialOfferSessions.getAsserted(preAuthorizedCode) credentialOfferSession.status = IssueStatus.ACCESS_TOKEN_CREATED credentialOfferSession.lastUpdatedAt = +new Date() - credentialOfferSessions.set(preAuthorizedCode, credentialOfferSession) + await credentialOfferSessions.set(preAuthorizedCode, credentialOfferSession) return response } From 3b4a7350de91ee01880d93e6100e68353b700d98 Mon Sep 17 00:00:00 2001 From: Niels Klomp Date: Tue, 12 Mar 2024 01:06:41 +0100 Subject: [PATCH 4/4] chore: store authorization response in state --- packages/client/lib/OpenID4VCIClient.ts | 11 +- packages/common/lib/types/OpenID4VCIErrors.ts | 6 +- packages/issuer/lib/VcIssuer.ts | 9 +- .../issuer/lib/__tests__/VcIssuer.spec.ts | 183 +++++++++--------- 4 files changed, 104 insertions(+), 105 deletions(-) diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index c367f97e..596ffedc 100644 --- a/packages/client/lib/OpenID4VCIClient.ts +++ b/packages/client/lib/OpenID4VCIClient.ts @@ -46,6 +46,7 @@ export interface OpenID4VCIClientState { endpointMetadata?: EndpointMetadataResult; accessTokenResponse?: AccessTokenResponse; authorizationRequestOpts?: AuthorizationRequestOpts; + authorizationCodeResponse?: AuthorizationResponse; pkce: PKCEOpts; authorizationURL?: string; } @@ -65,6 +66,7 @@ export class OpenID4VCIClient { endpointMetadata, accessTokenResponse, authorizationRequestOpts, + authorizationCodeResponse, authorizationURL, }: { credentialOffer?: CredentialOfferRequestWithBaseUrl; @@ -78,6 +80,7 @@ export class OpenID4VCIClient { endpointMetadata?: EndpointMetadataResult; accessTokenResponse?: AccessTokenResponse; authorizationRequestOpts?: AuthorizationRequestOpts; + authorizationCodeResponse?: AuthorizationResponse; authorizationURL?: string; }) { const issuer = credentialIssuer ?? (credentialOffer ? getIssuerFromCredentialOfferPayload(credentialOffer.credential_offer) : undefined); @@ -93,6 +96,7 @@ export class OpenID4VCIClient { clientId: clientId ?? (credentialOffer && getClientIdFromCredentialOfferPayload(credentialOffer.credential_offer)) ?? kid?.split('#')[0], pkce: { disabled: false, codeChallengeMethod: CodeChallengeMethod.S256, ...pkce }, authorizationRequestOpts, + authorizationCodeResponse, jwk, endpointMetadata, accessTokenResponse, @@ -254,7 +258,12 @@ export class OpenID4VCIClient { }): Promise { const { pin, clientId } = opts ?? {}; let { redirectUri } = opts ?? {}; - const code = opts?.code ?? (opts?.authorizationResponse ? toAuthorizationResponsePayload(opts.authorizationResponse).code : undefined); + if (opts?.authorizationResponse) { + this._state.authorizationCodeResponse = { ...toAuthorizationResponsePayload(opts.authorizationResponse) }; + } else if (opts?.code) { + this._state.authorizationCodeResponse = { code: opts.code }; + } + const code = this._state.authorizationCodeResponse?.code; if (opts?.codeVerifier) { this._state.pkce.codeVerifier = opts.codeVerifier; diff --git a/packages/common/lib/types/OpenID4VCIErrors.ts b/packages/common/lib/types/OpenID4VCIErrors.ts index 48432911..5951b5e2 100644 --- a/packages/common/lib/types/OpenID4VCIErrors.ts +++ b/packages/common/lib/types/OpenID4VCIErrors.ts @@ -1,4 +1,4 @@ -import { Alg } from './CredentialIssuance.types' +import { Alg } from './CredentialIssuance.types'; export const BAD_PARAMS = 'Wrong parameters provided'; export const URL_NOT_VALID = 'Request url is not valid'; @@ -6,7 +6,9 @@ export const JWS_NOT_VALID = 'JWS is not valid'; export const PROOF_CANT_BE_CONSTRUCTED = "Proof can't be constructed."; export const NO_JWT_PROVIDED = 'No JWT provided'; export const TYP_ERROR = 'Typ must be "openid4vci-proof+jwt"'; -export const ALG_ERROR = `Algorithm is a required field, you are free to use the signing algorithm of your choice or one of the following: ${Object.keys(Alg).join(', ')}`; +export const ALG_ERROR = `Algorithm is a required field, you are free to use the signing algorithm of your choice or one of the following: ${Object.keys( + Alg, +).join(', ')}`; export const KID_JWK_X5C_ERROR = 'Only one must be present: kid, jwk or x5c'; export const KID_DID_NO_DID_ERROR = 'A DID value needs to be returned when kid is present'; export const DID_NO_DIDDOC_ERROR = 'A DID Document needs to be resolved when a DID is encountered'; diff --git a/packages/issuer/lib/VcIssuer.ts b/packages/issuer/lib/VcIssuer.ts index 676b0be6..97c3fc4d 100644 --- a/packages/issuer/lib/VcIssuer.ts +++ b/packages/issuer/lib/VcIssuer.ts @@ -32,19 +32,14 @@ import { toUniformCredentialOfferRequest, TYP_ERROR, UniformCredentialRequest, - URIState + URIState, } from '@sphereon/oid4vci-common' import { CompactSdJwtVc, CredentialMapper, W3CVerifiableCredential } from '@sphereon/ssi-types' import { v4 } from 'uuid' import { assertValidPinNumber, createCredentialOfferObject, createCredentialOfferURIFromObject } from './functions' import { LookupStateManager } from './state-manager' -import { - CredentialDataSupplier, - CredentialDataSupplierArgs, - CredentialIssuanceInput, - CredentialSignerCallback -} from './types' +import { CredentialDataSupplier, CredentialDataSupplierArgs, CredentialIssuanceInput, CredentialSignerCallback } from './types' const SECOND = 1000 diff --git a/packages/issuer/lib/__tests__/VcIssuer.spec.ts b/packages/issuer/lib/__tests__/VcIssuer.spec.ts index 26bcbe76..4b4b08d3 100644 --- a/packages/issuer/lib/__tests__/VcIssuer.spec.ts +++ b/packages/issuer/lib/__tests__/VcIssuer.spec.ts @@ -6,7 +6,7 @@ import { CredentialSupported, IssuerCredentialSubjectDisplay, IssueStatus, - STATE_MISSING_ERROR + STATE_MISSING_ERROR, } from '@sphereon/oid4vci-common' import { IProofPurpose, IProofType } from '@sphereon/ssi-types' import { DIDDocument } from 'did-resolver' @@ -18,24 +18,18 @@ import { MemoryStates } from '../state-manager' const IDENTIPROOF_ISSUER_URL = 'https://issuer.research.identiproof.io' const verifiableCredential = { - '@context': [ - 'https://www.w3.org/2018/credentials/v1', - 'https://w3id.org/security/suites/jws-2020/v1' - ], + '@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/security/suites/jws-2020/v1'], id: 'http://university.example/credentials/1872', - type: [ - 'VerifiableCredential', - 'ExampleAlumniCredential' - ], + type: ['VerifiableCredential', 'ExampleAlumniCredential'], issuer: 'https://university.example/issuers/565049', issuanceDate: new Date().toISOString(), credentialSubject: { id: 'did:example:ebfeb1f712ebc6f1c276e12ec21', alumniOf: { id: 'did:example:c276e12ec21ebfeb1f712ebc6f1', - name: 'Example University' - } - } + name: 'Example University', + }, + }, } describe('VcIssuer', () => { @@ -275,27 +269,26 @@ describe('VcIssuer', () => { // Of course this doesn't work. The state is part of the proof to begin with it('should fail issuing credential if an invalid state is used', async () => { jwtVerifyCallback.mockResolvedValue({ - did: 'did:example:1234', - kid: 'did:example:1234#auth', - alg: Alg.ES256K, - didDocument: { - '@context': 'https://www.w3.org/ns/did/v1', - id: 'did:example:1234', + did: 'did:example:1234', + kid: 'did:example:1234#auth', + alg: Alg.ES256K, + didDocument: { + '@context': 'https://www.w3.org/ns/did/v1', + id: 'did:example:1234', + }, + jwt: { + header: { + typ: 'openid4vci-proof+jwt', + alg: Alg.ES256K, + kid: 'test-kid', }, - jwt: { - header: { - typ: 'openid4vci-proof+jwt', - alg: Alg.ES256K, - kid: 'test-kid', - }, - payload: { - aud: IDENTIPROOF_ISSUER_URL, - iat: +new Date(), - nonce: 'test-nonce', - }, - } - } - ) + payload: { + aud: IDENTIPROOF_ISSUER_URL, + iat: +new Date(), + nonce: 'test-nonce', + }, + }, + }) await expect( vcIssuer.issueCredential({ @@ -314,33 +307,32 @@ describe('VcIssuer', () => { it.each([...Object.values(Alg), 'CUSTOM'])('should issue %s signed credential if a valid state is passed in', async (alg: string) => { jwtVerifyCallback.mockResolvedValue({ - did: 'did:example:1234', - kid: 'did:example:1234#auth', - alg: alg, - didDocument: { - '@context': 'https://www.w3.org/ns/did/v1', - id: 'did:example:1234', + did: 'did:example:1234', + kid: 'did:example:1234#auth', + alg: alg, + didDocument: { + '@context': 'https://www.w3.org/ns/did/v1', + id: 'did:example:1234', + }, + jwt: { + header: { + typ: 'openid4vci-proof+jwt', + alg: alg, + kid: 'test-kid', }, - jwt: { - header: { - typ: 'openid4vci-proof+jwt', - alg: alg, - kid: 'test-kid', - }, - payload: { - aud: IDENTIPROOF_ISSUER_URL, - iat: +new Date(), - nonce: 'test-nonce', - }, - } - } - ) + payload: { + aud: IDENTIPROOF_ISSUER_URL, + iat: +new Date(), + nonce: 'test-nonce', + }, + }, + }) - let createdAt = +new Date() + const createdAt = +new Date() await vcIssuer.cNonces.set('test-nonce', { cNonce: 'test-nonce', preAuthorizedCode: 'test-pre-authorized-code', - createdAt: createdAt + createdAt: createdAt, }) await vcIssuer.credentialOfferSessions.set('test-pre-authorized-code', { createdAt: createdAt, @@ -348,11 +340,11 @@ describe('VcIssuer', () => { credentialOffer: { credential_offer: { credential_issuer: 'did:key:test', - credentials: [] - } + credentials: [], + }, }, lastUpdatedAt: createdAt, - status: IssueStatus.ACCESS_TOKEN_CREATED + status: IssueStatus.ACCESS_TOKEN_CREATED, }) expect( @@ -363,11 +355,11 @@ describe('VcIssuer', () => { format: 'jwt_vc_json', proof: { proof_type: 'jwt', - jwt: 'ye.ye.ye' - } + jwt: 'ye.ye.ye', + }, }, - newCNonce: 'new-test-nonce' - }) + newCNonce: 'new-test-nonce', + }), ).resolves.toEqual({ c_nonce: 'new-test-nonce', c_nonce_expires_in: 300000, @@ -381,54 +373,55 @@ describe('VcIssuer', () => { jwt: 'ye.ye.ye', proofPurpose: 'assertionMethod', type: 'JwtProof2020', - verificationMethod: 'sdfsdfasdfasdfasdfasdfassdfasdf' + verificationMethod: 'sdfsdfasdfasdfasdfasdfassdfasdf', }, - type: ['VerifiableCredential'] + type: ['VerifiableCredential'], }, - format: 'jwt_vc_json' + format: 'jwt_vc_json', }) }) it('should fail issuing credential if the signing algorithm is missing', async () => { - let createdAt = +new Date() + const createdAt = +new Date() await vcIssuer.cNonces.set('test-nonce', { cNonce: 'test-nonce', preAuthorizedCode: 'test-pre-authorized-code', - createdAt: createdAt + createdAt: createdAt, }) jwtVerifyCallback.mockResolvedValue({ - did: 'did:example:1234', - kid: 'did:example:1234#auth', - alg: undefined, - didDocument: { - '@context': 'https://www.w3.org/ns/did/v1', - id: 'did:example:1234', + did: 'did:example:1234', + kid: 'did:example:1234#auth', + alg: undefined, + didDocument: { + '@context': 'https://www.w3.org/ns/did/v1', + id: 'did:example:1234', + }, + jwt: { + header: { + typ: 'openid4vci-proof+jwt', + alg: undefined, + kid: 'test-kid', }, - jwt: { - header: { - typ: 'openid4vci-proof+jwt', - alg: undefined, - kid: 'test-kid', - }, - payload: { - aud: IDENTIPROOF_ISSUER_URL, - iat: +new Date(), - nonce: 'test-nonce', - }, - } - } - ) - - expect(vcIssuer.issueCredential({ - credentialRequest: { - types: ['VerifiableCredential'], - format: 'jwt_vc_json', - proof: { - proof_type: 'jwt', - jwt: 'ye.ye.ye', + payload: { + aud: IDENTIPROOF_ISSUER_URL, + iat: +new Date(), + nonce: 'test-nonce', }, }, - })).rejects.toThrow(Error(ALG_ERROR)) + }) + + expect( + vcIssuer.issueCredential({ + credentialRequest: { + types: ['VerifiableCredential'], + format: 'jwt_vc_json', + proof: { + proof_type: 'jwt', + jwt: 'ye.ye.ye', + }, + }, + }), + ).rejects.toThrow(Error(ALG_ERROR)) }) })