(core) Enable MFA configuration (and add SMS)

Summary:
Enables configuration of multi-factor authentication from the
account page (for users who sign in with email/password), and adds
SMS as an authentication method.

Test Plan: Project, browser and server tests.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D3215
This commit is contained in:
George Gevoian
2022-01-19 11:41:06 -08:00
parent 1b4580d92e
commit 0d005eb78d
13 changed files with 839 additions and 345 deletions

View File

@@ -271,6 +271,8 @@ export interface DocStateComparisonDetails {
*/
export interface UserMFAPreferences {
isSmsMfaEnabled: boolean;
// If SMS MFA is enabled, the destination number for receiving verification codes.
phoneNumber?: string;
isSoftwareTokenMfaEnabled: boolean;
}
@@ -278,10 +280,42 @@ export interface UserMFAPreferences {
* Cognito response to initiating software token MFA registration.
*/
export interface SoftwareTokenRegistrationInfo {
session: string;
secretCode: string;
}
/**
* Cognito response to initiating SMS MFA registration.
*/
export interface SMSRegistrationInfo {
deliveryDestination: string;
}
/**
* Cognito response to verifying a password (e.g. in a security verification form).
*/
export type PassVerificationResult = ChallengeRequired | ChallengeNotRequired;
/**
* Information about the follow-up authentication challenge.
*/
interface ChallengeRequired {
isChallengeRequired: true;
// Session identifier that must be re-used in response to auth challenge.
session: string;
challengeName: 'SMS_MFA' | 'SOFTWARE_TOKEN_MFA';
// If challenge is 'SMS_MFA', the destination number that the verification code was sent.
deliveryDestination?: string;
}
/**
* Successful authentication, with no additional challenge required.
*/
interface ChallengeNotRequired {
isChallengeRequired: false;
}
export type AuthMethod = 'TOTP' | 'SMS';
export {UserProfile} from 'app/common/LoginSessionAPI';
export interface UserAPI {
@@ -338,7 +372,13 @@ export interface UserAPI {
}): Promise<string>;
deleteUser(userId: number, name: string): Promise<void>;
registerSoftwareToken(): Promise<SoftwareTokenRegistrationInfo>;
confirmRegisterSoftwareToken(verificationCode: string): Promise<void>;
unregisterSoftwareToken(): Promise<void>;
registerSMS(phoneNumber: string): Promise<SMSRegistrationInfo>;
confirmRegisterSMS(verificationCode: string): Promise<void>;
unregisterSMS(): Promise<void>;
verifyPassword(password: string, preferredMfaMethod?: AuthMethod): Promise<PassVerificationResult>;
verifySecondStep(authMethod: AuthMethod, verificationCode: string, session: string): Promise<void>;
getBaseUrl(): string; // Get the prefix for all the endpoints this object wraps.
forRemoved(): UserAPI; // Get a version of the API that works on removed resources.
getWidgets(): Promise<ICustomWidget[]>;
@@ -695,10 +735,53 @@ export class UserAPIImpl extends BaseAPI implements UserAPI {
return this.requestJson(`${this._url}/api/auth/register_totp`, {method: 'POST'});
}
public async confirmRegisterSoftwareToken(verificationCode: string): Promise<void> {
await this.request(`${this._url}/api/auth/confirm_register_totp`, {
method: 'POST',
body: JSON.stringify({verificationCode}),
});
}
public async unregisterSoftwareToken(): Promise<void> {
await this.request(`${this._url}/api/auth/unregister_totp`, {method: 'POST'});
}
public async registerSMS(phoneNumber: string): Promise<SMSRegistrationInfo> {
return this.requestJson(`${this._url}/api/auth/register_sms`, {
method: 'POST',
body: JSON.stringify({phoneNumber}),
});
}
public async confirmRegisterSMS(verificationCode: string): Promise<void> {
await this.request(`${this._url}/api/auth/confirm_register_sms`, {
method: 'POST',
body: JSON.stringify({verificationCode}),
});
}
public async unregisterSMS(): Promise<void> {
await this.request(`${this._url}/api/auth/unregister_sms`, {method: 'POST'});
}
public async verifyPassword(password: string, preferredMfaMethod?: AuthMethod): Promise<any> {
return this.requestJson(`${this._url}/api/auth/verify_pass`, {
method: 'POST',
body: JSON.stringify({password, preferredMfaMethod}),
});
}
public async verifySecondStep(
authMethod: AuthMethod,
verificationCode: string,
session: string
): Promise<void> {
await this.request(`${this._url}/api/auth/verify_second_step`, {
method: 'POST',
body: JSON.stringify({authMethod, verificationCode, session}),
});
}
public getBaseUrl(): string { return this._url; }
// Recomputes the URL on every call to pick up changes in the URL when switching orgs.