193 lines
5.7 KiB
JavaScript
193 lines
5.7 KiB
JavaScript
const { inspect } = require('util');
|
|
const url = require('url');
|
|
|
|
const { RPError } = require('./errors');
|
|
const getClient = require('./client');
|
|
const registry = require('./issuer_registry');
|
|
const processResponse = require('./helpers/process_response');
|
|
const webfingerNormalize = require('./helpers/webfinger_normalize');
|
|
const request = require('./helpers/request');
|
|
const clone = require('./helpers/deep_clone');
|
|
const { keystore } = require('./helpers/issuer');
|
|
|
|
const AAD_MULTITENANT_DISCOVERY = [
|
|
'https://login.microsoftonline.com/common/.well-known/openid-configuration',
|
|
'https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration',
|
|
'https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration',
|
|
'https://login.microsoftonline.com/consumers/v2.0/.well-known/openid-configuration',
|
|
];
|
|
const AAD_MULTITENANT = Symbol();
|
|
const ISSUER_DEFAULTS = {
|
|
claim_types_supported: ['normal'],
|
|
claims_parameter_supported: false,
|
|
grant_types_supported: ['authorization_code', 'implicit'],
|
|
request_parameter_supported: false,
|
|
request_uri_parameter_supported: true,
|
|
require_request_uri_registration: false,
|
|
response_modes_supported: ['query', 'fragment'],
|
|
token_endpoint_auth_methods_supported: ['client_secret_basic'],
|
|
};
|
|
|
|
class Issuer {
|
|
#metadata;
|
|
constructor(meta = {}) {
|
|
const aadIssValidation = meta[AAD_MULTITENANT];
|
|
delete meta[AAD_MULTITENANT];
|
|
['introspection', 'revocation'].forEach((endpoint) => {
|
|
// if intro/revocation endpoint auth specific meta is missing use the token ones if they
|
|
// are defined
|
|
if (
|
|
meta[`${endpoint}_endpoint`] &&
|
|
meta[`${endpoint}_endpoint_auth_methods_supported`] === undefined &&
|
|
meta[`${endpoint}_endpoint_auth_signing_alg_values_supported`] === undefined
|
|
) {
|
|
if (meta.token_endpoint_auth_methods_supported) {
|
|
meta[`${endpoint}_endpoint_auth_methods_supported`] =
|
|
meta.token_endpoint_auth_methods_supported;
|
|
}
|
|
if (meta.token_endpoint_auth_signing_alg_values_supported) {
|
|
meta[`${endpoint}_endpoint_auth_signing_alg_values_supported`] =
|
|
meta.token_endpoint_auth_signing_alg_values_supported;
|
|
}
|
|
}
|
|
});
|
|
|
|
this.#metadata = new Map();
|
|
|
|
Object.entries(meta).forEach(([key, value]) => {
|
|
this.#metadata.set(key, value);
|
|
if (!this[key]) {
|
|
Object.defineProperty(this, key, {
|
|
get() {
|
|
return this.#metadata.get(key);
|
|
},
|
|
enumerable: true,
|
|
});
|
|
}
|
|
});
|
|
|
|
registry.set(this.issuer, this);
|
|
|
|
const Client = getClient(this, aadIssValidation);
|
|
|
|
Object.defineProperties(this, {
|
|
Client: { value: Client, enumerable: true },
|
|
FAPI1Client: { value: class FAPI1Client extends Client {}, enumerable: true },
|
|
FAPI2Client: { value: class FAPI2Client extends Client {}, enumerable: true },
|
|
});
|
|
}
|
|
|
|
get metadata() {
|
|
return clone(Object.fromEntries(this.#metadata.entries()));
|
|
}
|
|
|
|
static async webfinger(input) {
|
|
const resource = webfingerNormalize(input);
|
|
const { host } = url.parse(resource);
|
|
const webfingerUrl = `https://${host}/.well-known/webfinger`;
|
|
|
|
const response = await request.call(this, {
|
|
method: 'GET',
|
|
url: webfingerUrl,
|
|
responseType: 'json',
|
|
searchParams: { resource, rel: 'http://openid.net/specs/connect/1.0/issuer' },
|
|
headers: {
|
|
Accept: 'application/json',
|
|
},
|
|
});
|
|
const body = processResponse(response);
|
|
|
|
const location =
|
|
Array.isArray(body.links) &&
|
|
body.links.find(
|
|
(link) =>
|
|
typeof link === 'object' &&
|
|
link.rel === 'http://openid.net/specs/connect/1.0/issuer' &&
|
|
link.href,
|
|
);
|
|
|
|
if (!location) {
|
|
throw new RPError({
|
|
message: 'no issuer found in webfinger response',
|
|
body,
|
|
});
|
|
}
|
|
|
|
if (typeof location.href !== 'string' || !location.href.startsWith('https://')) {
|
|
throw new RPError({
|
|
printf: ['invalid issuer location %s', location.href],
|
|
body,
|
|
});
|
|
}
|
|
|
|
const expectedIssuer = location.href;
|
|
if (registry.has(expectedIssuer)) {
|
|
return registry.get(expectedIssuer);
|
|
}
|
|
|
|
const issuer = await this.discover(expectedIssuer);
|
|
|
|
if (issuer.issuer !== expectedIssuer) {
|
|
registry.del(issuer.issuer);
|
|
throw new RPError(
|
|
'discovered issuer mismatch, expected %s, got: %s',
|
|
expectedIssuer,
|
|
issuer.issuer,
|
|
);
|
|
}
|
|
return issuer;
|
|
}
|
|
|
|
static async discover(uri) {
|
|
const wellKnownUri = resolveWellKnownUri(uri);
|
|
|
|
const response = await request.call(this, {
|
|
method: 'GET',
|
|
responseType: 'json',
|
|
url: wellKnownUri,
|
|
headers: {
|
|
Accept: 'application/json',
|
|
},
|
|
});
|
|
const body = processResponse(response);
|
|
return new Issuer({
|
|
...ISSUER_DEFAULTS,
|
|
...body,
|
|
[AAD_MULTITENANT]: !!AAD_MULTITENANT_DISCOVERY.find((discoveryURL) =>
|
|
wellKnownUri.startsWith(discoveryURL),
|
|
),
|
|
});
|
|
}
|
|
|
|
async reloadJwksUri() {
|
|
await keystore.call(this, true);
|
|
}
|
|
|
|
/* istanbul ignore next */
|
|
[inspect.custom]() {
|
|
return `${this.constructor.name} ${inspect(this.metadata, {
|
|
depth: Infinity,
|
|
colors: process.stdout.isTTY,
|
|
compact: false,
|
|
sorted: true,
|
|
})}`;
|
|
}
|
|
}
|
|
|
|
function resolveWellKnownUri(uri) {
|
|
const parsed = url.parse(uri);
|
|
if (parsed.pathname.includes('/.well-known/')) {
|
|
return uri;
|
|
} else {
|
|
let pathname;
|
|
if (parsed.pathname.endsWith('/')) {
|
|
pathname = `${parsed.pathname}.well-known/openid-configuration`;
|
|
} else {
|
|
pathname = `${parsed.pathname}/.well-known/openid-configuration`;
|
|
}
|
|
return url.format({ ...parsed, pathname });
|
|
}
|
|
}
|
|
|
|
module.exports = Issuer;
|