@sphereon/oid4vci-issuer-server
Version:
OpenID 4 Verifiable Credential Issuance Server
1,019 lines (1,013 loc) • 42.4 kB
JavaScript
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