Skip to content

Commit 0ef5cfc

Browse files
feat(auth): Add TOTP support in Project and Tenant config (#1989)
* Sync with master (#1986) * build(deps): bump @types/node from 18.7.23 to 18.11.9 (#1983) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 18.7.23 to 18.11.9. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <[email protected]> Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump @firebase/auth-types from 0.11.0 to 0.11.1 (#1985) * build(deps-dev): bump sinon from 14.0.1 to 14.0.2 (#1984) Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Adding TOTP support for MFA * Merge with master (#1987) * build(deps): bump @types/node from 18.7.23 to 18.11.9 (#1983) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 18.7.23 to 18.11.9. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <[email protected]> Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump @firebase/auth-types from 0.11.0 to 0.11.1 (#1985) * build(deps-dev): bump sinon from 14.0.1 to 14.0.2 (#1984) Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Revert "Merge with master (#1987)" (#1988) This reverts commit 5ebf34b. * Revert "Sync with master (#1986)" This reverts commit 3290943. * Update auth-api-request.ts * Update auth-api-request.ts * Addressing comments, adding tests and cleaning up * Auto generated after running `$ npm run api-extractor:local` * Linter fixes * Resolving review comments * Sync MFA field with backend * Reviewed changes * Formatting fix * Reverting packagelock.json auto changes * Project server config updates * Import fix * Fix lint errors * Unit tests fix * api extractor fix * Import fix * Import fix * API extractor changes * Adding documentation * `npm run api-extractor:local` changes * Undo whitespace changes * Review fixes * Variable names fix * Removing whitespace changes from package-lock.json * Lint error * Minor updates * Minor updates * Adding some more validators and unit test * Minor fixes * Minor fixes * Minor fixes * Fix lint errors * Minor Fixes * Minor fixes * Minor fix * Removing whitespace only changes * Improvements on comments --------- Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent 8688b3f commit 0ef5cfc

File tree

8 files changed

+549
-10
lines changed

8 files changed

+549
-10
lines changed

etc/firebase-admin.auth.api.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ export interface ListUsersResult {
267267
// @public
268268
export interface MultiFactorConfig {
269269
factorIds?: AuthFactorType[];
270+
providerConfigs?: MultiFactorProviderConfig[];
270271
state: MultiFactorConfigState;
271272
}
272273

@@ -287,6 +288,12 @@ export abstract class MultiFactorInfo {
287288
readonly uid: string;
288289
}
289290

291+
// @public
292+
export interface MultiFactorProviderConfig {
293+
state: MultiFactorConfigState;
294+
totpProviderConfig?: TotpMultiFactorProviderConfig;
295+
}
296+
290297
// @public
291298
export class MultiFactorSettings {
292299
enrolledFactors: MultiFactorInfo[];
@@ -336,6 +343,7 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo {
336343

337344
// @public
338345
export class ProjectConfig {
346+
get multiFactorConfig(): MultiFactorConfig | undefined;
339347
readonly smsRegionConfig?: SmsRegionConfig;
340348
toJSON(): object;
341349
}
@@ -415,6 +423,11 @@ export class TenantManager {
415423
updateTenant(tenantId: string, tenantOptions: UpdateTenantRequest): Promise<Tenant>;
416424
}
417425

426+
// @public
427+
export interface TotpMultiFactorProviderConfig {
428+
adjacentIntervals?: number;
429+
}
430+
418431
// @public
419432
export interface UidIdentifier {
420433
// (undocumented)
@@ -434,6 +447,7 @@ export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactor
434447

435448
// @public
436449
export interface UpdateProjectConfigRequest {
450+
multiFactorConfig?: MultiFactorConfig;
437451
smsRegionConfig?: SmsRegionConfig;
438452
}
439453

src/auth/auth-config.ts

Lines changed: 130 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ const AUTH_FACTOR_SERVER_TO_CLIENT_TYPE: {[key: string]: AuthFactorType} =
478478
export interface MultiFactorAuthServerConfig {
479479
state?: MultiFactorConfigState;
480480
enabledProviders?: AuthFactorServerType[];
481+
providerConfigs?: MultiFactorProviderConfig[];
481482
}
482483

483484
/**
@@ -506,16 +507,58 @@ export interface MultiFactorConfig {
506507
* Currently only ‘phone’ is supported.
507508
*/
508509
factorIds?: AuthFactorType[];
510+
511+
/**
512+
* A list of multi-factor provider configurations.
513+
* MFA providers (except phone) indicate whether they're enabled through this field. */
514+
providerConfigs?: MultiFactorProviderConfig[];
515+
}
516+
517+
/**
518+
* Interface representing a multi-factor auth provider configuration.
519+
* This interface is used for second factor auth providers other than SMS.
520+
* Currently, only TOTP is supported.
521+
*/export interface MultiFactorProviderConfig {
522+
/**
523+
* Indicates whether this multi-factor provider is enabled or disabled. */
524+
state: MultiFactorConfigState;
525+
/**
526+
* TOTP multi-factor provider config. */
527+
totpProviderConfig?: TotpMultiFactorProviderConfig;
528+
}
529+
530+
/**
531+
* Interface representing configuration settings for TOTP second factor auth.
532+
*/
533+
export interface TotpMultiFactorProviderConfig {
534+
/**
535+
* The allowed number of adjacent intervals that will be used for verification
536+
* to compensate for clock skew. */
537+
adjacentIntervals?: number;
509538
}
510539

511540
/**
512541
* Defines the multi-factor config class used to convert client side MultiFactorConfig
513542
* to a format that is understood by the Auth server.
543+
*
544+
* @internal
514545
*/
515546
export class MultiFactorAuthConfig implements MultiFactorConfig {
516547

548+
/**
549+
* The multi-factor config state.
550+
*/
517551
public readonly state: MultiFactorConfigState;
552+
/**
553+
* The list of identifiers for enabled second factors.
554+
* Currently only ‘phone’ is supported.
555+
*/
518556
public readonly factorIds: AuthFactorType[];
557+
/**
558+
* A list of multi-factor provider specific config.
559+
* New MFA providers (except phone) will indicate enablement/disablement through this field.
560+
*/
561+
public readonly providerConfigs: MultiFactorProviderConfig[];
519562

520563
/**
521564
* Static method to convert a client side request to a MultiFactorAuthServerConfig.
@@ -543,6 +586,9 @@ export class MultiFactorAuthConfig implements MultiFactorConfig {
543586
request.enabledProviders = [];
544587
}
545588
}
589+
if (Object.prototype.hasOwnProperty.call(options, 'providerConfigs')) {
590+
request.providerConfigs = options.providerConfigs;
591+
}
546592
return request;
547593
}
548594

@@ -551,10 +597,11 @@ export class MultiFactorAuthConfig implements MultiFactorConfig {
551597
*
552598
* @param options - The options object to validate.
553599
*/
554-
private static validate(options: MultiFactorConfig): void {
600+
public static validate(options: MultiFactorConfig): void {
555601
const validKeys = {
556602
state: true,
557603
factorIds: true,
604+
providerConfigs: true,
558605
};
559606
if (!validator.isNonNullObject(options)) {
560607
throw new FirebaseAuthError(
@@ -599,6 +646,71 @@ export class MultiFactorAuthConfig implements MultiFactorConfig {
599646
}
600647
});
601648
}
649+
650+
if (typeof options.providerConfigs !== 'undefined') {
651+
if (!validator.isArray(options.providerConfigs)) {
652+
throw new FirebaseAuthError(
653+
AuthClientErrorCode.INVALID_CONFIG,
654+
'"MultiFactorConfig.providerConfigs" must be an array of valid "MultiFactorProviderConfig."',
655+
);
656+
}
657+
//Validate content of array.
658+
options.providerConfigs.forEach((multiFactorProviderConfig) => {
659+
if (typeof multiFactorProviderConfig === 'undefined' || !validator.isObject(multiFactorProviderConfig)) {
660+
throw new FirebaseAuthError(
661+
AuthClientErrorCode.INVALID_CONFIG,
662+
`"${multiFactorProviderConfig}" is not a valid "MultiFactorProviderConfig" type.`
663+
)
664+
}
665+
const validProviderConfigKeys = {
666+
state: true,
667+
totpProviderConfig: true,
668+
};
669+
for (const key in multiFactorProviderConfig) {
670+
if (!(key in validProviderConfigKeys)) {
671+
throw new FirebaseAuthError(
672+
AuthClientErrorCode.INVALID_CONFIG,
673+
`"${key}" is not a valid ProviderConfig parameter.`,
674+
);
675+
}
676+
}
677+
if (typeof multiFactorProviderConfig.state === 'undefined' ||
678+
(multiFactorProviderConfig.state !== 'ENABLED' &&
679+
multiFactorProviderConfig.state !== 'DISABLED')) {
680+
throw new FirebaseAuthError(
681+
AuthClientErrorCode.INVALID_CONFIG,
682+
'"MultiFactorConfig.providerConfigs.state" must be either "ENABLED" or "DISABLED".',
683+
)
684+
}
685+
// Since TOTP is the only provider config available right now, not defining it will lead into an error
686+
if (typeof multiFactorProviderConfig.totpProviderConfig === 'undefined') {
687+
throw new FirebaseAuthError(
688+
AuthClientErrorCode.INVALID_CONFIG,
689+
'"MultiFactorConfig.providerConfigs.totpProviderConfig" must be defined.'
690+
)
691+
}
692+
const validTotpProviderConfigKeys = {
693+
adjacentIntervals: true,
694+
};
695+
for (const key in multiFactorProviderConfig.totpProviderConfig) {
696+
if (!(key in validTotpProviderConfigKeys)) {
697+
throw new FirebaseAuthError(
698+
AuthClientErrorCode.INVALID_CONFIG,
699+
`"${key}" is not a valid TotpProviderConfig parameter.`,
700+
);
701+
}
702+
}
703+
const adjIntervals = multiFactorProviderConfig.totpProviderConfig.adjacentIntervals
704+
if (typeof adjIntervals !== 'undefined' &&
705+
(!Number.isInteger(adjIntervals) || adjIntervals < 0 || adjIntervals > 10)) {
706+
throw new FirebaseAuthError(
707+
AuthClientErrorCode.INVALID_ARGUMENT,
708+
'"MultiFactorConfig.providerConfigs.totpProviderConfig.adjacentIntervals" must' +
709+
' be a valid number between 0 and 10 (both inclusive).'
710+
)
711+
}
712+
});
713+
}
602714
}
603715

604716
/**
@@ -624,13 +736,29 @@ export class MultiFactorAuthConfig implements MultiFactorConfig {
624736
this.factorIds.push(AUTH_FACTOR_SERVER_TO_CLIENT_TYPE[enabledProvider]);
625737
}
626738
})
739+
this.providerConfigs = [];
740+
(response.providerConfigs || []).forEach((providerConfig) => {
741+
if (typeof providerConfig !== 'undefined') {
742+
if (typeof providerConfig.state === 'undefined' ||
743+
typeof providerConfig.totpProviderConfig === 'undefined' ||
744+
(typeof providerConfig.totpProviderConfig.adjacentIntervals !== 'undefined' &&
745+
typeof providerConfig.totpProviderConfig.adjacentIntervals !== 'number')) {
746+
throw new FirebaseAuthError(
747+
AuthClientErrorCode.INTERNAL_ERROR,
748+
'INTERNAL ASSERT FAILED: Invalid multi-factor configuration response');
749+
}
750+
this.providerConfigs.push(providerConfig);
751+
}
752+
})
627753
}
628754

629-
/** @returns The plain object representation of the multi-factor config instance. */
755+
/** Converts MultiFactorConfig to JSON object
756+
* @returns The plain object representation of the multi-factor config instance. */
630757
public toJSON(): object {
631758
return {
632759
state: this.state,
633760
factorIds: this.factorIds,
761+
providerConfigs: this.providerConfigs,
634762
};
635763
}
636764
}

src/auth/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export {
8080
MultiFactorConfigState,
8181
MultiFactorCreateSettings,
8282
MultiFactorUpdateSettings,
83+
MultiFactorProviderConfig,
8384
OAuthResponseType,
8485
OIDCAuthProviderConfig,
8586
OIDCUpdateAuthProviderRequest,
@@ -91,6 +92,7 @@ export {
9192
UpdateMultiFactorInfoRequest,
9293
UpdatePhoneMultiFactorInfoRequest,
9394
UpdateRequest,
95+
TotpMultiFactorProviderConfig,
9496
} from './auth-config';
9597

9698
export {

src/auth/project-config.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error';
1818
import {
1919
SmsRegionsAuthConfig,
2020
SmsRegionConfig,
21+
MultiFactorConfig,
22+
MultiFactorAuthConfig,
23+
MultiFactorAuthServerConfig,
2124
} from './auth-config';
2225
import { deepCopy } from '../utils/deep-copy';
2326

@@ -29,6 +32,10 @@ export interface UpdateProjectConfigRequest {
2932
* The SMS configuration to update on the project.
3033
*/
3134
smsRegionConfig?: SmsRegionConfig;
35+
/**
36+
* The multi-factor auth configuration to update on the project.
37+
*/
38+
multiFactorConfig?: MultiFactorConfig;
3239
}
3340

3441
/**
@@ -37,6 +44,7 @@ export interface UpdateProjectConfigRequest {
3744
*/
3845
export interface ProjectConfigServerResponse {
3946
smsRegionConfig?: SmsRegionConfig;
47+
mfa?: MultiFactorAuthServerConfig;
4048
}
4149

4250
/**
@@ -45,6 +53,7 @@ export interface ProjectConfigServerResponse {
4553
*/
4654
export interface ProjectConfigClientRequest {
4755
smsRegionConfig?: SmsRegionConfig;
56+
mfa?: MultiFactorAuthServerConfig;
4857
}
4958

5059
/**
@@ -57,13 +66,23 @@ export class ProjectConfig {
5766
* This is based on the calling code of the destination phone number.
5867
*/
5968
public readonly smsRegionConfig?: SmsRegionConfig;
69+
/**
70+
* The project's multi-factor auth configuration.
71+
* Supports only phone and TOTP.
72+
*/ private readonly multiFactorConfig_?: MultiFactorConfig;
73+
/**
74+
* The multi-factor auth configuration.
75+
*/
76+
get multiFactorConfig(): MultiFactorConfig | undefined {
77+
return this.multiFactorConfig_;
78+
}
6079

6180
/**
6281
* Validates a project config options object. Throws an error on failure.
6382
*
6483
* @param request - The project config options object to validate.
6584
*/
66-
private static validate(request: ProjectConfigClientRequest): void {
85+
private static validate(request: UpdateProjectConfigRequest): void {
6786
if (!validator.isNonNullObject(request)) {
6887
throw new FirebaseAuthError(
6988
AuthClientErrorCode.INVALID_ARGUMENT,
@@ -72,6 +91,7 @@ export class ProjectConfig {
7291
}
7392
const validKeys = {
7493
smsRegionConfig: true,
94+
multiFactorConfig: true,
7595
}
7696
// Check for unsupported top level attributes.
7797
for (const key in request) {
@@ -86,6 +106,11 @@ export class ProjectConfig {
86106
if (typeof request.smsRegionConfig !== 'undefined') {
87107
SmsRegionsAuthConfig.validate(request.smsRegionConfig);
88108
}
109+
110+
// Validate Multi Factor Config if provided
111+
if (typeof request.multiFactorConfig !== 'undefined') {
112+
MultiFactorAuthConfig.validate(request.multiFactorConfig);
113+
}
89114
}
90115

91116
/**
@@ -97,7 +122,16 @@ export class ProjectConfig {
97122
*/
98123
public static buildServerRequest(configOptions: UpdateProjectConfigRequest): ProjectConfigClientRequest {
99124
ProjectConfig.validate(configOptions);
100-
return configOptions as ProjectConfigClientRequest;
125+
const request = configOptions as any;
126+
if (configOptions.multiFactorConfig !== undefined) {
127+
request.mfa = MultiFactorAuthConfig.buildServerRequest(configOptions.multiFactorConfig);
128+
}
129+
// Backend API returns "mfa" in case of project config and "mfaConfig" in case of tenant config.
130+
// The SDK exposes it as multiFactorConfig always.
131+
// See https://cloud.google.com/identity-platform/docs/reference/rest/v2/projects.tenants#resource:-tenant
132+
// and https://cloud.google.com/identity-platform/docs/reference/rest/v2/Config
133+
delete request.multiFactorConfig;
134+
return request as ProjectConfigClientRequest;
101135
}
102136

103137
/**
@@ -111,6 +145,11 @@ export class ProjectConfig {
111145
if (typeof response.smsRegionConfig !== 'undefined') {
112146
this.smsRegionConfig = response.smsRegionConfig;
113147
}
148+
//Backend API returns "mfa" in case of project config and "mfaConfig" in case of tenant config.
149+
//The SDK exposes it as multiFactorConfig always.
150+
if (typeof response.mfa !== 'undefined') {
151+
this.multiFactorConfig_ = new MultiFactorAuthConfig(response.mfa);
152+
}
114153
}
115154
/**
116155
* Returns a JSON-serializable representation of this object.
@@ -121,10 +160,14 @@ export class ProjectConfig {
121160
// JSON serialization
122161
const json = {
123162
smsRegionConfig: deepCopy(this.smsRegionConfig),
163+
multiFactorConfig: deepCopy(this.multiFactorConfig),
124164
};
125165
if (typeof json.smsRegionConfig === 'undefined') {
126166
delete json.smsRegionConfig;
127167
}
168+
if (typeof json.multiFactorConfig === 'undefined') {
169+
delete json.multiFactorConfig;
170+
}
128171
return json;
129172
}
130173
}

0 commit comments

Comments
 (0)