206 lines
6.0 KiB
JavaScript
206 lines
6.0 KiB
JavaScript
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);
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
// state and nonce are handled in authenticate()
|
|
delete this._params.state;
|
|
delete this._params.nonce;
|
|
|
|
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;
|
|
|
|
const { 0: parameter, length } = Object.keys(reqParams);
|
|
|
|
/**
|
|
* Start authentication request if this has no authorization response parameters or
|
|
* this might a login initiated from a third party as per
|
|
* https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin.
|
|
*/
|
|
if (length === 0 || (length === 1 && parameter === 'iss')) {
|
|
// 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) {
|
|
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;
|