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 all 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
14 changes: 14 additions & 0 deletions etc/firebase-admin.auth.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ export interface ListUsersResult {
// @public
export interface MultiFactorConfig {
factorIds?: AuthFactorType[];
providerConfigs?: MultiFactorProviderConfig[];
state: MultiFactorConfigState;
}

Expand All @@ -287,6 +288,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 +343,7 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo {

// @public
export class ProjectConfig {
get multiFactorConfig(): MultiFactorConfig | undefined;
readonly smsRegionConfig?: SmsRegionConfig;
toJSON(): object;
}
Expand Down Expand Up @@ -415,6 +423,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 +447,7 @@ export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactor

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

Expand Down
132 changes: 130 additions & 2 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,16 +507,58 @@ 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 configurations.
* MFA providers (except phone) indicate whether they're enabled through this field. */
providerConfigs?: MultiFactorProviderConfig[];
}

/**
* Interface representing a multi-factor auth provider configuration.
* This interface is used for second factor auth providers other than SMS.
* Currently, only TOTP is supported.
*/export interface MultiFactorProviderConfig {
/**
* Indicates whether this multi-factor provider is enabled or disabled. */
state: MultiFactorConfigState;
/**
* TOTP multi-factor provider config. */
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 compensate for clock skew. */
adjacentIntervals?: number;
}

/**
* Defines the multi-factor config class used to convert client side MultiFactorConfig
* to a format that is understood by the Auth server.
*
* @internal
*/
export class MultiFactorAuthConfig implements MultiFactorConfig {

/**
* The multi-factor config state.
*/
public readonly state: MultiFactorConfigState;
/**
* The list of identifiers for enabled second factors.
* Currently only ‘phone’ is supported.
*/
public readonly factorIds: AuthFactorType[];
/**
* A list of multi-factor provider specific config.
* New MFA providers (except phone) will indicate enablement/disablement through this field.
*/
public readonly providerConfigs: MultiFactorProviderConfig[];

/**
* Static method to convert a client side request to a MultiFactorAuthServerConfig.
Expand Down Expand Up @@ -543,6 +586,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 +597,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 Down Expand Up @@ -599,6 +646,71 @@ 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.`
)
}
const validProviderConfigKeys = {
state: true,
totpProviderConfig: true,
};
for (const key in multiFactorProviderConfig) {
if (!(key in validProviderConfigKeys)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
`"${key}" is not a valid ProviderConfig parameter.`,
);
}
}
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.'
)
}
const validTotpProviderConfigKeys = {
adjacentIntervals: true,
};
for (const key in multiFactorProviderConfig.totpProviderConfig) {
if (!(key in validTotpProviderConfigKeys)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
`"${key}" is not a valid TotpProviderConfig parameter.`,
);
}
}
const adjIntervals = multiFactorProviderConfig.totpProviderConfig.adjacentIntervals
if (typeof adjIntervals !== 'undefined' &&
(!Number.isInteger(adjIntervals) || adjIntervals < 0 || adjIntervals > 10)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
'"MultiFactorConfig.providerConfigs.totpProviderConfig.adjacentIntervals" must' +
' be a valid number between 0 and 10 (both inclusive).'
)
}
});
}
}

/**
Expand All @@ -624,13 +736,29 @@ 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. */
/** Converts MultiFactorConfig to JSON object
* @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
2 changes: 2 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,7 @@ export {
UpdateMultiFactorInfoRequest,
UpdatePhoneMultiFactorInfoRequest,
UpdateRequest,
TotpMultiFactorProviderConfig,
} from './auth-config';

export {
Expand Down
47 changes: 45 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,23 @@ export class ProjectConfig {
* This is based on the calling code of the destination phone number.
*/
public readonly smsRegionConfig?: SmsRegionConfig;
/**
* The project's multi-factor auth configuration.
* Supports only phone and TOTP.
*/ private readonly multiFactorConfig_?: MultiFactorConfig;
/**
* The multi-factor auth configuration.
*/
get multiFactorConfig(): MultiFactorConfig | undefined {
return this.multiFactorConfig_;
}

/**
* 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 +91,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 +106,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 +122,16 @@ 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.
// The SDK exposes it as multiFactorConfig always.
// See https://cloud.google.com/identity-platform/docs/reference/rest/v2/projects.tenants#resource:-tenant
// and https://cloud.google.com/identity-platform/docs/reference/rest/v2/Config
delete request.multiFactorConfig;
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
return request as ProjectConfigClientRequest;
}

/**
Expand All @@ -111,6 +145,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 +160,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