Skip to content

Commit

Permalink
Adds rudimentary email enumeration protection for auth emulator (#6702)
Browse files Browse the repository at this point in the history
* Added rudimentary suppport for email enumeration protection

* Removed custom test script for auth

* Updated tests for improved email privacy

* Added Emulator-specific configuration for enableImprovedEmailPrivacy

* Updated integration tests to include enableImprovedEmailPrivacy config

* Added TODO message to implement case for VERIFY_AND_CHANGE_EMAIL

* Updated test title

* Ran new formatter

* Added a changelog entry
  • Loading branch information
aalej committed Jan 29, 2024
1 parent 3182a1b commit 02e711e
Show file tree
Hide file tree
Showing 9 changed files with 281 additions and 14 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Added rudimentary email enumeration protection for auth emulator. (#6702)
9 changes: 9 additions & 0 deletions scripts/emulator-import-export-tests/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,9 @@ describe("import/export end to end", () => {
signIn: {
allowDuplicateEmails: false,
},
emailPrivacyConfig: {
enableImprovedEmailPrivacy: false,
},
});

const accountsPath = path.join(exportPath, "auth_export", "accounts.json");
Expand Down Expand Up @@ -382,6 +385,9 @@ describe("import/export end to end", () => {
signIn: {
allowDuplicateEmails: false,
},
emailPrivacyConfig: {
enableImprovedEmailPrivacy: false,
},
});

const accountsPath = path.join(exportPath, "auth_export", "accounts.json");
Expand Down Expand Up @@ -447,6 +453,9 @@ describe("import/export end to end", () => {
signIn: {
allowDuplicateEmails: false,
},
emailPrivacyConfig: {
enableImprovedEmailPrivacy: false,
},
});

const accountsPath = path.join(exportPath, "auth_export", "accounts.json");
Expand Down
57 changes: 44 additions & 13 deletions src/emulator/auth/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -638,13 +638,20 @@ function createAuthUri(
}
}

return {
kind: "identitytoolkit#CreateAuthUriResponse",
registered,
allProviders,
sessionId,
signinMethods,
};
if (state.enableImprovedEmailPrivacy) {
return {
kind: "identitytoolkit#CreateAuthUriResponse",
sessionId,
};
} else {
return {
kind: "identitytoolkit#CreateAuthUriResponse",
registered,
allProviders,
sessionId,
signinMethods,
};
}
}

const SESSION_COOKIE_MIN_VALID_DURATION = 5 * 60; /* 5 minutes in seconds */
Expand Down Expand Up @@ -879,7 +886,14 @@ function sendOobCode(
mode = "resetPassword";
assert(reqBody.email, "MISSING_EMAIL");
email = canonicalizeEmailAddress(reqBody.email);
assert(state.getUserByEmail(email), "EMAIL_NOT_FOUND");
const maybeUser = state.getUserByEmail(email);
if (state.enableImprovedEmailPrivacy && !maybeUser) {
return {
kind: "identitytoolkit#GetOobConfirmationCodeResponse",
email,
};
}
assert(maybeUser, "EMAIL_NOT_FOUND");
break;
case "VERIFY_EMAIL":
mode = "verifyEmail";
Expand All @@ -898,7 +912,7 @@ function sendOobCode(
email = user.email;
}
break;

// TODO: implement case for requestType VERIFY_AND_CHANGE_EMAIL.
default:
throw new NotImplementedError(reqBody.requestType);
}
Expand Down Expand Up @@ -1726,10 +1740,21 @@ async function signInWithPassword(

const email = canonicalizeEmailAddress(reqBody.email);
let user = state.getUserByEmail(email);
assert(user, "EMAIL_NOT_FOUND");
assert(!user.disabled, "USER_DISABLED");
assert(user.passwordHash && user.salt, "INVALID_PASSWORD");
assert(user.passwordHash === hashPassword(reqBody.password, user.salt), "INVALID_PASSWORD");

if (state.enableImprovedEmailPrivacy) {
assert(user, "INVALID_LOGIN_CREDENTIALS");
assert(!user.disabled, "USER_DISABLED");
assert(user.passwordHash && user.salt, "INVALID_LOGIN_CREDENTIALS");
assert(
user.passwordHash === hashPassword(reqBody.password, user.salt),
"INVALID_LOGIN_CREDENTIALS",
);
} else {
assert(user, "EMAIL_NOT_FOUND");
assert(!user.disabled, "USER_DISABLED");
assert(user.passwordHash && user.salt, "INVALID_PASSWORD");
assert(user.passwordHash === hashPassword(reqBody.password, user.salt), "INVALID_PASSWORD");
}

const response = {
kind: "identitytoolkit#VerifyPasswordResponse",
Expand Down Expand Up @@ -1904,6 +1929,9 @@ function getEmulatorProjectConfig(state: ProjectState): Schemas["EmulatorV1Proje
signIn: {
allowDuplicateEmails: !state.oneAccountPerEmail,
},
emailPrivacyConfig: {
enableImprovedEmailPrivacy: state.enableImprovedEmailPrivacy,
},
};
}

Expand All @@ -1918,6 +1946,9 @@ function updateEmulatorProjectConfig(
if (reqBody.signIn?.allowDuplicateEmails != null) {
updateMask.push("signIn.allowDuplicateEmails");
}
if (reqBody.emailPrivacyConfig?.enableImprovedEmailPrivacy != null) {
updateMask.push("emailPrivacyConfig.enableImprovedEmailPrivacy");
}
ctx.params.query.updateMask = updateMask.join();

updateConfig(state, reqBody, ctx);
Expand Down
3 changes: 3 additions & 0 deletions src/emulator/auth/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4144,6 +4144,9 @@ export interface components {
signIn?: {
allowDuplicateEmails?: boolean;
};
emailPrivacyConfig?: {
enableImprovedEmailPrivacy?: boolean,
},
};
/** @description Details of all pending confirmation codes. */
EmulatorV1ProjectsOobCodes: {
Expand Down
22 changes: 22 additions & 0 deletions src/emulator/auth/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export abstract class ProjectState {

abstract get oneAccountPerEmail(): boolean;

abstract get enableImprovedEmailPrivacy(): boolean;

abstract get authCloudFunction(): AuthCloudFunction;

abstract get allowPasswordSignup(): boolean;
Expand Down Expand Up @@ -578,6 +580,9 @@ export class AgentProjectState extends ProjectState {
private _config: Config = {
signIn: { allowDuplicateEmails: false },
blockingFunctions: {},
emailPrivacyConfig: {
enableImprovedEmailPrivacy: false,
},
};

constructor(projectId: string) {
Expand All @@ -596,6 +601,14 @@ export class AgentProjectState extends ProjectState {
this._config.signIn.allowDuplicateEmails = !oneAccountPerEmail;
}

get enableImprovedEmailPrivacy() {
return !!this._config.emailPrivacyConfig.enableImprovedEmailPrivacy;
}

set enableImprovedEmailPrivacy(improveEmailPrivacy: boolean) {
this._config.emailPrivacyConfig.enableImprovedEmailPrivacy = improveEmailPrivacy;
}

get allowPasswordSignup() {
return true;
}
Expand Down Expand Up @@ -659,6 +672,8 @@ export class AgentProjectState extends ProjectState {
if (!updateMask) {
this.oneAccountPerEmail = !update.signIn?.allowDuplicateEmails ?? true;
this.blockingFunctionsConfig = update.blockingFunctions ?? {};
this.enableImprovedEmailPrivacy =
update.emailPrivacyConfig?.enableImprovedEmailPrivacy ?? false;
return this.config;
}
return applyMask(updateMask, this.config, update);
Expand Down Expand Up @@ -748,6 +763,10 @@ export class TenantProjectState extends ProjectState {
return this.parentProject.oneAccountPerEmail;
}

get enableImprovedEmailPrivacy() {
return this.parentProject.enableImprovedEmailPrivacy;
}

get authCloudFunction() {
return this.parentProject.authCloudFunction;
}
Expand Down Expand Up @@ -853,13 +872,16 @@ export type SignInConfig = MakeRequired<
export type BlockingFunctionsConfig =
Schemas["GoogleCloudIdentitytoolkitAdminV2BlockingFunctionsConfig"];

export type EmailPrivacyConfig = Schemas["GoogleCloudIdentitytoolkitAdminV2EmailPrivacyConfig"];

// Serves as a substitute for Schemas["GoogleCloudIdentitytoolkitAdminV2Config"],
// i.e. the configuration object for top-level AgentProjectStates. Emulator
// fixes certain configurations for ease of use / testing, so as non-standard
// behavior, Config only stores the configurable fields.
export type Config = {
signIn: SignInConfig;
blockingFunctions: BlockingFunctionsConfig;
emailPrivacyConfig: EmailPrivacyConfig;
};

export interface RefreshTokenRecord {
Expand Down
49 changes: 49 additions & 0 deletions src/test/emulators/auth/createAuthUri.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
signInWithEmailLink,
updateProjectConfig,
registerTenant,
updateConfig,
} from "./helpers";

describeAuthEmulator("accounts:createAuthUri", ({ authApi }) => {
Expand All @@ -23,6 +24,28 @@ describeAuthEmulator("accounts:createAuthUri", ({ authApi }) => {
});
});

it("should not show registered field with improved email privacy enabled", async () => {
await updateConfig(
authApi(),
PROJECT_ID,
{
emailPrivacyConfig: {
enableImprovedEmailPrivacy: true,
},
},
"emailPrivacyConfig",
);
await authApi()
.post("/identitytoolkit.googleapis.com/v1/accounts:createAuthUri")
.send({ continueUri: "http://example.com/", identifier: "notregistered@example.com" })
.query({ key: "fake-api-key" })
.then((res) => {
expectStatusCode(200, res);
expect(res.body).to.not.have.property("registered");
expect(res.body).to.have.property("sessionId").that.is.a("string");
});
});

it("should return providers for a registered user", async () => {
const user = { email: "alice@example.com", password: "notasecret" };
await registerUser(authApi(), user);
Expand All @@ -39,6 +62,32 @@ describeAuthEmulator("accounts:createAuthUri", ({ authApi }) => {
});
});

it("should not return providers for a registered user with improved email privacy enabled", async () => {
const user = { email: "alice@example.com", password: "notasecret" };
await updateConfig(
authApi(),
PROJECT_ID,
{
emailPrivacyConfig: {
enableImprovedEmailPrivacy: true,
},
},
"emailPrivacyConfig",
);
await registerUser(authApi(), user);
await authApi()
.post("/identitytoolkit.googleapis.com/v1/accounts:createAuthUri")
.send({ continueUri: "http://example.com/", identifier: user.email })
.query({ key: "fake-api-key" })
.then((res) => {
expectStatusCode(200, res);
expect(res.body).to.not.have.property("registered");
expect(res.body).to.not.have.property("allProviders").eql(["password"]);
expect(res.body).to.not.have.property("signinMethods").eql(["password"]);
expect(res.body).to.have.property("sessionId").that.is.a("string");
});
});

it("should return existing sessionId if provided", async () => {
await authApi()
.post("/identitytoolkit.googleapis.com/v1/accounts:createAuthUri")
Expand Down
71 changes: 70 additions & 1 deletion src/test/emulators/auth/misc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,9 @@ describeAuthEmulator("emulator utility APIs", ({ authApi }) => {
expect(res.body).to.have.property("signIn").eql({
allowDuplicateEmails: false /* default value */,
});
expect(res.body).to.have.property("emailPrivacyConfig").eql({
enableImprovedEmailPrivacy: false /* default value */,
});
});
});

Expand All @@ -564,7 +567,7 @@ describeAuthEmulator("emulator utility APIs", ({ authApi }) => {
});
});

it("should update allowDuplicateEmails on PATCH /emulator/v1/projects/{PROJECT_ID}/config", async () => {
it("should only update allowDuplicateEmails on PATCH /emulator/v1/projects/{PROJECT_ID}/config", async () => {
await authApi()
.patch(`/emulator/v1/projects/${PROJECT_ID}/config`)
.send({ signIn: { allowDuplicateEmails: true } })
Expand All @@ -573,6 +576,9 @@ describeAuthEmulator("emulator utility APIs", ({ authApi }) => {
expect(res.body).to.have.property("signIn").eql({
allowDuplicateEmails: true,
});
expect(res.body).to.have.property("emailPrivacyConfig").eql({
enableImprovedEmailPrivacy: false,
});
});
await authApi()
.patch(`/emulator/v1/projects/${PROJECT_ID}/config`)
Expand All @@ -582,6 +588,69 @@ describeAuthEmulator("emulator utility APIs", ({ authApi }) => {
expect(res.body).to.have.property("signIn").eql({
allowDuplicateEmails: false,
});
expect(res.body).to.have.property("emailPrivacyConfig").eql({
enableImprovedEmailPrivacy: false,
});
});
});

it("should only update enableImprovedEmailPrivacy on PATCH /emulator/v1/projects/{PROJECT_ID}/config", async () => {
await authApi()
.patch(`/emulator/v1/projects/${PROJECT_ID}/config`)
.send({ emailPrivacyConfig: { enableImprovedEmailPrivacy: true } })
.then((res) => {
expectStatusCode(200, res);
expect(res.body).to.have.property("signIn").eql({
allowDuplicateEmails: false,
});
expect(res.body).to.have.property("emailPrivacyConfig").eql({
enableImprovedEmailPrivacy: true,
});
});
await authApi()
.patch(`/emulator/v1/projects/${PROJECT_ID}/config`)
.send({ emailPrivacyConfig: { enableImprovedEmailPrivacy: false } })
.then((res) => {
expectStatusCode(200, res);
expect(res.body).to.have.property("signIn").eql({
allowDuplicateEmails: false,
});
expect(res.body).to.have.property("emailPrivacyConfig").eql({
enableImprovedEmailPrivacy: false,
});
});
});

it("should update both allowDuplicateEmails and enableImprovedEmailPrivacy on PATCH /emulator/v1/projects/{PROJECT_ID}/config", async () => {
await authApi()
.patch(`/emulator/v1/projects/${PROJECT_ID}/config`)
.send({
signIn: { allowDuplicateEmails: true },
emailPrivacyConfig: { enableImprovedEmailPrivacy: true },
})
.then((res) => {
expectStatusCode(200, res);
expect(res.body).to.have.property("signIn").eql({
allowDuplicateEmails: true,
});
expect(res.body).to.have.property("emailPrivacyConfig").eql({
enableImprovedEmailPrivacy: true,
});
});
await authApi()
.patch(`/emulator/v1/projects/${PROJECT_ID}/config`)
.send({
signIn: { allowDuplicateEmails: false },
emailPrivacyConfig: { enableImprovedEmailPrivacy: false },
})
.then((res) => {
expectStatusCode(200, res);
expect(res.body).to.have.property("signIn").eql({
allowDuplicateEmails: false,
});
expect(res.body).to.have.property("emailPrivacyConfig").eql({
enableImprovedEmailPrivacy: false,
});
});
});
});
Expand Down

0 comments on commit 02e711e

Please sign in to comment.