diff --git a/packages/siopv2-oid4vp-rp-rest-api/CHANGELOG.md b/packages/siopv2-oid4vp-rp-rest-api/CHANGELOG.md index 7bb8b3fdb..754aa1a50 100644 --- a/packages/siopv2-oid4vp-rp-rest-api/CHANGELOG.md +++ b/packages/siopv2-oid4vp-rp-rest-api/CHANGELOG.md @@ -7,27 +7,17 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline **Note:** Version bump only for package @sphereon/ssi-sdk.siopv2-oid4vp-rp-rest-api - - - - # [0.14.0](https://github.com/Sphereon-Opensource/SSI-SDK/compare/v0.13.0...v0.14.0) (2023-07-30) - ### Bug Fixes -* Fix relative DID resolution and Json websignature 2020 verification for ED25519 and some other algs ([ca2682c](https://github.com/Sphereon-Opensource/SSI-SDK/commit/ca2682c0b747f5052143c943a06f23acc7aa22cc)) -* Use agent resolver if not set, with fallback to universal resolver. Fix bug in response message ([43c9313](https://github.com/Sphereon-Opensource/SSI-SDK/commit/43c9313ee623fa0848dca8dcd4e2e509692c28d7)) -* VP did resolution from agent ([aa3f3f1](https://github.com/Sphereon-Opensource/SSI-SDK/commit/aa3f3f1173f502c5414a2237231306311ed4d1fc)) - +- Fix relative DID resolution and Json websignature 2020 verification for ED25519 and some other algs ([ca2682c](https://github.com/Sphereon-Opensource/SSI-SDK/commit/ca2682c0b747f5052143c943a06f23acc7aa22cc)) +- Use agent resolver if not set, with fallback to universal resolver. Fix bug in response message ([43c9313](https://github.com/Sphereon-Opensource/SSI-SDK/commit/43c9313ee623fa0848dca8dcd4e2e509692c28d7)) +- VP did resolution from agent ([aa3f3f1](https://github.com/Sphereon-Opensource/SSI-SDK/commit/aa3f3f1173f502c5414a2237231306311ed4d1fc)) ### Features -* Add global web resolution provider. Add json error handler ([f19d1d1](https://github.com/Sphereon-Opensource/SSI-SDK/commit/f19d1d135a9944a6c9e4c6040c58e7563c4442f2)) - - - - +- Add global web resolution provider. Add json error handler ([f19d1d1](https://github.com/Sphereon-Opensource/SSI-SDK/commit/f19d1d135a9944a6c9e4c6040c58e7563c4442f2)) # [0.13.0](https://github.com/Sphereon-Opensource/SSI-SDK/compare/v0.12.0...v0.13.0) (2023-06-24) diff --git a/packages/siopv2-oid4vp-rp-rest-api/__tests__/RestAPI.ts b/packages/siopv2-oid4vp-rp-rest-api/__tests__/RestAPI.ts index 7702b250f..28080dc95 100644 --- a/packages/siopv2-oid4vp-rp-rest-api/__tests__/RestAPI.ts +++ b/packages/siopv2-oid4vp-rp-rest-api/__tests__/RestAPI.ts @@ -1,11 +1,21 @@ +import { ExpressBuilder } from '@sphereon/ssi-sdk.express-support' import agent from './agent' -import { ISIOPv2RPRestAPIOpts, SIOPv2RPRestAPI } from '../src' +import { ISIOPv2RPRestAPIOpts, SIOPv2RPApiServer } from '../src' +import morgan from 'morgan' const opts: ISIOPv2RPRestAPIOpts = { - hostname: '0.0.0.0', - port: 5000, - webappBaseURI: 'http://192.168.2.18:5000', - siopBaseURI: 'http://192.168.2.18:5000', + webappCreateAuthRequest: { + webappBaseURI: 'http://192.168.2.18:5000', + siopBaseURI: 'http://192.168.2.18:5000', + }, } +const builder = ExpressBuilder.fromServerOpts({ + port: 5000, + hostname: '0.0.0.0', +}).withPassportAuth(false) +// .withSessionOptions({secret: '1234', name: 'oidc-session'}) +// .addHandler(morgan('dev')) +const expressArgs = builder.build({ startListening: true }) +expressArgs.express.use(morgan('dev')) -new SIOPv2RPRestAPI({ agent, opts }) +new SIOPv2RPApiServer({ agent, expressArgs, opts }) diff --git a/packages/siopv2-oid4vp-rp-rest-api/__tests__/database/test.sqlite b/packages/siopv2-oid4vp-rp-rest-api/__tests__/database/test.sqlite deleted file mode 100644 index 5224c3148..000000000 Binary files a/packages/siopv2-oid4vp-rp-rest-api/__tests__/database/test.sqlite and /dev/null differ diff --git a/packages/siopv2-oid4vp-rp-rest-api/package.json b/packages/siopv2-oid4vp-rp-rest-api/package.json index 0b33e863d..f6683a2ce 100644 --- a/packages/siopv2-oid4vp-rp-rest-api/package.json +++ b/packages/siopv2-oid4vp-rp-rest-api/package.json @@ -11,7 +11,9 @@ "start:dev": "ts-node __tests__/RestAPI.ts" }, "dependencies": { - "@sphereon/did-auth-siop": "0.3.2-unstable.6", + "@sphereon/did-auth-siop": "0.3.2-unstable.8", + "@sphereon/ssi-sdk.core": "workspace:*", + "@sphereon/ssi-sdk.express-support": "workspace:*", "@sphereon/ssi-sdk.kv-store-temp": "workspace:*", "@sphereon/ssi-sdk.presentation-exchange": "workspace:*", "@sphereon/ssi-sdk.siopv2-oid4vp-common": "workspace:*", @@ -30,10 +32,12 @@ "uuid": "^8.3.2" }, "devDependencies": { + "morgan": "^1.10.0", + "@types/morgan": "^1.9.4", "@decentralized-identity/ion-sdk": "^0.6.0", "@sphereon/did-uni-client": "^0.6.0", - "@sphereon/pex": "^2.0.1", - "@sphereon/pex-models": "^2.0.2", + "@sphereon/pex": "2.1.0", + "@sphereon/pex-models": "^2.0.3", "@sphereon/ssi-sdk-ext.did-provider-jwk": "0.13.0", "@sphereon/ssi-sdk.data-store": "workspace:*", "@sphereon/ssi-sdk.vc-handler-ld-local": "workspace:*", diff --git a/packages/siopv2-oid4vp-rp-rest-api/src/SIOPv2RPRestAPI.ts b/packages/siopv2-oid4vp-rp-rest-api/src/SIOPv2RPRestAPI.ts deleted file mode 100644 index 35d1f4bce..000000000 --- a/packages/siopv2-oid4vp-rp-rest-api/src/SIOPv2RPRestAPI.ts +++ /dev/null @@ -1,338 +0,0 @@ -// noinspection JSUnusedGlobalSymbols - -import * as dotenv from 'dotenv-flow' -import express, { Express, Response } from 'express' -import cookieParser from 'cookie-parser' -import uuid from 'short-uuid' -import { - AuthorizationRequestState, - AuthorizationResponsePayload, - AuthorizationResponseStateStatus, - PresentationDefinitionLocation, - VerifiedAuthorizationResponse, -} from '@sphereon/did-auth-siop' -import bodyParser from 'body-parser' -import { - AuthorizationRequestStateStatus, - AuthStatusResponse, - GenerateAuthRequestURIResponse, - uriWithBase, -} from '@sphereon/ssi-sdk.siopv2-oid4vp-common' -import { AuthorizationResponseStateWithVerifiedData, ISIOPv2RP, VerifiedDataMode } from '@sphereon/ssi-sdk.siopv2-oid4vp-rp-auth' -import { RequestWithAgent } from './request-agent-router' -import { TAgent } from '@veramo/core' -import { IPresentationExchange } from '@sphereon/ssi-sdk.presentation-exchange' - -export interface ISIOPv2RPRestAPIOpts { - siopBaseURI?: string // An externally communicated base URI for SIOP endpoints. Needs to be provided via this option, or environment variable! - webappBaseURI?: string // An externally communicated base URI for webapp endpoints. Needs to be provided via this option, or environment variable! - webappCreateAuthRequestPath?: string // Override the create Auth Request path. Needs to contain correlationId and definitionId path params! - webappDeleteAuthRequestPath?: string // Override the delete Auth Request path. Needs to contain correlationId and definitionId path params! - webappAuthStatusPath?: string // Override the Auth status path. CorrelationId and definitionId need to come from the body! - siopVerifyAuthResponsePath?: string // Override the siop Verify Response path. Needs to contain correlationId and definitionId path params! - siopGetAuthRequestPath?: string // Override the siop get Auth Request path. Needs to contain correlationId and definitionId path params! - port?: number // The port to listen on - cookieSigningKey?: string - hostname?: string // defaults to "0.0.0.0", meaning it will listen on all IP addresses. Can be an IP address or hostname -} - -export class SIOPv2RPRestAPI { - private express: Express - private agent: TAgent - private _opts?: ISIOPv2RPRestAPIOpts - - constructor(args: { agent: TAgent; express?: Express; opts?: ISIOPv2RPRestAPIOpts }) { - const { agent, opts } = args - this.agent = agent - this._opts = opts - const existingExpress = !!args.express - this.express = existingExpress ? args.express! : express() - this.setupExpress(existingExpress) - - // Webapp endpoints - this.createAuthRequestWebappEndpoint() - this.authStatusWebappEndpoint() - this.removeAuthRequestStateWebappEndpoint() - - // SIOPv2 endpoints - this.getAuthRequestSIOPv2Endpoint() - this.verifyAuthResponseSIOPv2Endpoint() - } - - private setupExpress(existingExpress: boolean) { - dotenv.config() - if (!existingExpress) { - const port = this._opts?.port || process.env.PORT || 5000 - const secret = this._opts?.cookieSigningKey || process.env.COOKIE_SIGNING_KEY - const hostname = this._opts?.hostname || '0.0.0.0' - this.express.use((req, res, next) => { - res.header('Access-Control-Allow-Origin', '*') - // Request methods you wish to allow - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE') - - // Request headers you wish to allow - res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type') - - // Set to true if you need the website to include cookies in the requests sent - // to the API (e.g. in case you use sessions) - res.setHeader('Access-Control-Allow-Credentials', 'true') - next() - }) - // this.express.use(cors({ credentials: true })); - // this.express.use('/proxy', proxy('www.gssoogle.com')); - this.express.use(bodyParser.urlencoded({ extended: true })) - this.express.use(bodyParser.json()) - this.express.use(cookieParser(secret)) - this.express.listen(port as number, hostname, () => console.log(`Listening on ${hostname}, port ${port}`)) - } - } - - private static sendErrorResponse(response: Response, statusCode: number, message: string) { - response.statusCode = statusCode - try { - response.status(statusCode).send(JSON.stringify(message)) - } catch (error) { - console.error(JSON.stringify(error)) - response.status(500).send() - } - } - - private removeAuthRequestStateWebappEndpoint() { - this.express.delete( - this._opts?.webappDeleteAuthRequestPath ?? '/webapp/definitions/:definitionId/auth-requests/:correlationId', - async (request, response) => { - const correlationId: string = request.params.correlationId - const definitionId: string = request.params.definitionId - if (!correlationId || !definitionId) { - console.log(`No authorization request could be found for the given url. correlationId: ${correlationId}, definitionId: ${definitionId}`) - return SIOPv2RPRestAPI.sendErrorResponse(response, 404, 'No authorization request could be found') - } - response.statusCode = 200 - return response.send(this.agent.siopDeleteAuthState({ definitionId, correlationId })) - } - ) - } - - private authStatusWebappEndpoint() { - this.express.post(this._opts?.webappAuthStatusPath ?? '/webapp/auth-status', async (request: RequestWithAgent, response) => { - console.log('Received auth-status request...') - const correlationId: string = request.body.correlationId as string - const definitionId: string = request.body.definitionId as string - - const requestState = - correlationId && definitionId - ? await this.agent.siopGetAuthRequestState({ - correlationId, - definitionId, - errorOnNotFound: false, - }) - : undefined - if (!requestState || !definitionId || !correlationId) { - console.log( - `No authentication request mapping could be found for the given URL. correlation: ${correlationId}, definitionId: ${definitionId}` - ) - response.statusCode = 404 - - const statusBody: AuthStatusResponse = { - status: requestState ? requestState.status : AuthorizationRequestStateStatus.ERROR, - error: 'No authentication request mapping could be found for the given URL.', - correlationId, - definitionId, - lastUpdated: requestState ? requestState.lastUpdated : Date.now(), - } - return response.send(statusBody) - } - - let includeVerifiedData: VerifiedDataMode = VerifiedDataMode.NONE - if ('includeVerifiedData' in request.body) { - includeVerifiedData = request.body.includeVerifiedData as VerifiedDataMode - } - - let responseState - if (requestState.status === AuthorizationRequestStateStatus.SENT) { - responseState = (await this.agent.siopGetAuthResponseState({ - correlationId, - definitionId, - includeVerifiedData: includeVerifiedData, - errorOnNotFound: false, - })) as AuthorizationResponseStateWithVerifiedData - } - const overallState: AuthorizationRequestState | AuthorizationResponseStateWithVerifiedData = responseState ?? requestState - - const statusBody: AuthStatusResponse = { - status: overallState.status, - ...(overallState.error ? { error: overallState.error?.message } : {}), - correlationId, - definitionId, - lastUpdated: overallState.lastUpdated, - ...(responseState && responseState.status === AuthorizationResponseStateStatus.VERIFIED - ? { payload: await responseState.response.mergedPayloads(), verifiedData: responseState.verifiedData } - : {}), - } - console.log(`Will send auth status: ${JSON.stringify(statusBody)}`) - if (overallState.status === AuthorizationRequestStateStatus.ERROR || overallState.status === AuthorizationResponseStateStatus.ERROR) { - response.statusCode = 500 - return response.send(statusBody) - } - response.statusCode = 200 - return response.send(statusBody) - }) - } - - private createAuthRequestWebappEndpoint() { - this.express.post( - this._opts?.webappCreateAuthRequestPath || '/webapp/definitions/:definitionId/auth-requests', - (request: RequestWithAgent, response) => { - try { - // if (!request.agent) throw Error('No agent configured') - const definitionId = request.params.definitionId - const state: string = uuid.uuid() - const correlationId = state - - const requestByReferenceURI = uriWithBase(`/siop/definitions/${definitionId}/auth-requests/${correlationId}`, { - baseURI: this._opts?.siopBaseURI, - }) - const redirectURI = uriWithBase(`/siop/definitions/${definitionId}/auth-responses/${correlationId}`, { baseURI: this._opts?.siopBaseURI }) - - this.agent - .siopCreateAuthRequestURI({ - definitionId, - correlationId, - state, - requestByReferenceURI, - redirectURI, - }) - .then((authRequestURI) => { - const authRequestBody: GenerateAuthRequestURIResponse = { - correlationId, - definitionId, - authRequestURI, - authStatusURI: `${uriWithBase(this._opts?.webappAuthStatusPath ?? '/webapp/auth-status', { baseURI: this._opts?.webappBaseURI })}`, - } - console.log(`Auth Request URI data to send back: ${JSON.stringify(authRequestBody)}`) - return response.send(authRequestBody) - }) - .catch((e: Error) => { - console.error(e, e.stack) - return SIOPv2RPRestAPI.sendErrorResponse(response, 500, 'Could not create an authorization request URI: ' + e.message) - }) - } catch (error) { - console.error(error) - return SIOPv2RPRestAPI.sendErrorResponse(response, 500, 'Could not create an authorization request URI') - } - } - ) - } - - private verifyAuthResponseSIOPv2Endpoint() { - this.express.post( - this._opts?.siopVerifyAuthResponsePath ?? '/siop/definitions/:definitionId/auth-responses/:correlationId', - async (request, response) => { - try { - const correlationId = request.params.correlationId - const definitionId = request.params.definitionId - if (!correlationId || !definitionId) { - console.log(`No authorization request could be found for the given url. correlationId: ${correlationId}, definitionId: ${definitionId}`) - return SIOPv2RPRestAPI.sendErrorResponse(response, 404, 'No authorization request could be found') - } - console.log('Authorization Response (siop-sessions') - console.log(JSON.stringify(request.body, null, 2)) - const definition = await this.agent.pexStoreGetDefinition({ definitionId }) - const authorizationResponse = typeof request.body === 'string' ? request.body : (request.body as AuthorizationResponsePayload) - console.log(`URI: ${JSON.stringify(authorizationResponse)}`) - if (!definition) { - response.statusCode = 404 - response.statusMessage = `No definition ${definitionId}` - return response.send() - } - await this.agent - .siopVerifyAuthResponse({ - authorizationResponse, - correlationId, - definitionId, - presentationDefinitions: [ - { - location: PresentationDefinitionLocation.CLAIMS_VP_TOKEN, - definition, - }, - ], - }) - .then((verifiedResponse: VerifiedAuthorizationResponse) => { - // console.log('verifiedResponse: ', JSON.stringify(verifiedResponse, null, 2)) - - const wrappedPresentation = verifiedResponse?.oid4vpSubmission?.presentations[0] - if (wrappedPresentation) { - const credentialSubject = wrappedPresentation.presentation.verifiableCredential[0].credential.credentialSubject - console.log('AND WE ARE DONE!') - console.log(JSON.stringify(credentialSubject, null, 2)) - console.log(JSON.stringify(wrappedPresentation.presentation, null, 2)) - response.statusCode = 200 - // todo: delete session - } else { - response.statusCode = 500 - response.statusMessage = 'Missing Credentials' - } - return response.send() - }) - .catch((reason: Error) => { - console.error('verifyAuthenticationResponseJwt failed:', reason) - response.statusCode = 500 - response.statusMessage = reason.message - return response.send() - }) - } catch (error) { - console.error(error) - return SIOPv2RPRestAPI.sendErrorResponse(response, 500, 'Could not verify auth status') - } - } - ) - } - - private getAuthRequestSIOPv2Endpoint() { - this.express.get( - this._opts?.siopGetAuthRequestPath ?? '/siop/definitions/:definitionId/auth-requests/:correlationId', - async (request, response) => { - try { - const correlationId = request.params.correlationId - const definitionId = request.params.definitionId - if (!correlationId || !definitionId) { - console.log(`No authorization request could be found for the given url. correlationId: ${correlationId}, definitionId: ${definitionId}`) - return SIOPv2RPRestAPI.sendErrorResponse(response, 404, 'No authorization request could be found') - } - const requestState = await this.agent.siopGetAuthRequestState({ - correlationId, - definitionId, - errorOnNotFound: false, - }) - if (!requestState) { - console.log( - `No authorization request could be found for the given url in the state manager. correlationId: ${correlationId}, definitionId: ${definitionId}` - ) - return SIOPv2RPRestAPI.sendErrorResponse(response, 404, `No authorization request could be found`) - } - const requestObject = await requestState.request?.requestObject?.toJwt() - console.log('JWT Request object:') - console.log(requestObject) - - let error: string | undefined - try { - response.statusCode = 200 - return response.send(requestObject) - } catch (e) { - error = typeof e === 'string' ? e : e instanceof Error ? e.message : undefined - } finally { - this.agent.siopUpdateAuthRequestState({ - correlationId, - definitionId, - state: AuthorizationRequestStateStatus.SENT, - error, - }) - } - } catch (error) { - console.error(error) - return SIOPv2RPRestAPI.sendErrorResponse(response, 500, 'Could not get authorization request') - } - } - ) - } -} diff --git a/packages/siopv2-oid4vp-rp-rest-api/src/index.ts b/packages/siopv2-oid4vp-rp-rest-api/src/index.ts index 3ff10f06f..c6791fa12 100644 --- a/packages/siopv2-oid4vp-rp-rest-api/src/index.ts +++ b/packages/siopv2-oid4vp-rp-rest-api/src/index.ts @@ -1,5 +1,7 @@ /** * @public */ -export * from './SIOPv2RPRestAPI' +export * from './siop-api-functions' +export * from './webapp-api-functions' export * from './types' +export * from './siopv2-rp-api-server' diff --git a/packages/siopv2-oid4vp-rp-rest-api/src/request-agent-router.ts b/packages/siopv2-oid4vp-rp-rest-api/src/request-agent-router.ts deleted file mode 100644 index b4f1e0130..000000000 --- a/packages/siopv2-oid4vp-rp-rest-api/src/request-agent-router.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { IAgent } from '@veramo/core' -import { Request, Router } from 'express' - -export interface RequestWithAgent extends Request { - agent?: IAgent -} - -/** - * @public - */ -export interface RequestWithAgentRouterOptions { - /** - * Optional. Pre-configured agent - */ - agent?: IAgent - - /** - * Optional. Function that returns a Promise that resolves to a configured agent for specific request - */ - getAgentForRequest?: (req: Request) => Promise -} - -/** - * Creates an expressjs router that adds a Veramo agent to the request object. - * - * This is needed by all other routers provided by this package to be able to perform their functions. - * - * @param options - Initialization option - * @returns Expressjs router - * - * @public - */ -export const RequestWithAgentRouter = (options: RequestWithAgentRouterOptions): Router => { - const router = Router() - router.use(async (req: RequestWithAgent, res, next) => { - if (options.agent) { - req.agent = options.agent - } else if (options.getAgentForRequest) { - req.agent = await options.getAgentForRequest(req) - } else { - throw Error('[RequestWithAgentRouter] agent or getAgentForRequest is required') - } - next() - }) - - return router -} diff --git a/packages/siopv2-oid4vp-rp-rest-api/src/siop-api-functions.ts b/packages/siopv2-oid4vp-rp-rest-api/src/siop-api-functions.ts new file mode 100644 index 000000000..3c61d4f90 --- /dev/null +++ b/packages/siopv2-oid4vp-rp-rest-api/src/siop-api-functions.ts @@ -0,0 +1,110 @@ +import { AuthorizationResponsePayload, PresentationDefinitionLocation } from '@sphereon/did-auth-siop' +import { checkAuth, ISingleEndpointOpts, sendErrorResponse } from '@sphereon/ssi-sdk.express-support' +import { AuthorizationRequestStateStatus } from '@sphereon/ssi-sdk.siopv2-oid4vp-common' +import { Request, Response, Router } from 'express' +import { IRequiredContext } from './types' + +export function verifyAuthResponseSIOPv2Endpoint(router: Router, context: IRequiredContext, opts?: ISingleEndpointOpts) { + if (opts?.enabled === false) { + console.log(`verifyAuthResponse SIOP endpoint is disabled`) + return + } + const path = opts?.path ?? '/siop/definitions/:definitionId/auth-responses/:correlationId' + router.post(path, checkAuth(opts?.endpoint), async (request: Request, response: Response) => { + try { + const correlationId = request.params.correlationId + const definitionId = request.params.definitionId + if (!correlationId || !definitionId) { + console.log(`No authorization request could be found for the given url. correlationId: ${correlationId}, definitionId: ${definitionId}`) + return sendErrorResponse(response, 404, 'No authorization request could be found') + } + console.log('Authorization Response (siop-sessions') + console.log(JSON.stringify(request.body, null, 2)) + const definition = await context.agent.pexStoreGetDefinition({ definitionId }) + const authorizationResponse = typeof request.body === 'string' ? request.body : (request.body as AuthorizationResponsePayload) + console.log(`URI: ${JSON.stringify(authorizationResponse)}`) + if (!definition) { + response.statusCode = 404 + response.statusMessage = `No definition ${definitionId}` + return response.send() + } + const verifiedResponse = await context.agent.siopVerifyAuthResponse({ + authorizationResponse, + correlationId, + definitionId, + presentationDefinitions: [ + { + location: PresentationDefinitionLocation.CLAIMS_VP_TOKEN, + definition, + }, + ], + }) + + const wrappedPresentation = verifiedResponse?.oid4vpSubmission?.presentations[0] + if (wrappedPresentation) { + // const credentialSubject = wrappedPresentation.presentation.verifiableCredential[0]?.credential?.credentialSubject + // console.log(JSON.stringify(credentialSubject, null, 2)) + console.log(JSON.stringify(wrappedPresentation.presentation, null, 2)) + response.statusCode = 200 + // todo: delete session + } else { + response.statusCode = 500 + response.statusMessage = 'Missing Credentials' + } + return response.send() + } catch (error) { + console.error(error) + return sendErrorResponse(response, 500, 'Could not verify auth status', error) + } + }) +} + +export function getAuthRequestSIOPv2Endpoint(router: Router, context: IRequiredContext, opts?: ISingleEndpointOpts) { + if (opts?.enabled === false) { + console.log(`getAuthRequest SIOP endpoint is disabled`) + return + } + const path = opts?.path ?? '/siop/definitions/:definitionId/auth-requests/:correlationId' + router.get(path, checkAuth(opts?.endpoint), async (request: Request, response: Response) => { + try { + const correlationId = request.params.correlationId + const definitionId = request.params.definitionId + if (!correlationId || !definitionId) { + console.log(`No authorization request could be found for the given url. correlationId: ${correlationId}, definitionId: ${definitionId}`) + return sendErrorResponse(response, 404, 'No authorization request could be found') + } + const requestState = await context.agent.siopGetAuthRequestState({ + correlationId, + definitionId, + errorOnNotFound: false, + }) + if (!requestState) { + console.log( + `No authorization request could be found for the given url in the state manager. correlationId: ${correlationId}, definitionId: ${definitionId}` + ) + return sendErrorResponse(response, 404, `No authorization request could be found`) + } + const requestObject = await requestState.request?.requestObject?.toJwt() + console.log('JWT Request object:') + console.log(requestObject) + + let error: string | undefined + try { + response.statusCode = 200 + return response.send(requestObject) + } catch (e) { + error = typeof e === 'string' ? e : e instanceof Error ? e.message : undefined + return sendErrorResponse(response, 500, 'Could not get authorization request', e) + } finally { + await context.agent.siopUpdateAuthRequestState({ + correlationId, + definitionId, + state: AuthorizationRequestStateStatus.SENT, + error, + }) + } + } catch (error) { + return sendErrorResponse(response, 500, 'Could not get authorization request', error) + } + }) +} diff --git a/packages/siopv2-oid4vp-rp-rest-api/src/siopv2-rp-api-server.ts b/packages/siopv2-oid4vp-rp-rest-api/src/siopv2-rp-api-server.ts new file mode 100644 index 000000000..771d585ab --- /dev/null +++ b/packages/siopv2-oid4vp-rp-rest-api/src/siopv2-rp-api-server.ts @@ -0,0 +1,58 @@ +import { ExpressBuildResult, ISingleEndpointOpts } from '@sphereon/ssi-sdk.express-support' +import { IPresentationExchange } from '@sphereon/ssi-sdk.presentation-exchange' +import { ISIOPv2RP } from '@sphereon/ssi-sdk.siopv2-oid4vp-rp-auth' +import { TAgent } from '@veramo/core' +import express, { Express, Router } from 'express' +import { agentContext } from '@sphereon/ssi-sdk.core' +import { getAuthRequestSIOPv2Endpoint, verifyAuthResponseSIOPv2Endpoint } from './siop-api-functions' +import { ICreateAuthRequestWebappEndpointOpts, IRequiredPlugins } from './types' +import { authStatusWebappEndpoint, createAuthRequestWebappEndpoint, removeAuthRequestStateWebappEndpoint } from './webapp-api-functions' + +export interface ISIOPv2RPRestAPIOpts { + webappCreateAuthRequest?: ICreateAuthRequestWebappEndpointOpts // Override the create Auth Request path. Needs to contain correlationId and definitionId path params! + webappDeleteAuthRequest?: ISingleEndpointOpts // Override the delete Auth Request path. Needs to contain correlationId and definitionId path params! + webappAuthStatus?: ISingleEndpointOpts // Override the Auth status path. CorrelationId and definitionId need to come from the body! + siopVerifyAuthResponse?: ISingleEndpointOpts // Override the siop Verify Response path. Needs to contain correlationId and definitionId path params! + siopGetAuthRequest?: ISingleEndpointOpts // Override the siop get Auth Request path. Needs to contain correlationId and definitionId path params! +} + +export class SIOPv2RPApiServer { + private readonly _express: Express + private readonly _router: Router + private readonly _agent: TAgent + private readonly _opts?: ISIOPv2RPRestAPIOpts + + constructor(args: { agent: TAgent; expressArgs: ExpressBuildResult; opts?: ISIOPv2RPRestAPIOpts }) { + const { agent, opts } = args + this._agent = agent + this._opts = opts + this._express = args.expressArgs.express + this._router = express.Router() + const context = agentContext(agent) + + // Webapp endpoints + createAuthRequestWebappEndpoint(this._router, context, opts?.webappCreateAuthRequest) + authStatusWebappEndpoint(this._router, context, opts?.webappAuthStatus) + removeAuthRequestStateWebappEndpoint(this._router, context, opts?.webappDeleteAuthRequest) + + // SIOPv2 endpoints + getAuthRequestSIOPv2Endpoint(this._router, context, opts?.siopGetAuthRequest) + verifyAuthResponseSIOPv2Endpoint(this._router, context, opts?.siopVerifyAuthResponse) + } + + get express(): Express { + return this._express + } + + get router(): Router { + return this._router + } + + get agent(): TAgent { + return this._agent + } + + get opts(): ISIOPv2RPRestAPIOpts | undefined { + return this._opts + } +} diff --git a/packages/siopv2-oid4vp-rp-rest-api/src/types.ts b/packages/siopv2-oid4vp-rp-rest-api/src/types.ts index 5257ea4f6..28f501a07 100644 --- a/packages/siopv2-oid4vp-rp-rest-api/src/types.ts +++ b/packages/siopv2-oid4vp-rp-rest-api/src/types.ts @@ -1,7 +1,13 @@ -import { IAgentContext, ICredentialIssuer, ICredentialVerifier, IDataStoreORM, IDIDManager, IKeyManager, IResolver } from '@veramo/core' -import { ISIOPv2RP } from '@sphereon/ssi-sdk.siopv2-oid4vp-rp-auth' +import { ISingleEndpointOpts } from '@sphereon/ssi-sdk.express-support' import { IPresentationExchange } from '@sphereon/ssi-sdk.presentation-exchange' +import { ISIOPv2RP } from '@sphereon/ssi-sdk.siopv2-oid4vp-rp-auth' +import { IAgentContext, ICredentialVerifier } from '@veramo/core' + +export interface ICreateAuthRequestWebappEndpointOpts extends ISingleEndpointOpts { + siopBaseURI?: string + webappAuthStatusPath?: string + webappBaseURI?: string +} -export type IRequiredContext = IAgentContext< - IDataStoreORM & IResolver & IDIDManager & IKeyManager & ICredentialIssuer & ICredentialVerifier & ISIOPv2RP & IPresentationExchange -> +export type IRequiredPlugins = /*IDataStoreORM & IResolver & IDIDManager & IKeyManager & */ ICredentialVerifier & ISIOPv2RP & IPresentationExchange +export type IRequiredContext = IAgentContext diff --git a/packages/siopv2-oid4vp-rp-rest-api/src/webapp-api-functions.ts b/packages/siopv2-oid4vp-rp-rest-api/src/webapp-api-functions.ts new file mode 100644 index 000000000..c98015cd6 --- /dev/null +++ b/packages/siopv2-oid4vp-rp-rest-api/src/webapp-api-functions.ts @@ -0,0 +1,147 @@ +import { AuthorizationRequestState, AuthorizationResponseStateStatus } from '@sphereon/did-auth-siop' +import { checkAuth, ISingleEndpointOpts, sendErrorResponse } from '@sphereon/ssi-sdk.express-support' +import { + AuthorizationRequestStateStatus, + AuthStatusResponse, + GenerateAuthRequestURIResponse, + uriWithBase, +} from '@sphereon/ssi-sdk.siopv2-oid4vp-common' +import { AuthorizationResponseStateWithVerifiedData, VerifiedDataMode } from '@sphereon/ssi-sdk.siopv2-oid4vp-rp-auth' +import { Request, Response, Router } from 'express' +import uuid from 'short-uuid' +import { ICreateAuthRequestWebappEndpointOpts, IRequiredContext } from './types' + +export function createAuthRequestWebappEndpoint(router: Router, context: IRequiredContext, opts?: ICreateAuthRequestWebappEndpointOpts) { + if (opts?.enabled === false) { + console.log(`createAuthRequest Webapp endpoint is disabled`) + return + } + const path = opts?.path ?? '/webapp/definitions/:definitionId/auth-requests' + router.post(path, checkAuth(opts?.endpoint), async (request: Request, response: Response) => { + try { + // if (!request.agent) throw Error('No agent configured') + const definitionId = request.params.definitionId + const state: string = uuid.uuid() + const correlationId = state + + const requestByReferenceURI = uriWithBase(`/siop/definitions/${definitionId}/auth-requests/${correlationId}`, { + baseURI: opts?.siopBaseURI, + }) + const redirectURI = uriWithBase(`/siop/definitions/${definitionId}/auth-responses/${correlationId}`, { baseURI: opts?.siopBaseURI }) + + const authRequestURI = await context.agent.siopCreateAuthRequestURI({ + definitionId, + correlationId, + state, + requestByReferenceURI, + redirectURI, + }) + const authRequestBody: GenerateAuthRequestURIResponse = { + correlationId, + definitionId, + authRequestURI, + authStatusURI: `${uriWithBase(opts?.webappAuthStatusPath ?? '/webapp/auth-status', { baseURI: opts?.webappBaseURI })}`, + } + console.log(`Auth Request URI data to send back: ${JSON.stringify(authRequestBody)}`) + return response.send(authRequestBody) + } catch (error) { + return sendErrorResponse(response, 500, 'Could not create an authorization request URI', error) + } + }) +} + +export function authStatusWebappEndpoint(router: Router, context: IRequiredContext, opts?: ISingleEndpointOpts) { + if (opts?.enabled === false) { + console.log(`authStatus Webapp endpoint is disabled`) + return + } + const path = opts?.path ?? '/webapp/auth-status' + router.post(path, checkAuth(opts?.endpoint), async (request: Request, response: Response) => { + try { + console.log('Received auth-status request...') + const correlationId: string = request.body.correlationId as string + const definitionId: string = request.body.definitionId as string + + const requestState = + correlationId && definitionId + ? await context.agent.siopGetAuthRequestState({ + correlationId, + definitionId, + errorOnNotFound: false, + }) + : undefined + if (!requestState || !definitionId || !correlationId) { + console.log( + `No authentication request mapping could be found for the given URL. correlation: ${correlationId}, definitionId: ${definitionId}` + ) + response.statusCode = 404 + const statusBody: AuthStatusResponse = { + status: requestState ? requestState.status : AuthorizationRequestStateStatus.ERROR, + error: 'No authentication request mapping could be found for the given URL.', + correlationId, + definitionId, + lastUpdated: requestState ? requestState.lastUpdated : Date.now(), + } + return response.send(statusBody) + } + + let includeVerifiedData: VerifiedDataMode = VerifiedDataMode.NONE + if ('includeVerifiedData' in request.body) { + includeVerifiedData = request.body.includeVerifiedData as VerifiedDataMode + } + + let responseState + if (requestState.status === AuthorizationRequestStateStatus.SENT) { + responseState = (await context.agent.siopGetAuthResponseState({ + correlationId, + definitionId, + includeVerifiedData: includeVerifiedData, + errorOnNotFound: false, + })) as AuthorizationResponseStateWithVerifiedData + } + const overallState: AuthorizationRequestState | AuthorizationResponseStateWithVerifiedData = responseState ?? requestState + + const statusBody: AuthStatusResponse = { + status: overallState.status, + ...(overallState.error ? { error: overallState.error?.message } : {}), + correlationId, + definitionId, + lastUpdated: overallState.lastUpdated, + ...(responseState && responseState.status === AuthorizationResponseStateStatus.VERIFIED + ? { payload: await responseState.response.mergedPayloads(), verifiedData: responseState.verifiedData } + : {}), + } + console.log(`Will send auth status: ${JSON.stringify(statusBody)}`) + if (overallState.status === AuthorizationRequestStateStatus.ERROR || overallState.status === AuthorizationResponseStateStatus.ERROR) { + response.statusCode = 500 + return response.send(statusBody) + } + response.statusCode = 200 + return response.send(statusBody) + } catch (error) { + return sendErrorResponse(response, 500, error.message, error) + } + }) +} + +export function removeAuthRequestStateWebappEndpoint(router: Router, context: IRequiredContext, opts?: ISingleEndpointOpts) { + if (opts?.enabled === false) { + console.log(`removeAuthStatus Webapp endpoint is disabled`) + return + } + const path = opts?.path ?? '/webapp/definitions/:definitionId/auth-requests/:correlationId' + router.delete(path, checkAuth(opts?.endpoint), async (request, response) => { + try { + const correlationId: string = request.params.correlationId + const definitionId: string = request.params.definitionId + if (!correlationId || !definitionId) { + console.log(`No authorization request could be found for the given url. correlationId: ${correlationId}, definitionId: ${definitionId}`) + return sendErrorResponse(response, 404, 'No authorization request could be found') + } + response.statusCode = 200 + return response.send(context.agent.siopDeleteAuthState({ definitionId, correlationId })) + } catch (error) { + return sendErrorResponse(response, 500, error.message, error) + } + }) +} diff --git a/packages/siopv2-oid4vp-rp-rest-api/tsconfig.json b/packages/siopv2-oid4vp-rp-rest-api/tsconfig.json index f45919e4d..27c158820 100644 --- a/packages/siopv2-oid4vp-rp-rest-api/tsconfig.json +++ b/packages/siopv2-oid4vp-rp-rest-api/tsconfig.json @@ -7,6 +7,9 @@ "esModuleInterop": true }, "references": [ + { + "path": "../express-support" + }, { "path": "../siopv2-oid4vp-common" },