diff --git a/packages/core/src/credentials/oidvc.test.ts b/packages/core/src/credentials/oidvc.test.ts new file mode 100644 index 00000000..8f8cbf7b --- /dev/null +++ b/packages/core/src/credentials/oidvc.test.ts @@ -0,0 +1,124 @@ +import { + acquireOpenIDCredentialFromURI, + getAuthURL, + decodeRequestJWT, + getPresentationSubmision, +} from './oidvc'; // replace with your actual file path +import {credentialServiceRPC} from '@docknetwork/wallet-sdk-wasm/src/services/credential'; +import {MetadataClient} from '@sphereon/oid4vci-client'; +import jwtDecode from 'jwt-decode'; +import axios from 'axios'; +import {pexService} from '@docknetwork/wallet-sdk-wasm/src/services/pex'; + +jest.mock('@docknetwork/wallet-sdk-wasm/src/services/credential'); +jest.mock('@sphereon/oid4vci-client'); +jest.mock('jwt-decode'); +jest.mock('axios'); +jest.mock('@docknetwork/wallet-sdk-wasm/src/services/pex'); + +describe('acquireOpenIDCredentialFromURI', () => { + const didProvider: any = { + getDIDKeyPairs: jest.fn(), + }; + + const uri = 'https://example.com/credential'; + + beforeEach(() => { + didProvider.getDIDKeyPairs.mockResolvedValue([{id: 'did:example:123'}]); + (credentialServiceRPC.acquireOIDCredential as jest.Mock).mockResolvedValue({ + credential: 'fake-credential', + }); + }); + + it('should acquire OID credential without authorization URL', async () => { + const response = await acquireOpenIDCredentialFromURI({didProvider, uri}); + expect(didProvider.getDIDKeyPairs).toHaveBeenCalled(); + expect(credentialServiceRPC.acquireOIDCredential).toHaveBeenCalledWith({ + uri, + holderKeyDocument: {id: 'did:example:123'}, + }); + expect(response).toBe('credential'); + }); + + it('should acquire OID credential with authorization URL', async () => { + const getAuthCode = jest.fn().mockResolvedValue('auth-code'); + (credentialServiceRPC.acquireOIDCredential as jest.Mock).mockResolvedValueOnce({ + authorizationURL: 'https://example.com/auth', + }); + + const response = await acquireOpenIDCredentialFromURI({ + didProvider, + uri, + getAuthCode, + }); + expect(getAuthCode).toHaveBeenCalledWith('https://example.com/auth'); + expect(credentialServiceRPC.acquireOIDCredential).toHaveBeenCalledWith({ + uri, + holderKeyDocument: {id: 'did:example:123'}, + authorizationCode: 'auth-code', + }); + expect(response).toBe('credential'); + }); +}); + +describe('getAuthURL', () => { + it('should generate an auth URL', async () => { + const uri = 'https://example.com?client_id=fake-client'; + const metadata = { + authorizationServerMetadata: { + request_object_signing_alg_values_supported: ['RS256'], + }, + authorization_endpoint: 'https://auth.example.com/authorize', + }; + (MetadataClient.retrieveAllMetadata as jest.Mock).mockResolvedValue(metadata); + + const result = await getAuthURL(uri); + expect(MetadataClient.retrieveAllMetadata).toHaveBeenCalledWith( + 'fake-client', + ); + expect(result).toContain('https://auth.example.com/authorize?'); + expect(result).toContain('client_id=dock-wallet'); + expect(result).toContain('redirect_uri=dockwallet://vp'); + }); +}); + +describe('decodeRequestJWT', () => { + it('should decode JWT from the request URI', async () => { + const uri = 'https://example.com?request_uri=https://example.com/jwt'; + const jwt = 'some-jwt'; + const decodedJWT = {sub: '1234567890', name: 'Testing', admin: true}; + (axios.get as jest.Mock).mockResolvedValue({data: jwt}); + (jwtDecode as jest.Mock).mockReturnValue(decodedJWT); + + const result = await decodeRequestJWT(uri); + expect(axios.get).toHaveBeenCalledWith('https://example.com/jwt'); + expect(jwtDecode).toHaveBeenCalledWith(jwt); + expect(result).toEqual(decodedJWT); + }); +}); + +describe('getPresentationSubmision', () => { + it('should get presentation submission', async () => { + const credentials = [{id: 'credential-1'}]; + const presentationDefinition = {id: 'presentation-definition-1'}; + const holderDID = 'did:example:123'; + const presentationSubmission = {definition_id: 'presentation-submission-1'}; + + (pexService.presentationFrom as jest.Mock).mockResolvedValue({ + presentation_submission: presentationSubmission, + }); + + const result = await getPresentationSubmision({ + credentials, + presentationDefinition, + holderDID, + }); + + expect(pexService.presentationFrom).toHaveBeenCalledWith({ + presentationDefinition, + credentials, + holderDID, + }); + expect(result).toEqual(presentationSubmission); + }); +}); diff --git a/packages/core/src/credentials/oidvc.ts b/packages/core/src/credentials/oidvc.ts index 99baac75..9638b104 100644 --- a/packages/core/src/credentials/oidvc.ts +++ b/packages/core/src/credentials/oidvc.ts @@ -1,7 +1,10 @@ - import {IWallet} from '../types'; import {IDIDProvider} from '../did-provider'; import {credentialServiceRPC} from '@docknetwork/wallet-sdk-wasm/src/services/credential'; +import {MetadataClient} from '@sphereon/oid4vci-client'; +import jwtDecode from 'jwt-decode'; +import axios from 'axios'; +import {pexService} from '@docknetwork/wallet-sdk-wasm/src/services/pex'; export async function acquireOpenIDCredentialFromURI({ didProvider, @@ -30,3 +33,78 @@ export async function acquireOpenIDCredentialFromURI({ return response.credential; } + +export async function getAuthURL( + uri: string, + walletClientId: string = 'dock-wallet', + requestedRedirectURI: string = 'dockwallet://vp', +) { + function buildOID4VPRequestURL(params, prefix = 'dockwallet://') { + return `${prefix}?${Object.keys(params) + .map( + key => + `${encodeURIComponent(key)}=${encodeURIComponent( + typeof params[key] === 'object' + ? JSON.stringify(params[key]) + : params[key], + )}`, + ) + .join('&')}`; + } + + const searchParams = new URL(uri).searchParams; + const params = new URLSearchParams(searchParams); + const clientId = params.get('client_id'); + const metadata = await MetadataClient.retrieveAllMetadata(clientId); + const requestedAlg = + metadata?.authorizationServerMetadata + ?.request_object_signing_alg_values_supported[0]; + const requestParams = { + scope: 'openid vp_token', + redirect_uri: requestedRedirectURI, + client_metadata: + requestedAlg && requestedAlg !== 'EdDSA' + ? JSON.stringify({ + vp_formats_supported: { + vc_json: { + alg_values_supported: [requestedAlg], + }, + }, + }) + : ['EdDSA'], + }; + + return buildOID4VPRequestURL( + { + ...requestParams, + client_id: walletClientId, + }, + metadata.authorization_endpoint, + ); +} + +export async function decodeRequestJWT(uri: string) { + const searchParams = new URL(uri).searchParams; + const params = new URLSearchParams(searchParams); + const requestUri = params.get('request_uri'); + const jwt = await axios.get(requestUri).then(res => res.data); + const decoded = jwtDecode(jwt); + + return decoded; +} + +export async function getPresentationSubmision({ + credentials, + presentationDefinition, + holderDID, +}) { + const presentation = await pexService.presentationFrom({ + presentationDefinition, + credentials, + holderDID, + }); + + return presentation.presentation_submission; +} + +pexService.evaluatePresentation; diff --git a/packages/wasm/src/services/pex/config.ts b/packages/wasm/src/services/pex/config.ts index b5f7af90..8bf064d1 100644 --- a/packages/wasm/src/services/pex/config.ts +++ b/packages/wasm/src/services/pex/config.ts @@ -19,6 +19,12 @@ export type FilterCredentialsParams = { holderDIDs: string[]; }; +export type CreatePresentationParams = { + credentials: any[]; + presentationDefinition: any; + holderDID: string; +}; + export type EvaluatePresentationParams = { presentation: any; presentationDefinition: any; diff --git a/packages/wasm/src/services/pex/service-rpc.js b/packages/wasm/src/services/pex/service-rpc.js index d735d567..e80a3c0a 100644 --- a/packages/wasm/src/services/pex/service-rpc.js +++ b/packages/wasm/src/services/pex/service-rpc.js @@ -4,6 +4,7 @@ import { FilterCredentialsParams, validation, EvaluatePresentationParams, + CreatePresentationParams, } from './config'; export class PEXServiceRPC extends RpcService { @@ -19,4 +20,8 @@ export class PEXServiceRPC extends RpcService { validation.evaluatePresentation(params); return this.call('evaluatePresentation', params); } + + async presentationFrom(params: CreatePresentationParams) { + return this.call('presentationFrom', params); + } } diff --git a/packages/wasm/src/services/pex/service.ts b/packages/wasm/src/services/pex/service.ts index 222f0d7d..cc3046b0 100644 --- a/packages/wasm/src/services/pex/service.ts +++ b/packages/wasm/src/services/pex/service.ts @@ -4,8 +4,9 @@ import { validation, EvaluatePresentationParams, FilterCredentialsParams, + CreatePresentationParams, } from './config'; -import {PEX} from '@sphereon/pex'; +import {IPresentationDefinition, PEX} from '@sphereon/pex'; const pex: PEX = new PEX(); @@ -65,6 +66,7 @@ class PEXService { rpcMethods = [ PEXService.prototype.filterCredentials, PEXService.prototype.evaluatePresentation, + PEXService.prototype.presentationFrom, ]; filterCredentials(params: FilterCredentialsParams) { @@ -89,6 +91,17 @@ class PEXService { return result; } + + presentationFrom(params: CreatePresentationParams) { + const {credentials, presentationDefinition, holderDID} = params; + const result: IPresentation = pex.presentationFrom( + removeOptionalAttribute(presentationDefinition), + credentials, + holderDID, + ); + + return result; + } } export const pexService = new PEXService(); diff --git a/yarn.lock b/yarn.lock index 5d63b2c8..b31f2759 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4775,6 +4775,13 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + axe-core@^4.6.2: version "4.8.1" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.8.1.tgz#6948854183ee7e7eae336b9877c5bafa027998ea"