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

feat(auth): Add Password Policies support in Project and Tenant config #2107

Merged
merged 57 commits into from
May 24, 2023
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
0e07227
Password Policy Tenant and Project Changes
pragatimodi Mar 13, 2023
d97386a
Adding unit tests for PasswordPolicyAuthConfig
pragatimodi Mar 13, 2023
d154481
Adding Project Config Unit Tests
pragatimodi Mar 14, 2023
828e820
Tenant Unit Tests
pragatimodi Mar 14, 2023
db00ab1
Lint fixes
pragatimodi Mar 14, 2023
62a444f
`npm run api-extractor:local` changes
pragatimodi Mar 14, 2023
a83afc5
1. `npm run api-extractor:local` changes
pragatimodi Mar 14, 2023
7bdbee0
Revert formatting changes
pragatimodi Mar 14, 2023
cf0f30e
Lint changes
pragatimodi Mar 14, 2023
a3af073
Line break lint changes
pragatimodi Mar 14, 2023
1e3a22b
`eslint --fix .` fixes
pragatimodi Mar 14, 2023
4254ddc
1. Changing type of passwordPolicies to PasswordPolicyConfig on Tenan…
pragatimodi Mar 15, 2023
3fc71f8
Minor changes
pragatimodi Mar 17, 2023
537db9e
Changes with approved config
pragatimodi Mar 29, 2023
0291c80
Merge branch 'password-policy' of https://github.com/firebase/firebas…
pragatimodi Mar 29, 2023
46084d1
Merge branch 'master' of https://github.com/firebase/firebase-admin-n…
pragatimodi Mar 30, 2023
b0a01d1
Undo package-lock.json changes
pragatimodi Mar 30, 2023
7ed0df0
Undo package-lock.json changes
pragatimodi Mar 30, 2023
e87cf4b
Undo package-lock.json changes
pragatimodi Mar 30, 2023
ea5edda
Update package-lock.json
pragatimodi Mar 30, 2023
8e7f520
Lint fixes
pragatimodi Mar 30, 2023
f01df33
Merge branch 'password-policy' of https://github.com/firebase/firebas…
pragatimodi Mar 30, 2023
eebc816
Cleanup
pragatimodi Mar 30, 2023
3098765
Merge remote-tracking branch 'origin/password-policy' into password-p…
pragatimodi Mar 30, 2023
e6a881f
Revert package-lock.json
pragatimodi Mar 30, 2023
d112cc6
Update project-config.ts
pragatimodi Mar 30, 2023
b5858c2
Minor formatting
pragatimodi Apr 4, 2023
6645095
Descriptive constants
pragatimodi Apr 4, 2023
a5453cd
Lint fixes
pragatimodi Apr 4, 2023
15022a6
Fix import
pragatimodi Apr 4, 2023
f2289e1
Update test/integration/auth.spec.ts
pragatimodi Apr 4, 2023
ce5d8bc
Apply suggestions from code review
pragatimodi Apr 4, 2023
222196d
Apply suggestions from code review
pragatimodi Apr 5, 2023
2db1735
Integration tests fix
pragatimodi Apr 5, 2023
cfaab07
Merge branch 'password-policy' of https://github.com/firebase/firebas…
pragatimodi Apr 5, 2023
5d0317f
Small fix
pragatimodi Apr 5, 2023
908c874
Reset password policy in integration tests to prevent breaking changes
pragatimodi Apr 6, 2023
f07a561
lint fix
pragatimodi Apr 6, 2023
7f98afd
Merge branch 'master' into password-policy
pragatimodi Apr 6, 2023
088dc00
Merge branch 'master' of https://github.com/firebase/firebase-admin-n…
pragatimodi Apr 24, 2023
363088e
package-lock.json undo changes
pragatimodi Apr 24, 2023
3eab3f6
update merge
pragatimodi Apr 24, 2023
18e6342
lint fixes
pragatimodi Apr 24, 2023
52dafbb
merge fixes
pragatimodi Apr 24, 2023
cb6cf64
Update package-lock.json
pragatimodi Apr 24, 2023
1eed89b
Update package.json
pragatimodi Apr 24, 2023
4db418e
Allow enforcementState `OFF` with default constraints
pragatimodi Apr 25, 2023
9206fa2
Lint fixes
pragatimodi Apr 25, 2023
a5b9188
Merge branch 'password-policy' of https://github.com/firebase/firebas…
pragatimodi Apr 25, 2023
0bdf730
Merge branch 'master' into password-policy
lahirumaramba May 2, 2023
2fd709a
Adding recaptcha to server request
pragatimodi May 3, 2023
59ba4ed
Fill default passwordPolicyVersions values
pragatimodi May 4, 2023
694a336
Lint fix
pragatimodi May 4, 2023
295d71c
Merge branch 'master' into password-policy
pragatimodi May 4, 2023
b4d7907
remove debug logging
pragatimodi May 4, 2023
1a4df37
Merge branch 'password-policy' of https://github.com/firebase/firebas…
pragatimodi May 4, 2023
3897bb2
Merge branch 'master' into password-policy
lahirumaramba May 23, 2023
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
24 changes: 24 additions & 0 deletions etc/firebase-admin.auth.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,16 @@ export interface CreateRequest extends UpdateRequest {
// @public
export type CreateTenantRequest = UpdateTenantRequest;

// @public
export interface CustomStrengthOptionsConfig {
maxLength?: number;
minLength?: number;
requireLowercase?: boolean;
requireNonAlphanumeric?: boolean;
requireNumeric?: boolean;
requireUppercase?: boolean;
}

// @alpha (undocumented)
export interface DecodedAuthBlockingToken {
// (undocumented)
Expand Down Expand Up @@ -329,6 +339,16 @@ export interface OIDCUpdateAuthProviderRequest {
responseType?: OAuthResponseType;
}

// @public
export interface PasswordPolicyConfig {
constraints?: CustomStrengthOptionsConfig;
enforcementState?: PasswordPolicyEnforcementState;
forceUpgradeOnSignin?: boolean;
}

// @public
export type PasswordPolicyEnforcementState = 'ENFORCE' | 'OFF';

// @public
export interface PhoneIdentifier {
// (undocumented)
Expand All @@ -344,6 +364,7 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo {
// @public
export class ProjectConfig {
get multiFactorConfig(): MultiFactorConfig | undefined;
readonly passwordPolicyConfig?: PasswordPolicyConfig;
readonly smsRegionConfig?: SmsRegionConfig;
toJSON(): object;
}
Expand Down Expand Up @@ -397,6 +418,7 @@ export class Tenant {
readonly displayName?: string;
get emailSignInConfig(): EmailSignInProviderConfig | undefined;
get multiFactorConfig(): MultiFactorConfig | undefined;
readonly passwordPolicyConfig?: PasswordPolicyConfig;
readonly smsRegionConfig?: SmsRegionConfig;
readonly tenantId: string;
readonly testPhoneNumbers?: {
Expand Down Expand Up @@ -448,6 +470,7 @@ export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactor
// @public
export interface UpdateProjectConfigRequest {
multiFactorConfig?: MultiFactorConfig;
passwordPolicyConfig?: PasswordPolicyConfig;
smsRegionConfig?: SmsRegionConfig;
}

Expand All @@ -471,6 +494,7 @@ export interface UpdateTenantRequest {
displayName?: string;
emailSignInConfig?: EmailSignInProviderConfig;
multiFactorConfig?: MultiFactorConfig;
passwordPolicyConfig?: PasswordPolicyConfig;
smsRegionConfig?: SmsRegionConfig;
testPhoneNumbers?: {
[phoneNumber: string]: string;
Expand Down
314 changes: 314 additions & 0 deletions src/auth/auth-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1722,3 +1722,317 @@ export class SmsRegionsAuthConfig {
}
}
}

/**
* The request configuration for the password policy on the project or tenant
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
*/
export interface PasswordPolicyConfig {
/**
* Enforcement state for the password policy
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
*/
enforcementState?: PasswordPolicyEnforcementState;
/**
* Users must have a password compliant with the password policy to sign-in
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
*/
forceUpgradeOnSignin?: boolean;
/**
* Set of strength constraints for the password
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
*/
constraints?: CustomStrengthOptionsConfig;
}

/**
* Identifies a password policy configuration state.
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
*/
export type PasswordPolicyEnforcementState = 'ENFORCE' | 'OFF';

/**
* Constraints to be enforced on the password policy
*/
export interface CustomStrengthOptionsConfig {
/**
* The password must contain an upper case character
*/
requireUppercase?: boolean;
/**
* The password must contain a lower case character
*/
requireLowercase?: boolean;
/**
* The password must contain a non alpha numeric character
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
*/
requireNonAlphanumeric?: boolean;
/**
* The password must contain a number
*/
requireNumeric?: boolean;
/**
* Minimum password length. Range from 6 to 30
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
*/
minLength?: number;
/**
* Maximum password length. No default max length
*/
maxLength?: number;
}

/**
* Defines the password policy config class used to convert client side PasswordPolicyConfig
* to a format that is understood by the Auth server.
*
* @internal
*/
export class PasswordPolicyAuthConfig implements PasswordPolicyConfig {

/**
* Identifies a password policy configuration state.
*/
public readonly enforcementState: PasswordPolicyEnforcementState;
/**
* Users must have a password compliant with the password policy to sign-in
*/
public readonly forceUpgradeOnSignin?: boolean;
/**
* Must be of length 1. Contains the strength attributes for the password policy
*/
public readonly constraints?: CustomStrengthOptionsConfig;

/**
* Static method to convert a client side request to a PasswordPolicyAuthServerConfig.
* Throws an error if validation fails.
*
* @param options - The options object to convert to a server request.
* @returns The resulting server request.
* @internal
*/
public static buildServerRequest(options: PasswordPolicyConfig): PasswordPolicyAuthServerConfig {
const request: PasswordPolicyAuthServerConfig = {};
PasswordPolicyAuthConfig.validate(options);
if (Object.prototype.hasOwnProperty.call(options, 'enforcementState')) {
request.passwordPolicyEnforcementState = options.enforcementState;
}
if (Object.prototype.hasOwnProperty.call(options, 'forceUpgradeOnSignin')) {
request.forceUpgradeOnSignin = options.forceUpgradeOnSignin;
}
if (Object.prototype.hasOwnProperty.call(options, 'constraints')) {
request.passwordPolicyVersions = [];
const constraintsRequest: CustomStrengthOptionsAuthServerConfig = {
containsUppercaseCharacter: options.constraints?.requireUppercase,
containsLowercaseCharacter: options.constraints?.requireLowercase,
containsNonAlphanumericCharacter: options.constraints?.requireNonAlphanumeric,
containsNumericCharacter: options.constraints?.requireNumeric,
minPasswordLength: options.constraints?.minLength,
maxPasswordLength: options.constraints?.maxLength,
};
request.passwordPolicyVersions.push({ customStrengthOptions: constraintsRequest })
}
return request;
}

/**
* Validates the PasswordPolicyConfig options object. Throws an error on failure.
*
* @param options - The options object to validate.
* @internal
*/
public static validate(options: PasswordPolicyConfig): void {
const validKeys = {
enforcementState: true,
forceUpgradeOnSignin: true,
constraints: true,
};
if (!validator.isNonNullObject(options)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"PasswordPolicyConfig" must be a non-null object.',
);
}
// Check for unsupported top level attributes.
for (const key in options) {
if (!(key in validKeys)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
`"${key}" is not a valid PasswordPolicyConfig parameter.`,
);
}
}
// Validate content.
if (typeof options.enforcementState === 'undefined' ||
!(options.enforcementState === 'ENFORCE' ||
options.enforcementState === 'OFF')) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"PasswordPolicyConfig.enforcementState" must be either "ENFORCE" or "OFF".',
);
}

if (typeof options.forceUpgradeOnSignin !== 'undefined') {
if (!validator.isBoolean(options.forceUpgradeOnSignin)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"PasswordPolicyConfig.forceUpgradeOnSignin" must be a boolean.',
);
}
}

if (typeof options.constraints !== 'undefined') {
if (options.enforcementState === 'ENFORCE' && !validator.isNonNullObject(options.constraints)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"PasswordPolicyConfig.constraints" must be a non-empty object.',
);
}

const validCharKeys = {
requireUppercase: true,
requireLowercase: true,
requireNumeric: true,
requireNonAlphanumeric: true,
minLength: true,
maxLength: true,
};

// Check for unsupported attributes.
for (const key in options.constraints) {
if (!(key in validCharKeys)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
`"${key}" is not a valid PasswordPolicyConfig.constraints parameter.`,
);
}
}
if (typeof options.constraints.requireUppercase !== undefined &&
!validator.isBoolean(options.constraints.requireUppercase)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"PasswordPolicyConfig.constraints.requireUppercase" must be a boolean.',
);
}
if (typeof options.constraints.requireLowercase !== undefined &&
!validator.isBoolean(options.constraints.requireLowercase)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"PasswordPolicyConfig.constraints.requireLowercase" must be a boolean.',
);
}
if (typeof options.constraints.requireNonAlphanumeric !== undefined &&
!validator.isBoolean(options.constraints.requireNonAlphanumeric)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"PasswordPolicyConfig.constraints.requireNonAlphanumeric"' +
' must be a boolean.',
);
}
if (typeof options.constraints.requireNumeric !== undefined &&
!validator.isBoolean(options.constraints.requireNumeric)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"PasswordPolicyConfig.constraints.requireNumeric" must be a boolean.',
);
}
if (!validator.isNumber(options.constraints.minLength)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"PasswordPolicyConfig.constraints.minLength" must be a number.',
);
}
if (!validator.isNumber(options.constraints.maxLength)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"PasswordPolicyConfig.constraints.maxLength" must be a number.',
);
}
if (options.constraints.minLength === undefined) {
options.constraints.minLength = 6;
} else {
if (!(options.constraints.minLength >= 6
&& options.constraints.minLength <= 30)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"PasswordPolicyConfig.constraints.minLength"' +
' must be an integer between 6 and 30, inclusive.',
);
}
}
if (options.constraints.maxLength === undefined) {
options.constraints.maxLength = 4096;
} else {
if (!(options.constraints.maxLength >= options.constraints.minLength &&
options.constraints.maxLength <= 4096)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"PasswordPolicyConfig.constraints.maxLength"' +
' must be greater than or equal to minLength and at max 4096.',
);
}
}
} else {
if (options.enforcementState === 'ENFORCE') {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"PasswordPolicyConfig.constraints" must be defined.',
);
}
}
}

/**
* The PasswordPolicyAuthConfig constructor.
*
* @param response - The server side response used to initialize the
* PasswordPolicyAuthConfig object.
* @constructor
* @internal
*/
constructor(response: PasswordPolicyAuthServerConfig) {
if (typeof response.passwordPolicyEnforcementState === 'undefined') {
throw new FirebaseAuthError(
AuthClientErrorCode.INTERNAL_ERROR,
'INTERNAL ASSERT FAILED: Invalid password policy configuration response');
}
this.enforcementState = response.passwordPolicyEnforcementState;
let constraintsResponse: CustomStrengthOptionsConfig = {};
if (typeof response.passwordPolicyVersions !== 'undefined') {
(response.passwordPolicyVersions || []).forEach((policyVersion) => {
constraintsResponse = {
requireLowercase: policyVersion.customStrengthOptions?.containsLowercaseCharacter,
requireUppercase: policyVersion.customStrengthOptions?.containsUppercaseCharacter,
requireNonAlphanumeric: policyVersion.customStrengthOptions?.containsNonAlphanumericCharacter,
requireNumeric: policyVersion.customStrengthOptions?.containsNumericCharacter,
minLength: policyVersion.customStrengthOptions?.minPasswordLength,
maxLength: policyVersion.customStrengthOptions?.maxPasswordLength,
};
});
}
this.constraints = constraintsResponse;
this.forceUpgradeOnSignin = response.forceUpgradeOnSignin;
}
}

/**
* Server side password policy configuration.
*/
export interface PasswordPolicyAuthServerConfig {
passwordPolicyEnforcementState?: PasswordPolicyEnforcementState;
passwordPolicyVersions?: PasswordPolicyVersionsAuthServerConfig[];
forceUpgradeOnSignin?: boolean;
}

/**
* Server side password policy versions configuration.
*/
export interface PasswordPolicyVersionsAuthServerConfig {
customStrengthOptions?: CustomStrengthOptionsAuthServerConfig;
}

/**
* Server side password policy constraints configuration.
*/
export interface CustomStrengthOptionsAuthServerConfig {
containsLowercaseCharacter?: boolean;
containsUppercaseCharacter?: boolean;
containsNumericCharacter?: boolean;
containsNonAlphanumericCharacter?: boolean;
minPasswordLength?: number;
maxPasswordLength?: number;
}
3 changes: 3 additions & 0 deletions src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ export {
UpdatePhoneMultiFactorInfoRequest,
UpdateRequest,
TotpMultiFactorProviderConfig,
PasswordPolicyConfig,
PasswordPolicyEnforcementState,
CustomStrengthOptionsConfig,
} from './auth-config';

export {
Expand Down
Loading