@sphereon/oid4vci-issuer-server
Version:
OpenID 4 Verifiable Credential Issuance Server
942 lines (934 loc) • 42.2 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// lib/index.ts
var index_exports = {};
__export(index_exports, {
ClientAuthMethod: () => import_oid4vci_common3.ClientAuthMethod,
ClientMetadata: () => import_oid4vci_common3.ClientMetadata,
ClientResponseType: () => import_oid4vci_common3.ClientResponseType,
OID4VCIServer: () => OID4VCIServer,
accessTokenEndpoint: () => accessTokenEndpoint,
authorizationChallengeEndpoint: () => authorizationChallengeEndpoint,
createCredentialOfferEndpoint: () => createCredentialOfferEndpoint,
deleteCredentialOfferEndpoint: () => deleteCredentialOfferEndpoint,
determinePath: () => determinePath,
getBasePath: () => getBasePath,
getBaseUrl: () => getBaseUrl,
getCredentialEndpoint: () => getCredentialEndpoint,
getCredentialOfferEndpoint: () => getCredentialOfferEndpoint,
getCredentialOfferReferenceEndpoint: () => getCredentialOfferReferenceEndpoint,
getIssueStatusEndpoint: () => getIssueStatusEndpoint,
getMetadataEndpoints: () => getMetadataEndpoints,
notificationEndpoint: () => notificationEndpoint,
pushedAuthorizationEndpoint: () => pushedAuthorizationEndpoint,
validateRequestBody: () => validateRequestBody
});
module.exports = __toCommonJS(index_exports);
// lib/OID4VCIServer.ts
var import_oid4vci_issuer3 = require("@sphereon/oid4vci-issuer");
var import_express = __toESM(require("express"), 1);
// lib/oid4vci-api-functions.ts
var import_oid4vc_common2 = require("@sphereon/oid4vc-common");
var import_oid4vci_common2 = require("@sphereon/oid4vci-common");
var import_oid4vci_issuer2 = require("@sphereon/oid4vci-issuer");
var import_ssi_express_support2 = require("@sphereon/ssi-express-support");
var import_ssi_types = require("@sphereon/ssi-types");
// lib/IssuerTokenEndpoint.ts
var import_oid4vc_common = require("@sphereon/oid4vc-common");
var import_oid4vci_common = require("@sphereon/oid4vci-common");
var import_oid4vci_issuer = require("@sphereon/oid4vci-issuer");
var import_ssi_express_support = require("@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 !== import_oid4vci_common.GrantTypes.PRE_AUTHORIZED_CODE) {
return (0, import_ssi_express_support.sendErrorResponse)(response, 400, {
error: import_oid4vci_common.TokenErrorResponse.invalid_request,
error_description: import_oid4vci_common.PRE_AUTHORIZED_CODE_REQUIRED_ERROR
});
}
if (request.headers.authorization && request.headers.authorization.startsWith("DPoP ") && !request.headers.DPoP) {
return (0, import_ssi_express_support.sendErrorResponse)(response, 400, {
error: import_oid4vci_common.TokenErrorResponse.invalid_request,
error_description: "DPoP header is required"
});
}
let dPoPJwk;
if (dpop?.requireDPoP && !request.headers.dpop) {
return (0, import_ssi_express_support.sendErrorResponse)(response, 400, {
error: import_oid4vci_common.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 (0, import_ssi_express_support.sendErrorResponse)(response, 400, {
error: import_oid4vci_common.TokenErrorResponse.invalid_request,
error_description: "Received unsupported DPoP header."
});
}
try {
const fullUrl = accessTokenEndpoint2 ?? request.protocol + "://" + request.get("host") + request.originalUrl;
dPoPJwk = await (0, import_oid4vc_common.verifyDPoP)({
method: request.method,
headers: request.headers,
fullUrl
}, {
jwtVerifyCallback: dpop.dPoPVerifyJwtCallback,
expectAccessToken: false,
maxIatAgeInSeconds: void 0
});
} catch (error) {
return (0, import_ssi_express_support.sendErrorResponse)(response, 400, {
error: import_oid4vci_common.TokenErrorResponse.invalid_dpop_proof,
error_description: error instanceof Error ? error.message : "Unknown error"
});
}
}
try {
const responseBody = await (0, import_oid4vci_issuer.createAccessTokenResponse)(request.body, {
credentialOfferSessions: issuer.credentialOfferSessions,
accessTokenIssuer,
cNonces: issuer.cNonces,
cNonce: (0, import_oid4vc_common.uuidv4)(),
accessTokenSignerCallback,
cNonceExpiresIn,
interval,
tokenExpiresIn,
dPoPJwk
});
return response.status(200).json(responseBody);
} catch (error) {
return (0, import_ssi_express_support.sendErrorResponse)(response, 400, {
error: import_oid4vci_common.TokenErrorResponse.invalid_request
}, error);
}
};
}, "handleTokenRequest");
var verifyTokenRequest = /* @__PURE__ */ __name(({ preAuthorizedCodeExpirationDuration, issuer }) => {
return async (request, response, next) => {
try {
await (0, import_oid4vci_issuer.assertValidAccessTokenRequest)(request.body, {
expirationDuration: preAuthorizedCodeExpirationDuration,
credentialOfferSessions: issuer.credentialOfferSessions
});
} catch (error) {
if (error instanceof import_oid4vci_common.TokenError) {
return (0, import_ssi_express_support.sendErrorResponse)(response, error.statusCode, {
error: error.responseError,
error_description: error.getDescription()
});
} else {
return (0, import_ssi_express_support.sendErrorResponse)(response, 400, {
error: import_oid4vci_common.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
});
import_oid4vci_issuer2.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 (0, import_ssi_express_support2.sendErrorResponse)(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 (0, import_ssi_express_support2.sendErrorResponse)(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
});
import_oid4vci_issuer2.LOG.log(`[OID4VCI] getCredentialOfferReferenceEndpoint endpoint enabled at ${path}`);
router.get(path, async (request, response) => {
try {
const { id } = request.params;
if (!id) {
return (0, import_ssi_express_support2.sendErrorResponse)(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) {
import_oid4vci_issuer2.LOG.warning(`[OID4VCI] credential offer reference URI request with ${id}, but request was already received earlier. Session status: ${session.status}`);
}
return (0, import_ssi_express_support2.sendErrorResponse)(response, 404, {
error: "invalid_request",
error_description: `Credential offer ${id} not found`
});
}
return response.json(session.credentialOffer.credential_offer);
} catch (e) {
return (0, import_ssi_express_support2.sendErrorResponse)(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) {
import_oid4vci_issuer2.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
});
import_oid4vci_issuer2.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: import_oid4vci_common2.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: import_oid4vci_common2.AuthorizationChallengeError.invalid_session,
error_description: "Session is invalid"
};
throw authorizationChallengeErrorResponse3;
}
const authRequestURI = await opts.createAuthRequestUriCallback(issuer_state);
const authorizationChallengeErrorResponse2 = {
error: import_oid4vci_common2.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: import_oid4vci_common2.AuthorizationChallengeError.invalid_session,
error_description: "Session is invalid"
};
throw authorizationChallengeErrorResponse2;
}
const verifiedResponse = await opts.verifyAuthResponseCallback(presentation_during_issuance_session);
if (verifiedResponse) {
const authorizationCode = (0, import_oid4vci_common2.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: import_oid4vci_common2.AuthorizationChallengeError.invalid_request
};
throw authorizationChallengeErrorResponse;
} catch (e) {
return (0, import_ssi_express_support2.sendErrorResponse)(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") {
import_oid4vci_issuer2.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) {
import_oid4vci_issuer2.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 ?? (0, import_oid4vci_common2.getNumberOrUndefined)(process.env.PRE_AUTHORIZED_CODE_EXPIRATION_DURATION) ?? 300;
const interval = opts?.interval ?? (0, import_oid4vci_common2.getNumberOrUndefined)(process.env.INTERVAL) ?? 300;
const tokenExpiresIn = opts?.tokenExpiresIn ?? 300;
if (opts?.accessTokenSignerCallback === void 0) {
throw new Error(import_oid4vci_common2.JWT_SIGNER_CALLBACK_REQUIRED_ERROR);
} else if (!accessTokenIssuer) {
throw new Error(import_oid4vci_common2.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}`);
import_oid4vci_issuer2.LOG.log(`[OID4VCI] Token endpoint enabled at ${url.toString()}`);
router.post(determinePath(baseUrl, url.pathname, {
stripBasePath: true
}), verifyTokenRequest({
issuer,
preAuthorizedCodeExpirationDuration
}), 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
});
import_oid4vci_issuer2.LOG.log(`[OID4VCI] getCredential endpoint enabled at ${path}`);
router.post(path, async (request, response) => {
try {
const credentialRequest = request.body;
import_oid4vci_issuer2.LOG.log(`credential request received`, credentialRequest);
try {
const jwt = (0, import_oid4vci_common2.extractBearerToken)(request.header("Authorization"));
await (0, import_oid4vci_common2.validateJWT)(jwt, {
accessTokenVerificationCallback: opts.accessTokenVerificationCallback ?? issuer.jwtVerifyCallback
});
} catch (e) {
import_oid4vci_issuer2.LOG.warning(e);
return (0, import_ssi_express_support2.sendErrorResponse)(response, 400, {
error: "invalid_token"
});
}
const credential = await issuer.issueCredential({
credentialRequest,
tokenExpiresIn: opts.tokenExpiresIn,
cNonceExpiresIn: opts.cNonceExpiresIn
});
return response.json(credential);
} catch (e) {
return (0, import_ssi_express_support2.sendErrorResponse)(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) {
import_oid4vci_issuer2.LOG.warning('Notification endpoint disabled as no "notification_endpoint" has been configured in issuer metadata');
return;
}
const path = determinePath(baseUrl, endpoint, {
stripBasePath: true
});
import_oid4vci_issuer2.LOG.log(`[OID4VCI] notification endpoint enabled at ${path}`);
router.post(path, async (request, response) => {
try {
const notificationRequest = request.body;
import_oid4vci_issuer2.LOG.log(`notification ${notificationRequest.event}/${notificationRequest.event_description} received for ${notificationRequest.notification_id}`);
const jwt = (0, import_oid4vci_common2.extractBearerToken)(request.header("Authorization"));
import_oid4vci_common2.EVENTS.emit(import_oid4vci_common2.NotificationStatusEventNames.OID4VCI_NOTIFICATION_RECEIVED, {
eventName: import_oid4vci_common2.NotificationStatusEventNames.OID4VCI_NOTIFICATION_RECEIVED,
id: (0, import_oid4vc_common2.uuidv4)(),
data: notificationRequest,
initiator: jwt,
initiatorType: import_ssi_types.InitiatorType.EXTERNAL,
system: import_ssi_types.System.OID4VCI,
subsystem: import_ssi_types.SubSystem.API
});
try {
const jwtResult = await (0, import_oid4vci_common2.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) {
import_oid4vci_common2.EVENTS.emit(import_oid4vci_common2.NotificationStatusEventNames.OID4VCI_NOTIFICATION_ERROR, {
eventName: import_oid4vci_common2.NotificationStatusEventNames.OID4VCI_NOTIFICATION_ERROR,
id: (0, import_oid4vc_common2.uuidv4)(),
data: notificationRequest,
initiator: jwtResult.jwt,
initiatorType: import_ssi_types.InitiatorType.EXTERNAL,
system: import_ssi_types.System.OID4VCI,
subsystem: import_ssi_types.SubSystem.API
});
return (0, import_ssi_express_support2.sendErrorResponse)(response, 400, errorOrSession.message);
} else {
import_oid4vci_common2.EVENTS.emit(import_oid4vci_common2.NotificationStatusEventNames.OID4VCI_NOTIFICATION_PROCESSED, {
eventName: import_oid4vci_common2.NotificationStatusEventNames.OID4VCI_NOTIFICATION_PROCESSED,
id: (0, import_oid4vc_common2.uuidv4)(),
data: notificationRequest,
initiator: jwtResult.jwt,
initiatorType: import_ssi_types.InitiatorType.EXTERNAL,
system: import_ssi_types.System.OID4VCI,
subsystem: import_ssi_types.SubSystem.API
});
}
} catch (e) {
import_oid4vci_issuer2.LOG.warning(e);
return (0, import_ssi_express_support2.sendErrorResponse)(response, 400, {
error: "invalid_token"
});
}
return response.status(204).send();
} catch (e) {
return (0, import_ssi_express_support2.sendErrorResponse)(response, 400, {
error: "invalid_notification_request",
error_description: e.message
}, e);
}
});
}
__name(notificationEndpoint, "notificationEndpoint");
function getCredentialOfferEndpoint(router, issuer, opts) {
const path = determinePath(opts?.baseUrl, opts?.path ?? "/webapp/credential-offers/:id", {
stripBasePath: true
});
import_oid4vci_issuer2.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 (0, import_ssi_express_support2.sendErrorResponse)(response, 404, {
error: "invalid_request",
error_description: `Credential offer ${id} not found`
});
}
return response.json(session.credentialOffer.credential_offer);
} catch (e) {
return (0, import_ssi_express_support2.sendErrorResponse)(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
});
import_oid4vci_issuer2.LOG.log(`[OID4VCI] deleteCredentialOffer endpoint enabled at ${path}`);
router.delete(path, async (request, response) => {
try {
const { id } = request.params;
if (!id) {
return (0, import_ssi_express_support2.sendErrorResponse)(response, 400, {
error: "invalid_request",
error_description: "id must be present"
});
}
await issuer.deleteCredentialOfferSessionById(id);
return response.sendStatus(204);
} catch (e) {
return (0, import_ssi_express_support2.sendErrorResponse)(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
});
import_oid4vci_issuer2.LOG.log(`[OID4VCI] createCredentialOffer endpoint enabled at ${path}`);
router.post(path, async (request, response) => {
try {
const specVersion = (0, import_oid4vci_common2.determineSpecVersionFromOffer)(request.body.original_credential_offer);
if (specVersion < import_oid4vci_common2.OpenId4VCIVersion.VER_1_0_13) {
return (0, import_ssi_express_support2.sendErrorResponse)(response, 400, {
error: import_oid4vci_common2.TokenErrorResponse.invalid_client,
error_description: "credential offer request should be of spec version 1.0.13 or above"
});
}
const grantTypes = (0, import_oid4vci_common2.determineGrantTypes)(request.body);
if (grantTypes.length === 0) {
return (0, import_ssi_express_support2.sendErrorResponse)(response, 400, {
error: import_oid4vci_common2.TokenErrorResponse.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 (0, import_ssi_express_support2.sendErrorResponse)(response, 400, {
error: import_oid4vci_common2.TokenErrorResponse.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 (0, import_ssi_express_support2.sendErrorResponse)(response, 500, {
error: import_oid4vci_common2.TokenErrorResponse.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) {
import_oid4vci_issuer2.LOG.log(`[OID4VCI] External Authorization Server ${issuer.issuerMetadata.authorization_servers} is being used. Not enabling internal PAR endpoint`);
return;
} else if (opts?.enabled === false) {
import_oid4vci_issuer2.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 (0, import_ssi_express_support2.sendErrorResponse)(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 (0, import_ssi_express_support2.sendErrorResponse)(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 (0, import_ssi_express_support2.sendErrorResponse)(res, 400, {
error: "invalid_scope",
error_description: "scope is not valid for the given client"
});
}
const uuid = (0, import_oid4vc_common2.uuidv4)();
const requestUri = `urn:ietf:params:oauth:request_uri:${uuid}`;
authRequestsData.set(requestUri, req.body);
setTimeout(() => {
authRequestsData.delete(requestUri);
}, expiresIn * 1e3);
return res.status(201).json({
request_uri: requestUri,
expires_in: expiresIn
});
});
}
__name(pushedAuthorizationEndpoint, "pushedAuthorizationEndpoint");
function getMetadataEndpoints(router, issuer) {
const credentialIssuerHandler = /* @__PURE__ */ __name((request, response) => {
return response.json(issuer.issuerMetadata);
}, "credentialIssuerHandler");
router.get(import_oid4vci_common2.WellKnownEndpoints.OPENID4VCI_ISSUER, credentialIssuerHandler);
const authorizationServerHandler = /* @__PURE__ */ __name((request, response) => {
return response.json(issuer.authorizationServerMetadata);
}, "authorizationServerHandler");
router.get(import_oid4vci_common2.WellKnownEndpoints.OAUTH_AS, authorizationServerHandler);
}
__name(getMetadataEndpoints, "getMetadataEndpoints");
function determinePath(baseUrl, endpoint, opts) {
const basePath = baseUrl ? getBasePath(baseUrl) : "";
let path = endpoint;
if (opts?.prependUrl) {
path = (0, import_oid4vci_common2.adjustUrl)(path, {
prepend: opts.prependUrl
});
}
if (opts?.skipBaseUrlCheck !== true) {
assertEndpointHasIssuerBaseUrl(baseUrl, endpoint);
}
if (endpoint.includes("://")) {
path = new URL(endpoint).pathname;
}
path = `/${(0, import_oid4vci_common2.trimBoth)(path, "/")}`;
if (opts?.stripBasePath && path.startsWith(basePath)) {
path = (0, import_oid4vci_common2.trimStart)(path, basePath);
path = `/${(0, import_oid4vci_common2.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 = (0, import_ssi_express_support2.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 (0, import_oid4vci_common2.trimEnd)(baseUrl.toString(), "/");
}
__name(getBaseUrl, "getBaseUrl");
function getBasePath(url) {
const basePath = new URL(getBaseUrl(url)).pathname;
if (basePath === "" || basePath === "/") {
return "";
}
return `/${(0, import_oid4vci_common2.trimBoth)(basePath, "/")}`;
}
__name(getBasePath, "getBasePath");
// lib/OID4VCIServer.ts
function buildVCIFromEnvironment() {
const credentialsSupported = new import_oid4vci_issuer3.CredentialSupportedBuilderV1_13().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
}).addCredentialSubjectPropertyDisplay(process.env.credential_subject_display_key1, {
name: process.env.credential_subject_display_key1_name,
locale: process.env.credential_subject_display_key1_locale
}).build();
const issuerBuilder = new import_oid4vci_issuer3.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).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 OID4VCIServer = class {
static {
__name(this, "OID4VCIServer");
}
_issuer;
authRequestsData = /* @__PURE__ */ new Map();
_app;
_baseUrl;
_expressSupport;
// private readonly _server?: http.Server
_router;
_asClientOpts;
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 = import_express.default.Router();
this._issuer = opts?.issuer ? opts.issuer : buildVCIFromEnvironment();
this._asClientOpts = opts.asClientOpts || this._issuer.asClientOpts ? {
...opts.asClientOpts,
...this._issuer.asClientOpts
} : void 0;
pushedAuthorizationEndpoint(this.router, this.issuer, this.authRequestsData);
getMetadataEndpoints(this.router, this.issuer);
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 ? (0, import_oid4vci_issuer3.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
});
}
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
});
}
this._app.use(getBasePath(this.baseUrl), 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.`);
}
}
}
get baseUrl() {
return this._baseUrl;
}
};
// lib/index.ts
var import_oid4vci_common3 = require("@sphereon/oid4vci-common");
//# sourceMappingURL=index.cjs.map