Skip to content

Update password policy in sign up flow #7392

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

Merged
merged 15 commits into from
Jul 6, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 4 additions & 1 deletion packages/auth/src/api/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ export const enum ServerError {
MISSING_CLIENT_TYPE = 'MISSING_CLIENT_TYPE',
MISSING_RECAPTCHA_VERSION = 'MISSING_RECAPTCHA_VERSION',
INVALID_RECAPTCHA_VERSION = 'INVALID_RECAPTCHA_VERSION',
INVALID_REQ_TYPE = 'INVALID_REQ_TYPE'
INVALID_REQ_TYPE = 'INVALID_REQ_TYPE',
PASSWORD_DOES_NOT_MEET_REQUIREMENTS = 'PASSWORD_DOES_NOT_MEET_REQUIREMENTS'
}

/**
Expand Down Expand Up @@ -177,6 +178,8 @@ export const SERVER_ERROR_MAP: Partial<ServerErrorMap<ServerError>> = {
// Other errors.
[ServerError.TOO_MANY_ATTEMPTS_TRY_LATER]:
AuthErrorCode.TOO_MANY_ATTEMPTS_TRY_LATER,
[ServerError.PASSWORD_DOES_NOT_MEET_REQUIREMENTS]:
AuthErrorCode.PASSWORD_DOES_NOT_MEET_REQUIREMENTS,

// Phone Auth related errors.
[ServerError.INVALID_CODE]: AuthErrorCode.INVALID_CODE,
Expand Down
7 changes: 5 additions & 2 deletions packages/auth/src/core/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ export const enum AuthErrorCode {
MISSING_RECAPTCHA_VERSION = 'missing-recaptcha-version',
INVALID_RECAPTCHA_VERSION = 'invalid-recaptcha-version',
INVALID_REQ_TYPE = 'invalid-req-type',
UNSUPPORTED_PASSWORD_POLICY_SCHEMA_VERSION = 'unsupported-password-policy-schema-version'
UNSUPPORTED_PASSWORD_POLICY_SCHEMA_VERSION = 'unsupported-password-policy-schema-version',
PASSWORD_DOES_NOT_MEET_REQUIREMENTS = 'password-does-not-meet-requirements'
}

function _debugErrorMap(): ErrorMap<AuthErrorCode> {
Expand Down Expand Up @@ -384,7 +385,9 @@ function _debugErrorMap(): ErrorMap<AuthErrorCode> {
[AuthErrorCode.INVALID_RECAPTCHA_VERSION]:
'The reCAPTCHA version is invalid when sending request to the backend.',
[AuthErrorCode.UNSUPPORTED_PASSWORD_POLICY_SCHEMA_VERSION]:
'The password policy received from the backend uses a schema version that is not supported by this version of the Firebase SDK.'
'The password policy received from the backend uses a schema version that is not supported by this version of the Firebase SDK.',
[AuthErrorCode.PASSWORD_DOES_NOT_MEET_REQUIREMENTS]:
'The password does not meet the requirements.'
};
}

Expand Down
143 changes: 143 additions & 0 deletions packages/auth/src/core/strategies/email_and_password.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,149 @@ describe('core/strategies/email_and_password/createUserWithEmailAndPassword', ()
expect(user.isAnonymous).to.be.false;
});
});

context('#passwordPolicy', () => {
const TEST_MIN_PASSWORD_LENGTH = 6;
const TEST_ALLOWED_NON_ALPHANUMERIC_CHARS = ['!', '(', ')'];
const TEST_SCHEMA_VERSION = 1;

const TEST_TENANT_ID = 'tenant-id';
const TEST_REQUIRE_NUMERIC_TENANT_ID = 'other-tenant-id';

const PASSWORD_ERROR_MSG =
'Firebase: The password does not meet the requirements. (auth/password-does-not-meet-requirements).';

const passwordPolicyResponse = {
customStrengthOptions: {
minPasswordLength: TEST_MIN_PASSWORD_LENGTH
},
allowedNonAlphanumericCharacters: TEST_ALLOWED_NON_ALPHANUMERIC_CHARS,
schemaVersion: TEST_SCHEMA_VERSION
};
const passwordPolicyResponseRequireNumeric = {
customStrengthOptions: {
minPasswordLength: TEST_MIN_PASSWORD_LENGTH,
containsNumericCharacter: true
},
allowedNonAlphanumericCharacters: TEST_ALLOWED_NON_ALPHANUMERIC_CHARS,
schemaVersion: TEST_SCHEMA_VERSION
};
const cachedPasswordPolicy = {
customStrengthOptions: {
minPasswordLength: TEST_MIN_PASSWORD_LENGTH
},
allowedNonAlphanumericCharacters: TEST_ALLOWED_NON_ALPHANUMERIC_CHARS
};
const cachedPasswordPolicyRequireNumeric = {
customStrengthOptions: {
minPasswordLength: TEST_MIN_PASSWORD_LENGTH,
containsNumericCharacter: true
},
allowedNonAlphanumericCharacters: TEST_ALLOWED_NON_ALPHANUMERIC_CHARS
};
let policyEndpointMock: mockFetch.Route;
let policyEndpointMockWithTenant: mockFetch.Route;
let policyEndpointMockWithOtherTenant: mockFetch.Route;

beforeEach(() => {
policyEndpointMock = mockEndpointWithParams(
Endpoint.GET_PASSWORD_POLICY,
{},
passwordPolicyResponse
);
policyEndpointMockWithTenant = mockEndpointWithParams(
Endpoint.GET_PASSWORD_POLICY,
{
tenantId: TEST_TENANT_ID
},
passwordPolicyResponse
);
policyEndpointMockWithOtherTenant = mockEndpointWithParams(
Endpoint.GET_PASSWORD_POLICY,
{
tenantId: TEST_REQUIRE_NUMERIC_TENANT_ID
},
passwordPolicyResponseRequireNumeric
);
});

it('does not update the cached password policy upon successful sign up when there is no existing policy cache', async () => {
await expect(
createUserWithEmailAndPassword(auth, 'some-email', 'some-password')
).to.be.fulfilled;

expect(policyEndpointMock.calls.length).to.eq(0);
expect(auth._getPasswordPolicy()).to.be.null;
});

it('does not update the cached password policy upon successful sign up when there is an existing policy cache', async () => {
await auth._updatePasswordPolicy();

await expect(
createUserWithEmailAndPassword(auth, 'some-email', 'some-password')
).to.be.fulfilled;

expect(policyEndpointMock.calls.length).to.eq(1);
expect(auth._getPasswordPolicy()).to.eql(cachedPasswordPolicy);
});

context('handles password validation errors', () => {
beforeEach(() => {
mockEndpoint(
Endpoint.SIGN_UP,
{
error: {
code: 400,
message: ServerError.PASSWORD_DOES_NOT_MEET_REQUIREMENTS
}
},
400
);
});

it('updates the cached password policy when password does not meet backend requirements', async () => {
await auth._updatePasswordPolicy();
expect(policyEndpointMock.calls.length).to.eq(1);
expect(auth._getPasswordPolicy()).to.eql(cachedPasswordPolicy);

// Password policy changed after previous fetch.
policyEndpointMock.response = passwordPolicyResponseRequireNumeric;
await expect(
createUserWithEmailAndPassword(auth, 'some-email', 'some-password')
).to.be.rejectedWith(FirebaseError, PASSWORD_ERROR_MSG);

expect(policyEndpointMock.calls.length).to.eq(2);
expect(auth._getPasswordPolicy()).to.eql(
cachedPasswordPolicyRequireNumeric
);
});

it('does not update the cached password policy upon error if policy has not previously been fetched', async () => {
expect(auth._getPasswordPolicy()).to.be.null;

await expect(
createUserWithEmailAndPassword(auth, 'some-email', 'some-password')
).to.be.rejectedWith(FirebaseError, PASSWORD_ERROR_MSG);

expect(policyEndpointMock.calls.length).to.eq(0);
expect(auth._getPasswordPolicy()).to.be.null;
});

it('does not update the cached password policy upon error if tenant changes and policy has not previously been fetched', async () => {
auth.tenantId = TEST_TENANT_ID;
await auth._updatePasswordPolicy();
expect(policyEndpointMockWithTenant.calls.length).to.eq(1);
expect(auth._getPasswordPolicy()).to.eql(cachedPasswordPolicy);

auth.tenantId = TEST_REQUIRE_NUMERIC_TENANT_ID;
await expect(
createUserWithEmailAndPassword(auth, 'some-email', 'some-password')
).to.be.rejectedWith(FirebaseError, PASSWORD_ERROR_MSG);
expect(policyEndpointMockWithOtherTenant.calls.length).to.eq(0);
expect(auth._getPasswordPolicy()).to.be.undefined;
});
});
});
});

describe('core/strategies/email_and_password/signInWithEmailAndPassword', () => {
Expand Down
10 changes: 10 additions & 0 deletions packages/auth/src/core/strategies/email_and_password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,16 @@ export async function createUserWithEmailAndPassword(
);
return signUp(authInternal, requestWithRecaptcha);
} else {
// Only fetch the password policy if the password did not meet policy requirements and there is an existing policy cached.
// A developer must call validatePassword at least once for the cache to be automatically updated.
if (
error.code ===
`auth/${AuthErrorCode.PASSWORD_DOES_NOT_MEET_REQUIREMENTS}` &&
authInternal._getPasswordPolicy()
) {
await authInternal._updatePasswordPolicy();
}

return Promise.reject(error);
}
});
Expand Down