Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OOB flow to reset email when updateEmail is used. #3096

Merged
merged 18 commits into from
Feb 17, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/emulator/auth/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,49 @@ export function registerHandlers(
const state = getProjectStateByApiKey(apiKey);

switch (mode) {
case "recoverEmail": {
const EXPIRED_LINK_ERROR = {
error: `Your request to revert your email has expired or the link has already been used.`,
instructions:
"Try reverting using this link again. If you're still unsuccessful. You can edit the email in the Emulator UI.",
ssbushi marked this conversation as resolved.
Show resolved Hide resolved
};
const oob = state.validateOobCode(oobCode);
if (oob?.requestType !== "RECOVER_EMAIL") {
return res.status(400).json({
authEmulator: EXPIRED_LINK_ERROR,
ssbushi marked this conversation as resolved.
Show resolved Hide resolved
});
}
if (!req.query.oldEmail) {
return res.status(400).json({
authEmulator: {
error: "missing oldEmail query parameter",
instructions: `To reset the email for ${oob.email}, you must provide the original email to reset the user information.`,
instructions2:
"Please modify the URL to specify the email to reset to, such as ...&oldEmail=YOUR_OLD_EMAIL",
},
});
}
try {
const { email } = setAccountInfoImpl(state, {
email: req.query.oldEmail as string,
oobCode,
});
return res.status(200).json({
authEmulator: { success: `The email has been successfully reset.`, email },
});
} catch (e) {
if (
e instanceof NotImplementedError ||
(e instanceof BadRequestError && e.message === "INVALID_OOB_CODE")
) {
return res.status(400).json({
authEmulator: EXPIRED_LINK_ERROR,
});
} else {
throw e;
}
}
}
case "resetPassword": {
const oob = state.validateOobCode(oobCode);
if (oob?.requestType !== "PASSWORD_RESET") {
Expand Down
83 changes: 69 additions & 14 deletions src/emulator/auth/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -823,7 +823,11 @@ function setAccountInfo(
reqBody: Schemas["GoogleCloudIdentitytoolkitV1SetAccountInfoRequest"],
ctx: ExegesisContext
): Schemas["GoogleCloudIdentitytoolkitV1SetAccountInfoResponse"] {
return setAccountInfoImpl(state, reqBody, { privileged: !!ctx.security?.Oauth2 });
const url = authEmulatorUrl(ctx.req as express.Request);
return setAccountInfoImpl(state, reqBody, {
privileged: !!ctx.security?.Oauth2,
emulatorUrl: url,
});
}

/**
Expand All @@ -832,12 +836,13 @@ function setAccountInfo(
* @param state the current project state
* @param reqBody request with fields to update
* @param privileged whether request is OAuth2 authenticated. Affects validation
* @param emulatorUrl url to the auth emulator instance. Needed for sending OOB link for email reset
* @return the HTTP response body
*/
export function setAccountInfoImpl(
state: ProjectState,
reqBody: Schemas["GoogleCloudIdentitytoolkitV1SetAccountInfoRequest"],
{ privileged = false }: { privileged?: boolean } = {}
{ privileged = false, emulatorUrl = undefined }: { privileged?: boolean; emulatorUrl?: URL } = {}
): Schemas["GoogleCloudIdentitytoolkitV1SetAccountInfoResponse"] {
// TODO: Implement these.
const unimplementedFields: (keyof typeof reqBody)[] = [
Expand Down Expand Up @@ -881,18 +886,35 @@ export function setAccountInfoImpl(
if (reqBody.oobCode) {
const oob = state.validateOobCode(reqBody.oobCode);
assert(oob, "INVALID_OOB_CODE");
if (oob.requestType !== "VERIFY_EMAIL") {
throw new NotImplementedError(oob.requestType);
}
state.deleteOobCode(reqBody.oobCode);

signInProvider = PROVIDER_PASSWORD;
const maybeUser = state.getUserByEmail(oob.email);
assert(maybeUser, "INVALID_OOB_CODE");
user = maybeUser;
updates.emailVerified = true;
if (oob.email !== user.email) {
updates.email = oob.email;
switch (oob.requestType) {
case "VERIFY_EMAIL": {
state.deleteOobCode(reqBody.oobCode);
signInProvider = PROVIDER_PASSWORD;
const maybeUser = state.getUserByEmail(oob.email);
assert(maybeUser, "INVALID_OOB_CODE");
user = maybeUser;
updates.emailVerified = true;
if (oob.email !== user.email) {
updates.email = oob.email;
}
break;
}
case "RECOVER_EMAIL": {
state.deleteOobCode(reqBody.oobCode);
// TODO: Ask if this is needed for RECOVER_EMAIL.
// signInProvider = PROVIDER_PASSWORD;
ssbushi marked this conversation as resolved.
Show resolved Hide resolved
const maybeUser = state.getUserByEmail(oob.email);
assert(maybeUser, "INVALID_OOB_CODE");
user = maybeUser;
assert(reqBody.email, "INVALID_EMAIL");
assert(isValidEmailAddress(reqBody.email), "INVALID_EMAIL");
if (reqBody.email !== user.email) {
updates.email = reqBody.email;
}
break;
}
default:
throw new NotImplementedError(oob.requestType);
}
} else {
if (reqBody.idToken) {
Expand All @@ -907,12 +929,20 @@ export function setAccountInfoImpl(

if (reqBody.email) {
assert(isValidEmailAddress(reqBody.email), "INVALID_EMAIL");
if (!emulatorUrl) {
throw new Error("Internal assertion error: Emulator URL invalid");
ssbushi marked this conversation as resolved.
Show resolved Hide resolved
}

const newEmail = canonicalizeEmailAddress(reqBody.email);
if (newEmail !== user.email) {
assert(!state.getUserByEmail(newEmail), "EMAIL_EXISTS");
yuchenshi marked this conversation as resolved.
Show resolved Hide resolved
updates.email = newEmail;
// TODO: Set verified if email is verified by IDP linked to account.
updates.emailVerified = false;
if (user.email) {
// If user email is present, then send a reset OOB.
sendOobForEmailReset(state, user.email, newEmail, emulatorUrl);
}
}
}
if (reqBody.password) {
Expand Down Expand Up @@ -1011,6 +1041,31 @@ export function setAccountInfoImpl(
});
}

function sendOobForEmailReset(state: ProjectState, oldEmail: string, newEmail: string, url: URL) {
const mode: string = "recoverEmail";

// Encode the new email with the OOB code. The old email is received as a URL parameter.
ssbushi marked this conversation as resolved.
Show resolved Hide resolved
const { oobCode, oobLink } = state.createOob(newEmail, "RECOVER_EMAIL", (oobCode) => {
ssbushi marked this conversation as resolved.
Show resolved Hide resolved
// TODO: Support custom handler links.
url.pathname = "/emulator/action";
url.searchParams.set("mode", mode);
url.searchParams.set("lang", "en");
url.searchParams.set("oobCode", oobCode);
url.searchParams.set("oldEmail", oldEmail);

// This doesn't matter for now, since any API key works for defaultProject.
// TODO: What if reqBody.targetProjectId is set?
url.searchParams.set("apiKey", "fake-api-key");
// No continue URL for reset email.
return url.toString();
});

// Print out a developer-friendly log containing the link, in lieu of
// sending a real email out to the email address.
const message = `To reset your email address to ${oldEmail}, follow this link: ${oobLink}`;
EmulatorLogger.forEmulator(Emulators.AUTH).log("BULLET", message);
}

function signInWithCustomToken(
state: ProjectState,
reqBody: Schemas["GoogleCloudIdentitytoolkitV1SignInWithCustomTokenRequest"]
Expand Down
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy