UNPKG

@sphereon/ssi-sdk.siopv2-oid4vp-rp-rest-api

Version:

571 lines (566 loc) • 22.2 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); // src/siop-api-functions.ts import { checkAuth, sendErrorResponse } from "@sphereon/ssi-express-support"; import { CredentialMapper } from "@sphereon/ssi-types"; import { validate as isValidUUID } from "uuid"; var parseAuthorizationResponse = /* @__PURE__ */ __name((request) => { const contentType = request.header("content-type"); if (contentType?.startsWith("application/json")) { const payload = typeof request.body === "string" ? JSON.parse(request.body) : request.body; return payload; } if (contentType?.startsWith("application/x-www-form-urlencoded")) { const payload = request.body; if (typeof payload.presentation_submission === "string") { console.log(`Supplied presentation_submission was a string instead of JSON. Correcting, but external party should fix their implementation!`); payload.presentation_submission = JSON.parse(payload.presentation_submission); } if (typeof payload.vp_token === "string") { const { vp_token } = payload; if (vp_token.startsWith("[") && vp_token.endsWith("]") || CredentialMapper.isJsonLdAsString(vp_token)) { payload.vp_token = JSON.parse(vp_token); } } return payload; } throw new Error(`Unsupported content type: ${contentType}. Currently only application/x-www-form-urlencoded and application/json (for direct_post) are supported`); }, "parseAuthorizationResponse"); var validatePresentationSubmission = /* @__PURE__ */ __name((query, submission) => { return query.credentials.every((credential) => credential.id in submission); }, "validatePresentationSubmission"); function verifyAuthResponseSIOPv2Endpoint(router, context, opts) { if (opts?.enabled === false) { console.log(`verifyAuthResponse SIOP endpoint is disabled`); return; } const path2 = opts?.path ?? "/siop/queries/:queryId/auth-responses/:correlationId"; router.post(path2, checkAuth(opts?.endpoint), async (request, response) => { try { const { correlationId, queryId, tenantId, version } = request.params; if (!correlationId) { console.log(`No authorization request could be found for the given url. correlationId: ${correlationId}`); return sendErrorResponse(response, 404, "No authorization request could be found"); } console.debug("Authorization Response (siop-sessions"); console.debug(JSON.stringify(request.body, null, 2)); const definitionItems = await context.agent.pdmGetDefinitions({ filter: buildQueryIdFilter(queryId, tenantId, version) }); if (definitionItems.length === 0) { console.log(`Could not get dcql query with id ${queryId} from agent. Will return 404`); response.statusCode = 404; response.statusMessage = `No definition ${queryId}`; return response.send(); } const authorizationResponse = parseAuthorizationResponse(request); console.log(`URI: ${JSON.stringify(authorizationResponse)}`); const definitionItem = definitionItems[0]; const verifiedResponse = await context.agent.siopVerifyAuthResponse({ authorizationResponse, correlationId, dcqlQuery: definitionItem.query }); const presentation = verifiedResponse?.oid4vpSubmission?.presentation; if (presentation && validatePresentationSubmission(definitionItem.query, presentation)) { console.log("PRESENTATIONS:" + JSON.stringify(presentation, null, 2)); response.statusCode = 200; const authorizationChallengeValidationResponse = { presentation_during_issuance_session: verifiedResponse.correlationId }; if (authorizationResponse.is_first_party) { response.setHeader("Content-Type", "application/json"); return response.send(JSON.stringify(authorizationChallengeValidationResponse)); } const responseRedirectURI = await context.agent.siopGetRedirectURI({ correlationId, state: verifiedResponse.state }); if (responseRedirectURI) { response.setHeader("Content-Type", "application/json"); return response.send(JSON.stringify({ redirect_uri: responseRedirectURI })); } } else { console.log("Missing Presentation (Verifiable Credentials)"); response.statusCode = 500; response.statusMessage = "Missing Presentation (Verifiable Credentials)"; } return response.send(); } catch (error) { console.error(error); return sendErrorResponse(response, 500, "Could not verify auth status", error); } }); } __name(verifyAuthResponseSIOPv2Endpoint, "verifyAuthResponseSIOPv2Endpoint"); function getAuthRequestSIOPv2Endpoint(router, context, opts) { if (opts?.enabled === false) { console.log(`getAuthRequest SIOP endpoint is disabled`); return; } const path2 = opts?.path ?? "/siop/queries/:queryId/auth-requests/:correlationId"; router.get(path2, checkAuth(opts?.endpoint), async (request, response) => { try { const correlationId = request.params.correlationId; const queryId = request.params.queryId; if (!correlationId || !queryId) { console.log(`No authorization request could be found for the given url. correlationId: ${correlationId}, queryId: ${queryId}`); return sendErrorResponse(response, 404, "No authorization request could be found"); } const requestState = await context.agent.siopGetAuthRequestState({ correlationId, errorOnNotFound: false }); if (!requestState) { console.log(`No authorization request could be found for the given url in the state manager. correlationId: ${correlationId}, definitionId: ${queryId}`); return sendErrorResponse(response, 404, `No authorization request could be found`); } const definitionItems = await context.agent.pdmGetDefinitions({ filter: buildQueryIdFilter(queryId) }); if (definitionItems.length === 0) { console.log(`Could not get dcql query with id ${queryId} from agent. Will return 404`); response.statusCode = 404; response.statusMessage = `No definition ${queryId}`; return response.send(); } const payload = requestState.request?.requestObject?.getPayload(); payload.dcql_query = definitionItems[0].query; const requestObject = await requestState.request?.requestObject?.toJwt(); console.log("JWT Request object:"); console.log(requestObject); let error; try { response.statusCode = 200; response.setHeader("Content-Type", "application/jwt"); return response.send(requestObject); } catch (e) { error = typeof e === "string" ? e : e instanceof Error ? e.message : void 0; return sendErrorResponse(response, 500, "Could not get authorization request", e); } finally { await context.agent.siopUpdateAuthRequestState({ correlationId, state: "authorization_request_created", error }); } } catch (error) { return sendErrorResponse(response, 500, "Could not get authorization request", error); } }); } __name(getAuthRequestSIOPv2Endpoint, "getAuthRequestSIOPv2Endpoint"); function buildQueryIdFilter(queryId, tenantId, version) { const queryFilter = { queryId, ...tenantId ? { tenantId } : {}, ...version ? { version } : {} }; return [ queryFilter, ...isValidUUID(queryId) ? [ { id: queryId } ] : [] ]; } __name(buildQueryIdFilter, "buildQueryIdFilter"); // src/universal-oid4vp-api-functions.ts import { AuthorizationRequestStateStatus, createAuthorizationRequestFromPayload, CreateAuthorizationRequestPayloadSchema } from "@sphereon/did-auth-siop"; import { checkAuth as checkAuth2, sendErrorResponse as sendErrorResponse2 } from "@sphereon/ssi-express-support"; import { uriWithBase } from "@sphereon/ssi-sdk.siopv2-oid4vp-common"; import uuid from "short-uuid"; // src/middleware/validationMiddleware.ts import { ZodError } from "zod"; var validateData = /* @__PURE__ */ __name((schema) => { return (req, res, next) => { try { schema.parse(req.body); next(); } catch (error) { if (error instanceof ZodError) { const errorMessages = error.issues.map((issue) => ({ message: `${issue.path.join(".")} is ${issue.message}` })); res.status(400).json({ status: 400, message: "Invalid data", error_details: errorMessages[0].message }); } else { res.status(500).json({ status: 500, message: "Internal Server Error" }); } } }; }, "validateData"); // src/universal-oid4vp-api-functions.ts function createAuthRequestUniversalOID4VPEndpoint(router, context, opts) { if (opts?.enabled === false) { console.log(`createAuthRequest universal OID4VP endpoint is disabled`); return; } const path2 = opts?.path ?? "/backend/auth/requests"; router.post(path2, checkAuth2(opts?.endpoint), validateData(CreateAuthorizationRequestPayloadSchema), async (request, response) => { try { const authRequest = createAuthorizationRequestFromPayload(request.body); const correlationId = authRequest.correlationId ?? uuid.uuid(); const qrCodeOpts = authRequest.qrCode ? { ...authRequest.qrCode } : opts?.qrCodeOpts; const queryId = authRequest.queryId; const definitionItems = await context.agent.pdmGetDefinitions({ filter: buildQueryIdFilter(queryId) }); if (definitionItems.length === 0) { console.log(`No query could be found for the given id. Query id: ${queryId}`); return sendErrorResponse2(response, 404, { status: 404, message: "No query could be found" }); } const requestByReferenceURI = uriWithBase(`/siop/queries/${queryId}/auth-requests/${correlationId}`, { baseURI: authRequest.requestUriBase ?? opts?.siopBaseURI }); const responseURI = uriWithBase(`/siop/queries/${queryId}/auth-responses/${correlationId}`, { baseURI: opts?.siopBaseURI }); const authRequestURI = await context.agent.siopCreateAuthRequestURI({ queryId, correlationId, nonce: uuid.uuid(), requestByReferenceURI, responseURIType: "response_uri", responseURI, ...authRequest.directPostResponseRedirectUri && { responseRedirectURI: authRequest.directPostResponseRedirectUri }, ...authRequest.callback && { callback: authRequest.callback } }); let qrCodeDataUri; if (qrCodeOpts) { const { AwesomeQR } = await import("awesome-qr"); const qrCode = new AwesomeQR({ text: authRequestURI, size: qrCodeOpts.size ?? 250, colorDark: qrCodeOpts.colorDark ?? "#000000", colorLight: qrCodeOpts.colorLight ?? "#FFFFFF" }); qrCodeDataUri = `data:image/png;base64,${(await qrCode.draw()).toString("base64")}`; } else { qrCodeDataUri = authRequestURI; } const authRequestBody = { query_id: queryId, correlation_id: correlationId, request_uri: authRequestURI, status_uri: `${uriWithBase(opts?.webappAuthStatusPath ?? `/backend/auth/status/${correlationId}`, { baseURI: opts?.webappBaseURI })}`, ...qrCodeDataUri && { qr_uri: qrCodeDataUri } }; console.log(`Auth Request URI data to send back: ${JSON.stringify(authRequestBody)}`); return response.status(201).json(authRequestBody); } catch (error) { return sendErrorResponse2(response, 500, { status: 500, message: "Could not create an authorization request URI" }, error); } }); } __name(createAuthRequestUniversalOID4VPEndpoint, "createAuthRequestUniversalOID4VPEndpoint"); function removeAuthRequestStateUniversalOID4VPEndpoint(router, context, opts) { if (opts?.enabled === false) { console.log(`removeAuthStatus universal OID4VP endpoint is disabled`); return; } const path2 = opts?.path ?? "/backend/auth/requests/:correlationId"; router.delete(path2, checkAuth2(opts?.endpoint), async (request, response) => { try { const correlationId = request.params.correlationId; const authRequestState = await context.agent.siopGetAuthRequestState({ correlationId, errorOnNotFound: false }); if (!authRequestState) { console.log(`No authorization request could be found for the given correlationId. correlationId: ${correlationId}`); return sendErrorResponse2(response, 404, { status: 404, message: "No authorization request could be found" }); } await context.agent.siopDeleteAuthState({ correlationId }); return response.status(204).json(); } catch (error) { return sendErrorResponse2(response, 500, { status: 500, message: error.message }, error); } }); } __name(removeAuthRequestStateUniversalOID4VPEndpoint, "removeAuthRequestStateUniversalOID4VPEndpoint"); function authStatusUniversalOID4VPEndpoint(router, context, opts) { if (opts?.enabled === false) { console.log(`authStatus universal OID4VP endpoint is disabled`); return; } const path2 = opts?.path ?? "/backend/auth/status/:correlationId"; router.get(path2, checkAuth2(opts?.endpoint), async (request, response) => { try { console.log("Received auth-status request..."); const correlationId = request.params.correlationId; const requestState = await context.agent.siopGetAuthRequestState({ correlationId, errorOnNotFound: false }); if (!requestState) { console.log(`No authorization request could be found for the given correlationId. correlationId: ${correlationId}`); return sendErrorResponse2(response, 404, { status: 404, message: "No authorization request could be found" }); } let responseState; if (requestState.status === AuthorizationRequestStateStatus.RETRIEVED) { responseState = await context.agent.siopGetAuthResponseState({ correlationId, errorOnNotFound: false }); } const overallState = responseState ?? requestState; const statusBody = { status: overallState.status, correlation_id: overallState.correlationId, query_id: overallState.queryId, last_updated: overallState.lastUpdated, ..."verifiedData" in overallState && { verified_data: overallState.verifiedData }, ...overallState.error && { message: overallState.error.message } }; console.debug(`Will send auth status: ${JSON.stringify(statusBody)}`); if (overallState.status === "error") { return response.status(500).json(statusBody); } return response.status(200).json(statusBody); } catch (error) { return sendErrorResponse2(response, 500, { status: 500, message: error.message }, error); } }); } __name(authStatusUniversalOID4VPEndpoint, "authStatusUniversalOID4VPEndpoint"); function getDefinitionsEndpoint(router, context, opts) { if (opts?.enabled === false) { console.log(`getDefinitions universal OID4VP endpoint is disabled`); return; } const path2 = opts?.path ?? "/backend/definitions"; router.get(path2, checkAuth2(opts?.endpoint), async (request, response) => { try { const definitions = await context.agent.pdmGetDefinitions(); response.statusCode = 200; return response.json(definitions); } catch (error) { return sendErrorResponse2(response, 500, { status: 500, message: error.message }, error); } }); } __name(getDefinitionsEndpoint, "getDefinitionsEndpoint"); // src/siopv2-rp-api-server.ts import { agentContext } from "@sphereon/ssi-sdk.core"; import { copyGlobalAuthToEndpoints } from "@sphereon/ssi-express-support"; import express from "express"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import swaggerUi from "swagger-ui-express"; var __filename = fileURLToPath(import.meta.url); var __dirname = path.dirname(__filename); var SIOPv2RPApiServer = class { static { __name(this, "SIOPv2RPApiServer"); } _express; _router; _agent; _opts; _basePath; OID4VP_OPENAPI_FILE = path.join(__dirname, "..", "oid4vp-openapi.yml"); OID4VP_SWAGGER_URL = "https://api.swaggerhub.com/apis/SphereonInt/OID4VP/0.1.0"; constructor(args) { const { agent, opts } = args; this._agent = agent; copyGlobalAuthToEndpoints({ opts, keys: [ "webappCreateAuthRequest", "webappAuthStatus", "webappDeleteAuthRequest", "webappGetDefinitions" ] }); if (opts?.endpointOpts?.globalAuth?.secureSiopEndpoints) { copyGlobalAuthToEndpoints({ opts, keys: [ "siopGetAuthRequest", "siopVerifyAuthResponse" ] }); } this._opts = opts; this._express = args.expressSupport.express; this._router = express.Router(); const context = agentContext(agent); const features = opts?.enableFeatures ?? [ "rp-status", "siop" ]; console.log(`SIOPv2 API enabled, with features: ${JSON.stringify(features)}}`); if (features.includes("rp-status")) { createAuthRequestUniversalOID4VPEndpoint(this._router, context, opts?.endpointOpts?.webappCreateAuthRequest); authStatusUniversalOID4VPEndpoint(this._router, context, opts?.endpointOpts?.webappAuthStatus); removeAuthRequestStateUniversalOID4VPEndpoint(this._router, context, opts?.endpointOpts?.webappDeleteAuthRequest); getDefinitionsEndpoint(this._router, context, opts?.endpointOpts?.webappGetDefinitions); } if (features.includes("siop")) { getAuthRequestSIOPv2Endpoint(this._router, context, opts?.endpointOpts?.siopGetAuthRequest); verifyAuthResponseSIOPv2Endpoint(this._router, context, opts?.endpointOpts?.siopVerifyAuthResponse); } this._basePath = opts?.endpointOpts?.basePath ?? ""; this._express.use(this._basePath, this.router); this._express.set("trust proxy", opts?.endpointOpts?.trustProxy ?? true); this.setupSwaggerUi(); } /** * Sets up Swagger UI for API documentation. * Spec source priority: * 1. OID4VP_OPENAPI_SPEC env var (URL or file path) * 2. Bundled universal-OID4VP-openapi.yaml file * 3. Fallback to SwaggerHub URL */ setupSwaggerUi() { const envSpec = process.env.OID4VP_OPENAPI_SPEC; const apiDocsPath = "/api-docs"; const specPath = "/api-docs/spec.yaml"; const fullApiDocsPath = `${this._basePath}${apiDocsPath}`; const fullSpecPath = `${this._basePath}${specPath}`; let cachedSpec = null; const isRemoteUrl = envSpec?.startsWith("http://") || envSpec?.startsWith("https://"); const isEnvLocalFile = envSpec && !isRemoteUrl; const resolvedEnvFile = isEnvLocalFile && !path.isAbsolute(envSpec) ? path.join(__dirname, "..", envSpec) : envSpec; const hasBundledFile = fs.existsSync(this.OID4VP_OPENAPI_FILE); const localFile = isEnvLocalFile ? resolvedEnvFile : hasBundledFile ? this.OID4VP_OPENAPI_FILE : void 0; const isLocalFile = !!localFile; if (isEnvLocalFile && !fs.existsSync(resolvedEnvFile)) { console.log(`[OID4VP] OpenAPI spec file not found at ${resolvedEnvFile}. Swagger UI disabled.`); return; } const specSource = isRemoteUrl ? envSpec : isLocalFile ? localFile : this.OID4VP_SWAGGER_URL; console.log(`[OID4VP] API docs: ${fullApiDocsPath} (spec: ${specSource})`); const getSpec = /* @__PURE__ */ __name(async (req) => { if (cachedSpec) { return cachedSpec; } if (isLocalFile && localFile) { const content = fs.readFileSync(localFile, "utf-8"); try { cachedSpec = JSON.parse(content); } catch { cachedSpec = content; } } else { const url = isRemoteUrl ? envSpec : this.OID4VP_SWAGGER_URL; const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const text = await response.text(); try { cachedSpec = JSON.parse(text); } catch { cachedSpec = text; } } if (typeof cachedSpec === "object" && cachedSpec !== null && req) { cachedSpec.servers = [ { url: `${req.protocol}://${req.get("host")}${this._basePath}`, description: "This server" } ]; } return cachedSpec; }, "getSpec"); getSpec().catch((err) => console.log(`[OID4VP] Spec pre-fetch failed: ${err.message}`)); this._router.get(specPath, async (req, res) => { try { const spec = await getSpec(req); if (typeof spec === "string") { res.type("text/yaml").send(spec); } else { res.json(spec); } } catch (err) { console.error(`[OID4VP] Spec fetch error: ${err.message}`); res.status(502).json({ error: "Failed to load OpenAPI spec", details: err.message }); } }); this._router.use(apiDocsPath, swaggerUi.serve, swaggerUi.setup(void 0, { swaggerOptions: { url: fullSpecPath } })); } get express() { return this._express; } get router() { return this._router; } get agent() { return this._agent; } get opts() { return this._opts; } }; export { SIOPv2RPApiServer, authStatusUniversalOID4VPEndpoint, buildQueryIdFilter, createAuthRequestUniversalOID4VPEndpoint, getAuthRequestSIOPv2Endpoint, getDefinitionsEndpoint, removeAuthRequestStateUniversalOID4VPEndpoint, verifyAuthResponseSIOPv2Endpoint }; //# sourceMappingURL=index.js.map