diff --git a/packages/callback-example/lib/__tests__/issuerCallback.spec.ts b/packages/callback-example/lib/__tests__/issuerCallback.spec.ts index 2311e217..12b1168f 100644 --- a/packages/callback-example/lib/__tests__/issuerCallback.spec.ts +++ b/packages/callback-example/lib/__tests__/issuerCallback.spec.ts @@ -7,6 +7,7 @@ import { CredentialSupported, IssuerCredentialSubjectDisplay, Jwt, + OpenId4VCIVersion, ProofOfPossession, } from '@sphereon/oid4vci-common' import { CredentialOfferSession } from '@sphereon/oid4vci-common/dist' @@ -200,6 +201,7 @@ xdescribe('issuerCallback', () => { callbacks: { signCallback: proofOfPossessionCallbackFunction, }, + version: OpenId4VCIVersion.VER_1_0_08, }) .withClientId(clientId) .withKid(kid) @@ -210,6 +212,7 @@ xdescribe('issuerCallback', () => { credentialTypes: ['VerifiableCredential'], format: 'jwt_vc_json', proofInput: proof, + version: OpenId4VCIVersion.VER_1_0_08, }) expect(credentialRequest).toEqual({ format: 'jwt_vc_json', diff --git a/packages/client/lib/AccessTokenClient.ts b/packages/client/lib/AccessTokenClient.ts index 73e25705..5b669e39 100644 --- a/packages/client/lib/AccessTokenClient.ts +++ b/packages/client/lib/AccessTokenClient.ts @@ -233,7 +233,7 @@ export class AccessTokenClient { } private static creatTokenURLFromURL(url: string, allowInsecureEndpoints?: boolean, tokenEndpoint?: string): string { - if (allowInsecureEndpoints !== true && url.startsWith('http://')) { + if (allowInsecureEndpoints !== true && url.startsWith('http:')) { throw Error(`Unprotected token endpoints are not allowed ${url}. Adjust settings if you really need this (dev/test settings only!!)`); } const hostname = url.replace(/https?:\/\//, '').replace(/\/$/, ''); diff --git a/packages/client/lib/CredentialRequestClient.ts b/packages/client/lib/CredentialRequestClient.ts index 94857ca0..aa4c21d4 100644 --- a/packages/client/lib/CredentialRequestClient.ts +++ b/packages/client/lib/CredentialRequestClient.ts @@ -23,7 +23,7 @@ export interface CredentialRequestOpts { format?: CredentialFormat | OID4VCICredentialFormat; proof: ProofOfPossession; token: string; - version?: OpenId4VCIVersion; + version: OpenId4VCIVersion; } export class CredentialRequestClient { @@ -48,7 +48,7 @@ export class CredentialRequestClient { }): Promise> { const { credentialTypes, proofInput, format } = opts; - const request = await this.createCredentialRequest({ proofInput, credentialTypes, format }); + const request = await this.createCredentialRequest({ proofInput, credentialTypes, format, version: this.version() }); return await this.acquireCredentialsUsingRequest(request); } @@ -77,6 +77,7 @@ export class CredentialRequestClient { proofInput: ProofOfPossessionBuilder | ProofOfPossession; credentialTypes?: string | string[]; format?: CredentialFormat | OID4VCICredentialFormat; + version: OpenId4VCIVersion; }): Promise { const { proofInput } = opts; const formatSelection = opts.format ?? this.credentialRequestOpts.format; @@ -100,7 +101,9 @@ export class CredentialRequestClient { } const proof = - 'proof_type' in proofInput ? await ProofOfPossessionBuilder.fromProof(proofInput as ProofOfPossession).build() : await proofInput.build(); + 'proof_type' in proofInput + ? await ProofOfPossessionBuilder.fromProof(proofInput as ProofOfPossession, opts.version).build() + : await proofInput.build(); return { types, format, @@ -108,7 +111,10 @@ export class CredentialRequestClient { } as UniformCredentialRequest; } + private version(): OpenId4VCIVersion { + return this.credentialRequestOpts?.version ?? OpenId4VCIVersion.VER_1_0_11; + } private isV11OrHigher(): boolean { - return !this.credentialRequestOpts.version || this.credentialRequestOpts.version >= OpenId4VCIVersion.VER_1_0_11; + return this.version() >= OpenId4VCIVersion.VER_1_0_11; } } diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index 3eec68db..fb06bdfa 100644 --- a/packages/client/lib/OpenID4VCIClient.ts +++ b/packages/client/lib/OpenID4VCIClient.ts @@ -293,6 +293,9 @@ export class OpenID4VCIClient { for (const type of types) { let typeSupported = false; for (const credentialSupported of metadata.credentials_supported) { + if (!credentialSupported.types || credentialSupported.types.length === 0) { + throw Error('types is required in the credentials supported'); + } if (credentialSupported.types.indexOf(type) != -1) { typeSupported = true; } @@ -313,12 +316,15 @@ export class OpenID4VCIClient { const proofBuilder = ProofOfPossessionBuilder.fromAccessTokenResponse({ accessTokenResponse: this.accessTokenResponse, callbacks: proofCallbacks, + version: this.version(), }) .withIssuer(this.getIssuer()) .withAlg(this.alg) - .withClientId(this.clientId) .withKid(this.kid); + if (this._clientId) { + proofBuilder.withClientId(this.clientId); + } if (jti) { proofBuilder.withJti(jti); } diff --git a/packages/client/lib/ProofOfPossessionBuilder.ts b/packages/client/lib/ProofOfPossessionBuilder.ts index 80e8163a..a6eabc12 100644 --- a/packages/client/lib/ProofOfPossessionBuilder.ts +++ b/packages/client/lib/ProofOfPossessionBuilder.ts @@ -4,6 +4,7 @@ import { EndpointMetadata, Jwt, NO_JWT_PROVIDED, + OpenId4VCIVersion, PROOF_CANT_BE_CONSTRUCTED, ProofOfPossession, ProofOfPossessionCallbacks, @@ -16,6 +17,8 @@ export class ProofOfPossessionBuilder { private readonly proof?: ProofOfPossession; private readonly callbacks?: ProofOfPossessionCallbacks; + private version: OpenId4VCIVersion; + private kid?: string; private clientId?: string; private issuer?: string; @@ -30,11 +33,13 @@ export class ProofOfPossessionBuilder { callbacks, jwt, accessTokenResponse, + version, }: { proof?: ProofOfPossession; callbacks?: ProofOfPossessionCallbacks; accessTokenResponse?: AccessTokenResponse; jwt?: Jwt; + version: OpenId4VCIVersion; }) { this.proof = proof; this.callbacks = callbacks; @@ -44,24 +49,35 @@ export class ProofOfPossessionBuilder { if (accessTokenResponse) { this.withAccessTokenResponse(accessTokenResponse); } + this.version = version; } - static fromJwt({ jwt, callbacks }: { jwt: Jwt; callbacks: ProofOfPossessionCallbacks }): ProofOfPossessionBuilder { - return new ProofOfPossessionBuilder({ callbacks, jwt }); + static fromJwt({ + jwt, + callbacks, + version, + }: { + jwt: Jwt; + callbacks: ProofOfPossessionCallbacks; + version: OpenId4VCIVersion; + }): ProofOfPossessionBuilder { + return new ProofOfPossessionBuilder({ callbacks, jwt, version }); } static fromAccessTokenResponse({ accessTokenResponse, callbacks, + version, }: { accessTokenResponse: AccessTokenResponse; callbacks: ProofOfPossessionCallbacks; + version: OpenId4VCIVersion; }): ProofOfPossessionBuilder { - return new ProofOfPossessionBuilder({ callbacks, accessTokenResponse }); + return new ProofOfPossessionBuilder({ callbacks, accessTokenResponse, version }); } - static fromProof(proof: ProofOfPossession): ProofOfPossessionBuilder { - return new ProofOfPossessionBuilder({ proof }); + static fromProof(proof: ProofOfPossession, version: OpenId4VCIVersion): ProofOfPossessionBuilder { + return new ProofOfPossessionBuilder({ proof, version }); } withClientId(clientId: string): ProofOfPossessionBuilder { @@ -116,15 +132,23 @@ export class ProofOfPossessionBuilder { throw new Error(NO_JWT_PROVIDED); } this.jwt = jwt; - if (jwt.header) { - if (jwt.header.kid) { - this.withKid(jwt.header.kid); - } - if (jwt.header.typ) { - this.withTyp(jwt.header.typ as Typ); - } - this.withAlg(jwt.header.alg); + if (!jwt.header) { + throw Error(`No JWT header present`); + } else if (!jwt.payload) { + throw Error(`No JWT payload present`); } + + if (jwt.header.kid) { + this.withKid(jwt.header.kid); + } + if (jwt.header.typ) { + this.withTyp(jwt.header.typ as Typ); + } + if (this.version >= OpenId4VCIVersion.VER_1_0_11) { + this.withTyp('openid4vci-proof+jwt'); + } + this.withAlg(jwt.header.alg); + if (jwt.payload) { if (jwt.payload.iss) this.withClientId(jwt.payload.iss); if (jwt.payload.aud) this.withIssuer(jwt.payload.aud); @@ -141,7 +165,7 @@ export class ProofOfPossessionBuilder { return await createProofOfPossession( this.callbacks, { - typ: this.typ, + typ: this.typ ?? (this.version < OpenId4VCIVersion.VER_1_0_11 ? 'jwt' : 'openid4vci-proof+jwt'), kid: this.kid, jti: this.jti, alg: this.alg, diff --git a/packages/client/lib/__tests__/CredentialRequestClient.spec.ts b/packages/client/lib/__tests__/CredentialRequestClient.spec.ts index ecdc7c8a..a9d748b4 100644 --- a/packages/client/lib/__tests__/CredentialRequestClient.spec.ts +++ b/packages/client/lib/__tests__/CredentialRequestClient.spec.ts @@ -5,6 +5,7 @@ import { EndpointMetadata, getIssuerFromCredentialOfferPayload, Jwt, + OpenId4VCIVersion, ProofOfPossession, URL_NOT_VALID, WellKnownEndpoints, @@ -82,13 +83,14 @@ describe('Credential Request Client ', () => { callbacks: { signCallback: proofOfPossessionCallbackFunction, }, + version: OpenId4VCIVersion.VER_1_0_08, }) // .withEndpointMetadata(metadata) .withClientId('sphereon:wallet') .withKid(kid) .build(); expect(credReqClient.getCredentialEndpoint()).toEqual(basePath + '/credential'); - const credentialRequest = await credReqClient.createCredentialRequest({ proofInput: proof }); + const credentialRequest = await credReqClient.createCredentialRequest({ proofInput: proof, version: OpenId4VCIVersion.VER_1_0_08 }); expect(credentialRequest.proof?.jwt?.includes(partialJWT)).toBeTruthy(); const result = await credReqClient.acquireCredentialsUsingRequest(credentialRequest); expect(result?.errorBody?.error).toBe('unsupported_format'); @@ -113,12 +115,17 @@ describe('Credential Request Client ', () => { callbacks: { signCallback: proofOfPossessionCallbackFunction, }, + version: OpenId4VCIVersion.VER_1_0_08, }) // .withEndpointMetadata(metadata) .withKid(kid) .withClientId('sphereon:wallet') .build(); - const credentialRequest = await credReqClient.createCredentialRequest({ proofInput: proof, format: 'jwt' }); + const credentialRequest = await credReqClient.createCredentialRequest({ + proofInput: proof, + format: 'jwt', + version: OpenId4VCIVersion.VER_1_0_08, + }); expect(credentialRequest.proof?.jwt?.includes(partialJWT)).toBeTruthy(); expect(credentialRequest.format).toEqual('jwt_vc_json_ld'); const result = await credReqClient.acquireCredentialsUsingRequest(credentialRequest); @@ -136,6 +143,7 @@ describe('Credential Request Client ', () => { callbacks: { signCallback: proofOfPossessionCallbackFunction, }, + version: OpenId4VCIVersion.VER_1_0_08, }) // .withEndpointMetadata(metadata) .withKid(kid) @@ -187,6 +195,7 @@ describe('Credential Request Client with different issuers ', () => { }, credentialTypes: ['OpenBadgeCredential'], format: 'jwt_vc_json_ld', + version: OpenId4VCIVersion.VER_1_0_08, }); expect(credentialRequest).toEqual(getMockData('spruce')?.credential.request); }); @@ -208,6 +217,7 @@ describe('Credential Request Client with different issuers ', () => { }, credentialTypes: ['OpenBadgeCredential'], format: 'jwt_vc', + version: OpenId4VCIVersion.VER_1_0_08, }); expect(credentialOffer).toEqual(getMockData('walt')?.credential.request); }); @@ -230,6 +240,7 @@ describe('Credential Request Client with different issuers ', () => { }, credentialTypes: ['OpenBadgeCredential'], format: 'jwt_vc', + version: OpenId4VCIVersion.VER_1_0_08, }); expect(credentialOffer).toEqual(getMockData('uniissuer')?.credential.request); }); @@ -251,6 +262,7 @@ describe('Credential Request Client with different issuers ', () => { }, credentialTypes: ['OpenBadgeCredential'], format: 'ldp_vc', + version: OpenId4VCIVersion.VER_1_0_08, }); expect(credentialOffer).toEqual(getMockData('mattr')?.credential.request); }); @@ -272,6 +284,7 @@ describe('Credential Request Client with different issuers ', () => { }, credentialTypes: ['OpenBadgeCredential'], format: 'ldp_vc', + version: OpenId4VCIVersion.VER_1_0_08, }); expect(credentialOffer).toEqual(getMockData('diwala')?.credential.request); }); diff --git a/packages/client/lib/__tests__/CredentialRequestClientBuilder.spec.ts b/packages/client/lib/__tests__/CredentialRequestClientBuilder.spec.ts index 466237d7..4abf0537 100644 --- a/packages/client/lib/__tests__/CredentialRequestClientBuilder.spec.ts +++ b/packages/client/lib/__tests__/CredentialRequestClientBuilder.spec.ts @@ -1,6 +1,14 @@ import { KeyObject } from 'crypto'; -import { Alg, CredentialIssuerMetadata, Jwt, JWTPayload, ProofOfPossession, UniformCredentialRequest } from '@sphereon/oid4vci-common'; +import { + Alg, + CredentialIssuerMetadata, + Jwt, + JWTPayload, + OpenId4VCIVersion, + ProofOfPossession, + UniformCredentialRequest, +} from '@sphereon/oid4vci-common'; import * as jose from 'jose'; import { CredentialRequestClientBuilder, ProofOfPossessionBuilder } from '..'; @@ -74,12 +82,16 @@ describe('Credential Request Client Builder', () => { signCallback: proofOfPossessionCallbackFunction, verifyCallback: proofOfPossessionVerifierCallbackFunction, }, + version: OpenId4VCIVersion.VER_1_0_08, }) .withClientId('sphereon:wallet') .withKid(kid) .build(); await proofOfPossessionVerifierCallbackFunction({ ...proof, kid }); - const credentialRequest: UniformCredentialRequest = await credReqClient.createCredentialRequest({ proofInput: proof }); + const credentialRequest: UniformCredentialRequest = await credReqClient.createCredentialRequest({ + proofInput: proof, + version: OpenId4VCIVersion.VER_1_0_08, + }); expect(credentialRequest.proof?.jwt).toContain(partialJWT); expect('types' in credentialRequest).toBe(true); if ('types' in credentialRequest) { diff --git a/packages/client/lib/__tests__/IT.spec.ts b/packages/client/lib/__tests__/IT.spec.ts index 0b4f19cc..6d667e8a 100644 --- a/packages/client/lib/__tests__/IT.spec.ts +++ b/packages/client/lib/__tests__/IT.spec.ts @@ -1,4 +1,12 @@ -import { AccessTokenResponse, Alg, AuthzFlowType, CredentialOfferRequestWithBaseUrl, Jwt, ProofOfPossession } from '@sphereon/oid4vci-common'; +import { + AccessTokenResponse, + Alg, + AuthzFlowType, + CredentialOfferRequestWithBaseUrl, + Jwt, + OpenId4VCIVersion, + ProofOfPossession, +} from '@sphereon/oid4vci-common'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import nock from 'nock'; @@ -148,6 +156,7 @@ describe('OID4VCI-Client should', () => { callbacks: { signCallback: proofOfPossessionCallbackFunction, }, + version: OpenId4VCIVersion.VER_1_0_11, }) .withEndpointMetadata({ issuer: 'https://issuer.research.identiproof.io', diff --git a/packages/client/lib/__tests__/IssuanceInitiation.spec.ts b/packages/client/lib/__tests__/IssuanceInitiation.spec.ts index f32819b2..3884e923 100644 --- a/packages/client/lib/__tests__/IssuanceInitiation.spec.ts +++ b/packages/client/lib/__tests__/IssuanceInitiation.spec.ts @@ -30,12 +30,12 @@ describe('Issuance Initiation', () => { }); it('Should return Issuance Initiation URI from request', async () => { - expect(await CredentialOfferClient.toURI(INITIATION_TEST)).toEqual(INITIATION_TEST_URI); + expect(CredentialOfferClient.toURI(INITIATION_TEST)).toEqual(INITIATION_TEST_URI); }); it('Should return URI from Issuance Initiation Request', async () => { const issuanceInitiationClient = await CredentialOfferClient.fromURI(INITIATION_TEST_HTTPS_URI); - expect(await CredentialOfferClient.toURI(issuanceInitiationClient)).toEqual(INITIATION_TEST_HTTPS_URI); + expect(CredentialOfferClient.toURI(issuanceInitiationClient)).toEqual(INITIATION_TEST_HTTPS_URI); }); it('Should throw error on invalid URI', async () => { diff --git a/packages/client/lib/__tests__/ProofOfPossessionBuilder.spec.ts b/packages/client/lib/__tests__/ProofOfPossessionBuilder.spec.ts index 093589b9..f17c0ade 100644 --- a/packages/client/lib/__tests__/ProofOfPossessionBuilder.spec.ts +++ b/packages/client/lib/__tests__/ProofOfPossessionBuilder.spec.ts @@ -1,6 +1,6 @@ import { KeyObject } from 'crypto'; -import { Alg, JWS_NOT_VALID, Jwt, NO_JWT_PROVIDED, PROOF_CANT_BE_CONSTRUCTED, ProofOfPossession } from '@sphereon/oid4vci-common'; +import { Alg, JWS_NOT_VALID, Jwt, NO_JWT_PROVIDED, OpenId4VCIVersion, PROOF_CANT_BE_CONSTRUCTED, ProofOfPossession } from '@sphereon/oid4vci-common'; import * as jose from 'jose'; import { ProofOfPossessionBuilder } from '..'; @@ -44,7 +44,7 @@ beforeAll(async () => { describe('ProofOfPossession Builder ', () => { it('should fail without supplied proof or callbacks', async function () { await expect( - ProofOfPossessionBuilder.fromProof(undefined as never) + ProofOfPossessionBuilder.fromProof(undefined as never, OpenId4VCIVersion.VER_1_0_11) .withIssuer(IDENTIPROOF_ISSUER_URL) .withClientId('sphereon:wallet') .withKid(kid) @@ -54,7 +54,7 @@ describe('ProofOfPossession Builder ', () => { it('should fail wit undefined jwt supplied', async function () { await expect(() => - ProofOfPossessionBuilder.fromJwt({ jwt, callbacks: { signCallback: proofOfPossessionCallbackFunction } }) + ProofOfPossessionBuilder.fromJwt({ jwt, callbacks: { signCallback: proofOfPossessionCallbackFunction }, version: OpenId4VCIVersion.VER_1_0_08 }) .withJwt(undefined as never) .withIssuer(IDENTIPROOF_ISSUER_URL) .withClientId('sphereon:wallet') @@ -69,6 +69,7 @@ describe('ProofOfPossession Builder ', () => { callbacks: { signCallback: proofOfPossessionCallbackFunction, }, + version: OpenId4VCIVersion.VER_1_0_08, }) .withIssuer(IDENTIPROOF_ISSUER_URL) .withKid(kid) @@ -84,7 +85,7 @@ describe('ProofOfPossession Builder ', () => { } await expect( - ProofOfPossessionBuilder.fromJwt({ jwt, callbacks: { signCallback: proofOfPossessionCallbackFunction } }) + ProofOfPossessionBuilder.fromJwt({ jwt, callbacks: { signCallback: proofOfPossessionCallbackFunction }, version: OpenId4VCIVersion.VER_1_0_08 }) .withIssuer(IDENTIPROOF_ISSUER_URL) .withClientId('sphereon:wallet') .withKid(kid) @@ -99,7 +100,7 @@ describe('ProofOfPossession Builder ', () => { } await expect( - ProofOfPossessionBuilder.fromJwt({ jwt, callbacks: { signCallback: proofOfPossessionCallbackFunction } }) + ProofOfPossessionBuilder.fromJwt({ jwt, callbacks: { signCallback: proofOfPossessionCallbackFunction }, version: OpenId4VCIVersion.VER_1_0_08 }) .withIssuer(IDENTIPROOF_ISSUER_URL) .withClientId('sphereon:wallet') .withKid(kid) diff --git a/packages/client/lib/functions/ProofUtil.ts b/packages/client/lib/functions/ProofUtil.ts index 14d771e4..ba5cc0df 100644 --- a/packages/client/lib/functions/ProofUtil.ts +++ b/packages/client/lib/functions/ProofUtil.ts @@ -32,7 +32,7 @@ export const createProofOfPossession = async ( const signerArgs = createJWT(jwtProps, existingJwt); const jwt = await callbacks.signCallback(signerArgs, signerArgs.header.kid); const proof = { - proof_type: 'jwt', + proof_type: signerArgs.header.typ ?? 'jwt', jwt, } as ProofOfPossession; @@ -87,7 +87,7 @@ const createJWT = (jwtProps?: JwtProps, existingJwt?: Jwt): Jwt => { }; const jwtHeader: JWTHeader = { - typ: 'jwt', + typ: jwtProps?.typ ?? existingJwt?.header.typ ?? 'jwt', alg, kid, }; diff --git a/packages/common/lib/functions/Encoding.ts b/packages/common/lib/functions/Encoding.ts index 38f2209a..a7218117 100644 --- a/packages/common/lib/functions/Encoding.ts +++ b/packages/common/lib/functions/Encoding.ts @@ -97,7 +97,7 @@ export function convertURIToJsonObject(uri: string, opts?: DecodeURIAsJsonOpts): } function decodeJsonProperties(parts: string[] | string[][]): unknown { - const json: { [s: string]: any } | ArrayLike = {}; + const json: { [s: string]: unknown } | ArrayLike = {}; for (const key in parts) { const value = parts[key]; if (!value) { diff --git a/packages/common/lib/functions/HttpUtils.ts b/packages/common/lib/functions/HttpUtils.ts index b1802bba..0f7e01d0 100644 --- a/packages/common/lib/functions/HttpUtils.ts +++ b/packages/common/lib/functions/HttpUtils.ts @@ -103,5 +103,5 @@ export const isValidURL = (url: string): boolean => { '(\\#[-a-z\\d_]*)?$', // validate fragment locator 'i' ); - return !!urlPattern.test(url); + return urlPattern.test(url); }; diff --git a/packages/issuer-rest/lib/OID4VCIServer.ts b/packages/issuer-rest/lib/OID4VCIServer.ts index 30dbcfa2..68d72afc 100644 --- a/packages/issuer-rest/lib/OID4VCIServer.ts +++ b/packages/issuer-rest/lib/OID4VCIServer.ts @@ -92,8 +92,7 @@ export class OID4VCIServer { dotenv.config() this._baseUrl = new URL(opts?.serverOpts?.baseUrl ?? process.env.BASE_URL ?? 'http://localhost') - // fixme: this is way too naive (fails for instance for base urls with a path) - const httpPort = getNumberOrUndefined(this._baseUrl.host.split(':')[1]) ?? getNumberOrUndefined(process.env.PORT) ?? 3000 + const httpPort = getNumberOrUndefined(this._baseUrl.port) ?? getNumberOrUndefined(process.env.PORT) ?? 3000 const host = this._baseUrl.host.split(':')[0] if (!opts?.serverOpts?.app) { @@ -140,7 +139,7 @@ export class OID4VCIServer { let url: URL let baseUrl = this._baseUrl?.toString() if (baseUrl.endsWith('/')) { - baseUrl = baseUrl.substring(0, baseUrl.length) + baseUrl = baseUrl.substring(0, baseUrl.length - 1) } if (!issuerEndpoint) { path = this.extractPath(tokenEndpointOpts?.tokenPath ?? process.env.TOKEN_PATH ?? '/token', true) @@ -174,7 +173,7 @@ export class OID4VCIServer { private metadataEndpoint() { let basePath = this.extractPath(this._baseUrl.toString()) if (basePath.endsWith('/')) { - basePath = basePath.substring(0, basePath.length) + basePath = basePath.substring(0, basePath.length - 1) } const path = basePath + '/.well-known/openid-credential-issuer' this.app.get(path, (request: Request, response: Response) => { @@ -186,24 +185,24 @@ export class OID4VCIServer { const endpoint = this.issuer.issuerMetadata.credential_endpoint let path: string if (!endpoint) { - path = '/credentials' - // last replace fixes any baseUrl ending with a slash and path starting with a slash - this.issuer.issuerMetadata.credential_endpoint = `${this._baseUrl}${path}`.replace('//', '/') + path = this._baseUrl.toString().endsWith('/') ? 'credentials' : '/credentials' + this.issuer.issuerMetadata.credential_endpoint = `${this._baseUrl}${path}` } else { this.assertEndpointHasIssuerBaseUrl(endpoint) path = this.extractPath(endpoint) } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - this.app.post(path, async (request: Request, _response: Response) => { - const credentialRequest = request.body as CredentialRequestV1_0_11 - this.issuer.issueCredentialFromIssueRequest({ - credentialRequest: credentialRequest, - tokenExpiresIn: this.tokenExpiresIn, - cNonceExpiresIn: this.cNonceExpiresIn, - //WTF - jwtVerifyCallback: request.body.jwtVerifyCallback, - issuerCallback: request.body.issuerCallback, - }) + this.app.post(path, async (request: Request, response: Response) => { + try { + const credentialRequest = request.body as CredentialRequestV1_0_11 + const credential = await this.issuer.issueCredentialFromIssueRequest({ + credentialRequest: credentialRequest, + tokenExpiresIn: this.tokenExpiresIn, + cNonceExpiresIn: this.cNonceExpiresIn, + }) + return response.send(credential) + } catch (e) { + return sendErrorResponse(response, 500, e) + } }) } diff --git a/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts b/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts index d0d4982a..2c93e29f 100644 --- a/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts +++ b/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts @@ -1,7 +1,9 @@ import { KeyObject } from 'crypto' +import * as didKeyDriver from '@digitalcredentials/did-method-key' import { OpenID4VCIClient } from '@sphereon/oid4vci-client' import { + AccessTokenResponse, Alg, AuthzFlowType, CredentialOfferSession, @@ -20,9 +22,27 @@ import { OID4VCIServer } from '../OID4VCIServer' const ISSUER_URL = 'http://localhost:3456/test' +let subjectKeypair: KeyPair // Proof of Possession JWT +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let subjectDIDKey: { didDocument: any; keyPairs: any; methodFor: any } // Json LD VC issuance + +export const generateDid = async () => { + const didKD = didKeyDriver.driver() + const { didDocument, keyPairs, methodFor } = await didKD.generate() + return { didDocument, keyPairs, methodFor } +} + +interface KeyPair { + publicKey: KeyObject + privateKey: KeyObject +} + +jest.setTimeout(15000) + describe('VcIssuer', () => { let vcIssuer: VcIssuer let server: OID4VCIServer + let accessToken: AccessTokenResponse const issuerState = 'previously-created-state' // const clientId = 'sphereon:wallet' const preAuthorizedCode = 'test_code' @@ -35,8 +55,12 @@ describe('VcIssuer', () => { beforeAll(async () => { jest.clearAllMocks() + const { privateKey, publicKey } = await jose.generateKeyPair('ES256') + subjectKeypair = { publicKey: publicKey as KeyObject, privateKey: privateKey as KeyObject } + subjectDIDKey = await generateDid() + // eslint-disable-next-line @typescript-eslint/no-unused-vars - const signerCallback = async (jwt: Jwt, kid?: string): Promise => { + const accessTokenSignerCallback = async (jwt: Jwt, kid?: string): Promise => { const privateKey = (await jose.generateKeyPair(Alg.ES256)).privateKey as KeyObject return new jose.SignJWT({ ...jwt.payload }).setProtectedHeader({ ...jwt.header, alg: Alg.ES256 }).sign(privateKey) } @@ -44,7 +68,7 @@ describe('VcIssuer', () => { const credentialsSupported: CredentialSupported = new CredentialSupportedBuilderV1_11() .withCryptographicSuitesSupported('ES256K') .withCryptographicBindingMethod('did') - //FIXME Here a CredentialFormatEnum is passed in, but later it is matched against a CredentialFormat + .withTypes('VerifiableCredential') .withFormat('jwt_vc_json') .withId('UniversityDegree_JWT') .withCredentialSupportedDisplay({ @@ -92,26 +116,18 @@ describe('VcIssuer', () => { }, }) ) - .withJWTVerifyCallback(() => - Promise.resolve({ - header: { - typ: 'openid4vci-proof+jwt', - alg: Alg.ES256K, - kid: 'test-kid', - }, - payload: { - aud: 'https://credential-issuer', - iat: +new Date(), - nonce: 'test-nonce', - }, - }) - ) + .withJWTVerifyCallback((args: { jwt: string; _kid?: string }) => { + return Promise.resolve({ + header: jose.decodeProtectedHeader(args.jwt), + payload: jose.decodeJwt(args.jwt), + } as Jwt) + }) .build() server = new OID4VCIServer({ issuer: vcIssuer, serverOpts: { baseUrl: 'http://localhost:3456/test', port: 3456 }, - tokenEndpointOpts: { accessTokenSignerCallback: signerCallback, tokenPath: 'test/token/path' }, + tokenEndpointOpts: { accessTokenSignerCallback, tokenPath: 'test/token/path' }, }) }) @@ -145,7 +161,12 @@ describe('VcIssuer', () => { }) it('should create client from credential offer URI', async () => { - client = await OpenID4VCIClient.fromURI({ uri, flowType: AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW }) + client = await OpenID4VCIClient.fromURI({ + uri, + flowType: AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW, + kid: subjectDIDKey.didDocument.authentication[0], + alg: 'ES256', + }) expect(client.credentialOffer).toEqual({ baseUrl: 'http://localhost:3456/test', credential_offer: { @@ -212,6 +233,7 @@ describe('VcIssuer', () => { ], format: 'jwt_vc_json', id: 'UniversityDegree_JWT', + types: ['VerifiableCredential'], }, ], display: [ @@ -237,6 +259,40 @@ describe('VcIssuer', () => { }) it('should acquire access token', async () => { - await expect(client.acquireAccessToken({ pin: credOfferSession.userPin })).resolves.toBeDefined() + accessToken = await client.acquireAccessToken({ pin: credOfferSession.userPin }) + expect(accessToken).toBeDefined() + }) + it('should issue credential', async () => { + async function proofOfPossessionCallbackFunction(args: Jwt, kid?: string): Promise { + return await new jose.SignJWT({ ...args.payload }) + .setProtectedHeader({ ...args.header }) + .setIssuedAt(+new Date()) + .setIssuer(kid!) + .setAudience(args.payload.aud!) + .setExpirationTime('2h') + .sign(subjectKeypair.privateKey) + } + + const credentialResponse = await client.acquireCredentials({ + credentialTypes: ['VerifiableCredential'], + format: 'jwt_vc_json', + proofCallbacks: { signCallback: proofOfPossessionCallbackFunction }, + }) + expect(credentialResponse).toMatchObject({ + c_nonce_expires_in: 90000, + credential: { + '@context': ['https://www.w3.org/2018/credentials/v1'], + credentialSubject: {}, + issuer: 'did:key:test', + proof: { + jwt: 'ye.ye.ye', + proofPurpose: 'assertionMethod', + type: 'JwtProof2020', + verificationMethod: 'sdfsdfasdfasdfasdfasdfassdfasdf', + }, + type: ['VerifiableCredential'], + }, + format: 'jwt_vc_json', + }) }) }) diff --git a/packages/issuer-rest/lib/__tests__/IssuerTokenServer.spec.ts b/packages/issuer-rest/lib/__tests__/IssuerTokenServer.spec.ts index d8e256f1..65717b1a 100644 --- a/packages/issuer-rest/lib/__tests__/IssuerTokenServer.spec.ts +++ b/packages/issuer-rest/lib/__tests__/IssuerTokenServer.spec.ts @@ -19,7 +19,7 @@ import requests from 'supertest' import { OID4VCIServer } from '../OID4VCIServer' -xdescribe('OID4VCIServer', () => { +describe('OID4VCIServer', () => { let app: Express let server: http.Server const preAuthorizedCode1 = 'SplxlOBeZQQYbYS6WxSbIA1' @@ -152,7 +152,7 @@ xdescribe('OID4VCIServer', () => { expect(res.statusCode).toEqual(200) const actual = JSON.parse(res.text) expect(actual).toEqual({ - access_token: expect.stringContaining('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE2ODQ'), + access_token: expect.stringContaining('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE2OD'), token_type: 'bearer', expires_in: 300000, c_nonce: expect.any(String), @@ -198,7 +198,7 @@ xdescribe('OID4VCIServer', () => { const actual = JSON.parse(res.text) expect(actual).toEqual({ error: 'invalid_grant', - error_message: 'PIN does not match', + error_message: 'PIN is invalid', }) }) it('should return http code 400 with message PIN must consist of maximum 8 numeric characters', async () => { @@ -220,7 +220,7 @@ xdescribe('OID4VCIServer', () => { const actual = JSON.parse(res.text) expect(actual).toEqual({ error: 'invalid_request', - error_message: STATE_MISSING_ERROR, + error_message: STATE_MISSING_ERROR + ' (test)', }) }) it('should return http code 400 with message User pin is not required', async () => { diff --git a/packages/issuer-rest/package.json b/packages/issuer-rest/package.json index 436178c4..73679774 100644 --- a/packages/issuer-rest/package.json +++ b/packages/issuer-rest/package.json @@ -22,6 +22,7 @@ }, "devDependencies": { "@sphereon/oid4vci-client": "workspace:*", + "@digitalcredentials/did-method-key": "^2.0.3", "@types/body-parser": "^1.19.2", "@types/cookie-parser": "^1.4.3", "@types/cors": "^2.8.13", diff --git a/packages/issuer/lib/VcIssuer.ts b/packages/issuer/lib/VcIssuer.ts index fea12471..f3cddf1c 100644 --- a/packages/issuer/lib/VcIssuer.ts +++ b/packages/issuer/lib/VcIssuer.ts @@ -15,7 +15,6 @@ import { CredentialResponse, Grant, IAT_ERROR, - ISS_MUST_BE_CLIENT_ID, ISSUER_CONFIG_ERROR, IssuerCredentialDefinition, IStateManager, @@ -23,7 +22,6 @@ import { JWT_VERIFY_CONFIG_ERROR, JWTVerifyCallback, NO_ISS_IN_AUTHORIZATION_CODE_CONTEXT, - NONCE_ERROR, TokenErrorResponse, TYP_ERROR, URIState, @@ -31,7 +29,7 @@ import { import { ICredential, W3CVerifiableCredential } from '@sphereon/ssi-types' import { v4 } from 'uuid' -import { createCredentialOfferObject, createCredentialOfferURIFromObject } from './functions' +import { assertValidPinNumber, createCredentialOfferObject, createCredentialOfferURIFromObject } from './functions' import { LookupStateManager } from './state-manager' import { CredentialIssuerCallback } from './types' @@ -72,7 +70,7 @@ export class VcIssuer { public getCredentialOfferSessionById(id: string): Promise { if (!this.uris) { - throw Error('Cannnot lookup credential offer by id, if URI state manager is not set') + throw Error('Cannot lookup credential offer by id if URI state manager is not set') } return new LookupStateManager(this.uris, this._credentialOfferSessions, 'uri').getAsserted(id) } @@ -84,8 +82,10 @@ export class VcIssuer { credentialOfferUri?: string baseUri?: string scheme?: string + pinLength?: number }): Promise { const { grants, credentials, credentialDefinition } = opts + if (!grants?.authorization_code && !grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']) { throw Error(`No grant issuer state or pre-authorized code could be deduced`) } @@ -131,7 +131,10 @@ export class VcIssuer { let userPin: string | undefined // todo: Double check this can only happen in pre-auth flow and if so make sure to not do the below when in a state is present (authorized flow) if (userPinRequired) { - userPin = ('' + Math.round(9999 * Math.random())).padStart(4, '0') + const pinLength = opts.pinLength ?? 4 + + userPin = ('' + Math.round((Math.pow(10, pinLength) - 1) * Math.random())).padStart(pinLength, '0') + assertValidPinNumber(userPin) } const createdAt = +new Date() if (opts?.credentialOfferUri) { @@ -252,88 +255,85 @@ export class VcIssuer { clientId?: string jwtVerifyCallback?: JWTVerifyCallback }) { - if (credentialRequest.format === 'jwt_vc_json' || credentialRequest.format === 'jwt_vc_json_ld') { - if (typeof this._verifyCallback !== 'function' && typeof jwtVerifyCallback !== 'function') { - throw new Error(JWT_VERIFY_CONFIG_ERROR) - } - if (!credentialRequest.proof) { - throw Error('Proof of possession is required. No proof value present in credential request') - } + if (credentialRequest.format !== 'jwt_vc_json' && credentialRequest.format !== 'jwt_vc_json_ld') { + throw Error(`Format ${credentialRequest.format} not supported yet`) + } else if (typeof this._verifyCallback !== 'function' && typeof jwtVerifyCallback !== 'function') { + throw new Error(JWT_VERIFY_CONFIG_ERROR) + } else if (!credentialRequest.proof) { + throw Error('Proof of possession is required. No proof value present in credential request') + } - const { payload, header }: Jwt = jwtVerifyCallback - ? await jwtVerifyCallback(credentialRequest.proof) - : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await this._verifyCallback!(credentialRequest.proof) + const { payload, header }: Jwt = jwtVerifyCallback + ? await jwtVerifyCallback(credentialRequest.proof) + : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await this._verifyCallback!(credentialRequest.proof) - const { typ, alg, kid, jwk, x5c } = header + const { typ, alg, kid, jwk, x5c } = header - if (typ !== 'openid4vci-proof+jwt') { - throw new Error(TYP_ERROR) - } else if (!alg || !(alg in Alg)) { - throw new Error(ALG_ERROR) - } else if (!([kid, jwk, x5c].filter((x) => !!x).length === 1)) { - // throw new Error(KID_JWK_X5C_ERROR) // todo: whut. An x5c is very specific to X509 certs only - } + if (typ !== 'openid4vci-proof+jwt') { + console.log(typ) + throw new Error(TYP_ERROR) + } else if (!alg || !(alg in Alg)) { + throw new Error(ALG_ERROR) + } else if (!([kid, jwk, x5c].filter((x) => !!x).length === 1)) { + // throw new Error(KID_JWK_X5C_ERROR) // todo: whut. An x5c is very specific to X509 certs only + } - const { iss, aud, iat, nonce } = payload - if (!nonce) { - throw Error('No nonce was found in the Proof of Possession') - } - const cNonceState = await this.cNonces.getAsserted(nonce) - const { preAuthorizedCode, createdAt, issuerState } = cNonceState + const { iss, aud, iat, nonce } = payload + if (!nonce) { + throw Error('No nonce was found in the Proof of Possession') + } + const cNonceState = await this.cNonces.getAsserted(nonce) + const { preAuthorizedCode, createdAt, issuerState } = cNonceState - const preAuthSession = preAuthorizedCode ? await this.credentialOfferSessions.get(preAuthorizedCode) : undefined - const authSession = issuerState ? await this.credentialOfferSessions.get(issuerState) : undefined - if (!preAuthSession && !authSession) { - throw Error('Either a pre-authorized code or issuer state needs to be present') - } - if (preAuthSession) { - if (!preAuthSession.preAuthorizedCode || preAuthSession.preAuthorizedCode !== preAuthorizedCode) { - throw Error('Invalid pre-authorized code') - } + const preAuthSession = preAuthorizedCode ? await this.credentialOfferSessions.get(preAuthorizedCode) : undefined + const authSession = issuerState ? await this.credentialOfferSessions.get(issuerState) : undefined + if (!preAuthSession && !authSession) { + throw Error('Either a pre-authorized code or issuer state needs to be present') + } + if (preAuthSession) { + if (!preAuthSession.preAuthorizedCode || preAuthSession.preAuthorizedCode !== preAuthorizedCode) { + throw Error('Invalid pre-authorized code') } - if (authSession) { - if (!authSession.issuerState || authSession.issuerState !== issuerState) { - throw Error('Invalid issuer state') - } + } + if (authSession) { + if (!authSession.issuerState || authSession.issuerState !== issuerState) { + throw Error('Invalid issuer state') } + } - // https://www.rfc-editor.org/rfc/rfc6749.html#section-3.2.1 - // A client MAY use the "client_id" request parameter to identify itself - // when sending requests to the token endpoint. In the - // "authorization_code" "grant_type" request to the token endpoint, an - // unauthenticated client MUST send its "client_id" to prevent itself - // from inadvertently accepting a code intended for a client with a - // different "client_id". This protects the client from substitution of - // the authentication code. (It provides no additional security for the - // protected resource.) - if (!iss && authSession?.credentialOffer.credential_offer?.grants?.authorization_code) { - throw new Error(NO_ISS_IN_AUTHORIZATION_CODE_CONTEXT) - } - // iss: OPTIONAL (string). The value of this claim MUST be the client_id of the client making the credential request. - // This claim MUST be omitted if the Access Token authorizing the issuance call was obtained from a Pre-Authorized Code Flow through anonymous access to the Token Endpoint. - // TODO We need to investigate further what the comment above means, because it's not clear if the client or the user may be authorized anonymously - // if (iss && grants && grants['urn:ietf:params:oauth:grant-type:pre-authorized_code']) { - // throw new Error(ISS_PRESENT_IN_PRE_AUTHORIZED_CODE_CONTEXT) - // } - if (iss && iss !== clientId) { - throw new Error(ISS_MUST_BE_CLIENT_ID) - } - if (!aud || aud !== this._issuerMetadata.credential_issuer) { - throw new Error(AUD_ERROR) - } - if (!iat) { - throw new Error(IAT_ERROR) - } else if (iat > createdAt + tokenExpiresIn) { - throw new Error(IAT_ERROR) - } - // todo: Add a check of iat against current TS on server with a skew - if (!nonce) { - throw new Error(NONCE_ERROR) - } - return { jwt: { header, payload } as Jwt, preAuthSession, authSession, cNonceState } + // https://www.rfc-editor.org/rfc/rfc6749.html#section-3.2.1 + // A client MAY use the "client_id" request parameter to identify itself + // when sending requests to the token endpoint. In the + // "authorization_code" "grant_type" request to the token endpoint, an + // unauthenticated client MUST send its "client_id" to prevent itself + // from inadvertently accepting a code intended for a client with a + // different "client_id". This protects the client from substitution of + // the authentication code. (It provides no additional security for the + // protected resource.) + if (!iss && authSession?.credentialOffer.credential_offer?.grants?.authorization_code) { + throw new Error(NO_ISS_IN_AUTHORIZATION_CODE_CONTEXT) + } + // iss: OPTIONAL (string). The value of this claim MUST be the client_id of the client making the credential request. + // This claim MUST be omitted if the Access Token authorizing the issuance call was obtained from a Pre-Authorized Code Flow through anonymous access to the Token Endpoint. + // TODO We need to investigate further what the comment above means, because it's not clear if the client or the user may be authorized anonymously + // if (iss && grants && grants['urn:ietf:params:oauth:grant-type:pre-authorized_code']) { + // throw new Error(ISS_PRESENT_IN_PRE_AUTHORIZED_CODE_CONTEXT) + // } + /*if (iss && iss !== clientId) { + throw new Error(ISS_MUST_BE_CLIENT_ID + `iss: ${iss}, client_id: ${clientId}`) + }*/ + if (!aud || aud !== this._issuerMetadata.credential_issuer) { + throw new Error(AUD_ERROR) } - throw Error(`Format ${credentialRequest.format} not supported yet`) + if (!iat) { + throw new Error(IAT_ERROR) + } else if (iat > createdAt + tokenExpiresIn) { + throw new Error(IAT_ERROR) + } + // todo: Add a check of iat against current TS on server with a skew + + return { jwt: { header, payload } as Jwt, preAuthSession, authSession, cNonceState } } private isMetadataSupportCredentialRequestFormat(requestFormat: string | string[]): boolean { diff --git a/packages/issuer/lib/__tests__/MemoryCNonceStateManager.spec.ts b/packages/issuer/lib/__tests__/MemoryCNonceStateManager.spec.ts index d24cec71..bacc16d4 100644 --- a/packages/issuer/lib/__tests__/MemoryCNonceStateManager.spec.ts +++ b/packages/issuer/lib/__tests__/MemoryCNonceStateManager.spec.ts @@ -47,6 +47,6 @@ describe('MemoryIssuerStateManager', () => { await expect(memoryCNonceStateManager.get(String(2))).resolves.toBeUndefined() }) it('should throw exception when state does not exist', async () => { - await expect(memoryCNonceStateManager.getAsserted(String(3))).rejects.toThrowError(Error(STATE_MISSING_ERROR)) + await expect(memoryCNonceStateManager.getAsserted(String(3))).rejects.toThrowError(Error(STATE_MISSING_ERROR + ' (3)')) }) }) diff --git a/packages/issuer/lib/__tests__/MemoryCredentialOfferStateManager.spec.ts b/packages/issuer/lib/__tests__/MemoryCredentialOfferStateManager.spec.ts index 7b4927fe..68c13c9d 100644 --- a/packages/issuer/lib/__tests__/MemoryCredentialOfferStateManager.spec.ts +++ b/packages/issuer/lib/__tests__/MemoryCredentialOfferStateManager.spec.ts @@ -49,6 +49,6 @@ describe('MemoryIssuerStateManager', () => { await expect(memoryIssuerStateManager.get(String(2))).resolves.toBeUndefined() }) it('should throw exception when state does not exist', async () => { - await expect(memoryIssuerStateManager.getAsserted(String(3))).rejects.toThrowError(Error(STATE_MISSING_ERROR)) + await expect(memoryIssuerStateManager.getAsserted(String(3))).rejects.toThrowError(Error(STATE_MISSING_ERROR + ' (3)')) }) }) diff --git a/packages/issuer/lib/__tests__/VcIssuer.spec.ts b/packages/issuer/lib/__tests__/VcIssuer.spec.ts index 8372b307..c041e6ff 100644 --- a/packages/issuer/lib/__tests__/VcIssuer.spec.ts +++ b/packages/issuer/lib/__tests__/VcIssuer.spec.ts @@ -27,8 +27,8 @@ describe('VcIssuer', () => { const credentialsSupported: CredentialSupported = new CredentialSupportedBuilderV1_11() .withCryptographicSuitesSupported('ES256K') .withCryptographicBindingMethod('did') - //FIXME Here a CredentialFormatEnum is passed in, but later it is matched against a CredentialFormat .withFormat('jwt_vc_json') + .withTypes('VerifiableCredential') .withId('UniversityDegree_JWT') .withCredentialSupportedDisplay({ name: 'University Credential', @@ -135,7 +135,7 @@ describe('VcIssuer', () => { baseUri: 'issuer-example.com', }) expect(uri).toEqual( - 'http://issuer-example.com?credential_offer=%7B%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22previously-created-state%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22test_code%22%2C%22user_pin_required%22%3Atrue%7D%7D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.research.identiproof.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22credentialSubject%22%3A%7B%22given_name%22%3A%7B%22name%22%3A%22given%20name%22%2C%22locale%22%3A%22en-US%22%7D%7D%2C%22cryptographic_suites_supported%22%3A%5B%22ES256K%22%5D%2C%22cryptographic_binding_methods_supported%22%3A%5B%22did%22%5D%2C%22id%22%3A%22UniversityDegree_JWT%22%2C%22display%22%3A%5B%7B%22name%22%3A%22University%20Credential%22%2C%22locale%22%3A%22en-US%22%2C%22logo%22%3A%7B%22url%22%3A%22https%3A%2F%2Fexampleuniversity.com%2Fpublic%2Flogo.png%22%2C%22alt_text%22%3A%22a%20square%20logo%20of%20a%20university%22%7D%2C%22background_color%22%3A%22%2312107c%22%2C%22text_color%22%3A%22%23FFFFFF%22%7D%5D%7D%5D%7D' + 'http://issuer-example.com?credential_offer=%7B%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22previously-created-state%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22test_code%22%2C%22user_pin_required%22%3Atrue%7D%7D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.research.identiproof.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%5D%2C%22credentialSubject%22%3A%7B%22given_name%22%3A%7B%22name%22%3A%22given%20name%22%2C%22locale%22%3A%22en-US%22%7D%7D%2C%22cryptographic_suites_supported%22%3A%5B%22ES256K%22%5D%2C%22cryptographic_binding_methods_supported%22%3A%5B%22did%22%5D%2C%22id%22%3A%22UniversityDegree_JWT%22%2C%22display%22%3A%5B%7B%22name%22%3A%22University%20Credential%22%2C%22locale%22%3A%22en-US%22%2C%22logo%22%3A%7B%22url%22%3A%22https%3A%2F%2Fexampleuniversity.com%2Fpublic%2Flogo.png%22%2C%22alt_text%22%3A%22a%20square%20logo%20of%20a%20university%22%7D%2C%22background_color%22%3A%22%2312107c%22%2C%22text_color%22%3A%22%23FFFFFF%22%7D%5D%7D%5D%7D' ) const client = await OpenID4VCIClient.fromURI({ uri, flowType: AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW }) @@ -167,6 +167,7 @@ describe('VcIssuer', () => { ], format: 'jwt_vc_json', id: 'UniversityDegree_JWT', + types: ['VerifiableCredential'], }, ], grants: { @@ -205,6 +206,7 @@ describe('VcIssuer', () => { ], format: 'jwt_vc_json', id: 'UniversityDegree_JWT', + types: ['VerifiableCredential'], }, ], grants: { @@ -252,7 +254,7 @@ describe('VcIssuer', () => { }, // issuerState: 'invalid state', }) - ).rejects.toThrow(Error(STATE_MISSING_ERROR)) + ).rejects.toThrow(Error(STATE_MISSING_ERROR + ' (test-nonce)')) }) // Of course this doesn't work. The state is part of the proof to begin with diff --git a/packages/issuer/lib/__tests__/VcIssuerBuilder.spec.ts b/packages/issuer/lib/__tests__/VcIssuerBuilder.spec.ts index 18b2517b..84bb37e7 100644 --- a/packages/issuer/lib/__tests__/VcIssuerBuilder.spec.ts +++ b/packages/issuer/lib/__tests__/VcIssuerBuilder.spec.ts @@ -20,6 +20,7 @@ describe('VcIssuer builder should', () => { background_color: '#12107c', text_color: '#FFFFFF', }) + .withTypes('VerifiableCredential') .addCredentialSubjectPropertyDisplay('given_name', { name: 'given name', locale: 'en-US', @@ -48,6 +49,7 @@ describe('VcIssuer builder should', () => { .withCryptographicSuitesSupported('ES256K') .withCryptographicBindingMethod('did') .withFormat('jwt_vc_json') + .withTypes('VerifiableCredential') .withId('UniversityDegree_JWT') .withCredentialSupportedDisplay({ name: 'University Credential', @@ -91,6 +93,7 @@ describe('VcIssuer builder should', () => { .withCryptographicSuitesSupported('ES256K') .withCryptographicBindingMethod('did') .withFormat('jwt_vc_json') + .withTypes('VerifiableCredential') .withId('UniversityDegree_JWT') .withCredentialSupportedDisplay({ name: 'University Credential', diff --git a/packages/issuer/lib/builder/CredentialSupportedBuilderV1_11.ts b/packages/issuer/lib/builder/CredentialSupportedBuilderV1_11.ts index fe85ecde..e66eb73e 100644 --- a/packages/issuer/lib/builder/CredentialSupportedBuilderV1_11.ts +++ b/packages/issuer/lib/builder/CredentialSupportedBuilderV1_11.ts @@ -111,6 +111,12 @@ export class CredentialSupportedBuilderV1_11 { const credentialSupported: Partial = { format: this.format, } + if (!this.types) { + throw new Error('types are required') + } else { + credentialSupported.types = this.types + } + if (this.credentialSubject) { credentialSupported.credentialSubject = this.credentialSubject } diff --git a/packages/issuer/lib/functions/CredentialOfferUtils.ts b/packages/issuer/lib/functions/CredentialOfferUtils.ts index 48772834..3e9df410 100644 --- a/packages/issuer/lib/functions/CredentialOfferUtils.ts +++ b/packages/issuer/lib/functions/CredentialOfferUtils.ts @@ -5,6 +5,7 @@ import { CredentialOfferSession, CredentialOfferV1_0_11, Grant, + PIN_VALIDATION_ERROR, } from '@sphereon/oid4vci-common' import { v4 as uuidv4 } from 'uuid' @@ -112,3 +113,9 @@ export const isPreAuthorizedCodeExpired = (state: CredentialOfferSession, expira const expirationTime = state.createdAt + expirationDuration return now >= expirationTime } + +export const assertValidPinNumber = (pin?: string) => { + if (pin && !/[0-9{,8}]/.test(pin)) { + throw Error(PIN_VALIDATION_ERROR) + } +} diff --git a/packages/issuer/lib/state-manager/LookupStateManager.ts b/packages/issuer/lib/state-manager/LookupStateManager.ts index cfb804eb..a06e10f2 100644 --- a/packages/issuer/lib/state-manager/LookupStateManager.ts +++ b/packages/issuer/lib/state-manager/LookupStateManager.ts @@ -1,3 +1,5 @@ +// noinspection ES6MissingAwait + import { IStateManager } from '@sphereon/oid4vci-common' import { StateType } from '@sphereon/oid4vci-common/dist/types/StateManager.types' diff --git a/packages/issuer/lib/state-manager/MemoryStates.ts b/packages/issuer/lib/state-manager/MemoryStates.ts index 7d08b50c..2531bc46 100644 --- a/packages/issuer/lib/state-manager/MemoryStates.ts +++ b/packages/issuer/lib/state-manager/MemoryStates.ts @@ -25,16 +25,22 @@ export class MemoryStates implements IStateManager { } async delete(id: string): Promise { + if (!id) { + throw Error('No id supplied') + } return this.states.delete(id) } async getAsserted(id: string): Promise { + if (!id) { + throw Error('No id supplied') + } let result: T | undefined if (await this.has(id)) { result = (await this.get(id)) as T } if (!result) { - throw new Error(STATE_MISSING_ERROR) + throw new Error(STATE_MISSING_ERROR + ` (${id})`) } return result } @@ -44,10 +50,16 @@ export class MemoryStates implements IStateManager { } async has(id: string): Promise { + if (!id) { + throw Error('No id supplied') + } return this.states.has(id) } async set(id: string, stateValue: T): Promise { + if (!id) { + throw Error('No id supplied') + } this.states.set(id, stateValue) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf030326..285d4496 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -252,6 +252,9 @@ importers: specifier: ^9.0.0 version: 9.0.0 devDependencies: + '@digitalcredentials/did-method-key': + specifier: ^2.0.3 + version: 2.0.3 '@sphereon/oid4vci-client': specifier: workspace:* version: link:../client @@ -1574,7 +1577,6 @@ packages: /@digitalcredentials/base58-universal@1.0.1: resolution: {integrity: sha512-1xKdJnfITMvrF/sCgwBx2C4p7qcNAARyIvrAOZGqIHmBaT/hAenpC8bf44qVY+UIMuCYP23kqpIfJQebQDThDQ==} engines: {node: '>=12'} - dev: false /@digitalcredentials/bnid@2.1.2(react-native@0.71.7): resolution: {integrity: sha512-pSQSZ7OH9dCVV/0CNoS5+7UHGekUjyJBOt4uTOLpCJWMINjD8RDVEpJLPvcV/kT9dq+S4DPA4ZyNDb/NmXCHGA==} @@ -1602,7 +1604,6 @@ packages: engines: {node: '>=12'} dependencies: '@digitalcredentials/lru-memoize': 2.1.4 - dev: false /@digitalcredentials/did-method-key@2.0.3: resolution: {integrity: sha512-b31TOIKJm+qcay7m9kxV6TDNyJwdb/XZIS/OjHarONlXGB3k0M38NXmbRDI95FTmy3GhYCFSJ3VEikqNKTXU2A==} @@ -1611,7 +1612,6 @@ packages: '@digitalcredentials/did-io': 1.0.2 '@digitalcredentials/ed25519-verification-key-2020': 3.2.2 '@digitalcredentials/x25519-key-agreement-key-2020': 2.0.2 - dev: false /@digitalcredentials/ed25519-signature-2020@3.0.2(expo@48.0.11)(react-native@0.71.7): resolution: {integrity: sha512-R8IrR21Dh+75CYriQov3nVHKaOVusbxfk9gyi6eCAwLHKn6fllUt+2LQfuUrL7Ts/sGIJqQcev7YvkX9GvyYRA==} @@ -1637,7 +1637,6 @@ packages: '@stablelib/ed25519': 1.0.3 base64url-universal: 1.1.0 crypto-ld: 6.0.0 - dev: false /@digitalcredentials/ed25519-verification-key-2020@4.0.0: resolution: {integrity: sha512-GrfITgp1guFbExZckj2q6LOxxm08PFSScr0lBYtDRezJa6CTpA9XQ8yXSSXE3LvpEi5/2uOMFxxIfKAtL1J2ww==} @@ -1700,7 +1699,6 @@ packages: engines: {node: '>=10.0.0'} dependencies: lru-cache: 6.0.0 - dev: false /@digitalcredentials/open-badges-context@0.1.2: resolution: {integrity: sha512-uaaL1htmUsJESfb3v7bAXKA4xu/xc7bzmK2R7BlYJEKY1QZpYOH9PZBrGqUEWfcMHfu1V2ILErIU4vMDz20lHQ==} @@ -1765,7 +1763,6 @@ packages: crypto-ld: 6.0.0 ed2curve: 0.3.0 tweetnacl: 1.0.3 - dev: false /@digitalcredentials/x25519-key-agreement-key-2020@3.0.0: resolution: {integrity: sha512-mCh6eRh6opBZiEtAWZ3RvCGs6JP9QpN2/xPxncQIKBK9WBUxONgL1CEsTUTRcisGvWQrUcqVXRHQ0Tl6b8weSQ==} @@ -3342,7 +3339,6 @@ packages: resolution: {integrity: sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q==} dependencies: '@stablelib/int': 1.0.1 - dev: false /@stablelib/ed25519@1.0.3: resolution: {integrity: sha512-puIMWaX9QlRsbhxfDc5i+mNPMY+0TmQEskunY1rZEBPi1acBCVQAhnsk/1Hk50DGPtVsZtAWQg4NHGlVaO9Hqg==} @@ -3350,22 +3346,18 @@ packages: '@stablelib/random': 1.0.2 '@stablelib/sha512': 1.0.1 '@stablelib/wipe': 1.0.1 - dev: false /@stablelib/hash@1.0.1: resolution: {integrity: sha512-eTPJc/stDkdtOcrNMZ6mcMK1e6yBbqRBaNW55XA1jU8w/7QdnCF0CmMmOD1m7VSkBR44PWrMHU2l6r8YEQHMgg==} - dev: false /@stablelib/int@1.0.1: resolution: {integrity: sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w==} - dev: false /@stablelib/random@1.0.2: resolution: {integrity: sha512-rIsE83Xpb7clHPVRlBj8qNe5L8ISQOzjghYQm/dZ7VaM2KHYwMW5adjQjrzTZCchFnNCNhkwtnOBa9HTMJCI8w==} dependencies: '@stablelib/binary': 1.0.1 '@stablelib/wipe': 1.0.1 - dev: false /@stablelib/sha512@1.0.1: resolution: {integrity: sha512-13gl/iawHV9zvDKciLo1fQ8Bgn2Pvf7OV6amaRVKiq3pjQ3UmEpXxWiAfV8tYjUpeZroBxtyrwtdooQT/i3hzw==} @@ -3373,11 +3365,9 @@ packages: '@stablelib/binary': 1.0.1 '@stablelib/hash': 1.0.1 '@stablelib/wipe': 1.0.1 - dev: false /@stablelib/wipe@1.0.1: resolution: {integrity: sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==} - dev: false /@tokenizer/token@0.3.0: resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} @@ -4356,12 +4346,10 @@ packages: engines: {node: '>=8.3.0'} dependencies: base64url: 3.0.1 - dev: false /base64url@3.0.1: resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} engines: {node: '>=6.0.0'} - dev: false /base@0.11.2: resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} @@ -5248,7 +5236,6 @@ packages: /crypto-ld@6.0.0: resolution: {integrity: sha512-XWL1LslqggNoaCI/m3I7HcvaSt9b2tYzdrXO+jHLUj9G1BvRfvV7ZTFDVY5nifYuIGAPdAGu7unPxLRustw3VA==} engines: {node: '>=8.3.0'} - dev: false /crypto-ld@7.0.0: resolution: {integrity: sha512-RrXy6aB0TOhSiqsgavTQt1G8mKomKIaNLb2JZxj7A/Vi0EwmXguuBQoeiAvePfK6bDR3uQbqYnaLLs4irTWwgw==} @@ -5555,7 +5542,6 @@ packages: resolution: {integrity: sha512-8w2fmmq3hv9rCrcI7g9hms2pMunQr1JINfcjwR9tAyZqhtyaMN991lF/ZfHfr5tzZQ8c7y7aBgZbjfbd0fjFwQ==} dependencies: tweetnacl: 1.0.3 - dev: false /ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -6570,7 +6556,7 @@ packages: resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} engines: {node: '>=14.14'} dependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jsonfile: 6.1.0 universalify: 2.0.0 dev: true @@ -8356,7 +8342,7 @@ packages: dependencies: universalify: 2.0.0 optionalDependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 /jsonld-document-loader@1.2.1: resolution: {integrity: sha512-CtFyIBZApeVvs6QgyS7Gcp8h1dUs+1XNHcV4Sr6O9ItPaL0hVgqe47Tgs3RNH0A5Bc4p3UFPKAJVHKSOQq4mhQ==} @@ -9587,7 +9573,7 @@ packages: dependencies: env-paths: 2.2.1 glob: 7.2.3 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 make-fetch-happen: 10.2.1 nopt: 6.0.0 npmlog: 6.0.2 @@ -12318,7 +12304,6 @@ packages: /tweetnacl@1.0.3: resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} - dev: false /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} @@ -12861,7 +12846,7 @@ packages: engines: {node: '>=6'} dependencies: detect-indent: 5.0.0 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 make-dir: 2.1.0 pify: 4.0.1 sort-keys: 2.0.0