/* global WP_DEFINE_IS_NODE */
/**
* Helper class that handles authentication for incoming express requests.
*/
import { randomBytes } from 'crypto';
import debugLib from 'debug';
import OidcClient from './OidcClient';
import { LOGIN_ENDPOINT, AUTH_TOKEN_EXPIRATION_OFFSET, RESET_AUTH_QUERY } from './constants';
import {
getAuthenticationState,
setAuthenticationState,
clearAuthenticationState,
persistUserPermissionState,
getUserPermissionState,
persistInitialUserState,
hasResetAuthFlag,
clearAuthTokens,
} from './stateUtils';
import { removeQueryParam } from './removeQueryParam';
const debug = debugLib('SlimmingWorld:AuthenticationHelper');
class AuthenticationHelper {
/**
* @param {Object} config The configuration for the OpenID provider
* @param {string} config.configuration_endpoint The url to the configuration endpoint of the
* OpenID provider
* @param {string} config.client_id The OpenID client ID.
* @param {string} config.client_secret The OpenID client secret.
* @param {string} config.callbackRoute The route that the OpenID provider will redirect to
* once authentication is completed
* @param {string[]} config.scopes The scopes to use in the authentication request to the
* OpenID provider
* @constructor
*/
constructor(config, oidcDiscovery) {
this.config = config;
const required = ['client_id', 'client_secret', 'callbackRoute', 'configuration_endpoint'];
if (!this.config || !required.every(name => !!this.config[name])) {
throw new Error(`The required config settings are not present [${required.join(', ')}]`);
}
this.client = new OidcClient(config, oidcDiscovery);
}
/**
* Create routes on an express instance to handle authentication
* @param app The express() app
*/
createRoutes(app) {
app.use(this.config.callbackRoute, this.processCallback);
app.use(LOGIN_ENDPOINT, this.authenticate);
}
/* eslint-enable */
/**
* Will redirect to the login endpoint with a generated token. This function
* should be called when authentication is required but not present.
* @method redirectToLogin
* @memberOf AuthenticationHelper
* @param req The express request object
* @param res The express response object
*/
redirectToLogin = (req, res) => {
randomBytes(48, (err, buffer) => {
const randomToken = buffer.toString('hex');
const state = {
state: randomToken,
returnUrl: removeQueryParam(req.url, RESET_AUTH_QUERY),
};
setAuthenticationState(res, state);
res.redirect(LOGIN_ENDPOINT);
});
};
/**
* Returns a Promise that resolves with the authentication tokens for the currently authenticated
* user. If not authenticated, will resolve with 'null'
* @function getAuthentication
* @memberOf AuthenticationHelper
* @param req The express request object
* @param res The express response object
*/
getAuthentication = (req, res) => {
const shouldResetAuth = hasResetAuthFlag(req);
const userPermissionState = getUserPermissionState(req);
if (
!shouldResetAuth &&
userPermissionState &&
typeof userPermissionState.accountState === 'number'
) {
return Promise.resolve(userPermissionState);
}
// when this flag is present in the url, the server will clear the auth cookie and request
// a new token. This is useful when returning from a page that updated user info.
if (shouldResetAuth) {
clearAuthTokens(res);
}
return Promise.resolve();
};
/**
* Will initialize an authentication sequence on an incoming express request.
* - If the user is already authenticated, it will redirect the user back to the url it
* was initially redirected from (or back to the root if that url was not found).
* - If the user was not authenticated yet, it will redirect the user to the OpenID connect
* endpoint to retrieve new authentication tokens
* @method authenticate
* @memberOf AuthenticationHelper
* @param req The express request object
* @param res The express response object
* @param next The express next function
*/
authenticate = (req, res, next) => {
const authState = getAuthenticationState(req);
this.getAuthentication(req, res)
.then(authentication => {
if (authentication) {
this.returnToUrl(res, authState ? authState.returnUrl : null);
} else {
if (!authState) {
debug('No authentication state set on login endpoint');
// If authentication state is not set, redirect to home
return res.redirect('/');
}
return this.client.login(req, res, authState.state);
}
return null;
})
.catch(next);
};
/**
* Handles a request on the route that the OpenID server redirects to after an
* authentication. Will validate the authentication response that the OpenID server attached
* as query parameters. If the response is valid, it will persist the new authentication
* tokens in a cookie and redirect the user back to the url it came from.
* @method processCallback
* @memberOf AuthenticationHelper
* @param req The express request object
* @param res The express response object
* @param next The express next function
*/
processCallback = (req, res, next) => {
if (req.query.error) {
next(new Error(`Authentication error: ${req.query.error}`));
} else if (req.query.code) {
const authState = getAuthenticationState(req);
if (!authState || !authState.state) {
debug('No state cookie present when processing authorization endpoint callback data');
res.redirect('/');
return;
}
if (req.query.state !== authState.state) {
throw new Error('State does not match session');
}
persistInitialUserState(req, res);
this.client
.getTokens(req)
.then(tokens => {
if (tokens) {
const expireCookieIn = tokens.expires_in - AUTH_TOKEN_EXPIRATION_OFFSET;
/* eslint-disable */
persistUserPermissionState(
req,
res,
{
access_token: tokens.access_token,
},
expireCookieIn,
);
/* eslint-enable */
clearAuthenticationState(res);
this.returnToUrl(res, authState.returnUrl);
} else {
next();
}
})
.catch(next);
} else {
next();
}
};
/**
* Redirects an express request back to an endpoint. Will do some checks to prevent ending
* up in redirect loops.
* @param res The express response object
* @param returnUrl The url to return to
*/
returnToUrl(res, returnUrl) {
// prevent redirect loops
if (returnUrl && returnUrl !== LOGIN_ENDPOINT && returnUrl !== this.config.callbackRoute) {
res.redirect(returnUrl);
return;
}
res.redirect('/');
}
}
export default AuthenticationHelper;