189 lines
5.6 KiB
JavaScript
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;
|