UNPKG

@sphereon/oid4vci-issuer-server

Version:

OpenID 4 Verifiable Credential Issuance Server

1,019 lines (1,013 loc) • 42.4 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); // lib/OID4VCIServer.ts import { CredentialSupportedBuilderV1_15, oidcAccessTokenVerifyCallback, VcIssuerBuilder } from "@sphereon/oid4vci-issuer"; import express from "express"; // lib/oid4vci-api-functions.ts import { uuidv4 as uuidv42 } from "@sphereon/oid4vc-common"; import { ACCESS_TOKEN_ISSUER_REQUIRED_ERROR, adjustUrl, AuthorizationChallengeError, determineGrantTypes, EVENTS, extractBearerToken, generateRandomString, getNumberOrUndefined, JWT_SIGNER_CALLBACK_REQUIRED_ERROR, NotificationStatusEventNames, TokenErrorResponse as TokenErrorResponse2, trimBoth, trimEnd, trimStart, validateJWT, WellKnownEndpoints } from "@sphereon/oid4vci-common"; import { LOG } from "@sphereon/oid4vci-issuer"; import { env, sendErrorResponse as sendErrorResponse2 } from "@sphereon/ssi-express-support"; import { InitiatorType, SubSystem, System } from "@sphereon/ssi-types"; // lib/IssuerTokenEndpoint.ts import { uuidv4, verifyDPoP } from "@sphereon/oid4vc-common"; import { GrantTypes, PRE_AUTHORIZED_CODE_REQUIRED_ERROR, TokenError, TokenErrorResponse } from "@sphereon/oid4vci-common"; import { assertValidAccessTokenRequest, createAccessTokenResponse } from "@sphereon/oid4vci-issuer"; import { sendErrorResponse } from "@sphereon/ssi-express-support"; var handleTokenRequest = /* @__PURE__ */ __name(({ tokenExpiresIn, accessTokenEndpoint: accessTokenEndpoint2, accessTokenSignerCallback, accessTokenIssuer, cNonceExpiresIn, issuer, interval, dpop }) => { return async (request, response) => { response.set({ "Cache-Control": "no-store", Pragma: "no-cache" }); if (request.body.grant_type !== GrantTypes.PRE_AUTHORIZED_CODE) { return sendErrorResponse(response, 400, { error: TokenErrorResponse.invalid_request, error_description: PRE_AUTHORIZED_CODE_REQUIRED_ERROR }); } if (request.headers.authorization && request.headers.authorization.startsWith("DPoP ") && !request.headers.DPoP) { return sendErrorResponse(response, 400, { error: TokenErrorResponse.invalid_request, error_description: "DPoP header is required" }); } let dPoPJwk; if (dpop?.requireDPoP && !request.headers.dpop) { return sendErrorResponse(response, 400, { error: TokenErrorResponse.invalid_request, error_description: "DPoP is required for requesting access tokens." }); } if (request.headers.dpop) { if (!dpop) { console.error("Received unsupported DPoP header. The issuer is not configured to work with DPoP. Provide DPoP options for it to work."); return sendErrorResponse(response, 400, { error: TokenErrorResponse.invalid_request, error_description: "Received unsupported DPoP header." }); } try { const fullUrl = accessTokenEndpoint2 ?? request.protocol + "://" + request.get("host") + request.originalUrl; dPoPJwk = await verifyDPoP({ method: request.method, headers: request.headers, fullUrl }, { jwtVerifyCallback: dpop.dPoPVerifyJwtCallback, expectAccessToken: false, maxIatAgeInSeconds: void 0 }); } catch (error) { return sendErrorResponse(response, 400, { error: TokenErrorResponse.invalid_dpop_proof, error_description: error instanceof Error ? error.message : "Unknown error" }); } } try { const responseBody = await createAccessTokenResponse(request.body, { credentialOfferSessions: issuer.credentialOfferSessions, accessTokenIssuer, cNonces: issuer.cNonces, cNonce: uuidv4(), accessTokenSignerCallback, cNonceExpiresIn, interval, tokenExpiresIn, dPoPJwk }); return response.status(200).json(responseBody); } catch (error) { return sendErrorResponse(response, 400, { error: TokenErrorResponse.invalid_request }, error); } }; }, "handleTokenRequest"); var verifyTokenRequest = /* @__PURE__ */ __name(({ preAuthorizedCodeExpirationDuration, issuer, authRequestsData }) => { return async (request, response, next) => { try { await assertValidAccessTokenRequest(request.body, { expirationDuration: preAuthorizedCodeExpirationDuration, credentialOfferSessions: issuer.credentialOfferSessions, authRequestsData }); } catch (error) { if (error instanceof TokenError) { return sendErrorResponse(response, error.statusCode, { error: error.responseError, error_description: error.getDescription() }); } else { return sendErrorResponse(response, 400, { error: TokenErrorResponse.invalid_request, error_description: error.message }, error); } } return next(); }; }, "verifyTokenRequest"); // lib/expressUtils.ts var validateRequestBody = /* @__PURE__ */ __name(({ required, conditional, body }) => { const keys = Object.keys(body); let message; if (required && !required.every((k) => keys.includes(k))) { message = `Request must contain ${required.toString()}`; } if (conditional && !conditional.some((k) => keys.includes(k))) { message = message ? `and request must contain ether ${conditional.toString()}` : `Request must contain ether ${conditional.toString()}`; } if (message) { throw new Error(message); } }, "validateRequestBody"); // lib/oid4vci-api-functions.ts var expiresIn = process.env.EXPIRES_IN ? parseInt(process.env.EXPIRES_IN) : 90; function getIssueStatusEndpoint(router, issuer, opts) { const path = determinePath(opts.baseUrl, opts?.path ?? "/webapp/credential-offer-status", { stripBasePath: true }); LOG.log(`[OID4VCI] getIssueStatus endpoint enabled at ${path}`); router.post(path, async (request, response) => { try { const { id } = request.body; const session = await issuer.getCredentialOfferSessionById(id); if (!session || !session.credentialOffer) { return sendErrorResponse2(response, 404, { error: "invalid_request", error_description: `Credential offer ${id} not found` }); } const authStatusBody = { createdAt: session.createdAt, lastUpdatedAt: session.lastUpdatedAt, expiresAt: session.expiresAt, status: session.status, statusLists: session.statusLists, ...session.error && { error: session.error }, ...session.clientId && { clientId: session.clientId } }; return response.json(authStatusBody); } catch (e) { return sendErrorResponse2(response, 500, { error: "invalid_request", error_description: e.message }, e); } }); } __name(getIssueStatusEndpoint, "getIssueStatusEndpoint"); function getCredentialOfferReferenceEndpoint(router, issuer, opts) { const path = determinePath(opts.baseUrl, opts?.path ?? "/credential-offers/:id", { stripBasePath: true }); LOG.log(`[OID4VCI] getCredentialOfferReferenceEndpoint endpoint enabled at ${path}`); router.get(path, async (request, response) => { try { const { id } = request.params; if (!id) { return sendErrorResponse2(response, 404, { error: "invalid_request", error_description: `query parameter 'id' is missing` }); } let session; try { session = await issuer.getCredentialOfferSessionById(id); } catch (e) { } if (!session || !session.credentialOffer || session.status !== "OFFER_CREATED") { if (session?.status) { LOG.warning(`[OID4VCI] credential offer reference URI request with ${id}, but request was already received earlier. Session status: ${session.status}`); } return sendErrorResponse2(response, 404, { error: "invalid_request", error_description: `Credential offer ${id} not found` }); } return response.json(session.credentialOffer.credential_offer); } catch (e) { return sendErrorResponse2(response, 500, { error: "invalid_request", error_description: e.message }, e); } }); return path; } __name(getCredentialOfferReferenceEndpoint, "getCredentialOfferReferenceEndpoint"); function isExternalAS(issuerMetadata) { return issuerMetadata.authorization_servers?.some((as) => !as.includes(issuerMetadata.credential_issuer)); } __name(isExternalAS, "isExternalAS"); function authorizationChallengeEndpoint(router, issuer, opts) { const endpoint = issuer.authorizationServerMetadata.authorization_challenge_endpoint ?? issuer.issuerMetadata.authorization_challenge_endpoint; const baseUrl = getBaseUrl(opts.baseUrl); if (!endpoint) { LOG.info('authorization challenge endpoint disabled as no "authorization_challenge_endpoint" has been configured in issuer metadata'); return; } const path = determinePath(baseUrl, endpoint, { stripBasePath: true }); LOG.log(`[OID4VCI] authorization challenge endpoint at ${path}`); router.post(path, async (request, response) => { const authorizationChallengeRequest = request.body; const { client_id, issuer_state, auth_session, presentation_during_issuance_session } = authorizationChallengeRequest; try { if (!client_id && !auth_session) { const authorizationChallengeErrorResponse2 = { error: AuthorizationChallengeError.invalid_request, error_description: "No client id or auth session present" }; throw authorizationChallengeErrorResponse2; } if (!auth_session && issuer_state) { const session = await issuer.credentialOfferSessions.get(issuer_state); if (!session) { const authorizationChallengeErrorResponse3 = { error: AuthorizationChallengeError.invalid_session, error_description: "Session is invalid" }; throw authorizationChallengeErrorResponse3; } const authRequestURI = await opts.createAuthRequestUriCallback(issuer_state); const authorizationChallengeErrorResponse2 = { error: AuthorizationChallengeError.insufficient_authorization, auth_session: issuer_state, presentation: authRequestURI }; throw authorizationChallengeErrorResponse2; } if (auth_session && presentation_during_issuance_session) { const session = await issuer.credentialOfferSessions.get(auth_session); if (!session) { const authorizationChallengeErrorResponse2 = { error: AuthorizationChallengeError.invalid_session, error_description: "Session is invalid" }; throw authorizationChallengeErrorResponse2; } const verifiedResponse = await opts.verifyAuthResponseCallback(presentation_during_issuance_session); if (verifiedResponse) { const authorizationCode = generateRandomString(16, "base64url"); session.authorizationCode = authorizationCode; await issuer.credentialOfferSessions.set(auth_session, session); const authorizationChallengeCodeResponse = { authorization_code: authorizationCode }; return response.json(authorizationChallengeCodeResponse); } } const authorizationChallengeErrorResponse = { error: AuthorizationChallengeError.invalid_request }; throw authorizationChallengeErrorResponse; } catch (e) { return sendErrorResponse2(response, 400, e, e); } }); } __name(authorizationChallengeEndpoint, "authorizationChallengeEndpoint"); function accessTokenEndpoint(router, issuer, opts) { const externalAS = isExternalAS(issuer.issuerMetadata) || issuer.asClientOpts; if (externalAS || opts.accessTokenProvider && opts.accessTokenProvider !== "internal") { LOG.log(`[OID4VCI] External Authorization Server ${issuer.issuerMetadata.authorization_servers} is being used. Not enabling internal issuer token endpoint`); return; } else if (opts?.enabled === false) { LOG.log(`[OID4VCI] Internal issuer token endpoint is not enabled`); return; } const accessTokenIssuer = opts?.accessTokenIssuer ?? process.env.ACCESS_TOKEN_ISSUER ?? issuer.issuerMetadata.authorization_servers?.[0] ?? issuer.issuerMetadata.credential_issuer; const preAuthorizedCodeExpirationDuration = opts?.preAuthorizedCodeExpirationDuration ?? getNumberOrUndefined(process.env.PRE_AUTHORIZED_CODE_EXPIRATION_DURATION) ?? 300; const interval = opts?.interval ?? getNumberOrUndefined(process.env.INTERVAL) ?? 300; const tokenExpiresIn = opts?.tokenExpiresIn ?? 300; if (opts?.accessTokenSignerCallback === void 0) { throw new Error(JWT_SIGNER_CALLBACK_REQUIRED_ERROR); } else if (!accessTokenIssuer) { throw new Error(ACCESS_TOKEN_ISSUER_REQUIRED_ERROR); } const baseUrl = getBaseUrl(opts.baseUrl); const path = determinePath(baseUrl, opts?.tokenPath ?? process.env.TOKEN_PATH ?? "/token", { skipBaseUrlCheck: false, stripBasePath: true }); const url = new URL(`${baseUrl}${path}`); LOG.log(`[OID4VCI] Token endpoint enabled at ${url.toString()}`); router.post(determinePath(baseUrl, url.pathname, { stripBasePath: true }), verifyTokenRequest({ issuer, preAuthorizedCodeExpirationDuration, authRequestsData: opts.authRequestsData }), handleTokenRequest({ issuer, accessTokenSignerCallback: opts.accessTokenSignerCallback, cNonceExpiresIn: issuer.cNonceExpiresIn, interval, tokenExpiresIn, accessTokenIssuer })); } __name(accessTokenEndpoint, "accessTokenEndpoint"); function getCredentialEndpoint(router, issuer, opts) { const endpoint = issuer.issuerMetadata.credential_endpoint; const baseUrl = getBaseUrl(opts.baseUrl); let path; if (!endpoint) { path = `/credentials`; issuer.issuerMetadata.credential_endpoint = `${baseUrl}${path}`; } else { path = determinePath(baseUrl, endpoint, { stripBasePath: true, skipBaseUrlCheck: false }); } path = determinePath(baseUrl, path, { stripBasePath: true }); LOG.log(`[OID4VCI] getCredential endpoint enabled at ${path}`); router.post(path, async (request, response) => { try { const credentialRequest = request.body; LOG.log(`credential request received`, credentialRequest); const issuerCorrelation = {}; try { const jwt = extractBearerToken(request.header("Authorization")); const jwtVerifyResult = await validateJWT(jwt, { accessTokenVerificationCallback: opts.accessTokenVerificationCallback ?? issuer.jwtVerifyCallback }); const tokenClaims = jwtVerifyResult.jwt.payload; if ("preAuthorizedCode" in tokenClaims && typeof tokenClaims.preAuthorizedCode === "string") { issuerCorrelation.preAuthorizedCode = tokenClaims.preAuthorizedCode; } if ("issuer_state" in tokenClaims && typeof tokenClaims.issuer_state === "string") { issuerCorrelation.issuerState = tokenClaims.issuer_state; } if ("authorization_details" in tokenClaims && Array.isArray(tokenClaims.authorization_details)) { issuerCorrelation.authorizationDetails = tokenClaims.authorization_details; if (credentialRequest.credential_identifier) { const validIdentifiers = tokenClaims.authorization_details.flatMap((detail) => detail.credential_identifiers || []); if (!validIdentifiers.includes(credentialRequest.credential_identifier)) { return sendErrorResponse2(response, 400, { error: "invalid_credential_request", error_description: "credential_identifier not found in authorization_details" }); } } } } catch (e) { LOG.warning(e); return sendErrorResponse2(response, 400, { error: "invalid_token" }); } const credential = await issuer.issueCredential({ credentialRequest, issuerCorrelation, tokenExpiresIn: opts.tokenExpiresIn, cNonceExpiresIn: opts.cNonceExpiresIn }); return response.json(credential); } catch (e) { return sendErrorResponse2(response, 500, { error: "invalid_request", error_description: e.message }, e); } }); } __name(getCredentialEndpoint, "getCredentialEndpoint"); function notificationEndpoint(router, issuer, opts) { const endpoint = issuer.issuerMetadata.notification_endpoint; const baseUrl = getBaseUrl(opts.baseUrl); if (!endpoint) { LOG.warning('Notification endpoint disabled as no "notification_endpoint" has been configured in issuer metadata'); return; } const path = determinePath(baseUrl, endpoint, { stripBasePath: true }); LOG.log(`[OID4VCI] notification endpoint enabled at ${path}`); router.post(path, async (request, response) => { try { const notificationRequest = request.body; LOG.log(`notification ${notificationRequest.event}/${notificationRequest.event_description} received for ${notificationRequest.notification_id}`); const jwt = extractBearerToken(request.header("Authorization")); EVENTS.emit(NotificationStatusEventNames.OID4VCI_NOTIFICATION_RECEIVED, { eventName: NotificationStatusEventNames.OID4VCI_NOTIFICATION_RECEIVED, id: uuidv42(), data: notificationRequest, initiator: jwt, initiatorType: InitiatorType.EXTERNAL, system: System.OID4VCI, subsystem: SubSystem.API }); try { const jwtResult = await validateJWT(jwt, { accessTokenVerificationCallback: opts.accessTokenVerificationCallback }); const accessToken = jwtResult.jwt.payload; const errorOrSession = await issuer.processNotification({ preAuthorizedCode: accessToken["pre-authorized_code"], /*TODO: authorizationCode*/ notification: notificationRequest }); if (errorOrSession instanceof Error) { EVENTS.emit(NotificationStatusEventNames.OID4VCI_NOTIFICATION_ERROR, { eventName: NotificationStatusEventNames.OID4VCI_NOTIFICATION_ERROR, id: uuidv42(), data: notificationRequest, initiator: jwtResult.jwt, initiatorType: InitiatorType.EXTERNAL, system: System.OID4VCI, subsystem: SubSystem.API }); return sendErrorResponse2(response, 400, errorOrSession.message); } else { EVENTS.emit(NotificationStatusEventNames.OID4VCI_NOTIFICATION_PROCESSED, { eventName: NotificationStatusEventNames.OID4VCI_NOTIFICATION_PROCESSED, id: uuidv42(), data: notificationRequest, initiator: jwtResult.jwt, initiatorType: InitiatorType.EXTERNAL, system: System.OID4VCI, subsystem: SubSystem.API }); } } catch (e) { LOG.warning(e); return sendErrorResponse2(response, 400, { error: "invalid_token" }); } return response.status(204).send(); } catch (e) { return sendErrorResponse2(response, 400, { error: "invalid_notification_request", error_description: e.message }, e); } }); } __name(notificationEndpoint, "notificationEndpoint"); function nonceEndpoint(router, issuer, opts) { const endpoint = issuer.issuerMetadata.nonce_endpoint; const baseUrl = getBaseUrl(opts.baseUrl); if (!endpoint) { LOG.warning('Nonce endpoint disabled as no "nonce_endpoint" has been configured in issuer metadata'); return; } const path = determinePath(baseUrl, endpoint, { stripBasePath: true }); LOG.log(`[OID4VCI] nonce endpoint enabled at ${path}`); router.post(path, async (request, response) => { try { const cNonce = uuidv42(); const cNonceExpiresIn = issuer.cNonceExpiresIn || 300; const createdAt = +Date.now(); const expiresAt = createdAt + Math.abs(cNonceExpiresIn) * 1e3; const cNonceState = { cNonce, createdAt, expiresAt }; await issuer.cNonces.set(cNonce, cNonceState); return response.json({ c_nonce: cNonce, c_nonce_expires_in: cNonceExpiresIn }); } catch (e) { return sendErrorResponse2(response, 500, { error: "server_error", error_description: e.message }, e); } }); } __name(nonceEndpoint, "nonceEndpoint"); function getCredentialOfferEndpoint(router, issuer, opts) { const path = determinePath(opts?.baseUrl, opts?.path ?? "/webapp/credential-offers/:id", { stripBasePath: true }); LOG.log(`[OID4VCI] getCredentialOffer endpoint enabled at ${path}`); router.get(path, async (request, response) => { try { const { id } = request.params; const session = await issuer.getCredentialOfferSessionById(id); if (!session || !session.credentialOffer) { return sendErrorResponse2(response, 404, { error: "invalid_request", error_description: `Credential offer ${id} not found` }); } return response.json(session.credentialOffer.credential_offer); } catch (e) { return sendErrorResponse2(response, 500, { error: "invalid_request", error_description: e.message }, e); } }); } __name(getCredentialOfferEndpoint, "getCredentialOfferEndpoint"); function deleteCredentialOfferEndpoint(router, issuer, opts) { const path = determinePath(opts?.baseUrl, opts?.path ?? "/webapp/credential-offers/:id", { stripBasePath: true }); LOG.log(`[OID4VCI] deleteCredentialOffer endpoint enabled at ${path}`); router.delete(path, async (request, response) => { try { const { id } = request.params; if (!id) { return sendErrorResponse2(response, 400, { error: "invalid_request", error_description: "id must be present" }); } await issuer.deleteCredentialOfferSessionById(id); return response.sendStatus(204); } catch (e) { return sendErrorResponse2(response, 500, { error: "invalid_request", error_description: e.message }, e); } }); } __name(deleteCredentialOfferEndpoint, "deleteCredentialOfferEndpoint"); function buildCredentialOfferReferenceUri(request, offerReferencePath) { if (!offerReferencePath) { return Promise.reject(Error("issuePayloadPath must bet set for offerMode REFERENCE!")); } const protocol = request.headers["x-forwarded-proto"]?.toString() ?? request.protocol; let host = request.headers["x-forwarded-host"]?.toString() ?? request.get("host"); const forwardedPort = request.headers["x-forwarded-port"]?.toString(); if (forwardedPort && !(protocol === "https" && forwardedPort === "443") && !(protocol === "http" && forwardedPort === "80")) { host += `:${forwardedPort}`; } const forwardedPrefix = request.headers["x-forwarded-prefix"]?.toString() ?? ""; return `${protocol}://${host}${forwardedPrefix}${request.baseUrl}${offerReferencePath}`; } __name(buildCredentialOfferReferenceUri, "buildCredentialOfferReferenceUri"); function createCredentialOfferEndpoint(router, issuer, opts, issuerPayloadPath) { const path = determinePath(opts?.baseUrl, opts?.path ?? "/webapp/credential-offers", { stripBasePath: true }); const offerReferencePath = opts?.credentialOfferReferenceBasePath ?? issuerPayloadPath ?? determinePath(opts?.baseUrl, "/credential-offers", { stripBasePath: true }); LOG.log(`[OID4VCI] createCredentialOffer endpoint enabled at ${path}`); router.post(path, async (request, response) => { try { const grantTypes = determineGrantTypes(request.body); if (grantTypes.length === 0) { return sendErrorResponse2(response, 400, { error: TokenErrorResponse2.invalid_grant, error_description: "No grant type supplied" }); } const grants = request.body.grants; const credentialConfigIds = request.body.credential_configuration_ids; if (!credentialConfigIds || credentialConfigIds.length === 0) { return sendErrorResponse2(response, 400, { error: TokenErrorResponse2.invalid_request, error_description: "credential_configuration_ids missing credential_configuration_ids in credential offer payload" }); } const qrCodeOpts = request.body.qrCodeOpts ?? opts?.qrCodeOpts; const offerMode = request.body.offerMode ?? opts?.defaultCredentialOfferMode ?? "VALUE"; const client_id = request.body.client_id ?? request.body.original_credential_offer?.client_id; const result = await issuer.createCredentialOfferURI({ ...request.body, offerMode, client_id, ...request.body.correlationId && { correlationId: request.body.correlationId }, ...offerMode === "REFERENCE" && { credentialOfferUri: buildCredentialOfferReferenceUri(request, offerReferencePath) }, qrCodeOpts, grants }); const resultResponse = result; if ("session" in resultResponse) { delete resultResponse.session; } return response.json(resultResponse); } catch (e) { return sendErrorResponse2(response, 500, { error: TokenErrorResponse2.invalid_request, error_description: e.message }, e); } }); } __name(createCredentialOfferEndpoint, "createCredentialOfferEndpoint"); function pushedAuthorizationEndpoint(router, issuer, authRequestsData, opts) { const externalAS = isExternalAS(issuer.issuerMetadata) || issuer.asClientOpts; if (externalAS) { LOG.log(`[OID4VCI] External Authorization Server ${issuer.issuerMetadata.authorization_servers} is being used. Not enabling internal PAR endpoint`); return; } else if (opts?.enabled === false) { LOG.log(`[OID4VCI] Internal PAR endpoint is not enabled`); return; } const handleHttpStatus400 = /* @__PURE__ */ __name(async (req, res, next) => { if (!req.body) { return res.status(400).json({ error: "invalid_request", error_description: "Request body must be present" }); } const required = [ "client_id", "code_challenge_method", "code_challenge", "redirect_uri" ]; const conditional = [ "authorization_details", "scope" ]; try { validateRequestBody({ required, conditional, body: req.body }); } catch (e) { return sendErrorResponse2(res, 400, { error: "invalid_request", error_description: e.message }); } return next(); }, "handleHttpStatus400"); router.post("/par", handleHttpStatus400, (req, res) => { const client = { scope: [ "openid", "test" ], redirectUris: [ "http://localhost:8080/*", "https://www.test.com/*", "https://test.nl", "http://*/chart", "http:*" ] }; const matched = client.redirectUris.filter((s) => new RegExp(s.replace("*", ".*")).test(req.body.redirect_uri)); if (!matched.length) { return sendErrorResponse2(res, 400, { error: "invalid_request", error_description: "redirect_uri is not valid for the given client" }); } if (!req.body.scope.split(",").every((scope) => client.scope.includes(scope))) { return sendErrorResponse2(res, 400, { error: "invalid_scope", error_description: "scope is not valid for the given client" }); } if (req.body.authorization_details) { const authDetails = Array.isArray(req.body.authorization_details) ? req.body.authorization_details : JSON.parse(req.body.authorization_details); for (const detail of authDetails) { if (detail.type !== "openid_credential") { return sendErrorResponse2(res, 400, { error: "invalid_authorization_details", error_description: "Only openid_credential type is supported" }); } if (detail.credential_configuration_id && !issuer.issuerMetadata.credential_configurations_supported[detail.credential_configuration_id]) { return sendErrorResponse2(res, 400, { error: "invalid_credential_request", error_description: `Unsupported credential configuration: ${detail.credential_configuration_id}` }); } } } const uuid = uuidv42(); const requestUri = `urn:ietf:params:oauth:request_uri:${uuid}`; let requestData = req.body; if (req.body.authorization_details) { const authDetails = Array.isArray(req.body.authorization_details) ? req.body.authorization_details : JSON.parse(req.body.authorization_details); requestData = { ...req.body, authorization_details: authDetails }; } authRequestsData.set(requestUri, requestData); setTimeout(() => { authRequestsData.delete(requestUri); }, expiresIn * 1e3); return res.status(201).json({ request_uri: requestUri, expires_in: expiresIn }); }); } __name(pushedAuthorizationEndpoint, "pushedAuthorizationEndpoint"); function getMetadataEndpoints(router, issuer, opts) { const credentialIssuerHandler = /* @__PURE__ */ __name((request, response) => { return response.json(issuer.issuerMetadata); }, "credentialIssuerHandler"); const authorizationServerHandler = /* @__PURE__ */ __name((request, response) => { return response.json(issuer.authorizationServerMetadata); }, "authorizationServerHandler"); const location = opts?.wellKnownHostLocation ?? WellKnownHostLocation.AT_BOTH; if (location === WellKnownHostLocation.AT_CONTEXT_PATH || location === WellKnownHostLocation.AT_BOTH) { router.get(WellKnownEndpoints.OPENID4VCI_ISSUER, credentialIssuerHandler); router.get(WellKnownEndpoints.OAUTH_AS, authorizationServerHandler); } if (opts?.rootRouter && opts?.basePath && opts.basePath !== "/" && (location === WellKnownHostLocation.AT_ROOT_PATH || location === WellKnownHostLocation.AT_BOTH)) { opts.rootRouter.get(`/.well-known/openid-credential-issuer${opts.basePath}`, credentialIssuerHandler); opts.rootRouter.get(`/.well-known/oauth-authorization-server${opts.basePath}`, authorizationServerHandler); } } __name(getMetadataEndpoints, "getMetadataEndpoints"); function determinePath(baseUrl, endpoint, opts) { const basePath = baseUrl ? getBasePath(baseUrl) : ""; let path = endpoint; if (opts?.prependUrl) { path = adjustUrl(path, { prepend: opts.prependUrl }); } if (opts?.skipBaseUrlCheck !== true) { assertEndpointHasIssuerBaseUrl(baseUrl, endpoint); } if (endpoint.includes("://")) { path = new URL(endpoint).pathname; } path = `/${trimBoth(path, "/")}`; if (opts?.stripBasePath && path.startsWith(basePath)) { path = trimStart(path, basePath); path = `/${trimBoth(path, "/")}`; } return path; } __name(determinePath, "determinePath"); function assertEndpointHasIssuerBaseUrl(baseUrl, endpoint) { if (!validateEndpointHasIssuerBaseUrl(baseUrl, endpoint)) { throw Error(`endpoint '${endpoint}' does not have base url '${baseUrl ? getBaseUrl(baseUrl) : "<no baseurl supplied>"}'`); } } __name(assertEndpointHasIssuerBaseUrl, "assertEndpointHasIssuerBaseUrl"); function validateEndpointHasIssuerBaseUrl(baseUrl, endpoint) { if (!endpoint) { return false; } else if (!endpoint.includes("://")) { return true; } else if (!baseUrl) { return true; } return endpoint.startsWith(getBaseUrl(baseUrl)); } __name(validateEndpointHasIssuerBaseUrl, "validateEndpointHasIssuerBaseUrl"); function getBaseUrl(url) { let baseUrl = url; if (!baseUrl) { const envUrl = env("BASE_URL", process?.env?.ENV_PREFIX); if (envUrl && envUrl.length > 0) { baseUrl = new URL(envUrl); } } if (!baseUrl) { throw Error(`No base URL provided`); } return trimEnd(baseUrl.toString(), "/"); } __name(getBaseUrl, "getBaseUrl"); function getBasePath(url) { const basePath = new URL(getBaseUrl(url)).pathname; if (basePath === "" || basePath === "/") { return ""; } return `/${trimBoth(basePath, "/")}`; } __name(getBasePath, "getBasePath"); // lib/OID4VCIServer.ts function buildVCIFromEnvironment() { const credentialsSupported = new CredentialSupportedBuilderV1_15().withCredentialSigningAlgValuesSupported(process.env.credential_signing_alg_values_supported).withCryptographicBindingMethod(process.env.cryptographic_binding_methods_supported).withFormat(process.env.credential_supported_format).withCredentialName(process.env.credential_supported_name_1).withCredentialDefinition({ type: [ process.env.credential_supported_1_definition_type_1, process.env.credential_supported_1_definition_type_2 ] }).withCredentialSupportedDisplay({ name: process.env.credential_display_name, locale: process.env.credential_display_locale, logo: { url: process.env.credential_display_logo_url, alt_text: process.env.credential_display_logo_alt_text }, background_color: process.env.credential_display_background_color, text_color: process.env.credential_display_text_color }).build(); const issuerBuilder = new VcIssuerBuilder().withTXCode({ length: process.env.user_pin_length, input_mode: process.env.user_pin_input_mode }).withAuthorizationServers(process.env.authorization_server).withCredentialEndpoint(process.env.credential_endpoint).withNonceEndpoint(process.env.nonce_endpoint).withCredentialIssuer(process.env.credential_issuer).withIssuerDisplay({ name: process.env.issuer_name, locale: process.env.issuer_locale }).withCredentialConfigurationsSupported(credentialsSupported).withInMemoryCredentialOfferState().withInMemoryCNonceState(); if (process.env.authorization_server_client_id) { if (!process.env.authorization_server_redirect_uri) { throw Error("Authorization server redirect uri is required when client id is set"); } issuerBuilder.withASClientMetadataParams({ client_id: process.env.authorization_server_client_id, client_secret: process.env.authorization_server_client_secret, redirect_uris: [ process.env.authorization_server_redirect_uri ] }); } return issuerBuilder.build(); } __name(buildVCIFromEnvironment, "buildVCIFromEnvironment"); var WellKnownHostLocation = /* @__PURE__ */ (function(WellKnownHostLocation2) { WellKnownHostLocation2["AT_CONTEXT_PATH"] = "AT_CONTEXT_PATH"; WellKnownHostLocation2["AT_ROOT_PATH"] = "AT_ROOT_PATH"; WellKnownHostLocation2["AT_BOTH"] = "AT_BOTH"; return WellKnownHostLocation2; })({}); var OID4VCIServer = class { static { __name(this, "OID4VCIServer"); } _issuer; authRequestsData = /* @__PURE__ */ new Map(); _app; _baseUrl; _expressSupport; // private readonly _server?: http.Server _router; _asClientOpts; _wellknownHostLocation; constructor(expressSupport, opts) { this._baseUrl = new URL(opts?.baseUrl ?? process.env.BASE_URL ?? opts?.issuer?.issuerMetadata?.credential_issuer ?? "http://localhost"); this._expressSupport = expressSupport; this._app = expressSupport.express; this._router = express.Router(); this._issuer = opts?.issuer ? opts.issuer : buildVCIFromEnvironment(); this._asClientOpts = opts.asClientOpts || this._issuer.asClientOpts ? { ...opts.asClientOpts, ...this._issuer.asClientOpts } : void 0; this._wellknownHostLocation = opts?.wellKnownHostLocation ?? process.env.WELLKNOWN_HOST_LOCATION ?? "AT_BOTH"; pushedAuthorizationEndpoint(this.router, this.issuer, this.authRequestsData); const basePath = getBasePath(this.baseUrl); let rootRouter; if (basePath && basePath !== "/" && (this.wellknownHostLocation == "AT_ROOT_PATH" || this.wellknownHostLocation == "AT_BOTH")) { rootRouter = express.Router(); this._app.use("/", rootRouter); } getMetadataEndpoints(this.router, this.issuer, { rootRouter, basePath, wellKnownHostLocation: this.wellknownHostLocation }); let issuerPayloadPath; if (this.isGetIssuePayloadEndpointEnabled(opts?.endpointOpts?.getIssuePayloadOpts)) { issuerPayloadPath = getCredentialOfferReferenceEndpoint(this.router, this.issuer, { ...opts?.endpointOpts?.getIssuePayloadOpts, baseUrl: this.baseUrl }); } if (opts?.endpointOpts?.createCredentialOfferOpts?.enabled !== false || process.env.CREDENTIAL_OFFER_ENDPOINT_ENABLED === "true") { createCredentialOfferEndpoint(this.router, this.issuer, opts?.endpointOpts?.createCredentialOfferOpts, issuerPayloadPath); deleteCredentialOfferEndpoint(this.router, this.issuer, opts?.endpointOpts?.deleteCredentialOfferOpts); } getCredentialOfferEndpoint(this.router, this.issuer, opts?.endpointOpts?.getCredentialOfferOpts); getCredentialEndpoint(this.router, this.issuer, { ...opts?.endpointOpts?.tokenEndpointOpts, baseUrl: this.baseUrl, accessTokenVerificationCallback: opts.endpointOpts?.tokenEndpointOpts?.accessTokenVerificationCallback ?? (this._asClientOpts ? oidcAccessTokenVerifyCallback({ clientMetadata: this._asClientOpts, credentialIssuer: this._issuer.issuerMetadata.credential_issuer, authorizationServer: this._issuer.issuerMetadata.authorization_servers[0] }) : void 0) }); this.assertAccessTokenHandling(); if (!this.isTokenEndpointDisabled(opts?.endpointOpts?.tokenEndpointOpts, opts?.asClientOpts)) { accessTokenEndpoint(this.router, this.issuer, { ...opts?.endpointOpts?.tokenEndpointOpts, baseUrl: this.baseUrl, authRequestsData: this.authRequestsData }); } if (this.isStatusEndpointEnabled(opts?.endpointOpts?.getStatusOpts)) { getIssueStatusEndpoint(this.router, this.issuer, { ...opts?.endpointOpts?.getStatusOpts, baseUrl: this.baseUrl }); } if (this.isAuthorizationChallengeEndpointEnabled(opts?.endpointOpts?.authorizationChallengeOpts)) { if (!opts?.endpointOpts?.authorizationChallengeOpts?.createAuthRequestUriCallback) { throw Error(`Unable to enable authorization challenge endpoint. No createAuthRequestUriCallback present in authorization challenge options`); } else if (!opts?.endpointOpts?.authorizationChallengeOpts?.verifyAuthResponseCallback) { throw Error(`Unable to enable authorization challenge endpoint. No verifyAuthResponseCallback present in authorization challenge options`); } authorizationChallengeEndpoint(this.router, this.issuer, { ...opts?.endpointOpts?.authorizationChallengeOpts, baseUrl: this.baseUrl }); } if (this.isNonceEndpointEnabled(opts?.endpointOpts?.nonceOpts)) { nonceEndpoint(this.router, this.issuer, { ...opts?.endpointOpts?.nonceOpts, baseUrl: this.baseUrl }); } this._app.use(basePath, this._router); } get app() { return this._app; } /*public get server(): http.Server | undefined { return this._server }*/ get router() { return this._router; } get issuer() { return this._issuer; } async stop() { if (!this._expressSupport) { throw Error("Cannot stop server is the REST API is only a router of an existing express app"); } await this._expressSupport.stop(); } isTokenEndpointDisabled(tokenEndpointOpts, asClientMetadata) { return tokenEndpointOpts?.tokenEndpointDisabled === true || process.env.TOKEN_ENDPOINT_DISABLED === "true" || asClientMetadata; } isStatusEndpointEnabled(statusEndpointOpts) { return statusEndpointOpts?.enabled !== false || process.env.STATUS_ENDPOINT_ENABLED !== "false"; } isGetIssuePayloadEndpointEnabled(payloadEndpointOpts) { return payloadEndpointOpts?.enabled !== false || process.env.STATUS_ENDPOINT_ENABLED !== "false"; } isAuthorizationChallengeEndpointEnabled(authorizationChallengeEndpointOpts) { return authorizationChallengeEndpointOpts?.enabled === true || process.env.AUTHORIZATION_CHALLENGE_ENDPOINT_ENABLED === "true"; } assertAccessTokenHandling(tokenEndpointOpts) { const authServer = this.issuer.issuerMetadata.authorization_servers; if (this.isTokenEndpointDisabled(tokenEndpointOpts, this.issuer.asClientOpts)) { if (!authServer || authServer.length === 0) { throw Error(`No Authorization Server (AS) is defined in the issuer metadata and the token endpoint is disabled. An AS or token endpoints needs to be present`); } if (this.issuer.asClientOpts) { console.log(`Token endpoint disabled because AS client metadata is set for ${authServer[0]}`); } else { console.log(`Token endpoint disabled by configuration`); } } else { if (authServer && authServer.some((as) => as !== this.issuer.issuerMetadata.credential_issuer)) { throw Error(`An external Authorization Server (AS) was already enabled in the issuer metadata (${authServer}). Cannot both have an AS and enable the token endpoint at the same time `); } else if (this._asClientOpts) { throw Error(`OIDC Client metadata is set, but the token endpoint is not disabled. This is not supported.`); } } } isNonceEndpointEnabled(nonceEndpointOpts) { return nonceEndpointOpts?.enabled !== false || process.env.NONCE_ENDPOINT_ENABLED !== "false"; } get baseUrl() { return this._baseUrl; } get wellknownHostLocation() { return this._wellknownHostLocation; } }; export { OID4VCIServer, WellKnownHostLocation, accessTokenEndpoint, authorizationChallengeEndpoint, createCredentialOfferEndpoint, deleteCredentialOfferEndpoint, determinePath, getBasePath, getBaseUrl, getCredentialEndpoint, getCredentialOfferEndpoint, getCredentialOfferReferenceEndpoint, getIssueStatusEndpoint, getMetadataEndpoints, nonceEndpoint, notificationEndpoint, pushedAuthorizationEndpoint, validateRequestBody }; //# sourceMappingURL=index.js.map