'use strict'; const debug = require('debug'); const debugError = debug('xssec:JwksReplica'); debugError.log = console.error.bind(console); const requests = require('../requests'); const nodeRSA = require('node-rsa'); const PROTOCOL = "https://"; class IdentityService { #serviceCredentials; #url; #oidcInfo; get serviceCredentials() { return this.#serviceCredentials; } get url() { return this.#url; } constructor(serviceCredentials) { if (serviceCredentials == null) { throw new Error("IdentityService requires service credentials."); } this.#serviceCredentials = serviceCredentials; this.#url = serviceCredentials.url; } /** Configures OIDC calls from this service to target the given domain instead of the url from the service credentials. */ withCustomDomain(domain) { this.#url = domain.startsWith(PROTOCOL) ? domain : `${PROTOCOL}${domain}`; return this; } async fetchOidcInfo() { if (!this.#oidcInfo) { this.#oidcInfo = await new Promise((res, rej) => { try { requests.requestOpenIDConfiguration(this.url, { clientId: this.serviceCredentials.clientId }, (err, oidcInfo) => { if (err) { return rej(err); } return res(oidcInfo); }); } catch (e) { return rej(e); } }); } return this.#oidcInfo; } async fetchJwks(params = {}) { if(params.client_id && params.client_id !== this.serviceCredentials.clientid) { return Promise.reject("Invalid state: IdentityService#fetchJwks called with client_id value that is different from the client_id of the IdentityService object."); } await this.fetchOidcInfo(); const jwksEndpoint = this.#oidcInfo["jwks_uri"]; return new Promise((res, rej) => { try { requests.fetchOIDCKey(jwksEndpoint, params, (err, json) => { if (err) { return rej(err); } const jwks = json.keys .map(key => { try { const pem = this.createPem(key); key.value = pem; } catch (e) { debugError(`Could not calculate PEM for key with kid ${key.kid}: ${e}. IdentityService: (${JSON.stringify(this)})`) } return key; }) .filter(key => key.value !== undefined); return res(jwks); }); } catch (e) { return rej(e); } }); } createPem(key) { if (key.kty !== "RSA") { throw new Error("KTY '" + key.kty + "' not supported"); } const modulus = Buffer.from(key.n, 'base64'); const exponent = Buffer.from(key.e, 'base64'); const pubKey = new nodeRSA().importKey({ n: modulus, e: exponent }, 'components-public'); return pubKey.exportKey('pkcs8-public-pem'); } toJSON() { return { url: this.url } } } module.exports = IdentityService;