UNPKG

@sphereon/oid4vci-common

Version:

OpenID 4 Verifiable Credential Issuance Common Types

440 lines • 21.7 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getTypesFromOfferV1_0_11 = exports.determineGrantTypes = exports.getCredentialOfferPayload = exports.determineFlowType = exports.toUniformCredentialOfferPayload = exports.resolveCredentialOfferURI = exports.assertedUniformCredentialOffer = exports.isPreAuthCode = exports.toUniformCredentialOfferRequest = exports.isCredentialOfferVersion = exports.determineSpecVersionFromOffer = exports.getStateFromCredentialOfferPayload = exports.getClientIdFromCredentialOfferPayload = exports.getIssuerFromCredentialOfferPayload = exports.getScheme = exports.determineSpecVersionFromScheme = exports.determineSpecVersionFromURI = void 0; const debug_1 = __importDefault(require("debug")); const jwt_decode_1 = require("jwt-decode"); const index_1 = require("../index"); const types_1 = require("../types"); const HttpUtils_1 = require("./HttpUtils"); const debug = (0, debug_1.default)('sphereon:oid4vci:offer'); function determineSpecVersionFromURI(uri) { var _a; let version = (_a = determineSpecVersionFromScheme(uri, types_1.OpenId4VCIVersion.VER_UNKNOWN)) !== null && _a !== void 0 ? _a : types_1.OpenId4VCIVersion.VER_UNKNOWN; version = getVersionFromURIParam(uri, version, [types_1.OpenId4VCIVersion.VER_1_0_08], 'initiate_issuance'); version = getVersionFromURIParam(uri, version, [types_1.OpenId4VCIVersion.VER_1_0_08], 'credential_type'); version = getVersionFromURIParam(uri, version, [types_1.OpenId4VCIVersion.VER_1_0_08], 'op_state'); // version = getVersionFromURIParam(uri, version, OpenId4VCIVersion.VER_1_0_09, 'credentials'); // version = getVersionFromURIParam(uri, version, OpenId4VCIVersion.VER_1_0_09, 'initiate_issuance_uri') // version = getVersionFromURIParam(uri, version, OpenId4VCIVersion.VER_1_0_11, 'credential_offer='); version = getVersionFromURIParam(uri, version, [types_1.OpenId4VCIVersion.VER_1_0_11], 'credentials'); version = getVersionFromURIParam(uri, version, [types_1.OpenId4VCIVersion.VER_1_0_11], 'grants.user_pin_required'); version = getVersionFromURIParam(uri, version, [types_1.OpenId4VCIVersion.VER_1_0_13], 'credential_configuration_ids'); version = getVersionFromURIParam(uri, version, [types_1.OpenId4VCIVersion.VER_1_0_13], 'tx_code'); if (version === types_1.OpenId4VCIVersion.VER_UNKNOWN) { version = types_1.OpenId4VCIVersion.VER_1_0_13; } return version; } exports.determineSpecVersionFromURI = determineSpecVersionFromURI; function determineSpecVersionFromScheme(credentialOfferURI, openId4VCIVersion) { const scheme = getScheme(credentialOfferURI); if (credentialOfferURI.includes(types_1.DefaultURISchemes.INITIATE_ISSUANCE)) { return recordVersion(openId4VCIVersion, [types_1.OpenId4VCIVersion.VER_1_0_08], scheme); } if (credentialOfferURI.includes('credential_offer_uri')) { return undefined; } // todo: drop support for v1_0_8. version 11 and version 13 have the same scheme 'openid-credential-offer' else if (credentialOfferURI.includes(types_1.DefaultURISchemes.CREDENTIAL_OFFER)) { if (credentialOfferURI.includes('credentials:') || credentialOfferURI.includes('credentials%22')) { return recordVersion(openId4VCIVersion, [types_1.OpenId4VCIVersion.VER_1_0_11], scheme); } return recordVersion(openId4VCIVersion, [types_1.OpenId4VCIVersion.VER_1_0_13], scheme); } else { return recordVersion(openId4VCIVersion, [types_1.OpenId4VCIVersion.VER_UNKNOWN], scheme); } } exports.determineSpecVersionFromScheme = determineSpecVersionFromScheme; function getScheme(credentialOfferURI) { if (!credentialOfferURI || !credentialOfferURI.includes('://')) { throw Error('Invalid credential offer URI'); } return credentialOfferURI.split('://')[0]; } exports.getScheme = getScheme; function getIssuerFromCredentialOfferPayload(request) { if (!request || (!('issuer' in request) && !('credential_issuer' in request))) { return undefined; } return 'issuer' in request ? request.issuer : request['credential_issuer']; } exports.getIssuerFromCredentialOfferPayload = getIssuerFromCredentialOfferPayload; const getClientIdFromCredentialOfferPayload = (credentialOffer) => { if (!credentialOffer) { return; } if ('client_id' in credentialOffer) { return credentialOffer.client_id; } const state = (0, exports.getStateFromCredentialOfferPayload)(credentialOffer); if (state && isJWT(state)) { const decoded = (0, jwt_decode_1.jwtDecode)(state, { header: false }); if ('client_id' in decoded && typeof decoded.client_id === 'string') { return decoded.client_id; } } return; }; exports.getClientIdFromCredentialOfferPayload = getClientIdFromCredentialOfferPayload; const isJWT = (input) => { if (!input) { return false; } const noParts = input === null || input === void 0 ? void 0 : input.split('.').length; return (input === null || input === void 0 ? void 0 : input.startsWith('ey')) && noParts === 3; }; const getStateFromCredentialOfferPayload = (credentialOffer) => { var _a, _b, _c, _d; if ('grants' in credentialOffer) { if ((_a = credentialOffer.grants) === null || _a === void 0 ? void 0 : _a.authorization_code) { return credentialOffer.grants.authorization_code.issuer_state; } else if ((_b = credentialOffer.grants) === null || _b === void 0 ? void 0 : _b[index_1.PRE_AUTH_GRANT_LITERAL]) { return (_d = (_c = credentialOffer.grants) === null || _c === void 0 ? void 0 : _c[index_1.PRE_AUTH_GRANT_LITERAL]) === null || _d === void 0 ? void 0 : _d[index_1.PRE_AUTH_CODE_LITERAL]; } } if ('op_state' in credentialOffer) { // older spec versions return credentialOffer.op_state; } else if (index_1.PRE_AUTH_CODE_LITERAL in credentialOffer) { return credentialOffer[index_1.PRE_AUTH_CODE_LITERAL]; } return; }; exports.getStateFromCredentialOfferPayload = getStateFromCredentialOfferPayload; function determineSpecVersionFromOffer(offer) { if (isCredentialOfferV1_0_13(offer)) { return types_1.OpenId4VCIVersion.VER_1_0_13; // We don't have full support for V12, so let's skip for now /*} else if (isCredentialOfferV1_0_12(offer)) { return OpenId4VCIVersion.VER_1_0_12;*/ } else if (isCredentialOfferV1_0_11(offer)) { return types_1.OpenId4VCIVersion.VER_1_0_11; } else if (isCredentialOfferV1_0_09(offer)) { return types_1.OpenId4VCIVersion.VER_1_0_09; } else if (isCredentialOfferV1_0_08(offer)) { return types_1.OpenId4VCIVersion.VER_1_0_08; } return types_1.OpenId4VCIVersion.VER_UNKNOWN; } exports.determineSpecVersionFromOffer = determineSpecVersionFromOffer; function isCredentialOfferVersion(offer, min, max) { if (max && max.valueOf() < min.valueOf()) { throw Error(`Cannot have a max ${max.valueOf()} version smaller than the min version ${min.valueOf()}`); } const version = determineSpecVersionFromOffer(offer); if (version.valueOf() < min.valueOf()) { debug(`Credential offer version (${version.valueOf()}) is lower than minimum required version (${min.valueOf()})`); return false; } else if (max && version.valueOf() > max.valueOf()) { debug(`Credential offer version (${version.valueOf()}) is higher than maximum required version (${max.valueOf()})`); return false; } return true; } exports.isCredentialOfferVersion = isCredentialOfferVersion; function isCredentialOfferV1_0_08(offer) { if (!offer) { return false; } if ('issuer' in offer && 'credential_type' in offer) { // payload return true; } if ('credential_offer' in offer && offer['credential_offer']) { // offer, so check payload return isCredentialOfferV1_0_08(offer['credential_offer']); } return false; } function isCredentialOfferV1_0_09(offer) { if (!offer) { return false; } if ('issuer' in offer && 'credentials' in offer) { // payload return true; } if ('credential_offer' in offer && offer['credential_offer']) { // offer, so check payload return isCredentialOfferV1_0_09(offer['credential_offer']); } return false; } function isCredentialOfferV1_0_11(offer) { if (!offer) { return false; } if ('credential_issuer' in offer && 'credentials' in offer) { // payload return true; } if ('credential_offer' in offer && offer['credential_offer']) { // offer, so check payload return isCredentialOfferV1_0_11(offer['credential_offer']); } return 'credential_offer_uri' in offer; } /* function isCredentialOfferV1_0_12(offer: CredentialOfferPayload | CredentialOffer): boolean { if (!offer) { return false; } if ('credential_issuer' in offer && 'credentials' in offer) { // payload return true; } if ('credential_offer' in offer && offer['credential_offer']) { // offer, so check payload return isCredentialOfferV1_0_12(offer['credential_offer']); } return 'credential_offer_uri' in offer; } */ function isCredentialOfferV1_0_13(offer) { if (!offer) { return false; } else if (typeof offer === 'string' && offer.startsWith('{')) { offer = JSON.parse(offer); } if ('credential_issuer' in offer && 'credential_configuration_ids' in offer) { // payload return true; } if ('credential_offer' in offer && offer['credential_offer']) { // offer, so check payload return isCredentialOfferV1_0_13(offer['credential_offer']); } return 'credential_offer_uri' in offer; } function toUniformCredentialOfferRequest(offer, opts) { return __awaiter(this, void 0, void 0, function* () { var _a; let version = (_a = opts === null || opts === void 0 ? void 0 : opts.version) !== null && _a !== void 0 ? _a : determineSpecVersionFromOffer(offer); let originalCredentialOffer = offer.credential_offer; let credentialOfferURI; if ('credential_offer_uri' in offer && (offer === null || offer === void 0 ? void 0 : offer.credential_offer_uri) !== undefined) { credentialOfferURI = offer.credential_offer_uri; if ((opts === null || opts === void 0 ? void 0 : opts.resolve) || (opts === null || opts === void 0 ? void 0 : opts.resolve) === undefined) { index_1.VCI_LOG_COMMON.log(`Credential offer contained a URI. Will use that to get the credential offer payload: ${credentialOfferURI}`); originalCredentialOffer = (yield resolveCredentialOfferURI(credentialOfferURI)); } else if (!originalCredentialOffer) { throw Error(`Credential offer uri (${credentialOfferURI}) found, but resolution was explicitly disabled and credential_offer was supplied`); } // We need to redetermine the version of the offer, as we only had the offer_uri until now version = determineSpecVersionFromOffer(originalCredentialOffer); index_1.VCI_LOG_COMMON.log(`Offer URI payload determined to be of version ${version}`); } if (!originalCredentialOffer) { throw Error('No credential offer available'); } const payload = toUniformCredentialOfferPayload(originalCredentialOffer, Object.assign(Object.assign({}, opts), { version })); const supportedFlows = determineFlowType(payload, version); return Object.assign(Object.assign({ credential_offer: payload, original_credential_offer: originalCredentialOffer }, (credentialOfferURI && { credential_offer_uri: credentialOfferURI })), { supportedFlows, version }); }); } exports.toUniformCredentialOfferRequest = toUniformCredentialOfferRequest; function isPreAuthCode(request) { var _a, _b; const payload = 'credential_offer' in request ? request.credential_offer : request; return ((_b = (_a = payload === null || payload === void 0 ? void 0 : payload.grants) === null || _a === void 0 ? void 0 : _a[index_1.PRE_AUTH_GRANT_LITERAL]) === null || _b === void 0 ? void 0 : _b[index_1.PRE_AUTH_CODE_LITERAL]) !== undefined; } exports.isPreAuthCode = isPreAuthCode; function assertedUniformCredentialOffer(origCredentialOffer, opts) { return __awaiter(this, void 0, void 0, function* () { const credentialOffer = JSON.parse(JSON.stringify(origCredentialOffer)); if (credentialOffer.credential_offer_uri && !credentialOffer.credential_offer) { if ((opts === null || opts === void 0 ? void 0 : opts.resolve) === undefined || opts.resolve) { credentialOffer.credential_offer = yield resolveCredentialOfferURI(credentialOffer.credential_offer_uri); } else { throw Error(`No credential_offer present, but we did get a URI, but resolution was explicitly disabled`); } } if (!credentialOffer.credential_offer) { throw Error(`No credential_offer present`); } credentialOffer.credential_offer = yield toUniformCredentialOfferPayload(credentialOffer.credential_offer, { version: credentialOffer.version }); return credentialOffer; }); } exports.assertedUniformCredentialOffer = assertedUniformCredentialOffer; function resolveCredentialOfferURI(uri) { return __awaiter(this, void 0, void 0, function* () { if (!uri) { return undefined; } const response = (yield (0, HttpUtils_1.getJson)(uri)); if (!response || !response.successBody) { throw Error(`Could not get credential offer from uri: ${uri}: ${JSON.stringify(response === null || response === void 0 ? void 0 : response.errorBody)}`); } return response.successBody; }); } exports.resolveCredentialOfferURI = resolveCredentialOfferURI; function toUniformCredentialOfferPayload(offer, opts) { var _a; // todo: create test to check idempotence once a payload is already been made uniform. const version = (_a = opts === null || opts === void 0 ? void 0 : opts.version) !== null && _a !== void 0 ? _a : determineSpecVersionFromOffer(offer); if (version >= types_1.OpenId4VCIVersion.VER_1_0_11) { const orig = offer; return Object.assign({}, orig); } const grants = 'grants' in offer ? offer.grants : {}; let offerPayloadAsV8V9 = offer; if (isCredentialOfferVersion(offer, types_1.OpenId4VCIVersion.VER_1_0_08, types_1.OpenId4VCIVersion.VER_1_0_09)) { if (offerPayloadAsV8V9.op_state) { grants.authorization_code = Object.assign(Object.assign({}, grants.authorization_code), { issuer_state: offerPayloadAsV8V9.op_state }); } let user_pin_required = false; if (typeof offerPayloadAsV8V9.user_pin_required === 'string') { user_pin_required = offerPayloadAsV8V9.user_pin_required === 'true' || offerPayloadAsV8V9.user_pin_required === 'yes'; } else if (offerPayloadAsV8V9.user_pin_required !== undefined) { user_pin_required = offerPayloadAsV8V9.user_pin_required; } if (offerPayloadAsV8V9[index_1.PRE_AUTH_CODE_LITERAL]) { grants[index_1.PRE_AUTH_GRANT_LITERAL] = { 'pre-authorized_code': offerPayloadAsV8V9[index_1.PRE_AUTH_CODE_LITERAL], user_pin_required, }; } } const issuer = getIssuerFromCredentialOfferPayload(offer); if (version === types_1.OpenId4VCIVersion.VER_1_0_09) { offerPayloadAsV8V9 = offer; return { // credential_definition: getCredentialsSupported(never, offerPayloadAsV8V9.credentials).map(sup => {credentialSubject: sup.credentialSubject})[0], credential_issuer: issuer !== null && issuer !== void 0 ? issuer : offerPayloadAsV8V9.issuer, credentials: offerPayloadAsV8V9.credentials, grants, }; } if (version === types_1.OpenId4VCIVersion.VER_1_0_08) { offerPayloadAsV8V9 = offer; return { credential_issuer: issuer !== null && issuer !== void 0 ? issuer : offerPayloadAsV8V9.issuer, credentials: Array.isArray(offerPayloadAsV8V9.credential_type) ? offerPayloadAsV8V9.credential_type : [offerPayloadAsV8V9.credential_type], grants, }; } throw Error(`Could not create uniform payload for version ${version}`); } exports.toUniformCredentialOfferPayload = toUniformCredentialOfferPayload; function determineFlowType(suppliedOffer, version) { var _a, _b, _c; const payload = getCredentialOfferPayload(suppliedOffer); const supportedFlows = []; if ((_a = payload.grants) === null || _a === void 0 ? void 0 : _a.authorization_code) { supportedFlows.push(types_1.AuthzFlowType.AUTHORIZATION_CODE_FLOW); } if ((_c = (_b = payload.grants) === null || _b === void 0 ? void 0 : _b[index_1.PRE_AUTH_GRANT_LITERAL]) === null || _c === void 0 ? void 0 : _c[index_1.PRE_AUTH_CODE_LITERAL]) { supportedFlows.push(types_1.AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW); } if (supportedFlows.length === 0 && version < types_1.OpenId4VCIVersion.VER_1_0_09) { // auth flow without op_state was possible in v08. The only way to know is that the detections would result in finding nothing. supportedFlows.push(types_1.AuthzFlowType.AUTHORIZATION_CODE_FLOW); } return supportedFlows; } exports.determineFlowType = determineFlowType; function getCredentialOfferPayload(offer) { let payload; if ('credential_offer' in offer && offer['credential_offer']) { payload = offer.credential_offer; } else { payload = offer; } return payload; } exports.getCredentialOfferPayload = getCredentialOfferPayload; function determineGrantTypes(offer) { let grants; if ('grants' in offer && offer.grants) { grants = offer.grants; } else { grants = getCredentialOfferPayload(offer).grants; } const types = []; if (grants) { if ('authorization_code' in grants) { types.push(types_1.GrantTypes.AUTHORIZATION_CODE); } if (index_1.PRE_AUTH_GRANT_LITERAL in grants) { types.push(types_1.GrantTypes.PRE_AUTHORIZED_CODE); } } return types; } exports.determineGrantTypes = determineGrantTypes; function getVersionFromURIParam(credentialOfferURI, currentVersion, matchingVersion, param, allowUpgrade = true) { if (credentialOfferURI.includes(param)) { return recordVersion(currentVersion, matchingVersion, param, allowUpgrade); } return currentVersion; } function recordVersion(currentVersion, matchingVersion, key, allowUpgrade = true) { matchingVersion = matchingVersion.sort().reverse(); if (currentVersion === types_1.OpenId4VCIVersion.VER_UNKNOWN) { return matchingVersion[0]; } else if (matchingVersion.includes(currentVersion)) { if (!allowUpgrade) { return currentVersion; } return matchingVersion[0]; } throw new Error(`Invalid param. Some keys have been used from version: ${currentVersion} version while '${key}' is used from version: ${JSON.stringify(matchingVersion)}`); } function getTypesFromOfferV1_0_11(credentialOffer, opts) { const types = credentialOffer.credentials.reduce((prev, curr) => { // FIXME returning the string value is wrong (as it's an id), but just matching the current behavior of this library // The credential_type (from draft 8) and the actual 'type' value in a VC (from draft 11) are mixed up // Fix for this here: https://github.com/Sphereon-Opensource/OID4VCI/pull/54 if (typeof curr === 'string') { return [...prev, curr]; } else if (curr.format === 'jwt_vc_json-ld' || curr.format === 'ldp_vc') { return [...prev, ...curr.credential_definition.types]; } else if (curr.format === 'jwt_vc_json' || curr.format === 'jwt_vc') { return [...prev, ...curr.types]; } else if (curr.format === 'vc+sd-jwt') { return [...prev, curr.vct]; } return prev; }, []); if (!types || types.length === 0) { throw Error('Could not deduce types from credential offer'); } if (opts === null || opts === void 0 ? void 0 : opts.filterVerifiableCredential) { return types.filter((type) => type !== 'VerifiableCredential'); } return types; } exports.getTypesFromOfferV1_0_11 = getTypesFromOfferV1_0_11; //# sourceMappingURL=CredentialOfferUtil.js.map