HyperSaaS
BackendAuthentication

Two-Factor Authentication

Codes from an authenticator app or by email, recovery codes, and confirming it's you before changes.

People can ask for a code from an authenticator app (1Password, Google Authenticator, Authy and others), as well as their password, when they sign in. It's optional, turned on in Settings → Security. Ten recovery codes cover a lost phone. People without an app can choose a code by email instead.

It's django-allauth's MFA, through its API mode:

MFA_SUPPORTED_TYPES = ["totp", "recovery_codes"]
MFA_TOTP_ISSUER = env("MFA_TOTP_ISSUER", default="HyperSaaS")  # the name the app shows
MFA_RECOVERY_CODES_SHOW_ONCE = True
ACCOUNT_EMAIL_NOTIFICATIONS = True

Signing In

With two-factor on, the password step answers 401, waiting for a code:

{
  "status": 401,
  "data": {
    "flows": [
      { "id": "login" },
      { "id": "mfa_authenticate", "is_pending": true, "types": ["recovery_codes", "totp"] }
    ]
  },
  "meta": { "is_authenticated": false, "session_token": "..." }
}

Send the code with that session token:

POST /_allauth/app/v1/auth/2fa/authenticate
X-Session-Token: <session_token>
{ "code": "123456" }

The answer is the same as a sign-in without two-factor: the access and refresh tokens. A recovery code goes in the same field. Each code from the app, and each recovery code, works once. A wrong code answers 400 (incorrect_code). Wrong codes count against the account's sign-in limit; once over it, even the right code is refused for a while.

No other path issues tokens without the code: djoser's old sign-in endpoint is gone, and an unfinished sign-in can't be traded for tokens.

Turning It On

All of these take Authorization: Bearer <access_token>, under /_allauth/app/v1/account/authenticators:

RequestDoes
GET /The account's methods: totp, and recovery_codes with how many are unused
GET /totpWhile it's off: 404 with meta.secret and meta.totp_url (for the QR code). The same secret comes back until it's confirmed
POST /totp {"code": "123456"}Turns it on with a code from the app, and makes the recovery codes
DELETE /totpTurns it off
GET /recovery-codesThe unused recovery codes, the first time only; afterwards just the counts
POST /recovery-codesNew recovery codes; the old ones stop working

Only an account whose email is verified can turn it on, so someone who signed up with another person's address can't lock them out.

People get an email when it's turned on or off, when recovery codes are made, and when the password changes. The email names the IP address and browser the change came from.

Confirming It's You

Changes to two-factor settings need a sign-in in the last five minutes. Otherwise allauth answers 401 with the ways to confirm:

{
  "status": 401,
  "data": { "flows": [{ "id": "reauthenticate" }, { "id": "mfa_reauthenticate" }] },
  "meta": { "is_authenticated": true }
}

Confirm with the password or a code, then try again:

POST /_allauth/app/v1/auth/reauthenticate      {"password": "..."}
POST /_allauth/app/v1/auth/2fa/reauthenticate  {"code": "123456"}

Sign-in Codes by Email

For people without an authenticator app: after the password, a 6-digit code is emailed. Each person turns it on in Settings → Security. When an authenticator app is on, the app's code is asked for instead.

The password step answers 401 with a pending login_by_code step:

{
  "status": 401,
  "data": { "flows": [{ "id": "login" }, { "id": "login_by_code", "is_pending": true }] },
  "meta": { "is_authenticated": false, "session_token": "..." }
}

Send the emailed code with that session token:

POST /_allauth/app/v1/auth/code/confirm
X-Session-Token: <session_token>
{ "code": "123456" }

The answer is the access and refresh tokens. Each code works once, lasts 5 minutes and allows 3 tries; after that, the confirm answers 409 and the sign-in starts again with a new code.

The setting:

RequestDoes
GET /auth/sign-in-email-code/{"available", "enabled", "authenticator_app"}
PUT /auth/sign-in-email-code/ {"enabled": true}Turns it on
PUT /auth/sign-in-email-code/ {"enabled": false, "current_password": "..."}Turns it off; needs the password

Both take Authorization: Bearer <access_token>, and either change emails the account. To stop offering it, set DJANGO_SIGN_IN_EMAIL_CODES=False. Signing in with an emailed code instead of the password isn't offered.

An emailed code is weaker than an authenticator app, since whoever controls the inbox can also reset the password, but it still stops someone who only has the password. Email has to be delivered reliably: people who chose it can't sign in without it.

Deleting an Account

Deleting an account removes its authenticators and recovery codes too.

On this page