import jwt from 'jsonwebtoken';
import stringify from 'qs/lib/stringify';
import debugLib from 'debug';
import fetch from 'node-fetch';
import serviceConfig from '../../../app/config/service.configdefinitions';
import routeToUrl from './routeToUrl';
import { OIDC_REQUEST_TIMEOUT, JWT_CLOCK_TOLERANCE } from './constants';
import hsAlgorithmData from '../../../app/data/hsAlgorithmData';
const debug = debugLib('SlimmingWorld:AuthenticationHelper');
/** @module */
/**
* Helper class that takes care of communicating with the OIDC authentication endpoint
* @default
*/
class OidcClient {
constructor(oidcConfig, oidcDiscovery) {
this.oidcDiscovery = oidcDiscovery;
this.config = oidcConfig;
this.jwtConfig = this.config.jwt || {
audience: oidcConfig.audience || oidcConfig.client_id,
issuer: oidcConfig.issuer,
};
// add 60 seconds leeway for time difference between servers
this.jwtConfig.clockTolerance = JWT_CLOCK_TOLERANCE;
}
get scope() {
return serviceConfig.clientScopes.concat(this.config.scopes || []).join(' ');
}
login(req, res, state) {
return this.oidcDiscovery.discoverComplete.then(() => {
/* eslint-disable */
const params = {
state,
response_type: 'code',
client_id: this.config.client_id,
redirect_uri: routeToUrl(req, req.headers.host, this.config['callbackRoute']),
scope: this.scope,
};
/* eslint-enable */
const endpoint = this.oidcDiscovery.providerConfig.authorization_endpoint;
const joinChar = endpoint.indexOf('?') === -1 ? '?' : '&';
debug(`login redirect params: ${params}`);
return res.redirect(`${endpoint}${joinChar}${stringify(params)}`);
});
}
/**
* This logout method is not used anymore in the new situation. But since it's still part
* of the OidcClient, i keep it here for now
*
* @param res
* @param authTokens
*/
logout(res, authTokens) {
/* eslint-disable */
const params = {
post_logout_redirect_uri: '/',
};
/* eslint-enable */
const tokenValidation =
authTokens && authTokens.id_token
? this.validateToken(authTokens.id_token).then(valid => {
if (valid) {
/* eslint-disable */
params.id_token_hint = authTokens.id_token;
/* eslint-enable */
}
})
: Promise.resolve();
tokenValidation.then(() => {
const endpoint = this.oidcDiscovery.providerConfig.end_session_endpoint;
const joinChar = endpoint.indexOf('?') === -1 ? '?' : '&';
return res.redirect(`${endpoint}${joinChar}${stringify(params)}`);
});
}
getTokens(req) {
/* eslint-disable */
const { ius, code } = req.query;
const params = {
grant_type: 'authorization_code',
code,
redirect_uri:
routeToUrl(req, req.headers.host, this.config['callbackRoute']) +
(ius ? `?ius=${ius}` : ''),
};
/* eslint-enable */
debug(`getTokens params: ${params}`);
return this.oidcDiscovery.discoverComplete
.then(() => this.getAccessToken(params))
.then(tokens => {
const valid = this.validateTokenSync(tokens.id_token);
if (valid) {
return tokens;
}
debug('Received invalid ID token on OpenID callback endpoint');
return null;
});
}
validateToken = token =>
this.oidcDiscovery.discoverComplete.then(() => this.validateTokenSync(token));
validateTokenSync = token => {
const { header: jwtHeader } = jwt.decode(token, { complete: true });
if (jwtHeader && jwtHeader.alg && hsAlgorithmData.includes(jwtHeader.alg)) return true;
try {
// ID Token Validation [http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation]
return jwt.verify(token, this.oidcDiscovery.publicKey, this.jwtConfig);
} catch (e) {
debug(`There was an error during validation of the ID token: ${e}`);
return false;
}
};
getAccessToken(_params) {
/* eslint-disable */
const params = {
..._params,
client_id: this.config.client_id,
client_secret: this.config.client_secret,
};
/* eslint-enable */
return fetch(this.oidcDiscovery.providerConfig.token_endpoint, {
method: 'POST',
body: Object.keys(params)
.map(param => `${param}=${encodeURIComponent(params[param])}`)
.join('&'),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
timeout: OIDC_REQUEST_TIMEOUT,
}).then(res => res.json());
}
}
export default OidcClient;