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 TOTP support in Project and Tenant config #1989

Merged
merged 57 commits into from
Mar 29, 2023
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
3290943
Sync with master (#1986)
pragatimodi Nov 23, 2022
4a16294
Adding TOTP support for MFA
pragatimodi Nov 23, 2022
5ebf34b
Merge with master (#1987)
pragatimodi Nov 23, 2022
cf310fa
Revert "Merge with master (#1987)" (#1988)
pragatimodi Nov 23, 2022
e3d47e4
Revert "Sync with master (#1986)"
pragatimodi Nov 23, 2022
bad1cc9
Update auth-api-request.ts
pragatimodi Nov 23, 2022
0a2e0cb
Update auth-api-request.ts
pragatimodi Nov 23, 2022
9f146a1
Addressing comments, adding tests and cleaning up
pragatimodi Dec 2, 2022
d261837
Auto generated after running `$ npm run api-extractor:local`
pragatimodi Dec 2, 2022
c345c0b
Merge branch 'master' into totp-release
pragatimodi Dec 2, 2022
09067e7
Merge remote-tracking branch 'refs/remotes/origin/totp-release' into …
pragatimodi Dec 2, 2022
1174ffc
Linter fixes
pragatimodi Dec 2, 2022
a2c75a6
Resolving review comments
pragatimodi Dec 13, 2022
58b5a74
Sync MFA field with backend
pragatimodi Dec 13, 2022
e784cb5
Reviewed changes
pragatimodi Dec 13, 2022
a0fa071
Formatting fix
pragatimodi Dec 15, 2022
a4ae79d
Reverting packagelock.json auto changes
pragatimodi Dec 15, 2022
81f699d
Project server config updates
pragatimodi Dec 15, 2022
b3f2861
Import fix
pragatimodi Dec 15, 2022
30ed6b4
Merge branch 'master' into totp-release
pragatimodi Dec 15, 2022
ca73502
Fix lint errors
pragatimodi Dec 15, 2022
bbe19d7
Merge branch 'totp-release' of https://github.com/firebase/firebase-a…
pragatimodi Dec 15, 2022
b4535ae
Unit tests fix
pragatimodi Dec 15, 2022
d7fac1f
api extractor fix
pragatimodi Dec 15, 2022
047180b
Import fix
pragatimodi Dec 15, 2022
4f572ff
Import fix
pragatimodi Dec 15, 2022
dd310fe
API extractor changes
pragatimodi Dec 15, 2022
78e4f64
Adding documentation
pragatimodi Dec 22, 2022
8602bfe
Merge branch 'master' into totp-release
pragatimodi Dec 22, 2022
351196a
`npm run api-extractor:local` changes
pragatimodi Dec 22, 2022
534aaa9
Merge branch 'totp-release' of https://github.com/firebase/firebase-a…
pragatimodi Dec 22, 2022
e71ea5d
Merge branch 'master' into totp-release
pragatimodi Jan 3, 2023
a86a64b
Undo whitespace changes
pragatimodi Jan 6, 2023
47a83bd
Review fixes
pragatimodi Jan 6, 2023
fad1cec
Variable names fix
pragatimodi Jan 6, 2023
c820e5f
Removing whitespace changes from package-lock.json
pragatimodi Jan 6, 2023
4258f62
Lint error
pragatimodi Jan 7, 2023
e1de761
Merge branch 'totp-release' of https://github.com/firebase/firebase-a…
pragatimodi Jan 7, 2023
f2540ad
Minor updates
pragatimodi Jan 9, 2023
637b527
Minor updates
pragatimodi Jan 10, 2023
17b0ffa
Adding some more validators and unit test
pragatimodi Jan 10, 2023
8e861cc
Minor fixes
pragatimodi Jan 10, 2023
d55865e
Minor fixes
pragatimodi Jan 12, 2023
1178082
Minor fixes
pragatimodi Jan 12, 2023
450ffc1
Merge branch 'master' into totp-release
pragatimodi Jan 12, 2023
74f18cb
Fix lint errors
pragatimodi Jan 12, 2023
6d611a7
Merge branch 'totp-release' of https://github.com/firebase/firebase-a…
pragatimodi Jan 12, 2023
c69eec7
Minor Fixes
pragatimodi Jan 12, 2023
e9ed941
Minor fixes
pragatimodi Jan 12, 2023
ff5b340
Merge branch 'totp-release' of https://github.com/firebase/firebase-a…
pragatimodi Jan 12, 2023
dcc3aa7
Minor fix
pragatimodi Jan 12, 2023
e2716c6
Merge branch 'master' into totp-release
pragatimodi Jan 23, 2023
8a27a63
Merge branch 'master' into totp-release
pragatimodi Feb 6, 2023
0c97b50
Removing whitespace only changes
pragatimodi Mar 16, 2023
3e60948
Improvements on comments
pragatimodi Mar 29, 2023
0db11fc
Merge branch 'totp-release' of https://github.com/firebase/firebase-a…
pragatimodi Mar 29, 2023
25affb4
Merge branch 'master' into totp-release
pragatimodi Mar 29, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 27 additions & 0 deletions etc/firebase-admin.auth.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,9 +264,23 @@ export interface ListUsersResult {
users: UserRecord[];
}

// @public
export class MultiFactorAuthConfig implements MultiFactorConfig {
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
// (undocumented)
readonly factorIds: AuthFactorType[];
// (undocumented)
readonly providerConfigs: MultiFactorProviderConfig[];
// (undocumented)
readonly state: MultiFactorConfigState;
// (undocumented)
toJSON(): object;
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
static validate(options: MultiFactorConfig): void;
}

// @public
export interface MultiFactorConfig {
factorIds?: AuthFactorType[];
providerConfigs?: MultiFactorProviderConfig[];
state: MultiFactorConfigState;
}

Expand All @@ -287,6 +301,12 @@ export abstract class MultiFactorInfo {
readonly uid: string;
}

// @public
export interface MultiFactorProviderConfig {
state: MultiFactorConfigState;
totpProviderConfig?: TotpMultiFactorProviderConfig;
}

// @public
export class MultiFactorSettings {
enrolledFactors: MultiFactorInfo[];
Expand Down Expand Up @@ -336,6 +356,7 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo {

// @public
export class ProjectConfig {
readonly multiFactorConfig?: MultiFactorAuthConfig;
readonly smsRegionConfig?: SmsRegionConfig;
toJSON(): object;
}
Expand Down Expand Up @@ -415,6 +436,11 @@ export class TenantManager {
updateTenant(tenantId: string, tenantOptions: UpdateTenantRequest): Promise<Tenant>;
}

// @public
export interface TotpMultiFactorProviderConfig {
adjacentIntervals?: number;
}

// @public
export interface UidIdentifier {
// (undocumented)
Expand All @@ -434,6 +460,7 @@ export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactor

// @public
export interface UpdateProjectConfigRequest {
multiFactorConfig?: MultiFactorConfig;
smsRegionConfig?: SmsRegionConfig;
}

Expand Down
99 changes: 96 additions & 3 deletions src/auth/auth-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ const AUTH_FACTOR_SERVER_TO_CLIENT_TYPE: {[key: string]: AuthFactorType} =
export interface MultiFactorAuthServerConfig {
state?: MultiFactorConfigState;
enabledProviders?: AuthFactorServerType[];
providerConfigs?: MultiFactorProviderConfig[];
}

/**
Expand Down Expand Up @@ -506,6 +507,38 @@ export interface MultiFactorConfig {
* Currently only ‘phone’ is supported.
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
*/
factorIds?: AuthFactorType[];
/**
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
* A list of multi-factor provider specific config.
* New MFA providers (except phone) will indicate enablement/disablement through this field.
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
*/
providerConfigs?: MultiFactorProviderConfig[];
}

/**
* Interface representing Multi Factor Provider configuration.
* This config is used to set second factor auth except for SMS.
* Currently, only TOTP is supported.
*/
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
export interface MultiFactorProviderConfig {
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
/**
* Indicates whether this multi-factor provider is enabled/disabled.
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
*/
state: MultiFactorConfigState;
/**
* TOTP MultiFactor provider config.
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
*/
totpProviderConfig?: TotpMultiFactorProviderConfig;
}

/**
* Interface representing configuration settings for TOTP second factor auth.
*/
export interface TotpMultiFactorProviderConfig {
/**
* The allowed number of adjacent intervals that will be used for verification
* to avoid clock skew.
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
*/
adjacentIntervals?: number;
}

/**
Expand All @@ -516,6 +549,7 @@ export class MultiFactorAuthConfig implements MultiFactorConfig {

public readonly state: MultiFactorConfigState;
public readonly factorIds: AuthFactorType[];
public readonly providerConfigs: MultiFactorProviderConfig[];

/**
* Static method to convert a client side request to a MultiFactorAuthServerConfig.
Expand Down Expand Up @@ -543,6 +577,9 @@ export class MultiFactorAuthConfig implements MultiFactorConfig {
request.enabledProviders = [];
}
}
if (Object.prototype.hasOwnProperty.call(options, 'providerConfigs')) {
request.providerConfigs = options.providerConfigs;
}
return request;
}

Expand All @@ -551,10 +588,11 @@ export class MultiFactorAuthConfig implements MultiFactorConfig {
*
* @param options - The options object to validate.
*/
private static validate(options: MultiFactorConfig): void {
public static validate(options: MultiFactorConfig): void {
const validKeys = {
state: true,
factorIds: true,
providerConfigs: true,
};
if (!validator.isNonNullObject(options)) {
throw new FirebaseAuthError(
Expand All @@ -573,8 +611,8 @@ export class MultiFactorAuthConfig implements MultiFactorConfig {
}
// Validate content.
if (typeof options.state !== 'undefined' &&
options.state !== 'ENABLED' &&
options.state !== 'DISABLED') {
options.state !== 'ENABLED' &&
options.state !== 'DISABLED') {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"MultiFactorConfig.state" must be either "ENABLED" or "DISABLED".',
Expand All @@ -599,6 +637,46 @@ export class MultiFactorAuthConfig implements MultiFactorConfig {
}
});
}

if (typeof options.providerConfigs !== 'undefined') {
if (!validator.isArray(options.providerConfigs)) {
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"MultiFactorConfig.providerConfigs" must be an array of valid "MultiFactorProviderConfig."',
);
}
//Validate content of array.
options.providerConfigs.forEach((multiFactorProviderConfig) => {
if (typeof multiFactorProviderConfig === 'undefined' || !validator.isObject(multiFactorProviderConfig)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
`"${multiFactorProviderConfig}" is not a valid "MultiFactorProviderConfig" type.`
)
}
if (typeof multiFactorProviderConfig.state === 'undefined' ||
(multiFactorProviderConfig.state !== 'ENABLED' &&
multiFactorProviderConfig.state !== 'DISABLED')) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"MultiFactorConfig.providerConfigs.state" must be either "ENABLED" or "DISABLED".',
)
}
// Since TOTP is the only provider config available right now, not defining it will lead into an error
if (typeof multiFactorProviderConfig.totpProviderConfig === 'undefined') {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"MultiFactorConfig.providerConfigs.totpProviderConfig" must be defined.'
)
}
if (typeof multiFactorProviderConfig.totpProviderConfig.adjacentIntervals !== 'undefined' &&
!validator.isNumber(multiFactorProviderConfig.totpProviderConfig.adjacentIntervals)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
'"MultiFactorConfig.providerConfigs.totpProviderConfig.adjacentIntervals" must be a valid number.'
)
}
});
}
}

/**
Expand All @@ -624,13 +702,28 @@ export class MultiFactorAuthConfig implements MultiFactorConfig {
this.factorIds.push(AUTH_FACTOR_SERVER_TO_CLIENT_TYPE[enabledProvider]);
}
})
this.providerConfigs = [];
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
(response.providerConfigs || []).forEach((providerConfig) => {
if (typeof providerConfig !== 'undefined') {
if (typeof providerConfig.state === 'undefined' ||
typeof providerConfig.totpProviderConfig === 'undefined' ||
(typeof providerConfig.totpProviderConfig.adjacentIntervals !== 'undefined' &&
typeof providerConfig.totpProviderConfig.adjacentIntervals !== 'number')) {
Xiaoshouzi-gh marked this conversation as resolved.
Show resolved Hide resolved
throw new FirebaseAuthError(
AuthClientErrorCode.INTERNAL_ERROR,
'INTERNAL ASSERT FAILED: Invalid multi-factor configuration response');
}
this.providerConfigs.push(providerConfig);
}
})
}

/** @returns The plain object representation of the multi-factor config instance. */
public toJSON(): object {
return {
state: this.state,
factorIds: this.factorIds,
providerConfigs: this.providerConfigs,
};
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export {
MultiFactorConfigState,
MultiFactorCreateSettings,
MultiFactorUpdateSettings,
MultiFactorProviderConfig,
OAuthResponseType,
OIDCAuthProviderConfig,
OIDCUpdateAuthProviderRequest,
Expand All @@ -91,6 +92,8 @@ export {
UpdateMultiFactorInfoRequest,
UpdatePhoneMultiFactorInfoRequest,
UpdateRequest,
TotpMultiFactorProviderConfig,
MultiFactorAuthConfig,
} from './auth-config';

export {
Expand Down
41 changes: 39 additions & 2 deletions src/auth/project-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error';
import {
SmsRegionsAuthConfig,
SmsRegionConfig,
MultiFactorConfig,
MultiFactorAuthConfig,
MultiFactorAuthServerConfig,
} from './auth-config';
import { deepCopy } from '../utils/deep-copy';

Expand All @@ -29,6 +32,10 @@ export interface UpdateProjectConfigRequest {
* The SMS configuration to update on the project.
*/
smsRegionConfig?: SmsRegionConfig;
/**
* The multi-factor auth configuration to update on the project.
*/
multiFactorConfig?: MultiFactorConfig;
}

/**
Expand All @@ -37,6 +44,7 @@ export interface UpdateProjectConfigRequest {
*/
export interface ProjectConfigServerResponse {
smsRegionConfig?: SmsRegionConfig;
mfa?: MultiFactorAuthServerConfig;
}

/**
Expand All @@ -45,6 +53,7 @@ export interface ProjectConfigServerResponse {
*/
export interface ProjectConfigClientRequest {
smsRegionConfig?: SmsRegionConfig;
mfa?: MultiFactorAuthServerConfig;
}

/**
Expand All @@ -57,13 +66,19 @@ export class ProjectConfig {
* This is based on the calling code of the destination phone number.
*/
public readonly smsRegionConfig?: SmsRegionConfig;
/**
* The Multi Factor Config for the project.
* Configures the multi factor settings for the project.
* Supports only phone and TOTP.
*/
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
public readonly multiFactorConfig?: MultiFactorAuthConfig;

/**
* Validates a project config options object. Throws an error on failure.
*
* @param request - The project config options object to validate.
*/
private static validate(request: ProjectConfigClientRequest): void {
private static validate(request: UpdateProjectConfigRequest): void {
if (!validator.isNonNullObject(request)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
Expand All @@ -72,6 +87,7 @@ export class ProjectConfig {
}
const validKeys = {
smsRegionConfig: true,
multiFactorConfig: true,
}
// Check for unsupported top level attributes.
for (const key in request) {
Expand All @@ -86,6 +102,11 @@ export class ProjectConfig {
if (typeof request.smsRegionConfig !== 'undefined') {
SmsRegionsAuthConfig.validate(request.smsRegionConfig);
}

// Validate Multi Factor Config if provided
if (typeof request.multiFactorConfig !== 'undefined') {
MultiFactorAuthConfig.validate(request.multiFactorConfig);
}
}

/**
Expand All @@ -97,7 +118,14 @@ export class ProjectConfig {
*/
public static buildServerRequest(configOptions: UpdateProjectConfigRequest): ProjectConfigClientRequest {
ProjectConfig.validate(configOptions);
return configOptions as ProjectConfigClientRequest;
const request = configOptions as any;
if (configOptions.multiFactorConfig !== undefined) {
request.mfa = MultiFactorAuthConfig.buildServerRequest(configOptions.multiFactorConfig);
}
/**Backend API returns "mfa" in case of project config and "mfaConfig" in case of tenant config.
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
The SDK exposes it as multiFactorConfig always.*/
delete request.multiFactorConfig;
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
return request as ProjectConfigClientRequest;
}

/**
Expand All @@ -111,6 +139,11 @@ export class ProjectConfig {
if (typeof response.smsRegionConfig !== 'undefined') {
this.smsRegionConfig = response.smsRegionConfig;
}
/**Backend API returns "mfa" in case of project config and "mfaConfig" in case of tenant config.
The SDK exposes it as multiFactorConfig always.*/
if (typeof response.mfa !== 'undefined') {
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
this.multiFactorConfig = new MultiFactorAuthConfig(response.mfa);
}
}
/**
* Returns a JSON-serializable representation of this object.
Expand All @@ -121,10 +154,14 @@ export class ProjectConfig {
// JSON serialization
const json = {
smsRegionConfig: deepCopy(this.smsRegionConfig),
multiFactorConfig: deepCopy(this.multiFactorConfig),
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
};
if (typeof json.smsRegionConfig === 'undefined') {
delete json.smsRegionConfig;
}
if (typeof json.multiFactorConfig === 'undefined') {
delete json.multiFactorConfig;
}
return json;
}
}
Expand Down