Two-Factor Authentication

Two-factor authentication is SMS-based and backed by Authy. It is enforced on the account-management area and can be triggered on demand elsewhere.

Moving parts

Piece Location

Feature slice

core/pyb/features/two-factor-authentication/

Orchestrating service

TwoFactorAuthenticationService

Modal service

TwoFactorAuthenticationModalService

Setup service

TwoFactorAuthenticationSetupService (views/partials/modals/two-factor/…)

Route guards

TwoFactorAuthenticationGuard, TwoFactorInfoLoadedGuard

Setup banner

TwoFactorSetupBannerInitializerService, constants/two-factor-banners.const.ts

Widgets

two-factor-widget, two-factor-setup-widget

Banner component

two-factor-authentication-required-banner

Permission

TWO_FACTOR_SETUP (default for every authenticated supplier)

State shape

export const twoFAFeatureKey = '2fa';

export interface TwoFAState {
    authyId?: string;                 // Authy registration identifier
    authyRegVerified?: boolean;       // phone number has been verified
    countryCode?: string;
    phoneNumber?: string;
    twoFactChangeConfirmed?: boolean;
    initialized: boolean;             // the slice has been loaded at least once
    loading?: boolean;
    error?: boolean;
    register2FAPhoneSend?: boolean;
    smsSend?: boolean;
}

A supplier counts as configured when authyId is present and authyRegVerified is true.

Enrolment steps

export enum TwoFactorAuthenticationSteps {
    RegisterPhoneNumber = 'register',
    SendSms             = 'sendSms',
    VerifyCode          = 'test',
}

Actions

Action Meaning

fetch2FAInfo

Load the current 2FA configuration.

loaded2FAInfo

Configuration received; sets initialized: true.

register2FAPhoneNumber

Register a phone number with Authy. Accepts bypassHttpErrorsInterceptor.

register2FAPhoneNumberSuccess

Registration accepted; resets authyRegVerified to false.

register2FAPhoneNumberFailed

Registration rejected.

sendSms

Request an SMS code.

sentSmsSuccess / sendSmsError

SMS dispatch outcome.

verifySmsCode

Verify the code during enrolment.

phoneVerified / verifySmsCodeError

Enrolment verification outcome.

confirmSMSCode

Verify the code during a challenge (accepts extra headers).

smsConfirmed

Challenge passed.

verifySmsCode and confirmSMSCode look similar but serve different purposes: the first completes enrolment, the second satisfies a step-up challenge on an already-enrolled account.

Enforcement logic

TwoFactorAuthenticationService.checkTwoFactorAuthenticationState(force) is the single decision point, called by TwoFactorAuthenticationGuard.

async checkTwoFactorAuthenticationState(force = false): Promise<boolean> {
    const is2FaConfigured  = await this.is2FaConfigured().toPromise();
    const is2FaSmsVerified = !force ? this.userService.isTFAConfirmed() : false;

    switch (true) {
        case !is2FaConfigured:
            !this.checkInProgress && this.configure2FA().toPromise().then();  (1)
            return false;
        case is2FaSmsVerified:
            return true;                                                      (2)
        default:
            return await this.open2FaSMSCodeVerificationModal(force).toPromise(); (3)
    }
}
1 Not enrolled → open the setup wizard and deny navigation. A checkInProgress flag prevents a second wizard from stacking.
2 Already challenged this session and force is not set → allow.
3 Enrolled but not challenged (or force is set) → open the SMS code modal and let the result decide.

force comes from route.data.force2FAConfirmation. ManageAccountModule sets it to true, so entering account settings always re-challenges even if the session was already verified.

Enrolment flow

/manage-account/security/two-factor
  → TwoFactorInfoLoadedGuard dispatches fetch2FAInfo() and waits for loaded2FAInfo
  → two-factor-setup-widget renders the current state

  Step 1  register  → register2FAPhoneNumber({ countryCode, phoneNumber })
                    → register2FAPhoneNumberSuccess (authyRegVerified = false)
  Step 2  sendSms   → sendSms() → sentSmsSuccess
  Step 3  test      → verifySmsCode({ verificationCode })
                    → phoneVerified  (authyId + authyRegVerified now set)

Phone input uses ngx-intl-tel-input with google-libphonenumber validation, so the country code and national number are validated client-side before the API is called.

Confirmation emails link to /login/confirm/email/:emailcode, which the root router redirects to manage-account/two-factor/confirm/:emailcode. That route renders ManageAccountComponent, which consumes the code and dispatches confirmSMSCode. Success dispatches userConfirmed2Fa; failure dispatches userConfirmed2FaError.

Setup banner

TwoFactorSetupBannerInitializerService runs from GlobalSubscriptionsService.init(). If the supplier has not enrolled it shows a persistent banner (definitions in constants/two-factor-banners.const.ts) whose CTA opens the setup wizard. The banner is also available as a standalone component, two-factor-authentication-required-banner.

Error handling

Both register2FAPhoneNumber and sendSms accept a bypassHttpErrorsInterceptor flag. When set, the effect adds the bypassHttpErrorHandlerInterceptor header so the generic error toast is suppressed and the modal can render an inline, field-level message instead. See HTTP Interceptors.