2021-12-07 13:18:08 -05:00

189 lines
5.6 KiB
JavaScript

/* eslint-disable no-underscore-dangle */
const url = require('url');
const { format } = require('util');
const cloneDeep = require('./helpers/deep_clone');
const { RPError, OPError } = require('./errors');
const { BaseClient } = require('./client');
const { random, codeChallenge } = require('./helpers/generators');
const pick = require('./helpers/pick');
const { resolveResponseType, resolveRedirectUri } = require('./helpers/client');
function verified(err, user, info = {}) {
if (err) {
this.error(err);
} else if (!user) {
this.fail(info);
} else {
this.success(user, info);
}
}
/**
* @name constructor
* @api public
*/
function OpenIDConnectStrategy({
client,
params = {},
passReqToCallback = false,
sessionKey,
usePKCE = true,
extras = {},
} = {}, verify) {
if (!(client instanceof BaseClient)) {
throw new TypeError('client must be an instance of openid-client Client');
}
if (typeof verify !== 'function') {
throw new TypeError('verify callback must be a function');
}
if (!client.issuer || !client.issuer.issuer) {
throw new TypeError('client must have an issuer with an identifier');
}
this._client = client;
this._issuer = client.issuer;
this._verify = verify;
this._passReqToCallback = passReqToCallback;
this._usePKCE = usePKCE;
this._key = sessionKey || `oidc:${url.parse(this._issuer.issuer).hostname}`;
this._params = cloneDeep(params);
this._extras = cloneDeep(extras);
if (!this._params.response_type) this._params.response_type = resolveResponseType.call(client);
if (!this._params.redirect_uri) this._params.redirect_uri = resolveRedirectUri.call(client);
if (!this._params.scope) this._params.scope = 'openid';
if (this._usePKCE === true) {
const supportedMethods = Array.isArray(this._issuer.code_challenge_methods_supported)
? this._issuer.code_challenge_methods_supported : false;
if (supportedMethods && supportedMethods.includes('S256')) {
this._usePKCE = 'S256';
} else if (supportedMethods && supportedMethods.includes('plain')) {
this._usePKCE = 'plain';
} else if (supportedMethods) {
throw new TypeError('neither code_challenge_method supported by the client is supported by the issuer');
} else {
this._usePKCE = 'S256';
}
} else if (typeof this._usePKCE === 'string' && !['plain', 'S256'].includes(this._usePKCE)) {
throw new TypeError(`${this._usePKCE} is not valid/implemented PKCE code_challenge_method`);
}
this.name = url.parse(client.issuer.issuer).hostname;
}
OpenIDConnectStrategy.prototype.authenticate = function authenticate(req, options) {
(async () => {
const client = this._client;
if (!req.session) {
throw new TypeError('authentication requires session support');
}
const reqParams = client.callbackParams(req);
const sessionKey = this._key;
/* start authentication request */
if (Object.keys(reqParams).length === 0) {
// provide options object with extra authentication parameters
const params = {
state: random(),
...this._params,
...options,
};
if (!params.nonce && params.response_type.includes('id_token')) {
params.nonce = random();
}
req.session[sessionKey] = pick(params, 'nonce', 'state', 'max_age', 'response_type');
if (this._usePKCE && params.response_type.includes('code')) {
const verifier = random();
req.session[sessionKey].code_verifier = verifier;
switch (this._usePKCE) { // eslint-disable-line default-case
case 'S256':
params.code_challenge = codeChallenge(verifier);
params.code_challenge_method = 'S256';
break;
case 'plain':
params.code_challenge = verifier;
break;
}
}
this.redirect(client.authorizationUrl(params));
return;
}
/* end authentication request */
/* start authentication response */
const session = req.session[sessionKey];
if (Object.keys(session || {}).length === 0) {
throw new Error(format('did not find expected authorization request details in session, req.session["%s"] is %j', sessionKey, session));
}
const {
state, nonce, max_age: maxAge, code_verifier: codeVerifier, response_type: responseType,
} = session;
try {
delete req.session[sessionKey];
} catch (err) {}
const opts = {
redirect_uri: this._params.redirect_uri,
...options,
};
const checks = {
state,
nonce,
max_age: maxAge,
code_verifier: codeVerifier,
response_type: responseType,
};
const tokenset = await client.callback(opts.redirect_uri, reqParams, checks, this._extras);
const passReq = this._passReqToCallback;
const loadUserinfo = this._verify.length > (passReq ? 3 : 2) && client.issuer.userinfo_endpoint;
const args = [tokenset, verified.bind(this)];
if (loadUserinfo) {
if (!tokenset.access_token) {
throw new RPError({
message: 'expected access_token to be returned when asking for userinfo in verify callback',
tokenset,
});
}
const userinfo = await client.userinfo(tokenset);
args.splice(1, 0, userinfo);
}
if (passReq) {
args.unshift(req);
}
this._verify(...args);
/* end authentication response */
})().catch((error) => {
if (
(error instanceof OPError && error.error !== 'server_error' && !error.error.startsWith('invalid'))
|| error instanceof RPError
) {
this.fail(error);
} else {
this.error(error);
}
});
};
module.exports = OpenIDConnectStrategy;