diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..3fb27da --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "tabWidth": 4 +} diff --git a/package.json b/package.json index 9aa11b7..c338876 100644 --- a/package.json +++ b/package.json @@ -1,41 +1,42 @@ { - "name": "@tanglelabs/oid4vc", - "version": "0.2.1-alpha.10", - "main": "dist/index.js", - "license": "MIT", - "type": "module", - "devDependencies": { - "@types/express": "^4.17.17", - "@types/jest": "^29.5.3", - "@types/mime": "^4.0.0", - "@types/node": "^20.4.2", - "express": "^4.18.2", - "express-async-handler": "^1.2.0", - "fix-esm-import-path": "^1.5.0", - "jest": "^29.6.2", - "key-did-resolver": "^3.0.0", - "nodemon": "^3.0.1", - "ts-jest": "^29.1.1", - "ts-jest-resolver": "^2.0.1", - "ts-node": "^10.9.1", - "typescript": "^5.1.6" - }, - "dependencies": { - "@sphereon/did-resolver-jwk": "0.10.2-unstable.5", - "@sphereon/pex": "2.1.0", - "@sphereon/pex-models": "^2.0.3", - "axios": "^1.6.0", - "buffer": "^6.0.3", - "did-jwt": "^7.2.4", - "did-resolver": "^4.1.0", - "nanoid": "^3.0.0", - "tweetnacl": "^1.0.3", - "tweetnacl-util": "^0.15.1" - }, - "scripts": { - "dev": "nodemon --watch './**/*.ts' --exec 'node --experimental-specifier-resolution=node --trace-warnings --loader ts-node/esm' src/test.ts", - "build": "tsc && fix-esm-import-path ./dist", - "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", - "test-cov": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage" - } -} \ No newline at end of file + "name": "@tanglelabs/oid4vc", + "version": "0.2.1-alpha.11", + "main": "dist/index.js", + "license": "MIT", + "type": "module", + "devDependencies": { + "@types/express": "^4.17.17", + "@types/jest": "^29.5.3", + "@types/mime": "^4.0.0", + "@types/node": "^20.4.2", + "express": "^4.18.2", + "express-async-errors": "^3.1.1", + "express-async-handler": "^1.2.0", + "fix-esm-import-path": "^1.5.0", + "jest": "^29.6.2", + "key-did-resolver": "^3.0.0", + "nodemon": "^3.0.1", + "ts-jest": "^29.1.1", + "ts-jest-resolver": "^2.0.1", + "ts-node": "^10.9.1", + "typescript": "^5.1.6" + }, + "dependencies": { + "@sphereon/did-resolver-jwk": "0.10.2-unstable.5", + "@sphereon/pex": "2.1.0", + "@sphereon/pex-models": "^2.0.3", + "axios": "^1.6.0", + "buffer": "^6.0.3", + "did-jwt": "^7.2.4", + "did-resolver": "^4.1.0", + "nanoid": "^3.0.0", + "tweetnacl": "^1.0.3", + "tweetnacl-util": "^0.15.1" + }, + "scripts": { + "dev": "nodemon --watch './**/*.ts' --exec 'node --experimental-specifier-resolution=node --trace-warnings --loader ts-node/esm' src/test.ts", + "build": "tsc && fix-esm-import-path ./dist", + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", + "test-cov": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea02ecb..60ec312 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,6 +54,9 @@ importers: express: specifier: ^4.18.2 version: 4.19.2 + express-async-errors: + specifier: ^3.1.1 + version: 3.1.1(express@4.19.2) express-async-handler: specifier: ^1.2.0 version: 1.2.0 @@ -936,6 +939,11 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + express-async-errors@3.1.1: + resolution: {integrity: sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==} + peerDependencies: + express: ^4.16.2 + express-async-handler@1.2.0: resolution: {integrity: sha512-rCSVtPXRmQSW8rmik/AIb2P0op6l7r1fMW538yyvTMltCO4xQEWMmobfrIxN2V1/mVrgxB8Az3reYF6yUZw37w==} @@ -3159,6 +3167,10 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 + express-async-errors@3.1.1(express@4.19.2): + dependencies: + express: 4.19.2 + express-async-handler@1.2.0: {} express@4.19.2: diff --git a/src/oid4vci/Holder/holder.ts b/src/oid4vci/Holder/holder.ts index bbe82aa..acc6eed 100644 --- a/src/oid4vci/Holder/holder.ts +++ b/src/oid4vci/Holder/holder.ts @@ -1,10 +1,11 @@ -import axios from "axios"; -import { parseQueryStringToJson } from "../../utils/query"; -import { CreateTokenRequestOptions } from "./index.types"; -import { KeyPairRequirements } from "../../common/index.types"; -import * as didJWT from "did-jwt"; -import { buildSigner, snakeToCamelRecursive } from "../../utils/utils"; -import { joinUrls } from "../../utils/url"; +import axios from 'axios'; +import { parseQueryStringToJson } from '../../utils/query'; +import { CreateTokenRequestOptions } from './index.types'; +import { KeyPairRequirements } from '../../common/index.types'; +import * as didJWT from 'did-jwt'; +import { buildSigner, snakeToCamelRecursive } from '../../utils/utils'; +import { joinUrls } from '../../utils/url'; +import qs from 'querystring'; export class VcHolder { private holderKeys: KeyPairRequirements; @@ -17,8 +18,8 @@ export class VcHolder { async createTokenRequest(args: CreateTokenRequestOptions) { const response = { - grant_type: "urn:ietf:params:oauth:grant-type:pre-authorized_code", - "pre-authorized_code": args.preAuthCode, + grant_type: 'urn:ietf:params:oauth:grant-type:pre-authorized_code', + 'pre-authorized_code': args.preAuthCode, }; // @ts-ignore if (args.userPin) response.user_pin = args.userPin; @@ -26,9 +27,9 @@ export class VcHolder { } async parseCredentialOffer(offer: string): Promise> { - const rawOffer = parseQueryStringToJson( - decodeURI(offer.split("openid-credential-offer://")[1]) - ); + const decodedUri = decodeURI(offer); + const search = new URL(decodedUri).search; + const rawOffer = parseQueryStringToJson(search); let credentialOffer; if (rawOffer.credentialOfferUri) { const { data } = await axios.get(rawOffer.credentialOfferUri); @@ -44,11 +45,11 @@ export class VcHolder { const offerRaw = await this.parseCredentialOffer(credentialOffer); const metadataEndpoint = joinUrls( offerRaw.credentialIssuer, - ".well-known/openid-credential-issuer" + '.well-known/openid-credential-issuer', ); const oauthMetadataUrl = joinUrls( offerRaw.credentialIssuer, - ".well-known/oauth-authorization-server" + '.well-known/oauth-authorization-server', ); const { data } = await axios.get(metadataEndpoint); const { data: oauthServerMetadata } = await axios.get(oauthMetadataUrl); @@ -59,40 +60,60 @@ export class VcHolder { const display = metadata.display && - metadata.display.find((d: any) => d.locale === "en-US"); + metadata.display.find((d: any) => d.locale === 'en-US'); metadata.display = display; return metadata; } + constructPayload( + credentials: string[], + conf: Record, + proof: string, + ) { + let payload; + if (credentials.length > 1) { + payload = { + credential_requests: [ + ...credentials.map((c) => { + const format = conf[c].format; + let p: Record = { + format, + proof: { + proof_type: 'jwt', + jwt: proof, + }, + credential_definition: + conf[c].credential_definition, + }; + if (format === 'vc+sd-jwt') p.vct = conf[c].vct; + return p; + }), + ], + }; + } else { + payload = { + format: conf[credentials[0]].format, + credential_definition: + conf[credentials[0]].credential_definition, + proof: { + proof_type: 'jwt', + jwt: proof, + }, + }; + } + return payload; + } + async retrieveCredential( path: string, accessToken: string, credentials: string[], - proof: string + proof: string, + conf: Record = null, ): Promise { - const payload = - credentials.length > 1 - ? { - credential_requests: [ - ...credentials.map((c) => ({ - format: "jwt_vc_json", - // credentials, - proof: { - proof_type: "jwt", - jwt: proof, - }, - })), - ], - } - : { - format: "jwt_vc_json", - // credentials, - proof: { - proof_type: "jwt", - jwt: proof, - }, - }; + const payload = this.constructPayload(credentials, conf, proof); + const { data } = await axios.post(path, payload, { headers: { Authorization: `Bearer ${accessToken}`, @@ -102,7 +123,7 @@ export class VcHolder { Object.keys(credentials).length > 1 ? data.credential_responses.map( (c: { format: string; credential: string }) => - c.credential + c.credential, ) : [data.credential]; return response; @@ -119,22 +140,22 @@ export class VcHolder { const credentialConfigsExist = this.checkArrayOverlap( credentialConfigurationIds, - Object.keys(metadata.credentialConfigurationsSupported) + Object.keys(metadata.credentialConfigurationsSupported), ); if (!credentialConfigsExist) - throw new Error("unsupported_credential_type"); + throw new Error('unsupported_credential_type'); const createTokenPayload: { preAuthCode: any; userPin?: number } = { preAuthCode: - grants["urn:ietf:params:oauth:grant-type:pre-authorized_code"][ - "pre-authorized_code" + grants['urn:ietf:params:oauth:grant-type:pre-authorized_code'][ + 'pre-authorized_code' ], }; if ( - grants["urn:ietf:params:oauth:grant-type:pre-authorized_code"][ - "user_pin_required" + grants['urn:ietf:params:oauth:grant-type:pre-authorized_code'][ + 'user_pin_required' ] ) createTokenPayload.userPin = Number(pin); @@ -143,7 +164,12 @@ export class VcHolder { const tokenResponse = await axios.post( metadata.tokenEndpoint, - tokenRequest + qs.stringify(tokenRequest), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }, ); const token = await didJWT.createJWT( @@ -152,7 +178,7 @@ export class VcHolder { nonce: tokenResponse.data.c_nonce, }, { signer: this.signer, issuer: this.holderKeys.did }, - { alg: this.holderKeys.signingAlgorithm, kid: this.holderKeys.kid } + { alg: this.holderKeys.signingAlgorithm, kid: this.holderKeys.kid }, ); const endpoint = @@ -164,9 +190,10 @@ export class VcHolder { endpoint, tokenResponse.data.access_token, credentialConfigurationIds, - token + token, + metadata.credentialConfigurationsSupported, ); } } -export * from "./index.types"; +export * from './index.types'; diff --git a/src/oid4vci/Issuer/index.types.ts b/src/oid4vci/Issuer/index.types.ts index 2410f09..afdf6c5 100644 --- a/src/oid4vci/Issuer/index.types.ts +++ b/src/oid4vci/Issuer/index.types.ts @@ -5,59 +5,60 @@ type CryptographicSuites = "EdDSA" | "ES256"; type ProofTypes = "jwt"; export type SupportedCredentials = { - name: string; - type: string[]; - display?: { - name?: string; - locale?: string; - logo?: { - uri: string; - alt_text?: string; - }; - }[]; - raw?: Record; + name: string; + type: string[]; + display?: { + name?: string; + locale?: string; + logo?: { + uri: string; + alt_text?: string; + }; + }[]; + raw?: Record; + format: "vc+sd-jwt" | "dc+sd-jwt" | "jwt_vc_json"; }; export type VcIssuerOptions = { - credentialEndpoint: string; - tokenEndpoint: string; - batchCredentialEndpoint: string; - credentialIssuer: string; - cryptographicBindingMethodsSupported: string[]; - credentialSigningAlgValuesSupported: CryptographicSuites[]; - proofTypesSupported: ProofTypes[]; - store: IIssuerStore; - logoUri?: string; - clientName?: string; - supportedCredentials?: SupportedCredentials[]; - resolver: Resolvable; + credentialEndpoint: string; + tokenEndpoint: string; + batchCredentialEndpoint: string; + credentialIssuer: string; + cryptographicBindingMethodsSupported: string[]; + credentialSigningAlgValuesSupported: CryptographicSuites[]; + proofTypesSupported: ProofTypes[]; + store: IIssuerStore; + logoUri?: string; + clientName?: string; + supportedCredentials?: SupportedCredentials[]; + resolver: Resolvable; } & KeyPairRequirements; export type IssuerStoreData = { id: string; pin: number }; type CreateCredentialOfferByValue = { - requestBy: "value"; - credentials: string[]; - pinRequired?: boolean; - expiresIn?: number; + requestBy: "value"; + credentials: string[]; + pinRequired?: boolean; + expiresIn?: number; }; type CreateCredentialOfferByReference = { - requestBy: "reference"; - credentialOfferUri: string; - credentials: string[]; - pinRequired?: boolean; - expiresIn?: number; + requestBy: "reference"; + credentialOfferUri: string; + credentials: string[]; + pinRequired?: boolean; + expiresIn?: number; }; export type CreateCredentialOfferOptions = - | CreateCredentialOfferByReference - | CreateCredentialOfferByValue; + | CreateCredentialOfferByReference + | CreateCredentialOfferByValue; export interface IIssuerStore { - create: (payload: T) => Promise | T; - getAll: () => Promise | T[]; - getById: (id: string) => Promise | T; - updateById: (id: string, payload: Partial) => Promise | T; - deleteById: (id: string) => Promise; + create: (payload: T) => Promise | T; + getAll: () => Promise | T[]; + getById: (id: string) => Promise | T; + updateById: (id: string, payload: Partial) => Promise | T; + deleteById: (id: string) => Promise; } diff --git a/src/oid4vci/Issuer/issuer.ts b/src/oid4vci/Issuer/issuer.ts index 9174a06..3aeb81c 100644 --- a/src/oid4vci/Issuer/issuer.ts +++ b/src/oid4vci/Issuer/issuer.ts @@ -1,24 +1,24 @@ -import { nanoid } from "nanoid"; +import { nanoid } from 'nanoid'; import { camelToSnakeCaseRecursive, objectToSnakeCaseQueryString, -} from "../../utils/query"; +} from '../../utils/query'; import { CreateCredentialOfferOptions, IIssuerStore, IssuerStoreData, SupportedCredentials, VcIssuerOptions, -} from "./index.types"; -import { generatePin } from "../../utils/pin"; -import { TokenRequest } from "../Holder/index.types"; -import * as didJWT from "did-jwt"; -import { buildSigner } from "../../utils/signer"; -import { Resolvable } from "did-resolver"; -import { SigningAlgs } from "../../siopv2/siop"; +} from './index.types'; +import { generatePin } from '../../utils/pin'; +import { TokenRequest } from '../Holder/index.types'; +import * as didJWT from 'did-jwt'; +import { buildSigner } from '../../utils/signer'; +import { Resolvable } from 'did-resolver'; +import { SigningAlgs } from '../../siopv2/siop'; export class VcIssuer { - metadata: Omit; + metadata: Omit; store: IIssuerStore; signer: didJWT.Signer; did: string; @@ -30,7 +30,7 @@ export class VcIssuer { const { store, did, privKeyHex, kid, ...others } = options; const proofTypes = this.transformProofs( others.proofTypesSupported, - others.credentialSigningAlgValuesSupported + others.credentialSigningAlgValuesSupported, ); this.metadata = others; // @ts-ignore @@ -50,7 +50,7 @@ export class VcIssuer { // @ts-ignore (proofsMap[p] = { proof_signing_alg_values_supported: algValues, - }) + }), ); return proofsMap; } @@ -67,18 +67,18 @@ export class VcIssuer { supportedCredentialsArray.forEach( (cred) => (credential_configurations_supported[cred.name] = { - format: "jwt_vc_json", + format: cred.format, cryptographic_binding_methods_supported: this.metadata.cryptographicBindingMethodsSupported, credential: this.metadata.credentialSigningAlgValuesSupported, proof_types_supported: this.metadata.proofTypesSupported, credential_definition: { - type: ["VerifiableCredential", ...cred.type], + type: ['VerifiableCredential', ...cred.type], }, scope: cred.name, display: cred.display, - }) + }), ); const metadata = { credential_issuer: this.metadata.credentialIssuer, @@ -91,7 +91,7 @@ export class VcIssuer { // @ts-ignore metadata.display = [ { - locale: "en-US", + locale: 'en-US', logo_uri: this.metadata.logoUri, client_name: this.metadata.clientName, }, @@ -108,7 +108,7 @@ export class VcIssuer { async createCredentialOffer( args: CreateCredentialOfferOptions, - extras: Record = {} + extras: Record = {}, ): Promise<{ uri: string; pin?: number; @@ -129,7 +129,7 @@ export class VcIssuer { signer: this.signer, expiresIn, }, - { alg: this.alg, kid: this.kid } + { alg: this.alg, kid: this.kid }, ); const offer = camelToSnakeCaseRecursive({ @@ -137,19 +137,19 @@ export class VcIssuer { ...options, credentialConfigurationIds: [...credentials], grants: { - "urn:ietf:params:oauth:grant-type:pre-authorized_code": { - "pre-authorized_code": code, + 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { + 'pre-authorized_code': code, }, }, }); if (pinNeeded) offer.grants.tx_code = { length: 6, - input_mode: "numeric", + input_mode: 'numeric', }; const pin = args.pinRequired ? generatePin() : null; const jsonEmbed = - requestBy === "value" + requestBy === 'value' ? { credential_offer: offer } : { credential_offer_uri: credentialOfferUri }; @@ -157,30 +157,29 @@ export class VcIssuer { const uri = encodeURI( `openid-credential-offer://${objectToSnakeCaseQueryString({ ...jsonEmbed, - })}` + })}`, ); return { uri, pin, offer }; } async createTokenResponse(request: TokenRequest) { - if (!request.grant_type || !request["pre-authorized_code"]) - throw new Error("invalid_request"); + if (!request.grant_type || !request['pre-authorized_code']) + throw new Error('invalid_request'); const { signer, payload } = await didJWT - .verifyJWT(request["pre-authorized_code"], { + .verifyJWT(request['pre-authorized_code'], { resolver: this.resolver, policies: { aud: false }, }) .catch((e) => { - console.log("invalid", request); console.error(e); - throw new Error("invalid_request"); + throw new Error('invalid_request'); }); const { pin } = await this.store.getById(payload.id); const { iat, aud, iss, ...extrasRaw } = payload; const extras = extrasRaw ?? {}; - if (pin && pin !== request.user_pin) throw new Error("invalid_grant"); - if (signer.controller !== this.did) throw new Error("invalid_token"); + if (pin && pin !== request.user_pin) throw new Error('invalid_grant'); + if (signer.controller !== this.did) throw new Error('invalid_token'); const access_token = await didJWT.createJWT( { aud: payload.aud, @@ -192,12 +191,12 @@ export class VcIssuer { signer: this.signer, expiresIn: 24 * 60 * 60, }, - { alg: this.alg, kid: this.kid } + { alg: this.alg, kid: this.kid }, ); return { access_token, - token_type: "bearer", + token_type: 'bearer', expires_in: 86400, c_nonce: nanoid(), c_nonce_expires_in: 86400, @@ -211,7 +210,7 @@ export class VcIssuer { token?: string; proof?: string; }) { - if (!token || !proof) throw new Error("invalid_request"); + if (!token || !proof) throw new Error('invalid_request'); const { payload, signer } = await didJWT.verifyJWT(token, { policies: { aud: false }, resolver: this.resolver, @@ -220,7 +219,7 @@ export class VcIssuer { signer.controller !== this.did || payload.exp < Math.floor(Date.now() / 1000) ) - throw new Error("invalid_token"); + throw new Error('invalid_token'); const { signer: didSigner } = await didJWT.verifyJWT(proof, { policies: { aud: false }, resolver: this.resolver, @@ -239,12 +238,12 @@ export class VcIssuer { // @ts-ignore response = { credential_responses: [] }; response.credential_responses = credentials.map((credential) => ({ - format: "jwt_vc_json", + format: 'jwt_vc_json', credential, })); } else { response = { - format: "jwt_vc_json", + format: 'jwt_vc_json', credential: credentials[0], }; } @@ -253,4 +252,4 @@ export class VcIssuer { } } -export * from "./index.types"; +export * from './index.types'; diff --git a/src/siopv2/OpenidProvider/op.ts b/src/siopv2/OpenidProvider/op.ts index 4449438..e35f6c2 100644 --- a/src/siopv2/OpenidProvider/op.ts +++ b/src/siopv2/OpenidProvider/op.ts @@ -1,15 +1,16 @@ -import { PEX } from "@sphereon/pex"; -import { parseQueryStringToJson } from "../../utils/query"; -import { OPOptions } from "./index.types"; -import * as didJWT from "did-jwt"; -import { PresentationDefinitionV2 } from "@sphereon/pex-models"; -import axios from "axios"; -import { buildSigner } from "../../utils/signer"; -import { Resolvable } from "did-resolver"; -import { SiopRequest } from "../index.types"; -import { snakeToCamelRecursive } from "../../utils/object"; -import { normalizePresentationDefinition } from "../../utils/definition"; -import { SigningAlgs } from "../siop"; +import { PEX } from '@sphereon/pex'; +import { parseQueryStringToJson } from '../../utils/query'; +import { OPOptions } from './index.types'; +import * as didJWT from 'did-jwt'; +import { PresentationDefinitionV2 } from '@sphereon/pex-models'; +import axios from 'axios'; +import { buildSigner } from '../../utils/signer'; +import { Resolvable } from 'did-resolver'; +import { SiopRequest } from '../index.types'; +import { snakeToCamelRecursive } from '../../utils/object'; +import { normalizePresentationDefinition } from '../../utils/definition'; +import { SigningAlgs } from '../siop'; +import qs from 'querystring'; export class OpenidProvider { private did: string; @@ -36,21 +37,21 @@ export class OpenidProvider { sub: this.did, exp: Math.floor(Date.now() / 1000) + 365 * 24 * 60 * 60, state: request.state, + nonce: request.nonce, }, { issuer: this.did, signer: this.signer, }, - { alg: this.alg, kid: this.kid } + { alg: this.alg, kid: this.kid }, ); return { id_token: jwt }; } async getRequestFromOffer(request: string): Promise { - const requestRaw = parseQueryStringToJson( - decodeURI(request.split("siopv2://idtoken")[1]) - ); + const url = new URL(request); + const requestRaw = parseQueryStringToJson(decodeURI(url.search)); let requestJwt: string; if (requestRaw.requestUri) { @@ -69,23 +70,24 @@ export class OpenidProvider { .catch((e) => { console.error(e); throw e; - }) + }), ).payload as SiopRequest; return requestOptions; } private async encodeJwtVp( vp: Record, - request: SiopRequest + request: SiopRequest, ): Promise { const vpToken = await didJWT.createJWT( { sub: this.did, aud: request.clientId, vp: { ...vp }, + nonce: request.nonce, }, { issuer: this.did, signer: this.signer }, - { alg: this.alg, kid: this.kid } + { alg: this.alg, kid: this.kid }, ); return vpToken; } @@ -97,90 +99,88 @@ export class OpenidProvider { async getCredentialsFromRequest( request: string, - credentials: any[] - ): Promise> { + credentials: any[], + ): Promise { const pex = new PEX(); const requestOptions = await this.getRequestFromOffer(request); - if (requestOptions.responseType !== "vp_token") - throw new Error("invalid response type"); + if (requestOptions.responseType !== 'vp_token') + throw new Error('invalid response type'); const selected = pex.selectFrom( normalizePresentationDefinition( - requestOptions.presentationDefinition + requestOptions.presentationDefinition, ), - credentials + credentials, ); - if (selected.areRequiredCredentialsPresent === "error") - throw new Error("credentials not found"); + if (selected.areRequiredCredentialsPresent === 'error') + throw new Error('credentials not found'); - return selected.verifiableCredential; + return selected.verifiableCredential as string[]; } async createVPTokenResponse( presentationDefinition: PresentationDefinitionV2, credentials: string[], - request: SiopRequest + request: SiopRequest, ) { const pex = new PEX(); - const rawCredentials = await Promise.all( - credentials.map(async (c) => this.decodeVcJwt(c) as any) - ); const evaluation = pex.evaluateCredentials( presentationDefinition, - rawCredentials + credentials, ); - if (evaluation.areRequiredCredentialsPresent === "error") - throw new Error("credentials are not present"); + if (evaluation.areRequiredCredentialsPresent === 'error') + throw new Error('credentials are not present'); const { presentation, presentationSubmission } = pex.presentationFrom( presentationDefinition, - rawCredentials, - { holderDID: this.did } - ); - - const selectedCreds = await Promise.all( - credentials.filter(async (c) => { - presentation.verifiableCredential.includes( - (await this.decodeVcJwt(c)) as any - ); - }) + credentials, + { holderDID: this.did }, ); - presentation.verifiableCredential = selectedCreds; const vp_token = await this.encodeJwtVp(presentation, request); return { vp_token, - presentation_submission: presentationSubmission, + presentation_submission: JSON.stringify(presentationSubmission), }; } async sendAuthResponse(request: string, credentials?: any[]) { const requestOptions = await this.getRequestFromOffer(request); let response: Record; - if (requestOptions.responseType === "id_token") { + if (requestOptions.responseType === 'id_token') { response = await this.createIDTokenResponse(requestOptions); - } else if (requestOptions.responseType === "vp_token") { - if (!credentials) throw new Error("credentials not passed"); + } else if (requestOptions.responseType === 'vp_token') { + if (!credentials) throw new Error('credentials not passed'); + const selected = await this.getCredentialsFromRequest( + request, + credentials, + ); response = await this.createVPTokenResponse( normalizePresentationDefinition( - requestOptions.presentationDefinition + requestOptions.presentationDefinition, ), - credentials, - requestOptions + selected, + requestOptions, ); } response = { ...response, - nonce: requestOptions.nonce, state: requestOptions.state, }; - await axios.post(requestOptions.redirectUri, response).catch((e) => { - throw new Error("unable to send response"); - }); + + await axios + .post(requestOptions.redirectUri, qs.stringify(response), { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }) + .catch((e) => { + throw new Error('unable to send response'); + }); return response; } } -export * from "./index.types"; +export * from './index.types'; diff --git a/src/siopv2/RelyingParty/rp.ts b/src/siopv2/RelyingParty/rp.ts index d31680d..fd64f54 100644 --- a/src/siopv2/RelyingParty/rp.ts +++ b/src/siopv2/RelyingParty/rp.ts @@ -1,19 +1,19 @@ -import { objectToSnakeCaseQueryString } from "../../utils/query"; +import { objectToSnakeCaseQueryString } from '../../utils/query'; import { CreateRequestOptions, RPOptions, AuthResponse, SiopRequestResult, SigningAlgs, -} from "./index.types"; -import * as didJWT from "did-jwt"; -import { PEX } from "@sphereon/pex"; -import { PresentationDefinitionV2 } from "@sphereon/pex-models"; -import { buildSigner } from "../../utils/signer"; -import { Resolvable } from "did-resolver"; -import { camelToSnakeRecursive } from "../../utils/object"; -import { nanoid } from "nanoid"; -import { normalizePresentationDefinition } from "../../utils/definition"; +} from './index.types'; +import * as didJWT from 'did-jwt'; +import { PEX } from '@sphereon/pex'; +import { PresentationDefinitionV2 } from '@sphereon/pex-models'; +import { buildSigner } from '../../utils/signer'; +import { Resolvable } from 'did-resolver'; +import { camelToSnakeRecursive } from '../../utils/object'; +import { nanoid } from 'nanoid'; +import { normalizePresentationDefinition } from '../../utils/definition'; export class RelyingParty { private metadata: RPOptions; @@ -45,7 +45,7 @@ export class RelyingParty { */ async createRequest( - args: CreateRequestOptions + args: CreateRequestOptions, ): Promise { const { requestBy, ...requestOptions } = args; const { privKeyHex, did, kid, ...metadata } = this.metadata; @@ -56,8 +56,8 @@ export class RelyingParty { ...metadata.clientMetadata, ...args.clientMetadata, }, - scope: "openid", - responseMode: "post", + scope: 'openid', + responseMode: 'post', }; const nonce = nanoid(); @@ -79,16 +79,16 @@ export class RelyingParty { const request = await didJWT.createJWT( { ...requestParams }, { issuer: this.did, signer: this.signer }, - { kid: this.kid, alg: this.alg } + { kid: this.kid, alg: this.alg }, ); - requestBy === "value" + requestBy === 'value' ? (requestQuery.request = request) : (requestQuery.requestUri = args.requestUri); return { uri: encodeURI( - `siopv2://idtoken${objectToSnakeCaseQueryString(requestQuery)}` + `siopv2://idtoken${objectToSnakeCaseQueryString(requestQuery)}`, ) as `siopv2://idtoken${string}`, request: request, requestOptions: requestParams, @@ -102,13 +102,13 @@ export class RelyingParty { aud: false, }, }); - if (!result.verified) throw new Error("Invalid JWT"); + if (!result.verified) throw new Error('Invalid JWT'); return result.payload; } async verifyAuthResponse( authResponse: AuthResponse, - presentationDefinition?: PresentationDefinitionV2 + presentationDefinition?: PresentationDefinitionV2, ) { if ( !( @@ -116,7 +116,7 @@ export class RelyingParty { (authResponse.vp_token && authResponse.presentation_submission) ) ) - throw new Error("Bad response"); + throw new Error('Bad response'); if (authResponse.id_token) { await this.validateJwt(authResponse.id_token); return; @@ -130,11 +130,11 @@ export class RelyingParty { authResponse.vp_token, { generatePresentationSubmission: true, - } + }, ); - if (result.areRequiredCredentialsPresent === "error") - throw new Error("Invalid Credentials Shared"); + if (result.areRequiredCredentialsPresent === 'error') + throw new Error('Invalid Credentials Shared'); } } @@ -147,10 +147,10 @@ export class RelyingParty { iss: this.did, }, { issuer: this.did, signer: this.signer }, - { alg: this.alg, kid: this.kid } + { alg: this.alg, kid: this.kid }, ); return token; } } -export * from "./index.types"; +export * from './index.types'; diff --git a/src/tests/mocks/openid.ts b/src/tests/mocks/openid.ts index dc2d8ac..217152e 100644 --- a/src/tests/mocks/openid.ts +++ b/src/tests/mocks/openid.ts @@ -1,4 +1,4 @@ -import { readFile, writeFile } from "fs/promises"; +import { readFile, writeFile } from 'fs/promises'; import { IssuerStoreData, OpenidProvider, @@ -8,52 +8,59 @@ import { VcHolder, VcIssuer, buildSigner, -} from "../.."; -import { resolver } from "./iota-resolver"; -import { testingKeys } from "./keys.mock"; -import path from "path"; -import { fileURLToPath } from "url"; +} from '../..'; +import { resolver } from './iota-resolver'; +import { testingKeys } from './keys.mock'; +import path from 'path'; +import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const file = path.resolve(__dirname, "./store.test-mock"); +const file = path.resolve(__dirname, './store.test-mock'); -// @ts-ignore -const reader = async () => { - const raw = await readFile(file).catch((e) => { - if (e.code === "ENOENT") writer([]); - return Buffer.from(JSON.stringify([])); - }); - return JSON.parse(raw.toString()); -}; - -const writer = async (data: IssuerStoreData[]) => { - await writeFile(file, JSON.stringify(data)); -}; +class Store { + create(payload: { id: string; pin: number }) { + return { id: payload.id, pin: null } as { + id: string; + pin: number | null; + }; + } + getAll: () => + | { id: string; pin: number }[] + | Promise<{ id: string; pin: number }[]>; + getById(id: string) { + return { id, pin: null } as { id: string; pin: number | null }; + } + updateById: ( + id: string, + payload: Partial<{ id: string; pin: number }>, + ) => { id: string; pin: number } | Promise<{ id: string; pin: number }>; + deleteById: (id: string) => Promise<{ id: string; pin: number }>; +} const baseIssuerConfig = { - batchCredentialEndpoint: "http://localhost:5999/api/credentials", - credentialEndpoint: "http://localhost:5999/api/credential", - credentialIssuer: "http://localhost:5999/", - proofTypesSupported: ["jwt"], - cryptographicBindingMethodsSupported: ["did:key"], - credentialSigningAlgValuesSupported: ["ES256"], + batchCredentialEndpoint: 'http://localhost:5999/api/credentials', + credentialEndpoint: 'http://localhost:5999/api/credential', + credentialIssuer: 'http://localhost:5999/', + proofTypesSupported: ['jwt'], + cryptographicBindingMethodsSupported: ['did:key'], + credentialSigningAlgValuesSupported: ['ES256'], resolver, - tokenEndpoint: "http://localhost:5999/token", - store: new SimpleStore({ reader, writer }), + tokenEndpoint: 'http://localhost:5999/token', + store: new Store(), supportedCredentials: {}, }; const baseRpConfig = { - clientId: "tanglelabs.io", - redirectUri: "http://localhost:5999/api/auth", + clientId: 'tanglelabs.io', + redirectUri: 'http://localhost:5999/api/auth', clientMetadata: { idTokenSigningAlgValuesSupported: [SigningAlgs.ES256], - subjectSyntaxTypesSupported: ["did:iota"], + subjectSyntaxTypesSupported: ['did:iota'], vpFormats: { jwt_vc_json: { - alg: ["EdDSA"], + alg: ['EdDSA'], }, }, }, @@ -79,13 +86,14 @@ export const issuer = new VcIssuer({ ...baseIssuerConfig, supportedCredentials: [ { - name: "wa_driving_license", - type: ["wa_driving_license"], + name: 'wa_driving_license', + type: ['wa_driving_license'], display: [ { - name: "Washington Driving License", + name: 'Washington Driving License', }, ], + format: 'jwt_vc_json', }, ], }); diff --git a/src/tests/mocks/server.ts b/src/tests/mocks/server.ts index c0caae5..5dbf3ee 100644 --- a/src/tests/mocks/server.ts +++ b/src/tests/mocks/server.ts @@ -1,9 +1,9 @@ -import express from "express"; -import asyncHandler from "express-async-handler"; -import { Server } from "http"; -import { issuer, rp } from "./openid"; -import { presentationDefinition } from "./presentation-defs"; -import { credentials } from "./keys.mock"; +import express from 'express'; +import { Server } from 'http'; +import { issuer, rp } from './openid'; +import { presentationDefinition } from './presentation-defs'; +import { credentials } from './keys.mock'; +import 'express-async-errors'; export const requestsMap = new Map(); export const offersMap = new Map>(); @@ -12,72 +12,57 @@ let server: Server; export function startServer(port = 5999) { const app = express(); app.use(express.json()); - app.route("/siop/:id").get( - asyncHandler(async (req, res) => { - res.send(requestsMap.get(req.params.id)); - }) - ); - app.route("/.well-known/openid-credential-issuer").get( - asyncHandler(async (req, res) => { - const metadata = issuer.getIssuerMetadata(); - res.json(metadata); - }) - ); + app.use(express.urlencoded({ extended: true })); + app.route('/siop/:id').get(async (req, res) => { + res.send(requestsMap.get(req.params.id)); + }); + app.route('/.well-known/openid-credential-issuer').get(async (req, res) => { + const metadata = issuer.getIssuerMetadata(); + res.json(metadata); + }); - app.route("/.well-known/oauth-authorization-server").get( - asyncHandler(async (req, res) => { + app.route('/.well-known/oauth-authorization-server').get( + async (req, res) => { const metadata = issuer.getOauthServerMetadata(); res.json(metadata); - }) + }, ); - app.route("/token").post( - asyncHandler(async (req, res) => { - const response = await issuer - .createTokenResponse(req.body) - .catch((e) => console.log(e)); - res.json(response); - }) - ); + app.route('/token').post(async (req, res) => { + const response = await issuer.createTokenResponse(req.body); + res.json(response); + }); - app.route("/api/credential").post( - asyncHandler(async (req, res) => { - await issuer.validateCredentialsResponse({ - token: req.headers.authorization?.split("Bearer ")[1], - proof: req.body.proof.jwt, - }); - const response = await issuer.createSendCredentialsResponse({ - credentials: credentials, - }); - res.json(response); - }) - ); + app.route('/api/credential').post(async (req, res) => { + await issuer.validateCredentialsResponse({ + token: req.headers.authorization?.split('Bearer ')[1], + proof: req.body.proof.jwt, + }); + const response = await issuer.createSendCredentialsResponse({ + credentials: credentials, + }); + res.json(response); + }); - app.route("/api/offers/:id").get( - asyncHandler(async (req, res) => { - res.json(offersMap.get(req.params.id)); - }) - ); + app.route('/api/offers/:id').get(async (req, res) => { + res.json(offersMap.get(req.params.id)); + }); - app.route("/api/credentials").post( - asyncHandler(async (req, res) => { - await issuer.validateCredentialsResponse({ - token: req.headers.authorization?.split("Bearer ")[1], - proof: req.body.credential_requests[0].proof.jwt, - }); - const response = await issuer.createSendCredentialsResponse({ - credentials: [...credentials, ...credentials], - }); - res.json(response); - }) - ); + app.route('/api/credentials').post(async (req, res) => { + await issuer.validateCredentialsResponse({ + token: req.headers.authorization?.split('Bearer ')[1], + proof: req.body.credential_requests[0].proof.jwt, + }); + const response = await issuer.createSendCredentialsResponse({ + credentials: [...credentials, ...credentials], + }); + res.json(response); + }); - app.route("/api/auth").post( - asyncHandler(async (req, res) => { - await rp.verifyAuthResponse(req.body, presentationDefinition); - res.status(204).send(); - }) - ); + app.route('/api/auth').post(async (req, res) => { + await rp.verifyAuthResponse(req.body, presentationDefinition); + res.status(204).send(); + }); server = app.listen(port); } diff --git a/src/tests/openid4vc.spec.ts b/src/tests/openid4vc.spec.ts index 4c3fb54..25635b7 100644 --- a/src/tests/openid4vc.spec.ts +++ b/src/tests/openid4vc.spec.ts @@ -7,11 +7,11 @@ import { issuer, op, rp, -} from "./mocks/openid"; -import { startServer, stopServer } from "./mocks/server"; -import { oid4vciSuite } from "./suites/oid4vci.suite"; -import { oid4vpSuite } from "./suites/oid4vp.suite"; -import { siopSuite } from "./suites/siop.suite"; +} from './mocks/openid'; +import { startServer, stopServer } from './mocks/server'; +import { oid4vciSuite } from './suites/oid4vci.suite'; +import { oid4vpSuite } from './suites/oid4vp.suite'; +import { siopSuite } from './suites/siop.suite'; beforeAll(() => { startServer(); @@ -21,9 +21,9 @@ afterAll(() => { stopServer(); }); -describe("SIOPv2", siopSuite(op, rp)); -describe("OpenID4VP", oid4vpSuite(op, rp)); -describe("OpenID4VCI", oid4vciSuite(holder, issuer)); -describe("SIOPv2: EXTERNAL", siopSuite(externalOp, externalRp)); -describe("OpenID4VP: EXTERNAL", oid4vpSuite(externalOp, externalRp)); -describe("OpenID4VCI: EXTERNAL", oid4vciSuite(externalHolder, externalIssuer)); +describe('SIOPv2', siopSuite(op, rp)); +describe('OpenID4VP', oid4vpSuite(op, rp)); +describe('OpenID4VCI', oid4vciSuite(holder, issuer)); +describe('SIOPv2: EXTERNAL', siopSuite(externalOp, externalRp)); +describe('OpenID4VP: EXTERNAL', oid4vpSuite(externalOp, externalRp)); +describe('OpenID4VCI: EXTERNAL', oid4vciSuite(externalHolder, externalIssuer)); diff --git a/src/utils/bytes.ts b/src/utils/bytes.ts index 3a1db45..7c2a70f 100644 --- a/src/utils/bytes.ts +++ b/src/utils/bytes.ts @@ -1,9 +1,9 @@ import { Buffer } from "buffer"; export const bytesToString = (bytes: Uint8Array) => { - return Buffer.from(bytes).toString("hex"); + return Buffer.from(bytes).toString("hex"); }; export const stringToBytes = (str: string) => { - return Uint8Array.from(Buffer.from(str, "hex")); + return Uint8Array.from(Buffer.from(str, "hex")); };