diff --git a/packages/client/lib/IssuerSessionClient.ts b/packages/client/lib/IssuerSessionClient.ts new file mode 100644 index 00000000..21e91f1b --- /dev/null +++ b/packages/client/lib/IssuerSessionClient.ts @@ -0,0 +1,18 @@ +import { IssuerSessionIdRequestOpts, IssuerSessionResponse, OpenIDResponse, post } from '@sphereon/oid4vci-common'; + +import { LOG } from './index'; + +export const acquireIssuerSessionId = async (opts: IssuerSessionIdRequestOpts): Promise => { + LOG.debug(`acquiring issuer session endpoint from endpoint ${opts.sessionEndpoint}`); + const sessionResponse = (await post(opts.sessionEndpoint)) as OpenIDResponse; + if (sessionResponse.errorBody !== undefined) { + return Promise.reject(`an error occurred while requesting a issuer session token from endpoint ${opts.sessionEndpoint}: + ${sessionResponse.errorBody.error} - ${sessionResponse.errorBody.error_description}`); + } + if (sessionResponse.successBody === undefined || !Object.keys(sessionResponse.successBody).includes('session_id')) { + return Promise.reject( + `an error occurred while requesting a issuer session token from endpoint ${opts.sessionEndpoint}, missing session_token response`, + ); + } + return sessionResponse.successBody; +}; diff --git a/packages/client/lib/MetadataClientV1_0_13.ts b/packages/client/lib/MetadataClientV1_0_13.ts index b9076236..a98ad9f4 100644 --- a/packages/client/lib/MetadataClientV1_0_13.ts +++ b/packages/client/lib/MetadataClientV1_0_13.ts @@ -50,6 +50,7 @@ export class MetadataClientV1_0_13 { let credential_endpoint: string | undefined; let deferred_credential_endpoint: string | undefined; let authorization_endpoint: string | undefined; + let session_endpoint: string | undefined; let authorizationServerType: AuthorizationServerType = 'OID4VCI'; let authorization_servers: string[] = [issuer]; const oid4vciResponse = await MetadataClientV1_0_13.retrieveOpenID4VCIServerMetadata(issuer, { errorOnNotFound: false }); // We will handle errors later, given we will also try other metadata locations @@ -157,11 +158,16 @@ export class MetadataClientV1_0_13 { credentialIssuerMetadata = authMetadata as CredentialIssuerMetadataV1_0_13; } debug(`Issuer ${issuer} token endpoint ${token_endpoint}, credential endpoint ${credential_endpoint}`); + + if (credentialIssuerMetadata?.session_endpoint !== undefined) { + session_endpoint = credentialIssuerMetadata.session_endpoint; + } return { issuer, token_endpoint, credential_endpoint, deferred_credential_endpoint, + session_endpoint, authorization_server: authorization_servers[0], authorization_endpoint, authorizationServerType, diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index f42c8e46..f7433331 100644 --- a/packages/client/lib/OpenID4VCIClient.ts +++ b/packages/client/lib/OpenID4VCIClient.ts @@ -24,6 +24,7 @@ import { getSupportedCredentials, getTypesFromCredentialSupported, getTypesFromObject, + IssuerSessionResponse, KID_JWK_X5C_ERROR, NotificationRequest, NotificationResult, @@ -44,6 +45,7 @@ import { CredentialOfferClient } from './CredentialOfferClient'; import { CredentialRequestOpts } from './CredentialRequestClient'; import { CredentialRequestClientBuilderV1_0_11 } from './CredentialRequestClientBuilderV1_0_11'; import { CredentialRequestClientBuilderV1_0_13 } from './CredentialRequestClientBuilderV1_0_13'; +import { acquireIssuerSessionId } from './IssuerSessionClient'; import { MetadataClient } from './MetadataClient'; import { OpenID4VCIClientStateV1_0_11 } from './OpenID4VCIClientV1_0_11'; import { OpenID4VCIClientStateV1_0_13 } from './OpenID4VCIClientV1_0_13'; @@ -361,6 +363,16 @@ export class OpenID4VCIClient { return this.accessTokenResponse; } + public async acquireIssuerSessionId(): Promise { + if (!this._state.endpointMetadata) { + return Promise.reject('endpointMetadata no loaded, retrieveServerMetadata()'); + } + if (!('session_endpoint' in this._state.endpointMetadata) || !this._state.endpointMetadata.session_endpoint) { + return undefined; + } + return acquireIssuerSessionId({ sessionEndpoint: this._state.endpointMetadata.session_endpoint }); + } + public async acquireCredentials({ credentialTypes, context, diff --git a/packages/client/lib/__tests__/IssuerSessionClient.spec.ts b/packages/client/lib/__tests__/IssuerSessionClient.spec.ts new file mode 100644 index 00000000..0b22dfdb --- /dev/null +++ b/packages/client/lib/__tests__/IssuerSessionClient.spec.ts @@ -0,0 +1,60 @@ +import { IssuerSessionIdRequestOpts } from '@sphereon/oid4vci-common'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import nock from 'nock'; + +import { acquireIssuerSessionId } from '../IssuerSessionClient'; + +describe('IssuerSessionClient', () => { + describe('acquireIssuerSessionId', () => { + const mockSessionEndpoint = 'https://server.example.com/session_endpoint'; + const mockSessionId = 'iOiJSUzI1NiIsInR'; + + beforeEach(() => { + nock.cleanAll(); + }); + + it('should successfully acquire an issuer session ID', async () => { + const mockResponse = { + session_id: mockSessionId, + }; + + nock('https://server.example.com').post('/session_endpoint').reply(200, mockResponse, { 'Content-Type': 'application/json' }); + + const opts: IssuerSessionIdRequestOpts = { + sessionEndpoint: mockSessionEndpoint, + }; + + const result = await acquireIssuerSessionId(opts); + + expect(result).toEqual(mockResponse); + }); + + it('should reject with an error if the response contains an error body', async () => { + const mockErrorResponse = { + error: 'invalid_request', + error_description: 'The request is missing a required parameter', + }; + + nock('https://server.example.com').post('/session_endpoint').reply(400, mockErrorResponse, { 'Content-Type': 'application/json' }); + + const opts: IssuerSessionIdRequestOpts = { + sessionEndpoint: mockSessionEndpoint, + }; + + await expect(acquireIssuerSessionId(opts)).rejects.toMatch(/an error occurred while requesting a issuer session token/); + }); + + it('should reject with an error if the response is missing the session_token', async () => { + nock('https://server.example.com').post('/session_endpoint').reply(200, undefined, { 'Content-Type': 'application/json' }); + + const opts: IssuerSessionIdRequestOpts = { + sessionEndpoint: mockSessionEndpoint, + }; + + await expect(acquireIssuerSessionId(opts)).rejects.toMatch( + /an error occurred while requesting a issuer session token.*missing session_token response/, + ); + }); + }); +}); diff --git a/packages/client/lib/index.ts b/packages/client/lib/index.ts index 8b959462..fbef3b70 100644 --- a/packages/client/lib/index.ts +++ b/packages/client/lib/index.ts @@ -22,4 +22,5 @@ export * from './MetadataClientV1_0_11'; export * from './OpenID4VCIClient'; export * from './OpenID4VCIClientV1_0_13'; export * from './OpenID4VCIClientV1_0_11'; +export * from './IssuerSessionClient'; export * from './ProofOfPossessionBuilder'; diff --git a/packages/oid4vci-common/lib/types/CredentialIssuance.types.ts b/packages/oid4vci-common/lib/types/CredentialIssuance.types.ts index 1f2275ef..e476b942 100644 --- a/packages/oid4vci-common/lib/types/CredentialIssuance.types.ts +++ b/packages/oid4vci-common/lib/types/CredentialIssuance.types.ts @@ -21,6 +21,10 @@ export interface CredentialResponse extends ExperimentalSubjectIssuance { notification_id?: string; } +export interface IssuerSessionResponse { + session_id: string; +} + export interface CredentialOfferRequestWithBaseUrl extends UniformCredentialOfferRequest { scheme: string; clientId?: string; diff --git a/packages/oid4vci-common/lib/types/Generic.types.ts b/packages/oid4vci-common/lib/types/Generic.types.ts index 6dca6a2b..ec622ffb 100644 --- a/packages/oid4vci-common/lib/types/Generic.types.ts +++ b/packages/oid4vci-common/lib/types/Generic.types.ts @@ -398,3 +398,7 @@ export type NotificationResult = { export interface NotificationErrorResponse { error: NotificationError | string; } + +export interface IssuerSessionIdRequestOpts { + sessionEndpoint: string; +} diff --git a/packages/oid4vci-common/lib/types/v1_0_13.types.ts b/packages/oid4vci-common/lib/types/v1_0_13.types.ts index 789df449..0e3fb298 100644 --- a/packages/oid4vci-common/lib/types/v1_0_13.types.ts +++ b/packages/oid4vci-common/lib/types/v1_0_13.types.ts @@ -32,6 +32,7 @@ export interface IssuerMetadataV1_0_13 { notification_endpoint?: string; credential_response_encryption?: ResponseEncryption; token_endpoint?: string; + session_endpoint?: string; display?: MetadataDisplay[]; [x: string]: unknown; @@ -191,6 +192,7 @@ export interface EndpointMetadataResultV1_0_13 extends EndpointMetadata { authorizationServerType: AuthorizationServerType; authorizationServerMetadata?: AuthorizationServerMetadata; credentialIssuerMetadata?: Partial & IssuerMetadataV1_0_13; + session_endpoint?: string; } // For now we extend the opts above. Only difference is that the credential endpoint is optional in the Opts, as it can come from other sources. The value is however required in the eventual Issuer Metadata