UNPKG

@earnaha/auth0-action-helper

Version:
811 lines (729 loc) 19.5 kB
/* eslint-disable no-await-in-loop */ /* eslint-disable no-console */ const axios = require('axios'); const Sentry = require('@sentry/node'); const LoggerHelper = require('./logger.js'); const { version, name } = require('../package.json'); const SIGN_UP_BY_SMS_DOMAIN = '@sign-up-by-sms.com'; class BaseHelper { // TIP: the input arguments must be destructed // otherwise, this parent class cannot get any data in constructor constructor({ ENV, SERVICE, DOMAIN, ACCESS_KEY, ACCESS_SALT, AUTH0_DOMAIN, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, SENTRY_DSN, SENTRY_TRACES_SAMPLE_RATE, LINK_ACCOUNT_TIME, OPEN_SEARCH_NODE, DEBUG_LEVEL, }) { const env = ENV || 'dev'; const isPROD = env === 'prod' || env === 'production'; // TIP: service = SERVICE || isPROD ? 'aha' : `aha-${env}`; doesn't work // always gets 'aha', cannot mixing or and ternary statement const service = SERVICE || `aha${isPROD ? '' : `-${env}`}`; const apiDomain = DOMAIN || `https://${isPROD ? 'api' : `api-${env}`}.earnaha.com`; const packageAppNameAndVersion = `${name}@${version}`; if (env !== 'test') { Sentry.init({ dsn: SENTRY_DSN, tracesSampleRate: (typeof SENTRY_TRACES_SAMPLE_RATE === 'string' ? parseFloat(SENTRY_TRACES_SAMPLE_RATE) : SENTRY_TRACES_SAMPLE_RATE) || 0.01, }); this._SENTRY = Sentry; } // TIP: setters don't work in constructor, set to internal variables directly this._APP = packageAppNameAndVersion; this._ENV = env; this._SERVICE = service; this._DOMAIN = apiDomain; this._ACCESS_KEY = ACCESS_KEY; this._ACCESS_SALT = Number(ACCESS_SALT); this._AUTH0_DOMAIN = AUTH0_DOMAIN; this._AUTH0_CLIENT_ID = AUTH0_CLIENT_ID; this._AUTH0_CLIENT_SECRET = AUTH0_CLIENT_SECRET; this._LINK_ACCOUNT_TIME = new Date(LINK_ACCOUNT_TIME).getTime(); this._LOGGER = new LoggerHelper(env, packageAppNameAndVersion, { CONSOLE: DEBUG_LEVEL === 'trace', }) .setOpenSearchNode(OPEN_SEARCH_NODE) .setSentryDsn(SENTRY_DSN) .setSentryTracesSampleRate(SENTRY_TRACES_SAMPLE_RATE) .setLogLevel(DEBUG_LEVEL) .setMeta() .initialize(); console.log('env values=>', { OPEN_SEARCH_NODE, DEBUG_LEVEL, }); this._LOGGER.logger.info( { OPEN_SEARCH_NODE, DEBUG_LEVEL, DOMAIN, ACCESS_KEY, ACCESS_SALT, AUTH0_DOMAIN, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, LINK_ACCOUNT_TIME, }, `Base.constructor | initializing arguments`, ); } get APP() { return this._APP; } set APP(value) { this._APP = value; } get ENV() { return this._ENV; } set ENV(value) { this._ENV = value; } get SERVICE() { return this._SERVICE; } set SERVICE(value) { this._SERVICE = value; } get DOMAIN() { return this._DOMAIN; } set DOMAIN(value) { this._DOMAIN = value; } get ACCESS_KEY() { return this._ACCESS_KEY; } set ACCESS_KEY(value) { this._ACCESS_KEY = value; } get ACCESS_SALT() { return this._ACCESS_SALT; } set ACCESS_SALT(value) { this._ACCESS_SALT = value; } get AUTH0_DOMAIN() { return this._AUTH0_DOMAIN; } set AUTH0_DOMAIN(value) { this._AUTH0_DOMAIN = value; } get AUTH0_CLIENT_ID() { return this._AUTH0_CLIENT_ID; } set AUTH0_CLIENT_ID(value) { this._AUTH0_CLIENT_ID = value; } get AUTH0_CLIENT_SECRET() { return this._AUTH0_CLIENT_SECRET; } set AUTH0_CLIENT_SECRET(value) { this._AUTH0_CLIENT_SECRET = value; } get LINK_ACCOUNT_TIME() { return this._LINK_ACCOUNT_TIME; } set LINK_ACCOUNT_TIME(value) { this._LINK_ACCOUNT_TIME = value; } get SENTRY_DSN() { return this._SENTRY_DSN; } set SENTRY_DSN(value) { this._SENTRY_DSN = value; } get SENTRY_TRACES_SAMPLE_RATE() { return this._SENTRY_TRACES_SAMPLE_RATE; } set SENTRY_TRACES_SAMPLE_RATE(value) { this._SENTRY_TRACES_SAMPLE_RATE = value; } get SENTRY_LOGGER_LEVEL() { return this._SENTRY_LOGGER_LEVEL; } set SENTRY_LOGGER_LEVEL(value) { this._SENTRY_LOGGER_LEVEL = value; } get SENTRY() { return this._SENTRY; } set SENTRY(value) { this._SENTRY = value; } get LOGGER() { return this._LOGGER; } get LOG() { return this._LOGGER.logger; } get DEFAULT_CONN() { return 'Username-Password-Authentication'; } async axiosGetWithBreadcrumb(url, options) { try { const res = await axios.get(url, options); Sentry.addBreadcrumb({ url, options, res }); return res; } catch (error) { this.LOG.error( { url, options, error }, `Base.axiosGetWithBreadcrumb`, ); throw error; } } async axiosPostWithBreadcrumb(url, payload, options) { try { const res = await axios.post(url, payload, options); Sentry.addBreadcrumb({ url, payload, options, res }); return res; } catch (error) { this.LOG.error( { url, payload, options, error }, `Base.axiosPostWithBreadcrumb`, ); throw error; } } async axiosPutWithBreadcrumb(url, payload, options) { try { const res = await axios.put(url, payload, options); Sentry.addBreadcrumb({ url, payload, options, res }); return res; } catch (error) { this.LOG.error( { url, payload, options, error }, `Base.axiosPutWithBreadcrumb`, ); throw error; } } getUtcDate(date = new Date(), addDay = 0) { const msUTC = Date.UTC( date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(), date.getUTCMilliseconds(), ); return new Date(msUTC + addDay * 86400000); } async getAccessToken() { /* const res = await axios.get( `${this.DOMAIN}/auth/v3/tool/server-access-key?service=${this.SERVICE}` + `&accessSalt=${this.ACCESS_SALT}&accessKey=${this.ACCESS_KEY}&roles=auth0` ); */ const memberId = '00000000-0000-0000-0000-000000000000'; const now = this.getUtcDate().getTime(); const tokenString = `${this.ACCESS_KEY}+${memberId}+${this.SERVICE}+${ now + this.ACCESS_SALT }+auth0`; const res = Buffer.from(tokenString, 'ascii').toString('base64'); return res; } async getServerAccessKey(auth0ApiObject) { const cachedKey = auth0ApiObject.cache.get('accessKey'); /* if (!cachedKey) { const accessKey = this.getAccessToken(); if (typeof accessKey === 'string') { auth0ApiObject.cache.set('accessKey', accessKey, { ttl: 50 * 1000, }); } return accessKey; } else { return cachedKey; } */ const accessKey = await this.getAccessToken(); this.LOGGER.setMeta({ auth0Id: auth0ApiObject?.user?.user_id, }); this.LOG.debug( { cachedKey, accessKey }, `Base.getServerAccessKey | cached key checking`, ); auth0ApiObject.cache.set('accessKey', accessKey, { ttl: 50 * 1000, }); return accessKey; } async getUserToken() { const res = await axios.post( `${this.AUTH0_DOMAIN}/oauth/token`, { client_id: this.AUTH0_CLIENT_ID, client_secret: this.AUTH0_CLIENT_SECRET, grant_type: 'client_credentials', audience: `${this.AUTH0_DOMAIN}/api/v2/`, }, { headers: { 'content-type': 'application/x-www-form-urlencoded', }, }, ); return res; } async getUsersByEmail(email, token = null) { if (!email || email.length <= 0) { return null; } const userToken = token || (await this.getUserToken()); const res = await axios.get( `${ this.AUTH0_DOMAIN }/api/v2/users-by-email?email=${encodeURIComponent( email.toLowerCase(), )}`, { headers: { Authorization: `Bearer ${userToken.data.access_token}`, }, }, ); return res; } async getUserById(userId, token = null) { const userToken = token || (await this.getUserToken()); const res = await axios.get( `${this.AUTH0_DOMAIN}/api/v2/users/${userId}`, { headers: { Authorization: `Bearer ${userToken.data.access_token}`, }, }, ); return res; } async deleteUserById(userId, token = null) { const userToken = token || (await this.getUserToken()); const res = await axios.delete( `${this.AUTH0_DOMAIN}/api/v2/users/${userId}`, { headers: { Authorization: `Bearer ${userToken.data.access_token}`, }, }, ); return res; } async updateUser(user, values = {}, token = null) { const userToken = token || (await this.getUserToken()); const res = await axios.patch( `${this.AUTH0_DOMAIN}/api/v2/users/${user.user_id}`, values, { headers: { Authorization: `Bearer ${userToken.data.access_token}`, }, }, ); return res; } async updateUserMetadata(user, values = {}, token = null) { const userToken = token || (await this.getUserToken()); const newMetadata = { ...user.user_metadata, ...values, }; const res = await axios.patch( `${this.AUTH0_DOMAIN}/api/v2/users/${user.user_id}`, { user_metadata: newMetadata, }, { headers: { Authorization: `Bearer ${userToken.data.access_token}`, }, }, ); return res; } async setEmailVerified(user, token = null) { if (user.email.includes(SIGN_UP_BY_SMS_DOMAIN)) { return null; } const userToken = token || (await this.getUserToken()); const res = await axios.patch( `${this.AUTH0_DOMAIN}/api/v2/users/${user.user_id}`, { email_verified: true }, { headers: { Authorization: `Bearer ${userToken.data.access_token}`, }, }, ); return res; } async searchChangePasswordRequestSucceededLogs(event, token = null) { const userToken = token || (await this.getUserToken()); const paramStr = `type:"scpr"` + // Change password request succeeded ` AND connection:"Username-Password-Authentication"` + // event.connection.name ` AND strategy:"auth0"` + // event.connection.strategy ` AND user_name:"${event?.user_name || event?.user?.email}"`; const res = await axios.get( `${this.AUTH0_DOMAIN}/api/v2/logs?q=${encodeURIComponent( paramStr, )}`, { headers: { Authorization: `Bearer ${userToken.data.access_token}`, }, }, ); return res; } async searchSuccessChangePasswordLogs(event, token = null) { const userToken = token || (await this.getUserToken()); const paramStr = `type:"scp"` + // Success Change Password ` AND connection:"Username-Password-Authentication"` + // event.connection.name ` AND strategy:"auth0"` + // event.connection.strategy ` AND user_id:"${event?.user_id || event?.user?.user_id}"`; const res = await axios.get( `${this.AUTH0_DOMAIN}/api/v2/logs?q=${encodeURIComponent( paramStr, )}`, { headers: { Authorization: `Bearer ${userToken.data.access_token}`, }, }, ); return res; } async linkAccount( primaryAccountUserId, secondaryAccountUserId, secondaryAccountProvider, token = null, ) { const userToken = token || (await this.getUserToken()); const res = await axios.post( `${this.AUTH0_DOMAIN}/api/v2/users/${primaryAccountUserId}/identities`, { provider: secondaryAccountProvider, user_id: secondaryAccountUserId, }, { headers: { Authorization: `Bearer ${userToken.data.access_token}`, }, }, ); return res; } isValidUserName(username) { if (username && username.length > 0) { const checkingValue = username.trim().toLowerCase(); if (checkingValue.length > 0 && checkingValue.length <= 36) { return /^[a-z0-9_.]*$/.test(checkingValue); } } return false; } isNamePwdAuthUser(user) { let result = false; if (user && user.user_id) { const userIdNum = user.user_id.split('|').pop(); if (userIdNum && Array.isArray(user.identities)) { const [identity] = user.identities.filter( e => e.user_id === userIdNum, ); if (identity) { result = identity.connection === this.DEFAULT_CONN; } } } return result; } isThirdPartyAuthUser(user) { let result = false; if (user && user.user_id) { const userIdNum = user.user_id.split('|').pop(); if (userIdNum && Array.isArray(user.identities)) { const [identity] = user.identities.filter( e => e.user_id === userIdNum, ); if (identity) { result = identity.connection !== this.DEFAULT_CONN; } } } return result; } async formUsersHavingTheSameEmail(event, userToken) { const resUsers = { eventUser: null, namePwdAuthUsers: [], otherAuthUsers: [], }; if (!event || !event?.user || !event?.user?.email) { return resUsers; } const { user: eventUser } = event; resUsers.eventUser = eventUser; const sameMailUsers = await this.getUsersByEmail( eventUser.email, userToken, ); if (sameMailUsers && Array.isArray(sameMailUsers.data)) { const userAry = sameMailUsers.data.map(u => ({ ...u, create_time_ms: new Date(u.created_at).getTime(), })); userAry.sort((a, b) => a.create_time_ms > b.create_time_ms ? 1 : -1, ); for (let idx = 0; idx < userAry.length; idx += 1) { const checkingUser = userAry[idx]; const isNamePwdAuth = this.isNamePwdAuthUser(checkingUser); if (isNamePwdAuth === true) { resUsers.namePwdAuthUsers.push(checkingUser); } else { resUsers.otherAuthUsers.push(checkingUser); } } } return resUsers; } async prioritizeUsersHavingTheSameEmail(event, userToken) { const resUsers = { primaryUser: null, secondaryUsers: [], isEventUserEqualPrimary: false, }; if (event.connection.name === 'sms') { // eslint-disable-next-line no-param-reassign event.user.email = `${event.user.phone_number.replace( '+', 'n', )}${SIGN_UP_BY_SMS_DOMAIN}`; } if (!event || !event?.user || !event?.user?.email) { return resUsers; } const { eventUser, namePwdAuthUsers, otherAuthUsers } = await this.formUsersHavingTheSameEmail(event, userToken); if (namePwdAuthUsers.length <= 0 && otherAuthUsers.length <= 0) { resUsers.primaryUser = eventUser; resUsers.isEventUserEqualPrimary = true; return resUsers; } // namePwdAuthUsers && otherAuthUsers are sorted by created time asc if (namePwdAuthUsers.length > 0) { let firstVerifiedUser = null; let maxLoginCountUser = null; let firstCreatedUser = null; for (let idx = 0; idx < namePwdAuthUsers.length; idx += 1) { const tempUser = namePwdAuthUsers[idx]; // 3rd priority : first created if (idx === 0) { firstCreatedUser = tempUser; } // 1st priority : email already verified if (tempUser.email_verified === true) { firstVerifiedUser = tempUser; break; } // 2nd priority : max logins_count if ( maxLoginCountUser === null || tempUser.logins_count > maxLoginCountUser.logins_count ) { maxLoginCountUser = tempUser; } } resUsers.primaryUser = firstVerifiedUser || maxLoginCountUser || firstCreatedUser; resUsers.secondaryUsers = namePwdAuthUsers .filter(e => e.user_id !== resUsers.primaryUser.user_id) .concat(otherAuthUsers); } else { const isNamePwdAuth = this.isNamePwdAuthUser(eventUser); if (isNamePwdAuth === true) { resUsers.primaryUser = eventUser; resUsers.secondaryUsers = otherAuthUsers; } else { // first created 3rdAuth user const [firstCreated3rdAuthUser] = otherAuthUsers; resUsers.primaryUser = firstCreated3rdAuthUser; resUsers.secondaryUsers = otherAuthUsers.slice( 1, otherAuthUsers.length, ); } } resUsers.isEventUserEqualPrimary = resUsers.primaryUser?.user_id === eventUser?.user_id; return resUsers; } async getMajorUser(event, userToken, isAutoMerge = true) { const { primaryUser, secondaryUsers } = await this.prioritizeUsersHavingTheSameEmail(event, userToken); const resUser = { ...primaryUser }; if (isAutoMerge !== false && secondaryUsers.length > 0) { for (let idx = 0; idx < secondaryUsers.length; idx += 1) { const secondaryUser = secondaryUsers[idx]; try { const linkedRes = await this.linkAccount( primaryUser.user_id, secondaryUser.user_id, secondaryUser.identities[0].provider, userToken, ); if (linkedRes && linkedRes?.data) { resUser.identities = linkedRes.data; } } catch (linkAccountErr) { this.LOG.error( { primaryUser, secondaryUser, error: linkAccountErr, }, `Base.getMajorUser | link account error`, ); } } } const isNamePwdAuth = this.isNamePwdAuthUser(resUser); if (!isNamePwdAuth && resUser.email_verified !== true) { await this.setEmailVerified(resUser, userToken); } return resUser; } safeEncodeParam(data, key = '') { let resVal = ''; if (!!data && Object.keys(data).length > 0) { const stringifyObj = JSON.stringify(data); const bufferingTxt = Buffer.from(stringifyObj, 'latin1'); const compressedTxt = bufferingTxt.toString('base64'); resVal = encodeURIComponent(compressedTxt); } if (!!key && typeof key === 'string' && key.length > 0) { resVal = `${key}=${resVal}`; } return resVal; } safeDecodeParam(rawTxt) { let resTxt = null; if (rawTxt && rawTxt.length > 0) { const trimmedTxt = rawTxt.trim(); const isEncoded = /%/i.test(trimmedTxt); const decodedTxt = isEncoded ? decodeURIComponent(trimmedTxt) : trimmedTxt; if (decodedTxt && decodedTxt.length > 0) { resTxt = decodedTxt; } } return resTxt; } safeDecompressParam(rawTxt) { let resTxt = null; if (rawTxt && rawTxt.length > 0) { const decodedTxt = this.safeDecodeParam(rawTxt); if (decodedTxt && decodedTxt.length > 0) { const tmpBuffer = Buffer.from(decodedTxt, 'base64'); if (tmpBuffer) { const decompressedTxt = tmpBuffer.toString('latin1'); if (decompressedTxt && decompressedTxt.length > 0) { resTxt = decompressedTxt; } } } } return resTxt; } organizeEventReqQueryParam(event) { const resData = {}; const queryObject = event?.request?.query; if (queryObject) { const refer = this.safeDecompressParam(queryObject.refer); if (refer) { resData.refer = refer; } const country = this.safeDecodeParam(queryObject.country); if (country) { resData.country = country; } const timezone = this.safeDecodeParam(queryObject.timezone); if (timezone) { resData.timezone = timezone; } const action = this.safeDecodeParam(queryObject.action); if (action) { resData.action = action; } const target = this.safeDecodeParam(queryObject.target); if (target) { resData.target = target; } const sourceId = this.safeDecodeParam(queryObject.sourceId); if (sourceId) { resData.sourceId = sourceId; } // for claiming guest account const claimUsername = this.safeDecodeParam( queryObject.claimUsername, ); if (claimUsername) { resData.claimUsername = claimUsername; } const claimUserEmail = this.safeDecompressParam( queryObject.claimUserEmail, ); if (claimUserEmail) { resData.claimUserEmail = claimUserEmail; } const verifiedRedirection = this.safeDecompressParam( queryObject.verifiedRedirection, ); if (verifiedRedirection) { resData.verifiedRedirection = verifiedRedirection; } const isAutoTest = `${queryObject.isAutoTest}`.toLowerCase(); resData.isAutoTest = isAutoTest === 'true'; } return resData; } parseJson(jsonStr, defaultVal) { if (jsonStr === null || jsonStr === undefined) { return defaultVal; } if (typeof jsonStr === 'object') { return jsonStr; } if (typeof jsonStr !== 'string') { return defaultVal; } try { return JSON.parse(jsonStr); } catch (e) { return defaultVal; } } } module.exports = BaseHelper;