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 3 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
4 changes: 0 additions & 4 deletions etc/firebase-admin.auth.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,13 +266,9 @@ export interface ListUsersResult {

// @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;
static validate(options: MultiFactorConfig): void;
}
Expand Down
129 changes: 83 additions & 46 deletions src/auth/auth-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,14 +462,14 @@ export interface EmailSignInConfigServerRequest {
type AuthFactorServerType = 'PHONE_SMS';

/** Client Auth factor type to server auth factor type mapping. */
const AUTH_FACTOR_CLIENT_TO_SERVER_TYPE: {[key: string]: AuthFactorServerType} = {
const AUTH_FACTOR_CLIENT_TO_SERVER_TYPE: { [key: string]: AuthFactorServerType } = {
phone: 'PHONE_SMS',
};

/** Server Auth factor type to client auth factor type mapping. */
const AUTH_FACTOR_SERVER_TO_CLIENT_TYPE: {[key: string]: AuthFactorType} =
const AUTH_FACTOR_SERVER_TO_CLIENT_TYPE: { [key: string]: AuthFactorType } =
Object.keys(AUTH_FACTOR_CLIENT_TO_SERVER_TYPE)
.reduce((res: {[key: string]: AuthFactorType}, key) => {
.reduce((res: { [key: string]: AuthFactorType }, key) => {
res[AUTH_FACTOR_CLIENT_TO_SERVER_TYPE[key]] = key as AuthFactorType;
return res;
}, {});
Expand Down Expand Up @@ -507,6 +507,7 @@ 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.
Expand Down Expand Up @@ -547,8 +548,19 @@ export interface TotpMultiFactorProviderConfig {
*/
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[];

/**
Expand Down Expand Up @@ -653,6 +665,18 @@ export class MultiFactorAuthConfig implements MultiFactorConfig {
`"${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')) {
Expand All @@ -668,11 +692,23 @@ export class MultiFactorAuthConfig implements MultiFactorConfig {
'"MultiFactorConfig.providerConfigs.totpProviderConfig" must be defined.'
)
}
if (typeof multiFactorProviderConfig.totpProviderConfig.adjacentIntervals !== 'undefined' &&
!validator.isNumber(multiFactorProviderConfig.totpProviderConfig.adjacentIntervals)) {
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.`,
);
}
}
var adjIntervals = multiFactorProviderConfig.totpProviderConfig.adjacentIntervals
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
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.'
'"MultiFactorConfig.providerConfigs.totpProviderConfig.adjacentIntervals" must be a valid number between 0 and 10 (both inclusive).'
)
}
});
Expand Down Expand Up @@ -718,7 +754,8 @@ export class MultiFactorAuthConfig implements MultiFactorConfig {
})
}

/** @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,
Expand All @@ -734,7 +771,7 @@ export class MultiFactorAuthConfig implements MultiFactorConfig {
* @param testPhoneNumbers - The phone number / code pairs to validate.
*/
export function validateTestPhoneNumbers(
testPhoneNumbers: {[phoneNumber: string]: string},
testPhoneNumbers: { [phoneNumber: string]: string },
): void {
if (!validator.isObject(testPhoneNumbers)) {
throw new FirebaseAuthError(
Expand All @@ -756,7 +793,7 @@ export function validateTestPhoneNumbers(

// Validate code.
if (!validator.isString(testPhoneNumbers[phoneNumber]) ||
!/^[\d]{6}$/.test(testPhoneNumbers[phoneNumber])) {
!/^[\d]{6}$/.test(testPhoneNumbers[phoneNumber])) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_TESTING_PHONE_NUMBER,
`"${testPhoneNumbers[phoneNumber]}" is not a valid 6 digit code string.`
Expand Down Expand Up @@ -840,14 +877,14 @@ export class EmailSignInConfig implements EmailSignInProviderConfig {
}
// Validate content.
if (typeof options.enabled !== 'undefined' &&
!validator.isBoolean(options.enabled)) {
!validator.isBoolean(options.enabled)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
'"EmailSignInConfig.enabled" must be a boolean.',
);
}
if (typeof options.passwordRequired !== 'undefined' &&
!validator.isBoolean(options.passwordRequired)) {
!validator.isBoolean(options.passwordRequired)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
'"EmailSignInConfig.passwordRequired" must be a boolean.',
Expand All @@ -862,7 +899,7 @@ export class EmailSignInConfig implements EmailSignInProviderConfig {
* EmailSignInConfig object.
* @constructor
*/
constructor(response: {[key: string]: any}) {
constructor(response: { [key: string]: any }) {
if (typeof response.allowPasswordSignup === 'undefined') {
throw new FirebaseAuthError(
AuthClientErrorCode.INTERNAL_ERROR,
Expand Down Expand Up @@ -1054,7 +1091,7 @@ export class SAMLConfig implements SAMLAuthProviderConfig {
options: Partial<SAMLAuthProviderConfig>,
ignoreMissingFields = false): SAMLConfigServerRequest | null {
const makeRequest = validator.isNonNullObject(options) &&
(options.providerId || ignoreMissingFields);
(options.providerId || ignoreMissingFields);
if (!makeRequest) {
return null;
}
Expand Down Expand Up @@ -1159,36 +1196,36 @@ export class SAMLConfig implements SAMLAuthProviderConfig {
);
}
if (!(ignoreMissingFields && typeof options.idpEntityId === 'undefined') &&
!validator.isNonEmptyString(options.idpEntityId)) {
!validator.isNonEmptyString(options.idpEntityId)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"SAMLAuthProviderConfig.idpEntityId" must be a valid non-empty string.',
);
}
if (!(ignoreMissingFields && typeof options.ssoURL === 'undefined') &&
!validator.isURL(options.ssoURL)) {
!validator.isURL(options.ssoURL)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"SAMLAuthProviderConfig.ssoURL" must be a valid URL string.',
);
}
if (!(ignoreMissingFields && typeof options.rpEntityId === 'undefined') &&
!validator.isNonEmptyString(options.rpEntityId)) {
!validator.isNonEmptyString(options.rpEntityId)) {
throw new FirebaseAuthError(
!options.rpEntityId ? AuthClientErrorCode.MISSING_SAML_RELYING_PARTY_CONFIG :
AuthClientErrorCode.INVALID_CONFIG,
'"SAMLAuthProviderConfig.rpEntityId" must be a valid non-empty string.',
);
}
if (!(ignoreMissingFields && typeof options.callbackURL === 'undefined') &&
!validator.isURL(options.callbackURL)) {
!validator.isURL(options.callbackURL)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"SAMLAuthProviderConfig.callbackURL" must be a valid URL string.',
);
}
if (!(ignoreMissingFields && typeof options.x509Certificates === 'undefined') &&
!validator.isArray(options.x509Certificates)) {
!validator.isArray(options.x509Certificates)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"SAMLAuthProviderConfig.x509Certificates" must be a valid array of X509 certificate strings.',
Expand All @@ -1203,21 +1240,21 @@ export class SAMLConfig implements SAMLAuthProviderConfig {
}
});
if (typeof (options as any).enableRequestSigning !== 'undefined' &&
!validator.isBoolean((options as any).enableRequestSigning)) {
!validator.isBoolean((options as any).enableRequestSigning)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"SAMLAuthProviderConfig.enableRequestSigning" must be a boolean.',
);
}
if (typeof options.enabled !== 'undefined' &&
!validator.isBoolean(options.enabled)) {
!validator.isBoolean(options.enabled)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"SAMLAuthProviderConfig.enabled" must be a boolean.',
);
}
if (typeof options.displayName !== 'undefined' &&
!validator.isString(options.displayName)) {
!validator.isString(options.displayName)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"SAMLAuthProviderConfig.displayName" must be a valid string.',
Expand All @@ -1233,14 +1270,14 @@ export class SAMLConfig implements SAMLAuthProviderConfig {
*/
constructor(response: SAMLConfigServerResponse) {
if (!response ||
!response.idpConfig ||
!response.idpConfig.idpEntityId ||
!response.idpConfig.ssoUrl ||
!response.spConfig ||
!response.spConfig.spEntityId ||
!response.name ||
!(validator.isString(response.name) &&
SAMLConfig.getProviderIdFromResourceName(response.name))) {
!response.idpConfig ||
!response.idpConfig.idpEntityId ||
!response.idpConfig.ssoUrl ||
!response.spConfig ||
!response.spConfig.spEntityId ||
!response.name ||
!(validator.isString(response.name) &&
SAMLConfig.getProviderIdFromResourceName(response.name))) {
throw new FirebaseAuthError(
AuthClientErrorCode.INTERNAL_ERROR,
'INTERNAL ASSERT FAILED: Invalid SAML configuration response');
Expand Down Expand Up @@ -1318,7 +1355,7 @@ export class OIDCConfig implements OIDCAuthProviderConfig {
options: Partial<OIDCAuthProviderConfig>,
ignoreMissingFields = false): OIDCConfigServerRequest | null {
const makeRequest = validator.isNonNullObject(options) &&
(options.providerId || ignoreMissingFields);
(options.providerId || ignoreMissingFields);
if (!makeRequest) {
return null;
}
Expand Down Expand Up @@ -1411,35 +1448,35 @@ export class OIDCConfig implements OIDCAuthProviderConfig {
);
}
if (!(ignoreMissingFields && typeof options.clientId === 'undefined') &&
!validator.isNonEmptyString(options.clientId)) {
!validator.isNonEmptyString(options.clientId)) {
throw new FirebaseAuthError(
!options.clientId ? AuthClientErrorCode.MISSING_OAUTH_CLIENT_ID : AuthClientErrorCode.INVALID_OAUTH_CLIENT_ID,
'"OIDCAuthProviderConfig.clientId" must be a valid non-empty string.',
);
}
if (!(ignoreMissingFields && typeof options.issuer === 'undefined') &&
!validator.isURL(options.issuer)) {
!validator.isURL(options.issuer)) {
throw new FirebaseAuthError(
!options.issuer ? AuthClientErrorCode.MISSING_ISSUER : AuthClientErrorCode.INVALID_CONFIG,
'"OIDCAuthProviderConfig.issuer" must be a valid URL string.',
);
}
if (typeof options.enabled !== 'undefined' &&
!validator.isBoolean(options.enabled)) {
!validator.isBoolean(options.enabled)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"OIDCAuthProviderConfig.enabled" must be a boolean.',
);
}
if (typeof options.displayName !== 'undefined' &&
!validator.isString(options.displayName)) {
!validator.isString(options.displayName)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"OIDCAuthProviderConfig.displayName" must be a valid string.',
);
}
if (typeof options.clientSecret !== 'undefined' &&
!validator.isNonEmptyString(options.clientSecret)) {
!validator.isNonEmptyString(options.clientSecret)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"OIDCAuthProviderConfig.clientSecret" must be a valid string.',
Expand Down Expand Up @@ -1499,11 +1536,11 @@ export class OIDCConfig implements OIDCAuthProviderConfig {
*/
constructor(response: OIDCConfigServerResponse) {
if (!response ||
!response.issuer ||
!response.clientId ||
!response.name ||
!(validator.isString(response.name) &&
OIDCConfig.getProviderIdFromResourceName(response.name))) {
!response.issuer ||
!response.clientId ||
!response.name ||
!(validator.isString(response.name) &&
OIDCConfig.getProviderIdFromResourceName(response.name))) {
throw new FirebaseAuthError(
AuthClientErrorCode.INTERNAL_ERROR,
'INTERNAL ASSERT FAILED: Invalid OIDC configuration response');
Expand Down Expand Up @@ -1596,12 +1633,12 @@ export interface AllowByDefault {
* allowlist.
*/
export interface AllowlistOnly {
/**
* Two letter unicode region codes to allow as defined by
* https://cldr.unicode.org/
* The full list of these region codes is here:
* https://github.com/unicode-cldr/cldr-localenames-full/blob/master/main/en/territories.json
*/
/**
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
* Two letter unicode region codes to allow as defined by
* https://cldr.unicode.org/
* The full list of these region codes is here:
* https://github.com/unicode-cldr/cldr-localenames-full/blob/master/main/en/territories.json
*/
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
allowedRegions: string[];
}

Expand Down
10 changes: 6 additions & 4 deletions src/auth/project-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,10 @@ export class ProjectConfig {
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.*/
//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
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
delete request.multiFactorConfig;
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
return request as ProjectConfigClientRequest;
}
Expand All @@ -139,8 +141,8 @@ 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.*/
//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);
}
Expand Down