UNPKG

@sphereon/oid4vci-client

Version:

OpenID for Verifiable Credential Issuance (OpenID4VCI) client

1,174 lines (1,157 loc) • 133 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); // lib/index.ts import { VCI_LOGGERS as VCI_LOGGERS2 } from "@sphereon/oid4vci-common"; // lib/AccessTokenClient.ts import { createDPoP, getCreateDPoPOptions } from "@sphereon/oid4vc-common"; import { assertedUniformCredentialOffer, AuthzFlowType, convertJsonToURI, formPost, getIssuerFromCredentialOfferPayload as getIssuerFromCredentialOfferPayload2, GrantTypes, JsonURIMode, PRE_AUTH_CODE_LITERAL as PRE_AUTH_CODE_LITERAL2, PRE_AUTH_GRANT_LITERAL as PRE_AUTH_GRANT_LITERAL2, TokenErrorResponse, toUniformCredentialOfferRequest } from "@sphereon/oid4vci-common"; import { ObjectUtils } from "@sphereon/ssi-types"; // lib/functions/AuthorizationUtil.ts import { assertValidCodeVerifier, CodeChallengeMethod, createCodeChallenge, generateCodeVerifier } from "@sphereon/oid4vci-common"; var generateMissingPKCEOpts = /* @__PURE__ */ __name((pkce) => { if (pkce.disabled) { return pkce; } if (!pkce.codeChallengeMethod) { pkce.codeChallengeMethod = CodeChallengeMethod.S256; } if (!pkce.codeVerifier) { pkce.codeVerifier = generateCodeVerifier(); } assertValidCodeVerifier(pkce.codeVerifier); if (!pkce.codeChallenge) { pkce.codeChallenge = createCodeChallenge(pkce.codeVerifier, pkce.codeChallengeMethod); } return pkce; }, "generateMissingPKCEOpts"); // lib/functions/notifications.ts import { post } from "@sphereon/oid4vci-common"; // lib/types/index.ts import { VCI_LOGGERS } from "@sphereon/oid4vci-common"; import { LogMethod } from "@sphereon/ssi-types"; var LOG = VCI_LOGGERS.options("sphereon:oid4vci:client", { methods: [ LogMethod.EVENT, LogMethod.DEBUG_PKG ] }).get("sphereon:oid4vci:client"); // lib/functions/notifications.ts async function sendNotification(credentialRequestOpts, request, accessToken) { LOG.info(`Sending status notification event '${request.event}' for id ${request.notification_id}`); if (!credentialRequestOpts.notificationEndpoint) { throw Error(`Cannot send notification when no notification endpoint is provided`); } const token = accessToken ?? credentialRequestOpts.token; const response = await post(credentialRequestOpts.notificationEndpoint, JSON.stringify(request), { ...token && { bearerToken: token } }); const error = response.errorBody?.error !== void 0; const result = { error, response: error ? response.errorBody : void 0 }; if (error) { LOG.warning(`Notification endpoint returned an error for event '${request.event}' and id ${request.notification_id}: ${response.errorBody}`); } else { LOG.debug(`Notification endpoint returned success for event '${request.event}' and id ${request.notification_id}`); } return result; } __name(sendNotification, "sendNotification"); // lib/functions/OpenIDUtils.ts import { getJson } from "@sphereon/oid4vci-common"; import { Loggers } from "@sphereon/ssi-types"; var logger = Loggers.DEFAULT.get("sphereon:openid4vci:openid-utils"); var retrieveWellknown = /* @__PURE__ */ __name(async (host, endpointType, opts) => { const result = await getJson(`${host.endsWith("/") ? host.slice(0, -1) : host}${endpointType}`, { exceptionOnHttpErrorStatus: opts?.errorOnNotFound }); if (result.origResponse.status >= 400) { logger.debug(`host ${host} with endpoint type ${endpointType} status: ${result.origResponse.status}, ${result.origResponse.statusText}`); } return result; }, "retrieveWellknown"); // lib/functions/AccessTokenUtil.ts import { uuidv4 } from "@sphereon/oid4vc-common"; import { OpenId4VCIVersion } from "@sphereon/oid4vci-common"; // lib/ProofOfPossessionBuilder.ts import { createProofOfPossession, NO_JWT_PROVIDED, PROOF_CANT_BE_CONSTRUCTED } from "@sphereon/oid4vci-common"; var ProofOfPossessionBuilder = class _ProofOfPossessionBuilder { static { __name(this, "ProofOfPossessionBuilder"); } proof; callbacks; // private readonly version: OpenId4VCIVersion mode = "pop"; kid; jwk; aud; clientId; issuer; jwt; alg; jti; cNonce; typ; constructor({ proof, callbacks, jwt, accessTokenResponse, version, mode = "pop" }) { this.mode = mode; this.proof = proof; this.callbacks = callbacks; if (jwt) { this.withJwt(jwt); } else { this.withTyp(mode === "JWT" ? "JWT" : "openid4vci-proof+jwt"); } if (accessTokenResponse) { this.withAccessTokenResponse(accessTokenResponse); } } static manual({ jwt, callbacks, version, mode = "JWT" }) { return new _ProofOfPossessionBuilder({ callbacks, jwt, version, mode }); } static fromJwt({ jwt, callbacks, version, mode = "pop" }) { return new _ProofOfPossessionBuilder({ callbacks, jwt, version, mode }); } static fromAccessTokenResponse({ accessTokenResponse, callbacks, version, mode = "pop" }) { return new _ProofOfPossessionBuilder({ callbacks, accessTokenResponse, version, mode }); } static fromProof(proof, version) { return new _ProofOfPossessionBuilder({ proof, version }); } withAud(aud) { this.aud = aud; return this; } withClientId(clientId) { this.clientId = clientId; return this; } withKid(kid) { this.kid = kid; return this; } withJWK(jwk) { this.jwk = jwk; return this; } withIssuer(issuer) { this.issuer = issuer; return this; } withAlg(alg) { this.alg = alg; return this; } withJti(jti) { this.jti = jti; return this; } withTyp(typ) { if (this.mode === "pop") { if (!!typ && typ !== "openid4vci-proof+jwt") { throw Error(`typ must be openid4vci-proof+jwt for version 1.0.11 and up. Provided: ${typ}`); } } else { if (!!typ && typ !== "JWT") { throw Error(`typ must be jwt for version 1.0.10 and below. Provided: ${typ}`); } } this.typ = typ; return this; } withAccessTokenNonce(cNonce) { this.cNonce = cNonce; return this; } withAccessTokenResponse(accessToken) { if (accessToken.c_nonce) { this.withAccessTokenNonce(accessToken.c_nonce); } return this; } withEndpointMetadata(endpointMetadata) { this.withIssuer(endpointMetadata.issuer); return this; } withJwt(jwt) { if (!jwt) { throw new Error(NO_JWT_PROVIDED); } this.jwt = jwt; if (!jwt.header) { throw Error(`No JWT header present`); } else if (!jwt.payload) { throw Error(`No JWT payload present`); } if (jwt.header.kid) { this.withKid(jwt.header.kid); } if (jwt.header.typ) { this.withTyp(jwt.header.typ); } if (!this.typ) { this.withTyp("openid4vci-proof+jwt"); } this.withAlg(jwt.header.alg); if (Array.isArray(jwt.payload.aud)) { throw Error("We cannot handle multiple aud values currently"); } if (jwt.payload) { if (jwt.payload.iss) this.mode === "pop" ? this.withClientId(jwt.payload.iss) : this.withIssuer(jwt.payload.iss); if (jwt.payload.aud) this.mode === "pop" ? this.withIssuer(jwt.payload.aud) : this.withAud(jwt.payload.aud); if (jwt.payload.jti) this.withJti(jwt.payload.jti); if (jwt.payload.nonce) this.withAccessTokenNonce(jwt.payload.nonce); } return this; } async build() { if (this.proof) { return Promise.resolve(this.proof); } else if (this.callbacks) { return await createProofOfPossession(this.mode, this.callbacks, { typ: this.typ ?? (this.mode === "JWT" ? "JWT" : "openid4vci-proof+jwt"), kid: this.kid, jwk: this.jwk, jti: this.jti, alg: this.alg, aud: this.aud, issuer: this.issuer, clientId: this.clientId, ...this.cNonce && { nonce: this.cNonce } }, this.jwt); } throw new Error(PROOF_CANT_BE_CONSTRUCTED); } }; // lib/functions/AccessTokenUtil.ts var createJwtBearerClientAssertion = /* @__PURE__ */ __name(async (request, opts) => { const { asOpts, credentialIssuer } = opts; if (asOpts?.clientOpts?.clientAssertionType === "urn:ietf:params:oauth:client-assertion-type:jwt-bearer") { const { clientId = request.client_id, signCallbacks, alg } = asOpts.clientOpts; let { kid } = asOpts.clientOpts; if (!clientId) { return Promise.reject(Error(`Not client_id supplied, but client-assertion jwt-bearer requested.`)); } else if (!kid) { return Promise.reject(Error(`No kid supplied, but client-assertion jwt-bearer requested.`)); } else if (typeof signCallbacks?.signCallback !== "function") { return Promise.reject(Error(`No sign callback supplied, but client-assertion jwt-bearer requested.`)); } else if (!credentialIssuer) { return Promise.reject(Error(`No credential issuer supplied, but client-assertion jwt-bearer requested.`)); } if (clientId.startsWith("http") && kid.includes("#")) { kid = kid.split("#")[1]; } const jwt = { header: { typ: "JWT", kid, alg: alg ?? "ES256" }, payload: { iss: clientId, sub: clientId, aud: credentialIssuer, jti: uuidv4(), exp: Math.floor(Date.now()) / 1e3 + 60, iat: Math.floor(Date.now()) / 1e3 - 60 } }; const pop = await ProofOfPossessionBuilder.fromJwt({ jwt, callbacks: signCallbacks, version: opts.version ?? OpenId4VCIVersion.VER_1_0_15, mode: "JWT" }).build(); request.client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; request.client_assertion = pop.jwt; } }, "createJwtBearerClientAssertion"); // lib/functions/CredentialOfferCommons.ts import { decodeJsonProperties, getClientIdFromCredentialOfferPayload, getURIComponentsAsArray, PRE_AUTH_CODE_LITERAL, PRE_AUTH_GRANT_LITERAL } from "@sphereon/oid4vci-common"; import fetch from "cross-fetch"; function isUriEncoded(str) { const pattern = /%[0-9A-F]{2}/i; return pattern.test(str); } __name(isUriEncoded, "isUriEncoded"); async function handleCredentialOfferUri(uri) { const uriObj = getURIComponentsAsArray(uri); const credentialOfferUri = decodeURIComponent(uriObj["credential_offer_uri"]); const decodedUri = isUriEncoded(credentialOfferUri) ? decodeURIComponent(credentialOfferUri) : credentialOfferUri; const response = await fetch(decodedUri); if (!(response && response.status >= 200 && response.status < 400)) { return Promise.reject(Error(`the credential offer URI endpoint call was not successful. http code ${response.status} - reason ${response.statusText}`)); } if (response.headers.get("Content-Type")?.startsWith("application/json") === false) { return Promise.reject(Error("the credential offer URI endpoint did not return content type application/json")); } return { credential_offer: decodeJsonProperties(await response.json()) }; } __name(handleCredentialOfferUri, "handleCredentialOfferUri"); function constructBaseResponse(request, scheme, baseUrl) { const clientId = getClientIdFromCredentialOfferPayload(request.credential_offer); const grants = request.credential_offer?.grants; return { scheme, baseUrl, ...clientId && { clientId }, ...request, ...grants?.authorization_code?.issuer_state && { issuerState: grants.authorization_code.issuer_state }, ...grants?.[PRE_AUTH_GRANT_LITERAL]?.[PRE_AUTH_CODE_LITERAL] && { preAuthorizedCode: grants[PRE_AUTH_GRANT_LITERAL][PRE_AUTH_CODE_LITERAL] }, ...request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code && { txCode: request.credential_offer.grants[PRE_AUTH_GRANT_LITERAL].tx_code } }; } __name(constructBaseResponse, "constructBaseResponse"); // lib/functions/dpopUtil.ts import { dpopTokenRequestNonceError } from "@sphereon/oid4vc-common"; function shouldRetryTokenRequestWithDPoPNonce(response) { if (!response.errorBody || response.errorBody.error !== dpopTokenRequestNonceError) { return { ok: false }; } const dPoPNonce = response.origResponse.headers.get("DPoP-Nonce"); if (!dPoPNonce) { throw new Error("Missing required DPoP-Nonce header."); } return { ok: true, dpopNonce: dPoPNonce }; } __name(shouldRetryTokenRequestWithDPoPNonce, "shouldRetryTokenRequestWithDPoPNonce"); function shouldRetryResourceRequestWithDPoPNonce(response) { if (!response.errorBody || response.origResponse.status !== 401) { return { ok: false }; } const wwwAuthenticateHeader = response.origResponse.headers.get("WWW-Authenticate"); if (!wwwAuthenticateHeader?.includes(dpopTokenRequestNonceError)) { return { ok: false }; } const dPoPNonce = response.origResponse.headers.get("DPoP-Nonce"); if (!dPoPNonce) { throw new Error("Missing required DPoP-Nonce header."); } return { ok: true, dpopNonce: dPoPNonce }; } __name(shouldRetryResourceRequestWithDPoPNonce, "shouldRetryResourceRequestWithDPoPNonce"); // lib/MetadataClientV1_0_15.ts import { getIssuerFromCredentialOfferPayload, WellKnownEndpoints } from "@sphereon/oid4vci-common"; import { Loggers as Loggers2 } from "@sphereon/ssi-types"; var logger2 = Loggers2.DEFAULT.get("sphereon:oid4vci:metadata"); var MetadataClientV1_0_15 = class _MetadataClientV1_0_15 { static { __name(this, "MetadataClientV1_0_15"); } /** * Retrieve metadata using the Initiation obtained from a previous step * * @param credentialOffer */ static async retrieveAllMetadataFromCredentialOffer(credentialOffer) { return _MetadataClientV1_0_15.retrieveAllMetadataFromCredentialOfferRequest(credentialOffer.credential_offer); } /** * Retrieve the metada using the initiation request obtained from a previous step * @param request */ static async retrieveAllMetadataFromCredentialOfferRequest(request) { const issuer = getIssuerFromCredentialOfferPayload(request); if (issuer) { return _MetadataClientV1_0_15.retrieveAllMetadata(issuer); } throw new Error("can't retrieve metadata from CredentialOfferRequest. No issuer field is present"); } /** * Retrieve all metadata from an issuer * @param issuer The issuer URL * @param opts */ static async retrieveAllMetadata(issuer, opts) { let token_endpoint; let credential_endpoint; let nonce_endpoint; let deferred_credential_endpoint; let notification_endpoint; let authorization_endpoint; let authorization_challenge_endpoint; let authorizationServerType = "OID4VCI"; let authorization_servers = [ issuer ]; const oid4vciResponse = await _MetadataClientV1_0_15.retrieveOpenID4VCIServerMetadata(issuer, { errorOnNotFound: false }); let credentialIssuerMetadata = oid4vciResponse?.successBody; if (credentialIssuerMetadata) { logger2.debug(`Issuer ${issuer} OID4VCI well-known server metadata\r ${JSON.stringify(credentialIssuerMetadata)}`); credential_endpoint = credentialIssuerMetadata.credential_endpoint; nonce_endpoint = credentialIssuerMetadata.nonce_endpoint; deferred_credential_endpoint = credentialIssuerMetadata.deferred_credential_endpoint; notification_endpoint = credentialIssuerMetadata.notification_endpoint; if (credentialIssuerMetadata.token_endpoint) { token_endpoint = credentialIssuerMetadata.token_endpoint; } authorization_challenge_endpoint = credentialIssuerMetadata.authorization_challenge_endpoint; if (credentialIssuerMetadata.authorization_servers) { authorization_servers = credentialIssuerMetadata.authorization_servers; } } let response = await retrieveWellknown(authorization_servers[0], WellKnownEndpoints.OPENID_CONFIGURATION, { errorOnNotFound: false }); let authMetadata = response.successBody; if (authMetadata) { logger2.debug(`Issuer ${issuer} has OpenID Connect Server metadata in well-known location`); authorizationServerType = "OIDC"; } else { response = await retrieveWellknown(authorization_servers[0], WellKnownEndpoints.OAUTH_AS, { errorOnNotFound: false }); authMetadata = response.successBody; } if (!authMetadata) { if (!authorization_servers.includes(issuer)) { throw Error(`Issuer ${issuer} provided a separate authorization server ${authorization_servers}, but that server did not provide metadata`); } } else { logger2.debug(`Issuer ${issuer} has ${authorizationServerType} Server metadata in well-known location`); if (!authMetadata.authorization_endpoint) { console.warn(`Issuer ${issuer} of type ${authorizationServerType} has no authorization_endpoint! Will use ${authorization_endpoint}. This only works for pre-authorized flows`); } else if (authorization_endpoint && authMetadata.authorization_endpoint !== authorization_endpoint) { throw Error(`Credential issuer has a different authorization_endpoint (${authorization_endpoint}) from the Authorization Server (${authMetadata.authorization_endpoint})`); } authorization_endpoint = authMetadata.authorization_endpoint; if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) { throw Error(`Credential issuer has a different authorization_challenge_endpoint (${authorization_challenge_endpoint}) from the Authorization Server (${authMetadata.authorization_challenge_endpoint})`); } authorization_challenge_endpoint = authMetadata.authorization_challenge_endpoint; if (!authMetadata.token_endpoint) { throw Error(`Authorization Server ${authorization_servers} did not provide a token_endpoint`); } else if (token_endpoint && authMetadata.token_endpoint !== token_endpoint) { throw Error(`Credential issuer has a different token_endpoint (${token_endpoint}) from the Authorization Server (${authMetadata.token_endpoint})`); } token_endpoint = authMetadata.token_endpoint; if (authMetadata.credential_endpoint) { if (credential_endpoint && authMetadata.credential_endpoint !== credential_endpoint) { logger2.debug(`Credential issuer has a different credential_endpoint (${credential_endpoint}) from the Authorization Server (${authMetadata.credential_endpoint}). Will use the issuer value`); } else { credential_endpoint = authMetadata.credential_endpoint; } } if (authMetadata.deferred_credential_endpoint) { if (deferred_credential_endpoint && authMetadata.deferred_credential_endpoint !== deferred_credential_endpoint) { logger2.debug(`Credential issuer has a different deferred_credential_endpoint (${deferred_credential_endpoint}) from the Authorization Server (${authMetadata.deferred_credential_endpoint}). Will use the issuer value`); } else { deferred_credential_endpoint = authMetadata.deferred_credential_endpoint; } } if (authMetadata.notification_endpoint) { if (notification_endpoint && authMetadata.notification_endpoint !== notification_endpoint) { logger2.debug(`Credential issuer has a different notification_endpoint (${notification_endpoint}) from the Authorization Server (${authMetadata.notification_endpoint}). Will use the issuer value`); } else { notification_endpoint = authMetadata.notification_endpoint; } } } if (!authorization_endpoint) { logger2.debug(`Issuer ${issuer} does not expose authorization_endpoint, so only pre-auth will be supported`); } if (!token_endpoint) { logger2.debug(`Issuer ${issuer} does not have a token_endpoint listed in well-known locations!`); if (opts?.errorOnNotFound) { throw Error(`Could not deduce the token_endpoint for ${issuer}`); } else { token_endpoint = `${issuer}${issuer.endsWith("/") ? "token" : "/token"}`; } } if (!credential_endpoint) { logger2.debug(`Issuer ${issuer} does not have a credential_endpoint listed in well-known locations!`); if (opts?.errorOnNotFound) { throw Error(`Could not deduce the credential endpoint for ${issuer}`); } else { credential_endpoint = `${issuer}${issuer.endsWith("/") ? "credential" : "/credential"}`; } } if (!credentialIssuerMetadata && authMetadata) { credentialIssuerMetadata = authMetadata; } const ci = credentialIssuerMetadata ?? {}; const ciAuthorizationServers = Array.isArray(ci.authorization_servers) && ci.authorization_servers.length > 0 ? ci.authorization_servers : authorization_servers; const v15CredentialIssuerMetadata = { credential_issuer: ci.credential_issuer ?? issuer, credential_endpoint, authorization_servers: ciAuthorizationServers, credential_configurations_supported: ci.credential_configurations_supported ?? {}, display: ci.display ?? [], ...nonce_endpoint && { nonce_endpoint }, ...deferred_credential_endpoint && { deferred_credential_endpoint }, ...notification_endpoint && { notification_endpoint } }; logger2.debug(`Issuer ${issuer} token endpoint ${token_endpoint}, credential endpoint ${credential_endpoint}`); return { issuer, token_endpoint, credential_endpoint, authorization_challenge_endpoint, notification_endpoint, authorizationServerType, credentialIssuerMetadata: v15CredentialIssuerMetadata, authorizationServerMetadata: authMetadata }; } /** * Retrieve only the OID4VCI metadata for the issuer. So no OIDC/OAuth2 metadata * * @param issuerHost The issuer hostname * @param opts */ static async retrieveOpenID4VCIServerMetadata(issuerHost, opts) { return retrieveWellknown(issuerHost, WellKnownEndpoints.OPENID4VCI_ISSUER, { errorOnNotFound: opts?.errorOnNotFound === void 0 ? true : opts.errorOnNotFound }); } }; // lib/AccessTokenClient.ts var AccessTokenClient = class _AccessTokenClient { static { __name(this, "AccessTokenClient"); } async acquireAccessToken(opts) { const { asOpts, pin, codeVerifier, code, redirectUri, metadata, createDPoPOpts } = opts; const credentialOffer = opts.credentialOffer ? await assertedUniformCredentialOffer(opts.credentialOffer) : void 0; const pinMetadata = credentialOffer && this.getPinMetadata(credentialOffer.credential_offer); const issuer = opts.credentialIssuer ?? (credentialOffer ? getIssuerFromCredentialOfferPayload2(credentialOffer.credential_offer) : metadata?.issuer); if (!issuer) { throw Error("Issuer required at this point"); } const issuerOpts = { issuer }; return await this.acquireAccessTokenUsingRequest({ accessTokenRequest: await this.createAccessTokenRequest({ credentialOffer, asOpts, codeVerifier, code, redirectUri, pin, credentialIssuer: issuer, metadata, additionalParams: opts.additionalParams, pinMetadata }), pinMetadata, metadata, asOpts, issuerOpts, createDPoPOpts }); } async acquireAccessTokenUsingRequest({ accessTokenRequest, pinMetadata, metadata, asOpts, issuerOpts, createDPoPOpts }) { this.validate(accessTokenRequest, pinMetadata); const requestTokenURL = _AccessTokenClient.determineTokenURL({ asOpts, issuerOpts, metadata: metadata ? metadata : issuerOpts?.fetchMetadata ? await MetadataClientV1_0_15.retrieveAllMetadata(issuerOpts.issuer, { errorOnNotFound: false }) : void 0 }); const useDpop = createDPoPOpts?.dPoPSigningAlgValuesSupported && createDPoPOpts.dPoPSigningAlgValuesSupported.length > 0; let dPoP = useDpop ? await createDPoP(getCreateDPoPOptions(createDPoPOpts, requestTokenURL)) : void 0; let response = await this.sendAuthCode(requestTokenURL, accessTokenRequest, dPoP ? { headers: { dpop: dPoP } } : void 0); let nextDPoPNonce = createDPoPOpts?.jwtPayloadProps.nonce; const retryWithNonce = shouldRetryTokenRequestWithDPoPNonce(response); if (retryWithNonce.ok && createDPoPOpts) { createDPoPOpts.jwtPayloadProps.nonce = retryWithNonce.dpopNonce; dPoP = await createDPoP(getCreateDPoPOptions(createDPoPOpts, requestTokenURL)); response = await this.sendAuthCode(requestTokenURL, accessTokenRequest, dPoP ? { headers: { dpop: dPoP } } : void 0); const successDPoPNonce = response.origResponse.headers.get("DPoP-Nonce"); nextDPoPNonce = successDPoPNonce ?? retryWithNonce.dpopNonce; } if (response.successBody && createDPoPOpts && response.successBody.token_type !== "DPoP") { throw new Error("Invalid token type returned. Expected DPoP. Received: " + response.successBody.token_type); } return { ...response, ...nextDPoPNonce && { params: { dpop: { dpopNonce: nextDPoPNonce } } } }; } async createAccessTokenRequest(opts) { const { asOpts, pin, codeVerifier, code, redirectUri } = opts; const credentialOfferRequest = opts.credentialOffer ? await toUniformCredentialOfferRequest(opts.credentialOffer) : void 0; const request = { ...opts.additionalParams }; if (asOpts?.clientOpts?.clientId) { request.client_id = asOpts.clientOpts.clientId; } const credentialIssuer = opts.credentialIssuer ?? credentialOfferRequest?.credential_offer?.credential_issuer ?? opts.metadata?.issuer; await createJwtBearerClientAssertion(request, { ...opts, credentialIssuer }); if (!credentialOfferRequest || credentialOfferRequest.supportedFlows.includes(AuthzFlowType.AUTHORIZATION_CODE_FLOW)) { request.grant_type = GrantTypes.AUTHORIZATION_CODE; request.code = code; request.redirect_uri = redirectUri; if (codeVerifier) { request.code_verifier = codeVerifier; } return request; } if (credentialOfferRequest?.supportedFlows.includes(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW)) { this.assertAlphanumericPin(opts.pinMetadata, pin); request.user_pin = pin; request.tx_code = pin; request.grant_type = GrantTypes.PRE_AUTHORIZED_CODE; request[PRE_AUTH_CODE_LITERAL2] = credentialOfferRequest?.credential_offer.grants?.[PRE_AUTH_GRANT_LITERAL2]?.[PRE_AUTH_CODE_LITERAL2]; return request; } throw new Error("Credential offer request follows neither pre-authorized code nor authorization code flow requirements."); } assertPreAuthorizedGrantType(grantType) { if (GrantTypes.PRE_AUTHORIZED_CODE !== grantType) { throw new Error("grant type must be 'urn:ietf:params:oauth:grant-type:pre-authorized_code'"); } } assertAuthorizationGrantType(grantType) { if (GrantTypes.AUTHORIZATION_CODE !== grantType) { throw new Error("grant type must be 'authorization_code'"); } } getPinMetadata(requestPayload) { if (!requestPayload) { throw new Error(TokenErrorResponse.invalid_request); } const issuer = getIssuerFromCredentialOfferPayload2(requestPayload); const grantDetails = requestPayload.grants?.[PRE_AUTH_GRANT_LITERAL2]; const isPinRequired = !!(grantDetails?.tx_code ?? false); LOG.warning(`Pin required for issuer ${issuer}: ${isPinRequired}`); return { txCode: grantDetails?.tx_code, isPinRequired }; } assertAlphanumericPin(pinMeta, pin) { if (pinMeta && pinMeta.isPinRequired) { let regex; if (pinMeta.txCode) { const { input_mode, length } = pinMeta.txCode; if (input_mode === "numeric") { regex = length ? new RegExp(`^\\d{1,${length}}$`) : /^\d+$/; } else if (input_mode === "text") { regex = length ? new RegExp(`^[a-zA-Z0-9]{1,${length}}$`) : /^[a-zA-Z0-9]+$/; } } regex = regex || /^[a-zA-Z0-9]+$|^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/; if (!pin || !regex.test(pin)) { LOG.warning(`Pin is not valid. Expected format: ${pinMeta?.txCode?.input_mode || "alphanumeric"}, Length: up to ${pinMeta?.txCode?.length || "any number of"} characters`); throw new Error("A valid pin must be present according to the specified transaction code requirements."); } } else if (pin) { LOG.warning("Pin set, whilst not required"); throw new Error("Cannot set a pin when the pin is not required."); } } assertNonEmptyPreAuthorizedCode(accessTokenRequest) { if (!accessTokenRequest[PRE_AUTH_CODE_LITERAL2]) { LOG.warning(`No pre-authorized code present, whilst it is required`, accessTokenRequest); throw new Error("Pre-authorization must be proven by presenting the pre-authorized code. Code must be present."); } } assertNonEmptyCodeVerifier(accessTokenRequest) { if (!accessTokenRequest.code_verifier) { LOG.warning("No code_verifier present, whilst it is required", accessTokenRequest); throw new Error("Authorization flow requires the code_verifier to be present"); } } assertNonEmptyCode(accessTokenRequest) { if (!accessTokenRequest.code) { LOG.warning("No code present, whilst it is required"); throw new Error("Authorization flow requires the code to be present"); } } validate(accessTokenRequest, pinMeta) { if (accessTokenRequest.grant_type === GrantTypes.PRE_AUTHORIZED_CODE) { this.assertPreAuthorizedGrantType(accessTokenRequest.grant_type); this.assertNonEmptyPreAuthorizedCode(accessTokenRequest); this.assertAlphanumericPin(pinMeta, accessTokenRequest.tx_code ?? accessTokenRequest.user_pin); } else if (accessTokenRequest.grant_type === GrantTypes.AUTHORIZATION_CODE) { this.assertAuthorizationGrantType(accessTokenRequest.grant_type); this.assertNonEmptyCodeVerifier(accessTokenRequest); this.assertNonEmptyCode(accessTokenRequest); } else { this.throwNotSupportedFlow(); } } async sendAuthCode(requestTokenURL, accessTokenRequest, opts) { return await formPost(requestTokenURL, convertJsonToURI(accessTokenRequest, { mode: JsonURIMode.X_FORM_WWW_URLENCODED }), { customHeaders: opts?.headers ? opts.headers : void 0 }); } static determineTokenURL({ asOpts, issuerOpts, metadata }) { if (!asOpts && !metadata?.token_endpoint && !issuerOpts) { throw new Error("Cannot determine token URL if no issuer, metadata and no Authorization Server values are present"); } let url; if (asOpts && asOpts.as) { url = this.creatTokenURLFromURL(asOpts.as, asOpts?.allowInsecureEndpoints, asOpts.tokenEndpoint); } else if (metadata?.token_endpoint) { url = metadata.token_endpoint; } else { if (!issuerOpts?.issuer) { throw Error("Either authorization server options, a token endpoint or issuer options are required at this point"); } url = this.creatTokenURLFromURL(issuerOpts.issuer, asOpts?.allowInsecureEndpoints, issuerOpts.tokenEndpoint); } if (!url || !ObjectUtils.isString(url)) { throw new Error("No authorization server token URL present. Cannot acquire access token"); } LOG.debug(`Token endpoint determined to be ${url}`); return url; } static creatTokenURLFromURL(url, allowInsecureEndpoints, tokenEndpoint) { if (allowInsecureEndpoints !== true && url.startsWith("http:")) { throw Error(`Unprotected token endpoints are not allowed ${url}. Use the 'allowInsecureEndpoints' param if you really need this for dev/testing!`); } const hostname = url.replace(/https?:\/\//, "").replace(/\/$/, ""); const endpoint = tokenEndpoint ? tokenEndpoint.startsWith("/") ? tokenEndpoint : tokenEndpoint.substring(1) : "/token"; const scheme = url.split("://")[0]; return `${scheme ? scheme + "://" : "https://"}${hostname}${endpoint}`; } throwNotSupportedFlow() { LOG.warning(`Only pre-authorized or authorization code flows supported.`); throw new Error("Only pre-authorized-code or authorization code flows are supported"); } }; // lib/AuthorizationCodeClient.ts import { CodeChallengeMethod as CodeChallengeMethod2, convertJsonToURI as convertJsonToURI2, CreateRequestObjectMode, determineSpecVersionFromOffer as determineSpecVersionFromOffer2, formPost as formPost2, isW3cCredentialSupported, JsonURIMode as JsonURIMode2, OpenId4VCIVersion as OpenId4VCIVersion3, PARMode, ResponseType } from "@sphereon/oid4vci-common"; import { Loggers as Loggers4 } from "@sphereon/ssi-types"; // lib/MetadataClient.ts import { determineSpecVersionFromOffer, getIssuerFromCredentialOfferPayload as getIssuerFromCredentialOfferPayload3, OpenId4VCIVersion as OpenId4VCIVersion2, WellKnownEndpoints as WellKnownEndpoints2 } from "@sphereon/oid4vci-common"; import { Loggers as Loggers3 } from "@sphereon/ssi-types"; var logger3 = Loggers3.DEFAULT.get("sphereon:oid4vci:metadata"); var MetadataClient = class _MetadataClient { static { __name(this, "MetadataClient"); } /** * Retrieve metadata using the Initiation obtained from a previous step * * @param credentialOffer */ static async retrieveAllMetadataFromCredentialOffer(credentialOffer) { const openId4VCIVersion = determineSpecVersionFromOffer(credentialOffer.credential_offer); if (openId4VCIVersion >= OpenId4VCIVersion2.VER_1_0_15) { return await MetadataClientV1_0_15.retrieveAllMetadataFromCredentialOffer(credentialOffer); } return Promise.reject(Error(`OpenId4VCIVersion ${openId4VCIVersion} is not supported in retrieveAllMetadataFromCredentialOffer`)); } /** * Retrieve the metada using the initiation request obtained from a previous step * @param request */ static async retrieveAllMetadataFromCredentialOfferRequest(request) { const issuer = getIssuerFromCredentialOfferPayload3(request); if (issuer) { const openId4VCIVersion = determineSpecVersionFromOffer(request); if (openId4VCIVersion >= OpenId4VCIVersion2.VER_1_0_15) { return MetadataClientV1_0_15.retrieveAllMetadataFromCredentialOfferRequest(request); } else { return Promise.reject(Error(`OpenId4VCIVersion ${openId4VCIVersion} is not supported in retrieveAllMetadataFromCredentialOfferRequest`)); } } throw new Error("can't retrieve metadata from CredentialOfferRequest. No issuer field is present"); } /** * Retrieve all metadata from an issuer * @param issuer The issuer URL * @param opts */ static async retrieveAllMetadata(issuer, opts) { let token_endpoint; let credential_endpoint; let deferred_credential_endpoint; let authorization_endpoint; let authorization_challenge_endpoint; let authorizationServerType = "OID4VCI"; let authorization_servers = [ issuer ]; let authorization_server = void 0; const oid4vciResponse = await _MetadataClient.retrieveOpenID4VCIServerMetadata(issuer, { errorOnNotFound: false }); let credentialIssuerMetadata = oid4vciResponse?.successBody; if (credentialIssuerMetadata) { logger3.debug(`Issuer ${issuer} OID4VCI well-known server metadata\r ${JSON.stringify(credentialIssuerMetadata)}`); credential_endpoint = credentialIssuerMetadata.credential_endpoint; deferred_credential_endpoint = credentialIssuerMetadata.deferred_credential_endpoint ? credentialIssuerMetadata.deferred_credential_endpoint : void 0; if (credentialIssuerMetadata.token_endpoint) { token_endpoint = credentialIssuerMetadata.token_endpoint; } authorization_challenge_endpoint = credentialIssuerMetadata.authorization_challenge_endpoint; if (credentialIssuerMetadata.authorization_servers) { authorization_servers = credentialIssuerMetadata.authorization_servers; } else if (credentialIssuerMetadata.authorization_server) { authorization_server = credentialIssuerMetadata.authorization_server; authorization_servers = [ authorization_server ]; } } else { throw new Error(`Issuer ${issuer} does not expose /.well-known/openid-credential-issuer`); } let response = await retrieveWellknown(authorization_servers[0], WellKnownEndpoints2.OPENID_CONFIGURATION, { errorOnNotFound: false }); let authMetadata = response.successBody; if (authMetadata) { logger3.debug(`Issuer ${issuer} has OpenID Connect Server metadata in well-known location`); authorizationServerType = "OIDC"; } else { response = await retrieveWellknown(authorization_servers[0], WellKnownEndpoints2.OAUTH_AS, { errorOnNotFound: false }); authMetadata = response.successBody; } if (!authMetadata) { if (!authorization_servers.includes(issuer)) { throw Error(`Issuer ${issuer} provided a separate authorization server ${authorization_servers}, but that server did not provide metadata`); } } else { if (!authorizationServerType) { authorizationServerType = "OAuth 2.0"; } logger3.debug(`Issuer ${issuer} has ${authorizationServerType} Server metadata in well-known location`); if (!authMetadata.authorization_endpoint) { console.warn(`Issuer ${issuer} of type ${authorizationServerType} has no authorization_endpoint! Will use ${authorization_endpoint}. This only works for pre-authorized flows`); } else if (authorization_endpoint && authMetadata.authorization_endpoint !== authorization_endpoint) { throw Error(`Credential issuer has a different authorization_endpoint (${authorization_endpoint}) from the Authorization Server (${authMetadata.authorization_endpoint})`); } authorization_endpoint = authMetadata.authorization_endpoint; if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) { throw Error(`Credential issuer has a different authorization_challenge_endpoint (${authorization_challenge_endpoint}) from the Authorization Server (${authMetadata.authorization_challenge_endpoint})`); } authorization_challenge_endpoint = authMetadata.authorization_challenge_endpoint; if (!authMetadata.token_endpoint) { throw Error(`Authorization Server ${authorization_servers} did not provide a token_endpoint`); } else if (token_endpoint && authMetadata.token_endpoint !== token_endpoint) { throw Error(`Credential issuer has a different token_endpoint (${token_endpoint}) from the Authorization Server (${authMetadata.token_endpoint})`); } token_endpoint = authMetadata.token_endpoint; if (authMetadata.credential_endpoint) { if (credential_endpoint && authMetadata.credential_endpoint !== credential_endpoint) { logger3.debug(`Credential issuer has a different credential_endpoint (${credential_endpoint}) from the Authorization Server (${authMetadata.credential_endpoint}). Will use the issuer value`); } else { credential_endpoint = authMetadata.credential_endpoint; } } if (authMetadata.deferred_credential_endpoint) { if (deferred_credential_endpoint && authMetadata.deferred_credential_endpoint !== deferred_credential_endpoint) { logger3.debug(`Credential issuer has a different deferred_credential_endpoint (${deferred_credential_endpoint}) from the Authorization Server (${authMetadata.deferred_credential_endpoint}). Will use the issuer value`); } else { deferred_credential_endpoint = authMetadata.deferred_credential_endpoint; } } } if (!authorization_endpoint) { logger3.debug(`Issuer ${issuer} does not expose authorization_endpoint, so only pre-auth will be supported`); } if (!token_endpoint) { logger3.debug(`Issuer ${issuer} does not have a token_endpoint listed in well-known locations!`); if (opts?.errorOnNotFound) { throw Error(`Could not deduce the token_endpoint for ${issuer}`); } else { token_endpoint = `${issuer}${issuer.endsWith("/") ? "token" : "/token"}`; } } if (!credential_endpoint) { logger3.debug(`Issuer ${issuer} does not have a credential_endpoint listed in well-known locations!`); if (opts?.errorOnNotFound) { throw Error(`Could not deduce the credential endpoint for ${issuer}`); } else { credential_endpoint = `${issuer}${issuer.endsWith("/") ? "credential" : "/credential"}`; } } if (!credentialIssuerMetadata && authMetadata) { return Promise.reject(Error(`No /.well-known/openid-credential-issuer at ${issuer}.`)); } logger3.debug(`Issuer ${issuer} token endpoint ${token_endpoint}, credential endpoint ${credential_endpoint}`); return { issuer, token_endpoint, credential_endpoint, deferred_credential_endpoint, nonce_endpoint: credentialIssuerMetadata.nonce_endpoint, authorization_servers: authorization_server ? [ authorization_server ] : authorization_servers ?? [ issuer ], authorization_endpoint, authorization_challenge_endpoint, authorizationServerType, credentialIssuerMetadata, authorizationServerMetadata: authMetadata }; } /** * Retrieve only the OID4VCI metadata for the issuer. So no OIDC/OAuth2 metadata * * @param issuerHost The issuer hostname * @param opts */ static async retrieveOpenID4VCIServerMetadata(issuerHost, opts) { return retrieveWellknown(issuerHost, WellKnownEndpoints2.OPENID4VCI_ISSUER, { errorOnNotFound: opts?.errorOnNotFound === void 0 ? true : opts.errorOnNotFound }); } }; // lib/AuthorizationCodeClient.ts var logger4 = Loggers4.DEFAULT.get("sphereon:oid4vci"); async function createSignedAuthRequestWhenNeeded(requestObject, opts) { if (opts.requestObjectMode === CreateRequestObjectMode.REQUEST_URI) { throw Error(`Request Object Mode ${opts.requestObjectMode} is not supported yet`); } else if (opts.requestObjectMode === CreateRequestObjectMode.REQUEST_OBJECT) { if (typeof opts.signCallbacks?.signCallback !== "function") { throw Error(`No request object sign callback found, whilst request object mode was set to ${opts.requestObjectMode}`); } else if (!opts.kid) { throw Error(`No kid found, whilst request object mode was set to ${opts.requestObjectMode}`); } let client_metadata; if (opts.clientMetadata || opts.jwksUri) { client_metadata = opts.clientMetadata ?? {}; if (opts.jwksUri) { client_metadata["jwks_uri"] = opts.jwksUri; } } let authorization_details = requestObject["authorization_details"]; if (typeof authorization_details === "string") { authorization_details = JSON.parse(requestObject.authorization_details); } if (!requestObject.aud && opts.aud) { requestObject.aud = opts.aud; } const iss = requestObject.iss ?? opts.iss ?? requestObject.client_id; const jwt = { header: { alg: "ES256", kid: opts.kid, typ: "JWT" }, payload: { ...requestObject, iss, authorization_details, ...client_metadata && { client_metadata } } }; const pop = await ProofOfPossessionBuilder.fromJwt({ jwt, callbacks: opts.signCallbacks, version: OpenId4VCIVersion3.VER_1_0_15, mode: "JWT" }).build(); requestObject["request"] = pop.jwt; } } __name(createSignedAuthRequestWhenNeeded, "createSignedAuthRequestWhenNeeded"); function filterSupportedCredentials(credentialOffer, credentialsSupported) { if (!credentialOffer.credential_configuration_ids || !credentialsSupported) { return []; } return Object.entries(credentialsSupported).filter((entry) => credentialOffer.credential_configuration_ids?.includes(entry[0])).map((entry) => { return { ...entry[1], configuration_id: entry[0] }; }); } __name(filterSupportedCredentials, "filterSupportedCredentials"); var createAuthorizationRequestUrl = /* @__PURE__ */ __name(async ({ pkce, endpointMetadata, authorizationRequest, credentialOffer, credentialConfigurationSupported, clientId, version }) => { function removeDisplayAndValueTypes(obj) { if (Array.isArray(obj)) { return obj.map((item) => removeDisplayAndValueTypes(item)); } if (typeof obj !== "object" || obj === null) { return obj; } const newObj = { ...obj }; for (const prop in newObj) { if ([ "display", "value_type" ].includes(prop)) { delete newObj[prop]; } else if (typeof newObj[prop] === "object" && newObj[prop] !== null) { newObj[prop] = removeDisplayAndValueTypes(newObj[prop]); } } return newObj; } __name(removeDisplayAndValueTypes, "removeDisplayAndValueTypes"); const { redirectUri, requestObjectOpts = { requestObjectMode: CreateRequestObjectMode.NONE } } = authorizationRequest; const client_id = clientId ?? authorizationRequest.clientId; const authorizationMetadata = endpointMetadata.authorizationServerMetadata ?? endpointMetadata.credentialIssuerMetadata; let { authorizationDetails } = authorizationRequest; const parMode = authorizationMetadata?.require_pushed_authorization_requests ? PARMode.REQUIRE : authorizationRequest.parMode ?? (client_id ? PARMode.AUTO : PARMode.NEVER); if (!authorizationRequest.scope && !authorizationDetails) { if (!credentialOffer) { throw Error("Please provide a scope or authorization_details if no credential offer is present"); } if ("credentials" in credentialOffer.credential_offer) { throw new Error("CredentialOffer format is wrong."); } const ver = version ?? determineSpecVersionFromOffer2(credentialOffer.credential_offer) ?? OpenId4VCIVersion3.VER_1_0_15; const creds = ver === OpenId4VCIVersion3.VER_1_0_15 ? filterSupportedCredentials(credentialOffer.credential_offer, credentialConfigurationSupported) : []; authorizationDetails = creds.flatMap((cred) => { const locations = [ credentialOffer?.credential_offer.credential_issuer ?? endpointMetadata.issuer ]; const credential_configuration_id = cred.configuration_id; const format = credential_configuration_id ? void 0 : cred.format; if (!credential_configuration_id && !cred.format) { throw Error("format is required in authorization details"); } const vct = cred.format === "dc+sd-jwt" ? cred.vct : void 0; const doctype = cred.format === "mso_mdoc" ? cred.doctype : void 0; let credential_definition = void 0; if (isW3cCredentialSupported(cred) && hasCredentialDefinition(cred)) { credential_definition = { ...cred.credential_definition, // type: OPTIONAL. Array as defined in Appendix A.1.1.2. This claim contains the type values the Wallet requests authorization for at the Credential Issuer. It MUST be present if the claim format is present in the root of the authorization details object. It MUST not be present otherwise. // It meens we have a config_id, already mapping it to an explicit format and types type: format ? cred.credential_definition.type : void 0, credentialSubject: cred.credential_definition.credentialSubject ? removeDisplayAndValueTypes(cred.credential_definition.credentialSubject) : void 0 }; } return { type: "openid_credential", locations, ...credential_definition && { credential_definition }, ...credential_configuration_id && { credential_configuration_id }, ...format && { format }, ...vct && { vct, claims: cred.claims ? removeDisplayAndValueTypes(cred.claims) : void 0 }, ...doctype && { doctype, claims: cred.claims ? removeDisplayAndValueTypes(cred.claims) : void 0 } }; }); if (!authorizationDetails || authorizationDetails.length === 0) { throw Error(`Could not create authorization details from credential offer. Please pass in explicit details`); } } const authorizationEndpoint = endpointMetadata.authorization_endpoint ?? endpointMetadata.authorizationServerMetadata?.authorization_endpoint ?? endpointMetadata.credentialIssuerMetadata?.authorization_endpoint; if (!authorizationEndpoint) { throw Error("Server metadata does not contain authorization endpoint"); } const parEndpoint = authorizationMetadata?.pushed_authorization_request_endpoint; let queryObj = { response_type: ResponseType.AUTH_CODE, ...!pkce.disabled && { code_challenge_method: pkce.codeChallengeMethod ?? CodeChallengeMethod2.S256, code_challenge: pkce.codeChallenge }, authorization_details: JSON.stringify(handleAuthorizationDetails(endpointMetadata, authorizationDetails)), ...redirectUri && { redirect_uri: redirectUri }, ...client_id && { client_id }, ...credentialOffer?.issuerState && { issuer_state: credentialOffer.issuerState }, scope: authorizationRequest.scope ?? "openid" }; if (credentialOffer?.issuerState) { queryObj.state = credentialOffer?.issuerState; } if (!parEndpoint && parMode === PARMode.REQUIRE) { throw Error(`PAR mode is set to required by Authorization Server does not support PAR!`); } else if (parEndpoint && parMode !== PARMode.NEVER) { logger4.debug(`USING PAR with endpoint ${parEndpoint}`); const parBody = convertJsonToURI2(queryObj, { mode: JsonURIMode2.X_FORM_WWW_URLENCODE