feat: import auth0 accounts into supertokens when doing a password reset (#349)

* feat: import auth0 accounts into supertokens when doing a password reset

* refactor: more detailed error messages
This commit is contained in:
Laurin Quast 2022-09-09 14:36:18 +02:00 committed by GitHub
parent 45946f0c5e
commit 7b9c2ec90a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 141 additions and 0 deletions

View file

@ -46,6 +46,7 @@ export interface PersistedOperationSelector extends ProjectSelector {
export interface Storage {
getUserBySuperTokenId(_: { superTokensUserId: string }): Promise<User | null>;
setSuperTokensUserId(_: { auth0UserId: string; superTokensUserId: string; externalUserId: string }): Promise<void>;
getUserWithoutAssociatedSuperTokenIdByAuth0Email(_: { email: string }): Promise<User | null>;
getUserById(_: { id: string }): Promise<User | null>;
createUser(_: {

View file

@ -169,6 +169,7 @@ export interface User {
provider: AuthProvider;
superTokensUserId: string | null;
isAdmin: boolean;
externalAuthUserId: string | null;
}
export interface Member {

View file

@ -25,6 +25,10 @@ const LegacySetUserIdMappingPayloadModel = zod.object({
superTokensUserId: zod.string(),
});
const LegacyCheckAuth0EmailUserExistsPayloadModel = zod.object({
email: zod.string(),
});
export async function main() {
Sentry.init({
serverName: 'api',
@ -302,6 +306,32 @@ export async function main() {
reply.status(200).send(); // eslint-disable-line @typescript-eslint/no-floating-promises -- false positive, FastifyReply.then returns void
},
});
server.route({
method: 'POST',
url: '/__legacy/check_auth0_email_user_without_associated_supertoken_id_exists',
async handler(req, reply) {
if (req.headers['x-authorization'] !== authLegacyAPIKey) {
reply.status(401).send({ error: 'Invalid update user id mapping key.', code: 'ERR_INVALID_KEY' }); // eslint-disable-line @typescript-eslint/no-floating-promises -- false positive, FastifyReply.then returns void
return;
}
const { email } = LegacyCheckAuth0EmailUserExistsPayloadModel.parse(req.body);
const user = await storage.getUserWithoutAssociatedSuperTokenIdByAuth0Email({
email,
});
await reply.status(200).send({
user: user
? {
id: user.id,
email: user?.email,
auth0UserId: user.externalAuthUserId,
}
: null,
});
},
});
}
if (process.env.METRICS_ENABLED === 'true') {

View file

@ -84,6 +84,7 @@ export async function createStorage(connection: string): Promise<Storage> {
fullName: user.full_name,
displayName: user.display_name,
isAdmin: user.is_admin ?? false,
externalAuthUserId: user.external_auth_user_id ?? null,
};
}
@ -294,6 +295,25 @@ export async function createStorage(connection: string): Promise<Storage> {
return null;
},
async getUserWithoutAssociatedSuperTokenIdByAuth0Email({ email }) {
const user = await pool.maybeOne<Slonik<users>>(sql`
SELECT
*
FROM
public."users"
WHERE
"email" = ${email}
AND "supertoken_user_id" IS NULL
AND "external_auth_user_id" LIKE 'auth0|%'
LIMIT 1
`);
if (user) {
return transformUser(user);
}
return null;
},
async setSuperTokensUserId({ auth0UserId, superTokensUserId, externalUserId }) {
await pool.query(sql`
UPDATE

View file

@ -6,6 +6,7 @@ import type { TypeInput as ThirdPartEmailPasswordTypeInput } from 'supertokens-n
import { fetch } from 'cross-undici-fetch';
import { appInfo } from './app-info';
import zod from 'zod';
import * as crypto from 'crypto';
import { createTRPCClient } from '@trpc/client';
import type { EmailsApi } from '@hive/emails';
@ -180,6 +181,43 @@ export const backendConfig = (): TypeInput => {
const getAuth0Overrides = (config: LegacyAuth0ConfigEnabled) => {
const override: ThirdPartEmailPasswordTypeInput['override'] = {
apis(originalImplementation) {
return {
...originalImplementation,
async generatePasswordResetTokenPOST(input) {
const email = input.formFields.find(formField => formField.id === 'email')?.value;
if (email) {
// We first use the existing implementation for looking for users within supertokens.
const users = await ThirdPartyEmailPasswordNode.getUsersByEmail(email);
// If there is no email/password SuperTokens user yet, we need to check if there is an Auth0 user for this email.
if (users.some(user => user.thirdParty == null) === false) {
// RPC call to check if email/password user exists in Auth0
const dbUser = await checkWhetherAuth0EmailUserWithoutAssociatedSuperTokensIdExists(config, { email });
if (dbUser) {
// If we have this user within our database we create our new supertokens user
const newUserResult = await ThirdPartyEmailPasswordNode.emailPasswordSignUp(
dbUser.email,
await generateRandomPassword()
);
if (newUserResult.status === 'OK') {
// link the db record to the new supertokens user
await setUserIdMapping(config, {
auth0UserId: dbUser.auth0UserId,
supertokensUserId: newUserResult.user.id,
});
}
}
}
}
return await originalImplementation.generatePasswordResetTokenPOST!(input);
},
};
},
functions(originalImplementation) {
return {
...originalImplementation,
@ -358,6 +396,45 @@ async function setUserIdMapping(
}
}
const CheckAuth0EmailUserExistsResponseModel = zod.object({
user: zod.nullable(zod.object({ id: zod.string(), email: zod.string(), auth0UserId: zod.string() })),
});
/**
* Check whether a specific user that SIGNED UP VIA EMAIL and password THAT DOES NOT YET EXIST IN SUPER TOKENS exists in the database as an Auth0 user.
*/
async function checkWhetherAuth0EmailUserWithoutAssociatedSuperTokensIdExists(
config: LegacyAuth0ConfigEnabled,
params: { email: string }
): Promise<zod.TypeOf<typeof CheckAuth0EmailUserExistsResponseModel>['user']> {
const response = await fetch(
config['AUTH_LEGACY_AUTH0_INTERNAL_API_ENDPOINT'] +
'/check_auth0_email_user_without_associated_supertoken_id_exists',
{
method: 'POST',
headers: {
'content-type': 'application/json',
'x-authorization': config['AUTH_LEGACY_AUTH0_INTERNAL_API_KEY'],
},
body: JSON.stringify({
email: params.email,
}),
}
);
const body = await response.text();
if (response.status !== 200) {
throw new Error(
`Failed to check whether the Auth0 email user without an associated supertokenId exists. Status: ${response.status}. Body: ${body}`
);
}
const { user } = CheckAuth0EmailUserExistsResponseModel.parse(JSON.parse(body));
return user;
}
/**
* Generate a Auth0 access token that is required for making API calls to Auth0.
*/
@ -381,3 +458,15 @@ const generateAuth0AccessToken = async (config: LegacyAuth0ConfigEnabled): Promi
return JSON.parse(body).access_token;
};
async function generateRandomPassword(): Promise<string> {
return await new Promise<string>((resolve, reject) =>
crypto.randomBytes(20, (err, buf) => {
if (err) {
reject(err);
return;
}
resolve(buf.toString('hex'));
})
);
}