UNPKG

@tiledesk/tiledesk-server

Version:
832 lines (638 loc) 32.4 kB
var passportJWT = require("passport-jwt"); var JwtStrategy = passportJWT.Strategy; var ExtractJwt = passportJWT.ExtractJwt; var passportHttp = require("passport-http"); var BasicStrategy = passportHttp.BasicStrategy; var GoogleStrategy = require('passport-google-oidc'); var winston = require('../config/winston'); // var AnonymousStrategy = require('passport-anonymous').Strategy; // load up the user model var User = require('../models/user'); var config = require('../config/database'); // get db config file var Faq_kb = require("../models/faq_kb"); var Project = require('../models/project'); var Subscription = require('../models/subscription'); var Auth = require('../models/auth'); var userService = require('../services/userService'); var UserUtil = require('../utils/userUtil'); var jwt = require('jsonwebtoken'); const url = require('url'); var cacheUtil = require('../utils/cacheUtil'); var cacheEnabler = require("../services/cacheEnabler"); var uniqid = require('uniqid'); const MaskData = require("maskdata"); const maskOptions = { // Character to mask the data. default value is '*' maskWith: "*", // If the starting 'n' digits needs to be unmasked // Default value is 4 unmaskedStartDigits: 3, //Should be positive Integer //If the ending 'n' digits needs to be unmasked // Default value is 1 unmaskedEndDigits: 3 // Should be positive Integer }; var alg = process.env.GLOBAL_SECRET_ALGORITHM; winston.info('Authentication Global Algorithm : ' + alg); // TODO STAMPA ANCHE PUBLIC var configSecret = process.env.GLOBAL_SECRET || config.secret; var pKey = process.env.GLOBAL_SECRET_OR_PUB_KEY; // console.log("pKey",pKey); if (pKey) { configSecret = pKey.replace(/\\n/g, '\n'); } // console.log("configSecret",configSecret); // if (process.env.GLOBAL_SECRET_OR_PUB_KEY) { // console.log("GLOBAL_SECRET_OR_PUB_KEY defined"); // }else { // console.log("GLOBAL_SECRET_OR_PUB_KEY undefined"); // } var maskedconfigSecret = MaskData.maskPhone(configSecret, maskOptions); winston.info('Authentication Global Secret : ' + maskedconfigSecret); var enableGoogleSignin = false; if (process.env.GOOGLE_SIGNIN_ENABLED == "true" || process.env.GOOGLE_SIGNIN_ENABLED == true) { enableGoogleSignin = true; } winston.info('Authentication Google Signin enabled : ' + enableGoogleSignin); var enableOauth2Signin = false; if (process.env.OAUTH2_SIGNIN_ENABLED == "true" || process.env.OAUTH2_SIGNIN_ENABLED == true) { enableOauth2Signin = true; } winston.info('Authentication Oauth2 Signin enabled : ' + enableOauth2Signin); var jwthistory = undefined; try { jwthistory = require('@tiledesk-ent/tiledesk-server-jwthistory'); } catch(err) { winston.info("jwthistory not present", err); } let JWT_HISTORY_ENABLED = false; if (process.env.JWT_HISTORY_ENABLED==true || process.env.JWT_HISTORY_ENABLED=="true") { JWT_HISTORY_ENABLED = true; } winston.debug("JWT_HISTORY_ENABLED: " + JWT_HISTORY_ENABLED); module.exports = function(passport) { // passport.serializeUser(function(user, done) { // console.log("serializeUser"); // done(null, user); // }); // passport.deserializeUser(function(user, done) { // done(null, user); // }); var opts = { // jwtFromRequest: ExtractJwt.fromAuthHeader(), jwtFromRequest: ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderWithScheme("jwt"), ExtractJwt.fromUrlQueryParameter('secret_token')]), //this will help you to pass request body to passport passReqToCallback: true, //https://stackoverflow.com/questions/55163015/how-to-bind-or-pass-req-parameter-to-passport-js-jwt-strategy // secretOrKey: configSecret, secretOrKeyProvider: function (request, rawJwtToken, done) { // winston.info("secretOrKeyProvider ", request ); // if (request.project) { // winston.info("secretOrKeyProvider.request.project.jwtSecret: "+request.project.jwtSecret ); // } // winston.info("secretOrKeyProvider: "+request.project.name ); // winston.info("secretOrKeyProvider: "+rawJwtToken ); var decoded = request.preDecodedJwt winston.debug("decoded: ", decoded); if (!decoded) { //fallback winston.debug("load decoded after: "); decoded = jwt.decode(rawJwtToken); } winston.debug("decoded after: ", decoded); // qui arriva questo // decoded: {"_id":"5ce3ee855c520200176c189e","updatedAt":"2019-05-31T09:50:22.949Z","createdAt":"2019-05-21T12:26:45.192Z","name":"botext","url":"https://tiledesk-v2-simple--andrealeo83.repl.co","id_project":"5ce3d1ceb25ad30017274bc5","trashed":false,"createdBy":"5ce3d1c7b25ad30017274bc2","__v":0,"external":true,"iat":1559297130,"aud":"https://tiledesk.com","iss":"https://tiledesk.com","sub":"5ce3ee855c520200176c189e@tiledesk.com/bot"} if (decoded && decoded.aud) { winston.debug("decoded.aud: " + decoded.aud); const audUrl = new URL(decoded.aud); winston.debug("audUrl: " + audUrl); const path = audUrl.pathname; winston.debug("audUrl path: " + path); const AudienceType = path.split("/")[1]; winston.debug("audUrl AudienceType: " + AudienceType); const AudienceId = path.split("/")[2]; winston.debug("audUrl AudienceId: " + AudienceId); if (AudienceType == "bots") { if (!AudienceId) { winston.error("AudienceId for bots is required: ", decoded); return done(null, null); } winston.debug("bot id AudienceId: " + AudienceId); let qbot = Faq_kb.findById(AudienceId).select('+secret'); if (cacheEnabler.faq_kb) { let id_project = decoded.id_project; winston.debug("decoded.id_project:" + decoded.id_project); qbot.cache(cacheUtil.defaultTTL, id_project + ":faq_kbs:id:" + AudienceId + ":secret") winston.debug('faq_kb AudienceId cache enabled'); } qbot.exec(function (err, faq_kb) { //TODO add cache_bot_here if (err) { winston.error("auth Faq_kb err: ", {error: err, decoded: decoded}); return done(null, null); } if (!faq_kb) { winston.warn("faq_kb not found with id: " + AudienceId, decoded); return done(null, null); } winston.debug("faq_kb: ", faq_kb); // winston.debug("faq_kb.secret: "+ faq_kb.secret ); done(null, faq_kb.secret); }); } else if (AudienceType == "projects") { if (!AudienceId) { winston.error("AudienceId for projects is required: ", decoded); return done(null, null); } winston.debug("project id: " + AudienceId); Project.findOne({_id: AudienceId, status: 100}).select('+jwtSecret') //@DISABLED_CACHE .cache(cacheUtil.queryTTL, "projects:query:id:status:100:"+AudienceId+":select:+jwtSecret") //project_cache .exec(function (err, project) { if (err) { winston.error("auth Project err: ", {error: err, decoded: decoded}); return done(null, null); } if (!project) { winston.warn("Project not found with id: " + AudienceId, decoded); return done(null, null); } winston.debug("project: ", project); winston.debug("project.jwtSecret: " + project.jwtSecret); done(null, project.jwtSecret); }); } else if (AudienceType == "subscriptions") { if (!AudienceId) { winston.error("AudienceId for subscriptions is required: ", decoded); return done(null, null); } winston.debug("Subscription id: " + AudienceId); Subscription.findById(AudienceId).select('+secret').exec(function (err, subscription) { if (err) { winston.error("auth Subscription err: ", {error: err, decoded: decoded}); return done(null, null); } if (!subscription) { winston.warn("subscription not found with id: " + AudienceId, decoded); return done(null, null); } winston.debug("subscription: ", subscription); winston.debug("subscription.secret: " + subscription.secret); done(null, subscription.secret); }); } else if (decoded.aud == "https://tiledesk.com") { winston.debug("configSecret: " + maskedconfigSecret); done(null, configSecret); //pub_jwt pp_jwt } else { winston.debug("configSecret: " + maskedconfigSecret); done(null, configSecret); //pub_jwt pp_jwt } } else { winston.debug("configSecret: " + maskedconfigSecret); done(null, configSecret); //pub_jwt pp_jwt } } }; winston.debug("passport opts: ", opts); passport.use(new JwtStrategy(opts, async(req, jwt_payload, done) => { // passport.use(new JwtStrategy(opts, function(req, jwt_payload, done) { winston.debug("jwt_payload",jwt_payload); // console.log("req",req); // console.log("jwt_payload._doc._id",jwt_payload._doc._id); if (jwt_payload._id == undefined && (jwt_payload._doc == undefined || (jwt_payload._doc && jwt_payload._doc._id==undefined))) { var err = "jwt_payload._id or jwt_payload._doc._id can t be undefined" ; winston.error(err); return done(null, false); } //JWT OLD format const identifier = jwt_payload._id || jwt_payload._doc._id; // const subject = jwt_payload.sub || jwt_payload._id || jwt_payload._doc._id; winston.debug("passport identifier: " + identifier); const subject = jwt_payload.sub; winston.debug("passport subject: " + subject); winston.debug("passport identifier: " + identifier + " subject " + subject); var fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl; winston.debug("fullUrl:"+ fullUrl); winston.debug("req.disablePassportEntityCheck:"+req.disablePassportEntityCheck); if (req && req.disablePassportEntityCheck) { //req can be null // jwt_payload.id = jwt_payload._id; //often req.user.id is used inside code. req.user.id is a mongoose getter of _id // is better to rename req.user.id to req.user._id in all files winston.debug("req.disablePassportEntityCheck enabled"); return done(null, jwt_payload); } winston.debug("jwthistory passport",jwthistory); //TODO check into DB if JWT is revoked if (jwthistory && JWT_HISTORY_ENABLED==true) { var jwtRevoked = await jwthistory.isJWTRevoked(jwt_payload.jti); winston.debug("passport jwt jwtRevoked: "+ jwtRevoked); if (jwtRevoked) { winston.warn("passport jwt is revoked with jti: "+ jwt_payload.jti); return done(null, false); } } if (subject == "bot") { winston.debug("Passport JWT bot"); let qbot = Faq_kb.findOne({_id: identifier}); //TODO add cache_bot_here if (cacheEnabler.faq_kb) { let id_project = jwt_payload.id_project; winston.debug("jwt_payload.id_project:"+jwt_payload.id_project); qbot.cache(cacheUtil.defaultTTL, id_project+":faq_kbs:id:"+identifier) winston.debug('faq_kb cache enabled'); } qbot.exec(function(err, faq_kb) { if (err) { winston.error("Passport JWT bot err", err); return done(err, false); } if (faq_kb) { winston.debug("Passport JWT bot user", faq_kb); return done(null, faq_kb); } else { winston.warn("Passport JWT bot not user"); return done(null, false); } }); // } else if (subject=="projects") { } else if (subject=="subscription") { Subscription.findOne({_id: identifier}, function(err, subscription) { if (err) { winston.error("Passport JWT subscription err", err); return done(err, false); } if (subscription) { winston.debug("Passport JWT subscription user", subscription); return done(null, subscription); } else { winston.warn("Passport JWT subscription not user", subscription); return done(null, false); } }); } else if (subject=="userexternal") { if (jwt_payload) { // const audUrl = new URL(jwt_payload.aud); // winston.info("audUrl: "+ audUrl ); // const path = audUrl.pathname; // winston.info("audUrl path: " + path ); // const AudienceType = path.split("/")[1]; // winston.info("audUrl AudienceType: " + AudienceType ); // const AudienceId = path.split("/")[2]; // winston.info("audUrl AudienceId: " + AudienceId ); // jwt_payload._id = AudienceId + "-" + jwt_payload._id; winston.debug("Passport JWT userexternal", jwt_payload); var userM = UserUtil.decorateUser(jwt_payload); winston.debug("Passport JWT userexternal userM", userM); return done(null, userM ); } else { var err = {msg: "No jwt_payload passed. Its required"}; winston.error("Passport JWT userexternal err", err); return done(err, false); } } else if (subject=="guest") { if (jwt_payload) { winston.debug("Passport JWT guest", jwt_payload); var userM = UserUtil.decorateUser(jwt_payload); winston.debug("Passport JWT guest userM", userM); return done(null, userM ); } else { var err = {msg: "No jwt_payload passed. Its required"}; winston.error("Passport JWT guest err", err); return done(err, false); } } else { winston.debug("Passport JWT generic user"); let quser = User.findOne({_id: identifier, status: 100}) //TODO user_cache_here //@DISABLED_CACHE .cache(cacheUtil.defaultTTL, "users:id:"+identifier) if (cacheEnabler.user) { quser.cache(cacheUtil.defaultTTL, "users:id:"+identifier) winston.debug('user cache enabled'); } quser.exec(function(err, user) { if (err) { winston.error("Passport JWT generic err ", err); return done(err, false); } if (user) { winston.debug("Passport JWT generic user ", user); return done(null, user); } else { winston.debug("Passport JWT generic not user"); return done(null, false); } }); } })); passport.use(new BasicStrategy(function(userid, password, done) { winston.debug("BasicStrategy: " + userid); var email = userid.toLowerCase(); winston.debug("email lowercase: " + email); User.findOne({ email: email, status: 100}, 'email firstname lastname password emailverified id') //TODO user_cache_here. NOT used frequently. ma attento select. ATTENTO QUI NN USEREI LA SELECT altrimenti con JWT ho tuttto USER mentre con basich auth solo aluni campi //@DISABLED_CACHE .cache(cacheUtil.defaultTTL, "users:email:"+email) .exec(function (err, user) { if (err) { // console.log("BasicStrategy err.stop"); return done(err); } if (!user) { return done(null, false); } user.comparePassword(password, function (err, isMatch) { if (isMatch && !err) { // if user is found and password is right create a token // console.log("BasicStrategy ok"); return done(null, user); } else { return done(err); } }); }); })); if (enableGoogleSignin == true) { let googleClientId = process.env.GOOGLE_CLIENT_ID; let googleClientSecret = process.env.GOOGLE_CLIENT_SECRET; let googleCallbackURL = process.env.GOOGLE_CALLBACK_URL || "http://localhost:3000/auth/google/callback"; winston.info("Enabling Google Signin strategy with ClientId: " + googleClientId + " callbackURL: " + googleCallbackURL + " clientSecret: " + googleClientSecret); passport.use(new GoogleStrategy({ clientID: googleClientId, clientSecret: googleClientSecret, callbackURL: googleCallbackURL, }, async function (issuer, profile, cb) { try { winston.debug("issuer: " + issuer); winston.debug("profile", profile); const rawEmail = profile?.emails?.[0]?.value; if (!rawEmail) { winston.warn("Google profile has no email", {issuer: issuer, profileId: profile?.id}); return cb(null, false, {message: "Missing email from Google profile."}); } const email = rawEmail.toLowerCase().trim(); winston.debug("email: " + email); const query = {providerId: issuer, subject: profile.id}; winston.debug("query", query); const cred = await Auth.findOne(query).exec(); winston.debug("cred", cred); if (cred) { // Already linked: return the corresponding user const user = await User.findOne({email: email, status: 100}) .select('email firstname lastname password emailverified id') .exec(); if (!user) { winston.warn("Auth link exists but user not found/active", {email: email, query: query}); return cb(null, false, {message: "User not found."}); } return cb(null, user); } // Not linked yet: try to reuse existing account by email let user = await User.findOne({email: email, status: 100}) .select('email firstname lastname password emailverified id') .exec(); if (!user) { // No existing user -> create one const password = uniqid(); try { user = await userService.signup(email, password, profile.displayName, "", true); } catch (err) { // Race/duplicate: fall back to existing user if (err && (err.code === 11000 || err.code === "E11000")) { user = await User.findOne({email: email, status: 100}).exec(); } else { winston.error("Error signup google", err); return cb(err); } } } if (!user) { return cb(null, false, {message: "User not found."}); } // Ensure Auth link is created (idempotent) await Auth.findOneAndUpdate( query, {$setOnInsert: {providerId: issuer, email: email, subject: profile.id}}, {upsert: true, new: true} ).exec(); return cb(null, user); } catch (err) { winston.error("Google strategy verify error", err); return cb(err); } } )); } if (enableOauth2Signin == true) { const OAuth2Strategy = require('passport-oauth2'); OAuth2Strategy.prototype.userProfile = function (accessToken, done) { winston.debug("accessToken " + accessToken) /* https://stackoverflow.com/questions/66452108/keycloak-get-users-returns-403-forbidden The service account associated with your client needs to be allowed to view the realm users. Go to http://localhost:8080/auth/admin/{realm_name}/console/#/realms/{realm_name}/clients Select your client (which must be a confidential client) In the settings tab, switch Service Account Enabled to ON Click on save, the Service Account Roles tab will appear In Client Roles, select realm_management Scroll through available roles until you can select view_users Click on Add selected You should have something like this : */ // ATTENTION You have to add a client scope after as described here: https://keycloak.discourse.group/t/issue-on-userinfo-endpoint-at-keycloak-20/18461/4 // console.log("this._oauth2", this._oauth2) this._oauth2._useAuthorizationHeaderForGET = true; this._oauth2.get(process.env.OAUTH2_USER_INFO_URL, accessToken, (err, body) => { if (err) { return done(err); } try { winston.debug("body", body); const json = JSON.parse(body); const userInfo = { keycloakId: json.sub, fullName: json.name, firstName: json.given_name, lastName: json.family_name, username: json.preferred_username, email: json.email, // avatar: json.avatar, // realm: this.options.realm, }; winston.debug("userInfo", userInfo); done(null, userInfo); } catch (e) { done(e); } }); }; passport.use(new OAuth2Strategy({ authorizationURL: process.env.OAUTH2_AUTH_URL, tokenURL: process.env.OAUTH2_TOKEN_URL, clientID: process.env.OAUTH2_CLIENT_ID, clientSecret: process.env.OAUTH2_CLIENT_SECRET, callbackURL: process.env.OAUTH2_CALLBACK_URL || "http://localhost:3000/auth/oauth2/callback", scope: ['openid'], }, function (accessToken, refreshToken, params, profile, cb) { winston.debug("params", params); const token = jwt.decode(accessToken); // user id lives in here winston.debug("token", token); const profileInfo = jwt.decode(params.access_token); // user email lives in here winston.debug("profileInfo", profileInfo); winston.debug("profile", profile); winston.debug("accessToken", accessToken); winston.debug("refreshToken", refreshToken); var issuer = token.iss; var email = profile.email; var query = {providerId: issuer, subject: profile.keycloakId}; winston.debug("query", query) Auth.findOne(query, function (err, cred) { winston.debug("cred", cred, err); if (err) { return cb(err); } if (!cred) { // The oauth account has not logged in to this app before. Create a // new user record and link it to the oauth account. var password = uniqid() // signup ( email, password, firstname, lastname, emailverified) { userService.signup(email, password, profileInfo.name || profileInfo.preferred_username, "", true) .then(function (savedUser) { winston.debug("savedUser", savedUser) var auth = new Auth({ providerId: issuer, email: email, subject: profile.keycloakId, }); auth.save(function (err, authSaved) { if (err) { return cb(err); } winston.debug("authSaved", authSaved); return cb(null, savedUser); }); }).catch(function (err) { winston.error("Error signup oauth ", err); return cb(err); }); } else { // The Oauth account has previously logged in to the app. Get the // user record linked to the Oauth account and log the user in. User.findOne({ email: email, status: 100 }, 'email firstname lastname emailverified id', function (err, user) { winston.debug("user", user, err); // winston.debug("usertoJSON()",user.toJSON()); if (err) { winston.error("Error getting user", user, err); return cb(err); } if (!user) { winston.info("User not found", user, err); return cb(null, false); } return cb(null, user); }); } }); } )); } // const KeycloakStrategy = require('@exlinc/keycloak-passport') // // Register the strategy with passport // passport.use( // "keycloak", // new KeycloakStrategy( // { // host: process.env.KEYCLOAK_HOST, // realm: process.env.KEYCLOAK_REALM, // clientID: process.env.KEYCLOAK_CLIENT_ID, // clientSecret: process.env.KEYCLOAK_CLIENT_SECRET, // callbackURL: `${process.env.AUTH_KEYCLOAK_CALLBACK}`, // authorizationURL : `${process.env.KEYCLOAK_HOST}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/auth`, // tokenURL : `${process.env.KEYCLOAK_HOST}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`, // userInfoURL : `${process.env.KEYCLOAK_HOST}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/userinfo` // // authorizationURL: '123', // // tokenURL : '123', // // userInfoURL: '123' // }, // (accessToken, refreshToken, profile, done) => { // const token = jwt.decode(accessToken); // user id lives in here // console.log("token", token); // console.log("profile", profile); // console.log("accessToken", accessToken); // console.log("refreshToken", refreshToken); // var issuer = token.iss; // var email = profile.email; // var query = {providerId : issuer, subject: profile.keycloakId}; // winston.info("query", query) // Auth.findOne(query, function(err, cred){ // winston.info("cred", cred, err); // if (err) { return cb(err); } // if (!cred) { // // The oauth account has not logged in to this app before. Create a // // new user record and link it to the oauth account. // var password = uniqid() // // signup ( email, password, firstname, lastname, emailverified) { // userService.signup(email, password, profile.displayName, "", true) // .then(function (savedUser) { // winston.info("savedUser", savedUser) // var auth = new Auth({ // providerId: issuer, // email: email, // subject: profile.keycloakId, // }); // auth.save(function (err, authSaved) { // if (err) { return cb(err); } // winston.info("authSaved", authSaved); // return cb(null, savedUser); // }); // }).catch(function(err) { // winston.error("Error signup oauth ", err); // return cb(err); // }); // } else { // // The Oauth account has previously logged in to the app. Get the // // user record linked to the Oauth account and log the user in. // User.findOne({ // email: email, status: 100 // }, 'email firstname lastname emailverified id', function (err, user) { // winston.info("user",user, err); // winston.info("usertoJSON()",user.toJSON()); // if (err) { // winston.error("Error getting user",user, err); // return cb(err); // } // if (!user) { // winston.info("User not found",user, err); // return cb(null, false); // } // return done(null, user); // }); // } // }); // } // )); // var OidcStrategy = require('passport-openidconnect').Strategy; // https://github.com/jaredhanson/passport-anonymous // passport.use(new AnonymousStrategy()); // link utili // https://codeburst.io/how-to-implement-openid-authentication-with-openid-client-and-passport-in-node-js-43d020121e87?gi=4bb439e255a7 // https://developer.wordpress.com/docs/oauth2/ // openidconnect // https://docs.simplelogin.io/docs/passport/ // oauth2 /** * BasicStrategy & ClientPasswordStrategy * * These strategies are used to authenticate registered OAuth clients. They are * employed to protect the `token` endpoint, which consumers use to obtain * access tokens. The OAuth 2.0 specification suggests that clients use the * HTTP Basic scheme to authenticate. Use of the client password strategy * allows clients to send the same credentials in the request body (as opposed * to the `Authorization` header). While this approach is not recommended by * the specification, in practice it is quite common. */ /* const ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy; function verifyClient(clientId, clientSecret, done) { db.clients.findByClientId(clientId, (error, client) => { if (error) return done(error); if (!client) return done(null, false); if (client.clientSecret !== clientSecret) return done(null, false); return done(null, client); }); } //passport.use(new BasicStrategy(verifyClient)); passport.use(new ClientPasswordStrategy(verifyClient)); */ };