@earnaha/auth0-action-helper
Version:
AHA auth0 action helper
384 lines (350 loc) • 10.6 kB
JavaScript
/* eslint-disable camelcase */
/* eslint-disable no-console */
const { customAlphabet } = require('nanoid'); // TIP: nanoid 4 cannot use require statement!
const BaseHelper = require('./base.js');
class PostLoginHelper extends BaseHelper {
emailToNames(email) {
const [validDisplayName] = email.split('@');
// TIP: symbols # & + are not allowed for using in url (public sharing link)
// the new username from the first part of an email converts all symbols to dot.
const noSymbolLowerCaseName = validDisplayName
.replace(/[^a-zA-Z0-9]/g, ' ')
.trim()
.replace(/\s/g, '.')
.replace(/(\.)+/g, '.')
.toLowerCase();
const randomNumber = customAlphabet('1234567890', 13);
const validNickName = `${noSymbolLowerCaseName}.${(
new Date().getTime() + Number(randomNumber())
).toString(36)}`;
return {
nickname: validNickName,
name: validDisplayName,
};
}
async login(api, payload) {
const accessKey = await super.getServerAccessKey(api);
const res = await super.axiosPostWithBreadcrumb(
`${super.DOMAIN}/auth/v3/login`,
payload,
{
headers: {
ServerAccessKey: accessKey,
},
},
);
return res;
}
async claimAccount(api, payload) {
const accessKey = await super.getServerAccessKey(api);
const res = super.axiosPostWithBreadcrumb(
`${super.DOMAIN}/auth/v3/member/guest/claim`,
payload,
{
headers: {
ServerAccessKey: accessKey,
},
},
);
return res;
}
async userCount(api) {
const accessKey = await super.getServerAccessKey(api);
const res = await super.axiosGetWithBreadcrumb(
`${super.DOMAIN}/auth/v3/member/count/raw`,
{
headers: {
ServerAccessKey: accessKey,
},
},
);
return res;
}
async checkIfOverwriteEmail(api, payload) {
const state = super.safeEncodeParam(
{
user_id: payload.user_id,
email: payload.email.toLowerCase(),
identities: payload.identities,
},
'state',
);
const accessKey = await super.getServerAccessKey(api);
const res = await super.axiosGetWithBreadcrumb(
`${super.DOMAIN}/auth/v3/login/existing-check?${state}`,
{
headers: {
ServerAccessKey: accessKey,
},
},
);
return res;
}
async updateReferrer(referrer, token) {
if (referrer?.auth0Id && !referrer.auth0Id.startsWith('guest')) {
const referrerUser = await super.getUserById(
referrer.auth0Id,
token,
);
super.LOG.debug(
{ referrer: referrerUser },
`PostLogin.updateReferrer | referrer's profile in auth0`,
);
if (referrerUser?.data && referrer?.userMetadata) {
const referrerRes = await super.updateUserMetadata(
referrerUser?.data,
super.parseJson(referrer.userMetadata, {}),
token,
);
super.LOG.debug(
{ referrer: referrerRes?.data || referrerRes },
`PostLogin.updateReferrer | referrer's userMetadata updated result`,
);
}
}
}
async updateByMemberPayload(auth0User, memberPayload, token) {
if (!auth0User || !memberPayload || !token) {
return null;
}
super.LOGGER.setMeta({
auth0Id: auth0User.user_id,
email: auth0User.email,
});
const updateValues = {
user_metadata: {
...super.parseJson(memberPayload.userMetadata, {}),
memberId: memberPayload.id,
},
};
const isNamePwdAuth = super.isNamePwdAuthUser(auth0User);
if (
isNamePwdAuth === true &&
auth0User.nickname !== memberPayload.nickname
) {
updateValues.nickname = memberPayload.nickname;
}
if (isNamePwdAuth === true && auth0User.name !== memberPayload.name) {
updateValues.name = memberPayload.name;
}
const updateRes = await super.updateUser(
auth0User,
updateValues,
token,
);
super.LOG.debug(
{ updateRes: updateRes?.data },
`PostLogin.updateByMemberPayload | update results`,
);
return updateRes?.data;
}
memberPayload(event, majorUser) {
if (!event || !majorUser || !majorUser.user_id) {
return null;
}
const { request, transaction, authorization } = event;
super.LOGGER.setMeta({
auth0Id: majorUser.user_id,
email: majorUser.email,
});
const resData = {
auth0Id: majorUser.user_id,
email: majorUser.email,
nickname: majorUser.nickname,
name: majorUser.name,
picture: majorUser.picture,
loginsCount: (majorUser.logins_count || 0) + 1,
lastIp: request?.ip,
emailVerified: majorUser.email_verified,
scope: (transaction?.requested_scopes || []).join(' '),
appMetadata: majorUser.app_metadata,
userMetadata: majorUser.user_metadata || {},
roles: authorization?.roles,
identities: majorUser.identities,
language: transaction?.locale,
geoip: request?.geoip,
service: super.SERVICE,
phoneNumber: majorUser.phone_number,
phoneNumberVerified: majorUser.phone_verified,
};
// change to valid names, basically for brand new users
const isValidNickname = super.isValidUserName(majorUser.nickname);
if (isValidNickname !== true) {
const namesFromEmail = this.emailToNames(majorUser.email);
resData.nickname = namesFromEmail.nickname;
resData.name = namesFromEmail.name;
}
super.LOG.debug(
{ resData },
`PostLogin.memberPayload | return payload`,
);
return resData;
}
/**
* https://auth0.com/docs/customize/actions/flows-and-triggers/login-flow
* Handler that will be invoked when this action is resuming after an external redirect. If your
* onExecutePostLogin function does not perform a redirect, this function can be safely ignored.
* @param {Event} event - Details about the user and the context in which they are logging in.
* @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
*/
async exec(event, api) {
super.LOGGER.setMeta({
auth0Id: event?.user?.user_id,
email: event?.user?.email,
});
try {
if (!event || !event?.user || !event?.user?.user_id) {
return api.access.deny('Invalid auth event');
}
const userToken = await super.getUserToken();
const majorUser = await super.getMajorUser(event, userToken, true);
if (!majorUser || !majorUser?.user_id) {
return api.access.deny('Primary user not found');
}
const isThirdPartyAuth = super.isThirdPartyAuthUser(majorUser);
if (isThirdPartyAuth === true) {
const checkRes = await this.checkIfOverwriteEmail(
api,
majorUser,
);
// TIP: this API may update Members.email
// we should ensure it executes successfully
if (!(checkRes && checkRes.data)) {
return api.access.deny('Email validates failed');
}
}
// for calling api-auth apis
const payload = this.memberPayload(event, majorUser);
super.LOG.debug({ payload }, `PostLogin.exec | member payload`);
const queryParams = super.organizeEventReqQueryParam(event);
super.LOG.debug(
{ queryParams },
`PostLogin.exec | query parameters`,
);
if (queryParams) {
payload.queryParams = queryParams;
payload.userMetadata = {
...payload.userMetadata,
...queryParams,
};
}
let authRes = null;
// claim existing guest account
if (queryParams?.claimUsername) {
const { data: claimRes } = await this.claimAccount(api, {
claimUsername: queryParams.claimUsername,
memberPayload: payload,
});
super.LOG.debug(
{ claimRes },
`PostLogin.exec | claim account results`,
);
if (claimRes && claimRes.data) {
authRes = claimRes.data;
}
}
// general login or signup or claimAccount failed
if (authRes === null) {
const { data: loginRes } = await this.login(api, payload);
super.LOG.debug(
{ loginRes },
`PostLogin.exec | login or signup results`,
);
if (loginRes && loginRes.data) {
authRes = loginRes.data;
}
}
super.LOG.debug({ authRes }, `PostLogin.exec | api-auth response`);
if (authRes === null) {
return api.access.deny('Login failed');
}
const {
memberLogin,
member,
referrer,
removeAuth0UserId,
isSignup,
} = authRes;
/*
const updateRes = await this.updateByMemberPayload(
majorUser,
member,
userToken,
);
super.LOG.debug(
{ updateRes },
`PostLogin.exec | update primary user results`,
);
*/
// Not gonna wait
this.updateByMemberPayload(majorUser, member, userToken);
/*
const updatedRef = await this.updateReferrer(referrer, userToken);
super.LOG.debug(
{ updatedRef },
`PostLogin.exec | update referrer results`,
);
*/
// Not gonna wait
this.updateReferrer(referrer, userToken);
if (removeAuth0UserId) {
const delRes = await super.deleteUserById(
removeAuth0UserId,
userToken,
);
super.LOG.debug(
{ delRes },
`PostLogin.exec | remove guest results`,
);
}
// -------------- handle session data (return to frontend) --------------------
if (event.authorization) {
api.idToken.setCustomClaim(`roles`, event.authorization.roles);
api.accessToken.setCustomClaim(
`roles`,
event.authorization.roles,
);
}
api.idToken.setCustomClaim('accessToken', memberLogin.accessToken);
api.idToken.setCustomClaim(
`provider`,
payload.identities[0].provider,
);
api.idToken.setCustomClaim(`memberId`, member.id);
// api.idToken.setCustomClaim( `userMetadata`, super.parseJson(member.userMetadata, {}) );
api.idToken.setCustomClaim(`nickname`, member.nickname);
api.idToken.setCustomClaim(`name`, member.name);
api.accessToken.setCustomClaim(`memberId`, member.id);
api.user.setUserMetadata('memberId', member.id);
api.authentication.setPrimaryUser(member.auth0Id);
api.idToken.setCustomClaim(`isSignup`, isSignup);
const resData = {
member: {
...member,
// TIP: the returning data will be saved in cookie,
// therefore we have to prevent too large data size.
appMetadata: undefined, // super.parseJson(member.appMetadata),
userMetadata: undefined, // super.parseJson(member.userMetadata),
identities: undefined, // super.parseJson(member.identities),
roles: super.parseJson(member.roles),
geoip: super.parseJson(member.geoip),
email_verified: member.auth0Id.includes('sms|')
? true
: member.emailVerified,
...memberLogin,
},
isSignup,
};
super.LOG.info({ resData }, `PostLogin.exec | done`);
return resData;
} catch (e) {
super.LOG.error(
{ error: e?.response?.data || e?.message || `${e}` },
`PostLogin.exec | caught exception`,
);
super.SENTRY.captureException(e);
return api.access.deny(e.message);
}
}
}
module.exports = PostLoginHelper;