Skip to content

Commit

Permalink
feat(auth): Add TOTP support in Project and Tenant config (#1989)
Browse files Browse the repository at this point in the history
* 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] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
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] <support@github.com>
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] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
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] <support@github.com>
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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
  • Loading branch information
pragatimodi and dependabot[bot] committed Mar 29, 2023
1 parent 8688b3f commit 0ef5cfc
Show file tree
Hide file tree
Showing 8 changed files with 549 additions and 10 deletions.
14 changes: 14 additions & 0 deletions etc/firebase-admin.auth.api.md
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
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.
*/
factorIds?: AuthFactorType[];

/**
* 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)) {
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 = [];
(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')) {
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
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
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;
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') {
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),
};
if (typeof json.smsRegionConfig === 'undefined') {
delete json.smsRegionConfig;
}
if (typeof json.multiFactorConfig === 'undefined') {
delete json.multiFactorConfig;
}
return json;
}
}
Expand Down

0 comments on commit 0ef5cfc

Please sign in to comment.