akima-client
Version:
Client side support for the Akima oauth2 framework
951 lines (814 loc) • 33.7 kB
JavaScript
;
const util = require('util');
const http = require("http");
const https = require("https");
const request = require('request');
const querystring = require('querystring');
const passport = require('passport');
const merge = require('merge');
const OAuth2Strategy = require('passport-oauth2').Strategy;
const InternalOAuthError = require('passport-oauth2').InternalOAuthError;
const BearerStrategy = require('passport-http-bearer').Strategy;
const tokenStore = require('./token-store');
const Redis = require("ioredis");
let app = null;
let client = {externalURL: null, id: null, secret: null};
let repository = null;
let tokenCache = {};
let ucValid = {};
let tokenObjectCache = {};
let sharedSessionId = null;
let redisClient = null;
let redisSubscriber = null;
let logger = {};
let authServer;
let authLocal;
let cookieOptions = {
secure: false,
proxy: false,
httpOnly: true
};
logger.error = console.log;
function getAuthURL(authURLStem,clientSchema) {
if(clientSchema == null) clientSchema = defaultClientSchema;
return authURLStem + '/oauth/' + clientSchema + '/oauth/';
}
function getTokenViaClientCredentials(user, password, authURLStem, clientSchema, callback) {
let authURL = getAuthURL(authURLStem,clientSchema);
request.post({
url: authURL + 'token',
auth: {'user': user, 'pass': password},
headers: [
{name: 'Content-Type', value: 'application/json'}
],
form: {
grant_type: 'client_credentials'
}
},
function (err, res, body) {
if (err) return callback(err);
if (res.statusCode !== 200 && (res.statusCode < 400 && res.statusCode >= 500)) {
return callback("Authorization server returned " + statusCode);
}
let token;
try {
token = JSON.parse(body);
} catch(e) {
if(logger.error) logger.error("Akima: getTokenViaClientCredentials could not authenticate to " + authURL + " (" + body + ")");
return callback(e);
}
if (token && token.access_token) callback(null, token.access_token);
});
};
function sendPasswordResetLink(username, user, password, authURLStem, clientSchema, callback) {
let authURL = getAuthURL(authURLStem,clientSchema);
request.post({
url: authURLStem + '/oauth/' + clientSchema + '/' + 'send-password-reset',
auth: {'user': user, 'pass': password},
headers: [ {name: 'Content-Type', value: 'application/json'} ],
form: {
username: username,
scrambleExistingPassword: false,
messageType: 'directUserRequest'
}
},
function (err, res, body) {
if (err) return callback(err);
else if (res.statusCode !== 200 && (res.statusCode < 400 && res.statusCode >= 500)) {
return callback("Authorization server returned " + statusCode);
}
else return callback(null);
});
};
function setEffectiveUserForToken(authURLStem,clientSchema,token,username,callback) {
let authURL = getAuthURL(authURLStem,clientSchema);
request.get({
url: authURL + 'su',
auth: { 'bearer': token },
qs: { username: username }
},
function (err, res, body) {
if (err) return callback(err);
if (res.statusCode !== 200 && (res.statusCode < 400 && res.statusCode >= 500)) {
return callback("Authorization server returned " + statusCode);
}
callback(err, token);
});
}
function getLocalCredentials(username, authURLStem, clientSchema, clientID, clientPassword, callback) {
let authURL = getAuthURL(authURLStem,clientSchema);
getTokenViaClientCredentials(clientID, clientPassword, authURLStem,clientSchema , function (err, token) {
if (err) return callback(err);
setEffectiveUserForToken(authURLStem,clientSchema,token,username,callback);
});
};
function AkimaOAuth2Strategy(options) {
options = options || {};
OAuth2Strategy.call(this, options, function (req, accessToken, refreshToken, params, profile, done) {
req.token = accessToken;
params.refreshTokenIdentifier = refreshToken;
let token = {};
token.accessTokenIdentifier = params.access_token;
token.refreshTokenIdentifier = refreshToken;
token.refreshAfter = params.refreshAfter;
token.expiresAt = params.expiresAt;
token.tokenType = params.token_type;
token.scope = "*";
tokenStore[repository.type].upsertTokenAndUser(accessToken,token,profile,function(err) {
if (err) return done(err);
done(null, {"id": profile.id});
});
});
if (!options.userProfileURL) throw new TypeError("Akima requires a userProfileURL option");
this.name = "akima-oauth2";
this._userProfileURL = options.userProfileURL;
}
passport.serializeUser(function (user, done) {
done(null, user.id);
});
passport.deserializeUser(function (id, done) {
tokenStore[repository.type].getUser(id, function (err, user) {
if (err) {
return done(err);
}
done(null, user);
});
});
util.inherits(AkimaOAuth2Strategy, OAuth2Strategy);
function toProfileJSON(userJSON) {
let profile = {provider: "akima-oauth2"};
profile.id = userJSON.id;
profile.displayName = userJSON.userDisplayName;
profile.username = userJSON.username;
profile.emails = [{value: userJSON.email}];
profile.properties = userJSON.userProperties;
return profile;
}
AkimaOAuth2Strategy.prototype.userProfile = function (accessToken, done) {
this._oauth2.get(this._userProfileURL, accessToken, function (err, body, res) {
if (err) {
return done(new InternalOAuthError("failed to fetch user profile", err));
}
try {
let json = JSON.parse(body);
let profile = toProfileJSON(json);
done(null, profile);
}
catch (e) {
done(e);
}
});
};
function getTokenInfo (tokenId, authURLStem, clientSchema, callback) {
let authURL = getAuthURL(authURLStem,clientSchema);
request.get({
url: authURL + 'tokeninfo',
auth: { 'bearer': tokenId }
},
function (err, res, body) {
if (err) return callback(err);
if (res.statusCode != 200 && (res.statusCode < 400 && res.statusCode >= 500)) {
return callback("Authorization server returned " + statusCode);
}
let tokenInfo;
try {
tokenInfo = JSON.parse(body);
} catch(e) {
if (logger.debug) logger.debug("Akima: getTokenInfo could not authenticate to " + authURL + " (" + body + ")");
return callback(e);
}
if (!tokenInfo) return callback();
tokenInfo.user.id = tokenInfo.user.userId;
tokenInfo.user.userId = null;
let token = tokenInfo;
let user = toProfileJSON(tokenInfo.user);
delete token.user;
callback(null, token, user);
});
};
function getUserInfo(tokenId, authURLStem, clientSchema, callback) {
let authURL = getAuthURL(authURLStem,clientSchema);
request.get({
url: authURL + 'userinfo',
auth: { 'bearer': tokenId }
},
function (err, res, body) {
if (err) return callback(err);
if (res.statusCode !== 200 && (res.statusCode < 400 && res.statusCode >= 500)) {
if(logger.error) logger.debug("Akima: could not retrieve user associated with " + tokenId);
return callback("Authorization server returned " + statusCode);
}
let userInfo;
try {
userInfo = JSON.parse(body);
} catch(e) {
if(logger.error) logger.error("Akima: getUserInfo could not authenticate to " + authURL + " (" + body + ")");
return callback(e);
}
if (!userInfo) return callback();
let user = toProfileJSON(userInfo);
callback(null, user);
});
};
function refreshTokenInfo(refreshTokenId, authURLStem, clientSchema, clientID, clientPassword, callback) {
let authURL = getAuthURL(authURLStem,clientSchema);
request.post({
url: authURL + 'token',
auth: { 'user': clientID, 'pass': clientPassword },
headers: [
{name: 'Content-Type', value: 'application/x-www-form-urlencoded'}
],
form: {
grant_type: 'refresh_token',
refresh_token: refreshTokenId
}
},
function (err, res, body) {
if (err) return callback(err);
if (res.statusCode !== 200 && (res.statusCode < 400 && res.statusCode >= 500)) {
return callback("Authorization server returned " + statusCode);
}
let token;
try {
token = JSON.parse(body);
} catch(e) {
if(logger.error) logger.error("Akima: refreshTokenInfo could not authenticate to " + authURL + " (" + body + ")");
return callback(e);
}
getUserInfo(token.access_token, authURLStem, clientSchema, function (err, user) {
if(err) return callback(err);
if(!user) {
if(logger.debug) logger.debug("Akima: no user associated with " + token.access_token);
return callback();
}
let canonicalToken = {};
canonicalToken.accessTokenIdentifier = token.access_token;
canonicalToken.refreshTokenIdentifier = token.refresh_token;
canonicalToken.refreshAfter = token.refreshAfter;
canonicalToken.expiresAt = token.expiresAt;
canonicalToken.tokenType = token.token_type;
canonicalToken.scope = "*";
callback(null, canonicalToken, user);
});
});
};
function checkToken(token, authURLStem, clientSchema, done) {
let expiresAt = Date.parse(token.params.expiresAt);
let now = Date.now();
if (logger.debug) logger.debug("Akima: check token");
if (now < expiresAt) {
if (logger.debug) logger.debug("Akima: now < expiresAt" + token.params.expiresAt + ")");
let refreshAfter = Date.parse(token.params.refreshAfter);
if (now > refreshAfter) {
if (logger.debug) logger.debug("Akima: now > refreshAfter (" + refreshAfter + ")");
refreshTokenInfo(token.params.refreshTokenIdentifier, authURLStem, clientSchema, client.id, client.secret, function (err, newTokenInfo, user) {
if (err) {
return done(err);
}
if (!user) {
if(logger.debug) logger.debug("Akima: no user following refresh using " + token.params.refreshTokenIdentifier);
return done();
}
if(logger.debug) logger.debug("Akima: refreshed token: ",newTokenInfo.accessTokenIdentifier);
let profile = toProfileJSON(user);
// Avoid a token-store race condition by saving the token locally
// and check for it prior to storing it. Because node JS top is
// singly threaded, this check is fine.
if (!tokenCache.hasOwnProperty(newTokenInfo.accessTokenIdentifier)) {
tokenCache[newTokenInfo.accessTokenIdentifier] = true;
tokenStore[repository.type].removeToken(token.id, function (err) {
if (err) {
return done(err);
}
if (logger.debug) logger.debug("Akima: discarding token: ",token.id);
tokenStore[repository.type].upsertToken(newTokenInfo.accessTokenIdentifier, newTokenInfo, profile, function (err) {
if (err) {
return done(err);
}
user.token = newTokenInfo.accessTokenIdentifier;
done(null, user, {"token": {"id": newTokenInfo.accessTokenIdentifier}, "scope": "*"});
});
});
}
else {
user.token = newTokenInfo.accessTokenIdentifier;
done(null, user, {"token": {"id": newTokenInfo.accessTokenIdentifier}, "scope": "*"});
}
});
}
else {
if (logger.debug) logger.debug("Akima: now <= refreshAfter (" + refreshAfter + ")");
tokenStore[repository.type].getUser(token.user.id, function (err, user) {
if (err) {
return done(err);
}
if(user != null) {
user.token = token.accessTokenIdentifier;
done(null, user, {"token": token, "scope": "*"});
}
else {
/* this can happen if the cache is invalidated: refresh of user info is forced */
getUserInfo(token.accessTokenIdentifier,authURLStem,clientSchema,function(err,user) {
if (err) return done(err);
tokenStore[repository.type].upsertUser(user.id,user,function (err) {
if (err) return done(err);
user.token = newTokenInfo.accessTokenIdentifier;
done(null,user,{"token": {"id": newTokenInfo.accessTokenIdentifier}, "scope": "*"});
});
});
}
});
}
}
else {
if (logger.debug) logger.debug("Akima: now >= expiresAt (" + token.params.expiresAt + "," + token.params.expiresAt, ")");
tokenStore[repository.type].removeToken(token.id, function (err) { done(err, null); });
}
}
function AkimaBearerStrategy(options) {
options = options || {};
options.passReqToCallback = true;
if(options.localMode) this._localMode = true;
BearerStrategy.call(this, options, function (req, tokenId, done) {
if (tokenId == "undefined") {
if (logger.debug) logger.debug("Akima: bearer strategy detected no token");
return done();
}
if (logger.debug) logger.debug("Akima: bearer strategy found token = " + tokenId);
tokenStore[repository.type].getToken(tokenId, function (err, token) {
if (err) {
if (logger.debug) logger.debug("Akima: failed to retrieve token from store " + err);
return done(err);
}
if (!token || !token.params || !token.params.refreshAfter) {
if (logger.debug) logger.debug("Akima: attempting to get token info");
getTokenInfo(tokenId, authServer.internalStem, authServer.clientSchema, function (err, token, user) {
if (err) {
if (logger.debug) logger.debug("Akima: failed to get token info");
return done(null,null,err);
}
if(!token) return done();
if(logger.debug) logger.debug("Akima: storing token info");
tokenStore[repository.type].upsertTokenAndUser(tokenId,token,user,function(err) {
if(err) return done(null,null,err);
if(logger.debug) {
let msg = "Akima: upsert token and user";
if(user != null) msg += " " + user.username;
if(token != null) msg += " " + token.id;
console.log(msg);
}
tokenStore[repository.type].getToken(tokenId,function(err,token) {
if(err) return done(null,null,err);
checkToken(token, authServer.internalStem, authServer.clientSchema, done);
});
});
});
}
else checkToken(token, authServer.internalStem, authServer.clientSchema, done);
});
});
this.name = "akima-bearer";
}
util.inherits(AkimaBearerStrategy, BearerStrategy);
AkimaBearerStrategy.prototype.authenticate = function (req) {
if(req.headers && !req.headers.authorization && this._localMode && authLocal != null) {
let self = this;
if(logger.debug) logger.debug("Akima: AuthLocal User = ",authLocal.user);
getLocalCredentials(authLocal.user, authServer.internalStem, authServer.clientSchema, client.id, client.secret, function(err,token) {
req.headers.authorization = "Bearer " + token;
BearerStrategy.prototype.authenticate.call(self,req);
});
}
else BearerStrategy.prototype.authenticate.call(this,req);
};
function tokenManager(req, res, next) {
if (req && req.cookies && req.cookies.Bearer) {
req.headers.authorization = "Bearer " + req.cookies.Bearer;
}
next();
}
function expireToken(tokenId, authURLStem, clientSchema, callback) {
let authURL = getAuthURL(authURLStem,clientSchema);
request.get({
url: authURL + 'expire',
auth: { 'bearer': tokenId }
},
function (err, res, body) {
if (err) return callback(err);
if (res.statusCode !== 200 && (res.statusCode < 400 && res.statusCode >= 500)) {
return callback("Authorization server returned " + statusCode);
}
callback();
});
};
function relinguishBearerCookie(req, res, callback) {
let tokenId = req.cookies.Bearer;
if(logger.debug) logger.debug("Akima: relinguish bearer cookie: ",tokenId);
res.clearCookie('Bearer');
tokenStore[repository.type].removeToken(tokenId, function (err) {
if (err) return callback(err);
expireToken(tokenId, authServer.internalStem, authServer.clientSchema, callback);
});
}
// TODO there are clauses where done() never gets called
function logout(req, res, done) {
if (req.logout) req.logout(); // Passport.js adds req.logout(), this will clear req.user if it exists
if (req.user) {
tokenStore[repository.type].removeUser(req.user.userId, function (err) {
if (err) {
console.log('Akima: error removing user from tokenStore', req.user, err);
return done(err);
}
if (req.cookies.Bearer) relinguishBearerCookie(req, res, done);
});
}
else if (req.cookies.Bearer) relinguishBearerCookie(req, res, done);
else console.log('Akima: logout found no Bearer cookie', req.cookies);
}
function su(tokenId,userId,newUser,done) {
if (userId != null && tokenId != null) {
setEffectiveUserForToken(authServer.internalStem,authServer.clientSchema,tokenId,newUser,function(err) {
tokenStore[repository.type].removeUser(userId,function(err) {
if(err) console.log('Akima: error removing userId from tokenStore',userId,err);
tokenStore[repository.type].removeToken(tokenId,function(err) {
done(err);
});
});
});
}
else console.log('Akima: su found no Bearer and/or User');
}
function initialize(expressApp, options) {
// global var to akima file
app = expressApp;
if (app.config.behind_nginx) {
cookieOptions.proxy = true;
cookieOptions.secure = true;
}
authServer = app.config.akima.authServer;
redisClient = Redis.createClient(app.config.redis);
if(app.config.akima.authLocal) authLocal = app.config.akima.authLocal;
client.id = app.config.akima.clientID;
client.secret = app.config.akima.clientSecret;
client.externalURL = app.config.akima.authorizationExternalHostURL + app.config.appName + "/";
repository = app.config.akima.repository;
app.akimaClient = module.exports;
if (options) {
if (options.logger) logger = options.logger;
else if (options.debug) logger.debug = console.log;
}
sharedSessionId = app.config.akima.sharedSession.id;
tokenStore.initialize(app.config.akima.repository);
app.use(passport.initialize());
app.use(tokenManager);
passport.use(new AkimaBearerStrategy({ localMode:(authLocal != null) }));
if(app.config.akima.authTokenFlow != null) {
passport.use(new AkimaOAuth2Strategy(
{
authorizationURL: getAuthURL(authServer.externalStem,authServer.clientSchema) + app.config.akima.authTokenFlow.authorizationPath,
tokenURL: getAuthURL(authServer.internalStem,authServer.clientSchema) + app.config.akima.tokenPath,
clientID: client.id,
clientSecret: client.secret,
callbackURL: app.config.akima.authTokenFlow.callbackURL,
userProfileURL: getAuthURL(authServer.externalStem,authServer.clientSchema) + app.config.akima.userProfilePath,
passReqToCallback: true
}));
}
app.all("*", function (req, res, next) {
let regex1 = new RegExp("^/" + app.config.appName + "/.*$");
let regex2 = new RegExp("^/" + app.config.appName + "/");
if (req.url.match(regex1)) {
req.url = req.url.replace(regex2, "/");
next();
} else {
next();
}
});
app.get("/auth/callback", passport.authenticate("akima-oauth2", {failureRedirect: "/" + app.config.appName + "/auth/login"}), function (req, res) {
let redirectURL = "/" + req.headers["x-subsite-path"] + "/app" + (app.config.homeUrl || "/");
res.cookie("Bearer", req.token, cookieOptions);
res.redirect(redirectURL);
});
app.get("/auth/login", passport.authenticate("akima-oauth2", {failureRedirect: "/" + app.config.appName + "/auth/login"}), function (req, res) {
res.json(req.user);
});
app.get("/auth/logout", function (req, res, next) {
logout(req, res, function (err) {
// TODO consider notifying user about error
if (err && logger.error) logger.error("Akima: error performing logout " + err);
res.redirect('/oauth/main/logout');
});
});
app.post("/auth/send-password-reset", passport.authenticate('akima-bearer'), function(req, res, next) {
let username = req.user.username;
sendPasswordResetLink(username, client.id, client.secret, authServer.externalStem, authServer.clientSchema, function() { res.send('reset link sent for ' + username); });
});
return {
getTokenExpiration: function (tokenId, callback) {
getTokenInfo(tokenId, authServer.internalStem, authServer.clientSchema, function (err, token) {
if (err) {
return callback(err);
}
return callback(null, token.expiresAt);
});
}
};
}
function formatUserInfo(user) {
let fnln = user.displayName.split(" ");
let userInfo = {};
userInfo.akima_user_id = user.id;
userInfo.first_name = fnln[0];
userInfo.last_name = fnln[1];
userInfo.email = user.emails[0].value;
userInfo.username = user.username;
userInfo.misc_flags = user.properties;
return userInfo;
}
function getSKey(userId,userCollection) {
if(userCollection != null) return sharedSessionId + "-" + userCollection + "-" + userId;
return sharedSessionId + "-" + userId;
}
function updateValidity(channel,message) {
let messageObject = JSON.parse(message);
if (logger.debug) logger.debug("Akima: updating validity of " + messageObject.userCollection + " to " + messageObject.validAt);
ucValid[messageObject.userCollection] = messageObject.validAt;
}
function mergeWithCache(userId, userCollection, obj) {
if (sharedSessionId && userId && userCollection) {
let sKey = getSKey(userId,userCollection);
let now = new Date();
if (logger.debug) logger.debug("Akima: merging shared session object for " + sKey);
if (!tokenObjectCache[sKey]) {
tokenObjectCache[sKey] = { obj:obj, validAt:now.valueOf() };
if (logger.debug) logger.debug("Akima: saving shared session object to redis using sKey " + sKey);
redisClient.set(sKey, JSON.stringify(tokenObjectCache[sKey]), function (err) {
if (err && logger.error) logger.error("Akima: failed to save shared object " + err);
else if(userCollection) redisClient.publish(sharedSessionId,JSON.stringify({ userCollection:userCollection, validAt:tokenObjectCache[sKey].validAt }));
});
return obj;
}
else {
merge.recursive(tokenObjectCache[sKey].obj, obj);
tokenObjectCache[sKey].validAt = now.valueOf();
if (logger.debug) logger.debug("Akima: saving shared session object to redis using sKey " + sKey);
redisClient.set(sKey, JSON.stringify(tokenObjectCache[sKey]), function (err) {
if (err && logger.error) logger.error("Akima: failed to save shared object " + err);
else if(userCollection) redisClient.publish(sharedSessionId,JSON.stringify({ userCollection:userCollection, validAt:tokenObjectCache[sKey].validAt }));
});
return tokenObjectCache[sKey].obj;
}
}
}
function getSharedObject(sKey, done) {
if (logger.debug) logger.debug("Akima: retrieving session object from redis for session id " + sKey);
redisClient.get(sKey, function (err, res) {
if (err) {
if (logger.debug) logger.debug("Akima: error while retrieving session object from redis ");
return done(err);
}
if (logger.debug) logger.debug("Akima: retrieved session object");
if (res) tokenObjectCache[sKey] = JSON.parse(res);
else if(tokenObjectCache[sKey] != null) tokenObjectCache[sKey] = undefined;
done();
});
}
function checkSharedSessionObjectCache(req, userInfo, next) {
let userId = userInfo.akima_user_id;
let userCollection = req.headers['x-subsite-path'];
let sKey = getSKey(userId,userCollection);
if (tokenObjectCache[sKey] != null && (userCollection == null || ucValid[userCollection] == null || ucValid[userCollection] < tokenObjectCache[sKey].validAt)) {
if (logger.debug) logger.debug("Akima: inserting object cache for user " + sKey);
if (logger.debug) logger.debug("Akima: copying preexisting cache object");
next(null,tokenObjectCache[sKey].obj);
}
else {
getSharedObject(sKey, function (err) {
let obj;
if (err) return next(err);
if (logger.debug) logger.debug("Akima: inserting object cache for user " + sKey);
if(tokenObjectCache[sKey] != null) {
if(logger.debug) logger.debug("Akima: transferring cache object from redis");
obj = tokenObjectCache[sKey].obj;
}
else
{
let now = Date.now();
if (logger.debug) logger.debug("Akima: initializing new cache object");
obj = {};
tokenObjectCache[sKey] = { obj:obj, validAt:now.valueOf() };
}
if(!redisSubscriber) {
redisSubscriber = Redis.createClient(app.config.redis);
redisSubscriber.subscribe(sharedSessionId,function(err,count) {
if(err) {
if(logger.debug) logger.debug("Akima: could not subscribe to " + sharedSessionId + ": " + err);
} else {
if(logger.debug) logger.debug("Akima: Subscribing to " + sharedSessionId);
redisSubscriber.on('message',updateValidity);
}
next(null,obj);
});
}
else next(null,obj);
});
}
}
function doSubsite(req,config,options) {
let loginURL;
if (options && config.akima.useLegacyOpenACSLogin && options.useSubsiteSchema) {
if(!req.headers['x-subsite-path'] && config.akima.authLocal) req.headers['x-subsite-path'] = config.akima.authLocal.subsite;
loginURL = "/" + req.headers['x-subsite-path'];
} else {
loginURL = "/" + app.config.appName + "/auth/login";
}
return loginURL;
}
function addRedirect(route, verb, options) {
app[verb](route, function (req, res, next) {
if (req.session) {
if (!req.session.akima) req.session.akima = {};
req.session.akima.returnTo = req.path;
}
next();
});
app[verb](route, function (req, res, next) {
if (logger.debug) logger.debug("Akima: Authenticate non ajax request");
passport.authenticate("akima-bearer", function (err, user, info) {
if (err) {
if (logger.debug) logger.debug("Akima: Passport error " + err);
return next(err);
}
let loginURL = doSubsite(req,app.config,options);
if (!user) {
if (logger.debug) logger.debug("Akima: unauthorized (user not found) redirecting to " + loginURL);
if(req.cookies && req.cookies.Bearer) res.clearCookie('Bearer');
return res.redirect(loginURL);
}
// See the websocket comment on why this must be a local variable
let userInfo = formatUserInfo(user);
if (info.token && info.token.id) {
if (logger.debug) logger.debug("Akima: creating bearer cookie from token");
if (logger.debug) logger.debug("Akima: token = " + info.token.id);
let localCookieOptions = JSON.parse(JSON.stringify(cookieOptions));
localCookieOptions.maxAge = 1000000;
res.cookie("Bearer", info.token.id, localCookieOptions);
userInfo.token = info.token.id;
}
if (userInfo.akima_user_id) {
if (app.config.akima && app.config.akima.sharedSession && app.config.akima.sharedSession.id) {
checkSharedSessionObjectCache(req,userInfo,function(err,userShared) {
if(logger.debug) logger.debug("Akima: Authentication done");
req.session.user_info = userInfo;
if(err) return next(err);
req.session.user_info.shared = userShared;
next();
});
}
else {
if (logger.error) logger.error("config.akima.sharedSession.id is not defined");
if (logger.debug) logger.debug("Akima: Authentication done");
req.session.user_info = userInfo;
next("sharedSessionId undefined");
}
} else {
if (logger.debug) logger.debug("Akima: Authentication done");
req.session.user_info = userInfo;
next();
}
})(req, res, next);
});
}
function addAuthentication(route, verb, options) {
app[verb](route, function (req, res, next) {
if (logger.debug) logger.debug("Akima: Authenticate ajax request");
passport.authenticate("akima-bearer", function (err, user, info) {
if (err) {
if (logger.error) logger.error("Akima: Passport error " + err);
return next(err);
}
let loginURL = doSubsite(req,app.config,options);
if (!user) {
if (logger.debug) logger.debug("Akima: unauthorized (user not found) setting X-Akima-Login-URL to " + loginURL);
if(req.cookies && req.cookies.Bearer) res.clearCookie('Bearer');
res.statusCode = 401;
res.header("X-Akima-Login-URL", loginURL);
res.end("Unauthorized");
}
else {
// See the websocket comment on why this must be a local variable
let userInfo = formatUserInfo(user);
req.session.user_info = formatUserInfo(user);
if (info.token && info.token.id) {
if (logger.debug) logger.debug("Akima: creating bearer cookie from token");
if (logger.debug) logger.debug("Akima: token = " + info.token.id);
res.cookie("Bearer", info.token.id, cookieOptions);
userInfo.token = info.token.id;
}
if(userInfo.akima_user_id) {
if (app.config.akima && app.config.akima.sharedSession && app.config.akima.sharedSession.id) {
checkSharedSessionObjectCache(req,userInfo,function(err,userShared) {
if(logger.debug) logger.debug("Akima: Authentication done");
req.session.user_info = userInfo;
if(err) return next(err);
req.session.user_info.shared = userShared;
next();
});
}
else {
if (logger.error) logger.error("config.akima.sharedSession.id is not defined");
if (logger.debug) logger.debug("Akima: Authentication done");
req.session.user_info = userInfo;
next("sharedSessionId undefined");
}
} else {
if (logger.debug) logger.debug("Akima: Authentication done");
req.session.user_info = userInfo;
next();
}
}
})(req, res, next);
});
}
function authenticateWebSocket(req,res,next) {
if (logger.debug) logger.debug("Akima: Authenticate websocket request");
if (req && req.cookies && req.cookies.Bearer) {
req.headers.authorization = "Bearer " + req.cookies.Bearer;
}
passport.authenticate("akima-bearer", function (err, user, info) {
if (err) {
if (logger.error) logger.error("Akima: Passport error " + err);
return next(err);
}
let loginURL = doSubsite(req,app.config,{ useSubsiteSchema:true });
if (!user) {
if (logger.debug) logger.debug("Akima: unauthorized (user not found)");
if(req.cookies && req.cookies.Bearer && typeof res.clearCookie == 'function') res.clearCookie('Bearer');
next("Unauthorized");
} else {
// Be careful here and only update session atomically. Javascript is single threaded
// but it's also asynchronous. So if a global has it's value updated across an asynchronous
// call then it can be used half-initialized. Avoid this by only setting the global
// only at the end.
let userInfo = formatUserInfo(user);
if (info.token && info.token.id) {
if (logger.debug) logger.debug("Akima: token = " + info.token.id);
userInfo.token = info.token.id;
}
if (userInfo.akima_user_id != null) {
if (app.config.akima && app.config.akima.sharedSession && app.config.akima.sharedSession.id) {
checkSharedSessionObjectCache(req,userInfo,function(err,userShared) {
if(logger.debug) logger.debug("Akima: Authentication done");
req.session.user_info = userInfo;
if(err) return next(err);
req.session.user_info.shared = userShared;
next(null,info.token.id);
});
}
else {
if(logger.error) logger.error("config.akima.sharedSession.id is not defined");
if(logger.debug) logger.debug("Akima: Authentication done");
req.session.user_info = userInfo;
next("sharedSessionId undefined");
}
}
else {
if(logger.debug) logger.debug("Akima: Authentication done");
req.session.user_info = userInfo;
next(null,info.token.id);
}
}
})(req, res, next);
}
module.exports =
{
tokenManager: tokenManager,
OAuth2Strategy: AkimaOAuth2Strategy,
BearerStrategy: AkimaBearerStrategy,
initialize: initialize,
getTokenInfo: getTokenInfo,
get: function (route, options) {
if (!options) options = {};
if(options.useSubsiteSchema == null) options.useSubsiteSchema = true;
if (options.doRedirect) addRedirect(route, 'get', options);
else addAuthentication(route, 'get', options);
},
post: function (route, options) {
if (!options) options = {};
if(options.useSubsiteSchema == null) options.useSubsiteSchema = true;
if (options.doRedirect) addRedirect(route, 'post', options);
else addAuthentication(route, 'post', options);
},
all: function (route, options) {
if (!options) options = {};
if(options.useSubsiteSchema == null) options.useSubsiteSchema = true;
if (options.doRedirect) addRedirect(route, 'all', options);
else addAuthentication(route, 'all', options);
},
logout: logout,
su: su,
store: function (userId, userCollection, obj) {
return mergeWithCache(userId, userCollection, obj);
},
authenticateWebSocket: authenticateWebSocket
};