UNPKG

akima-client

Version:

Client side support for the Akima oauth2 framework

951 lines (814 loc) 33.7 kB
"use strict"; 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 };