Account

Two-factor authentication

Turn on an authenticator-app second factor, what it changes at sign-in, how recovery codes work, and the credentials it deliberately does not cover.

Envless signs you in with a code emailed to your address, so by default the account is exactly as strong as that inbox. Two-factor authentication adds a second, unrelated factor on top: a six-digit code from an authenticator app on your phone. It is off by default, it is per user, and you turn it on for yourself from Account settings. Nobody can turn it on for you. A workspace owner or admin can now require it of every member, which is a different thing from enrolling them: see Requiring it across a workspace.

What it is

PropertyValue
StandardTOTP, RFC 6238, implemented in the API with node:crypto and no third-party library
AlgorithmSHA-1, 6 digits, a new code every 30 seconds
Clock driftOne step either side is accepted, so a code is usable for roughly 90 seconds
Seed20 random bytes, base32 encoded, shown once as a QR code and as a typed key
Seed at restAES-256-GCM encrypted before it is stored. If the server has no encryption key configured, enrollment fails with 500 rather than writing a seed in the clear
AppsAnything that reads a standard otpauth:// URI: 1Password, Bitwarden, Authy, Google Authenticator, Yubico Authenticator

There is still no SMS code, no WebAuthn, no passkey and no hardware key. The second factor is an authenticator app or a recovery code, and nothing else.

Turn it on

Open Account settings

envless.cloud/dashboard/account, the Two-Factor Authentication section, then Enable two-factor.

Scan the QR code

Envless generates a seed and shows it as a QR code, with the same key printed underneath in groups of four characters for apps that cannot scan. Nothing is switched on at this point: the seed is stored as a pending enrollment and it expires in 15 minutes if you stop here.

Enter the code your app shows

Six digits. The form submits on its own once the sixth digit lands.

Confirm the account challenge

Turning the factor on is an account-level action, so Envless emails you a separate verification code and asks for that too. Both are required: the app code proves you hold the new device, the emailed code proves you still hold the account. A stolen browser session alone cannot enroll an attacker's phone.

Save the recovery codes

Ten codes appear, with Copy all and Download buttons. They are shown exactly once. Tick I saved these recovery codes somewhere safe to close the dialog, and mean it: this list is the only way back into the account if you lose the phone.

Every other browser you are signed in on stops working normally the moment the factor is on. Those sessions are not signed out, but their next request is refused and they land on the code screen, where they have to enter a code from the authenticator app, or a recovery code, before they can continue. That includes your own other laptop and your phone browser.

Signing in afterwards

The first factor does not change. You still type your email and the six-character code Envless mails you, or use Continue with Google or Continue with GitHub. What changes is what happens next: instead of landing on the dashboard, you land on a second screen asking for the six digits from your authenticator app. A link under the field switches the same screen to a recovery code, and Cancel signs you back out.

At sign-in the second factor is the app or a recovery code, never an emailed code. That is deliberate. The first factor is already a code sent to your inbox, so offering an emailed second factor would let one compromised mailbox clear both gates. In-app actions are the other way round, because there Envless cannot assume the phone is still to hand. See Step-up verification.

How long a verification lasts

A successful verification is recorded against the exact sign-in it belongs to, not against the browser, and it lasts 30 days. Inside that window the token your browser holds is refreshed roughly hourly without asking you for anything. Signing out and signing back in starts a new sign-in, which has no verification recorded against it, so it asks again.

Wrong codes

RuleBehaviour
Attempt limit10 wrong app codes inside a rolling 15 minutes locks the app code path
The lock lifts on its ownIt clears 15 minutes after the last failed attempt. There is no permanent lockout and nothing to contact anyone about
Recovery codes keep working while lockedThe lock covers app codes only, so a lost phone plus a wrong-code loop still leaves you a way in
A correct code clears the counterA valid app code or a valid recovery code resets it to zero. Until one lands, each further wrong code re-arms the 15 minute lock
No replayA code from the 30 second step you already used, or from any earlier step, is refused and counts as a failed attempt

Recovery codes

Ten codes are issued the moment two-factor is switched on. Each is 10 characters drawn from a 32-symbol alphabet with the ambiguous I, O, 0 and 1 left out, printed in two groups of five. Case and hyphens are ignored when you type one back.

PropertyDetail
Count10, issued together
Single useA code is consumed the first time it works and never works again
Shown onceThe dialog that lists them is the only time they are displayed. Reopening the section shows a count, never the codes
Stored as hashesOnly a SHA-256 digest of each code is stored, so nobody at Envless can read them back or recite one to you
RegenerateRegenerate recovery codes on the Account page issues a fresh set of 10 and deletes the old set outright, used and unused alike. It asks for the account challenge first, which a recovery code satisfies, so you can do it from a machine that no longer has the authenticator app

The Account page shows how many are left, and warns once you are down to 3 or fewer. Regenerating is the fix, and it invalidates whatever is left of the old list, so replace the copy you saved.

If you lose the device

Use a recovery code. On the sign-in screen, click Lost your device? Enter one of your recovery codes instead., paste one in, and you are through. Envless emails you a notice saying a recovery code was used and how many are left. Then set the factor up again on the new phone: turn it off from Account settings, turn it back on, and scan the new QR code. The old seed is not transferable.

If the phone and the recovery codes are both gone, the account is unreachable. There is no self-service path back in, no identity-check flow, and no support reset. The recovery codes are stored as hashes, the seed is encrypted, and nothing in the dashboard or the API can turn the factor off without one of the two. Store the codes somewhere separate from the phone, in a password manager or on paper, before you need them.

A recovery code gets you back into the account. It does not unlock the workspace. Variable values are encrypted in your browser under the workspace passphrase, which is a separate secret Envless never receives, so a new device still shows a locked workspace until you type it. See Unlocking the dashboard.

Turn it off

Account settings, Turn off, confirm, then answer the account challenge with a code from your authenticator app, a recovery code, or an emailed code. Switching it off deletes the seed, deletes every remaining recovery code, drops every recorded verification, and emails you that it happened.

Reaching this endpoint at all already costs a second factor, because the API refuses every request from a sign-in that has not cleared one. So the mailbox on its own can never strip the factor: it cannot get you a session in the first place. The challenge on top of that is a fresh proof at the moment of the change.

Requiring it across a workspace

A workspace can insist on it. When the requirement is on, every member of that workspace who has not turned the factor on for their own account is refused everything inside that workspace, and stays refused until they enroll. It is off by default, it is per workspace, and it is not the same as enrolling anyone: it cannot switch the factor on for a member, it cannot read their seed, and it cannot stop them switching their own factor off again.

Turning it on

The switch lives on the workspace Settings page, and the endpoint behind it is POST /internal/workspaces/require-two-factor with { "workspaceId": "...", "enabled": true }. Two things have to be true before it writes.

RequirementDetail
The workspace:update permissionHeld by the owner and admin roles, and grantable to a custom role. It is the same permission that renames the workspace and flips Virtual Envs
A step-up verificationIt is a security control, so it asks for a code first, the same one hour elevation described in Step-up verification. An emailed code satisfies it

You cannot require of others what you do not have yourself. Enabling is refused with 409 and Turn on two factor authentication for your own account before requiring it of everyone else. unless your own factor is already on. Without that check the write would lock the person making it out of the workspace they were trying to protect, in the same request.

What a blocked member sees

SurfaceWhat happens
The workspace itselfEvery permission check inside it answers no, so reads and writes alike are refused. In the dashboard that arrives as the ordinary 401 any missing permission produces, and the reason is read off the workspace record rather than off the error
Their accountUntouched. They sign in exactly as before, and Account settings keep working, including the page that turns the factor on
Their other workspacesUntouched. The requirement is a property of one workspace, never of the user, so a member blocked in one workspace works normally in the rest
Listing, resolving and switching workspacesStill works. Those endpoints check membership rather than permissions, which is what lets the dashboard name the workspace and explain the block instead of going blank
Leaving the workspaceStill works. A member is never trapped in a workspace they cannot use
Accepting an invite to itStill succeeds. Refusing the invite would strand it, so the member joins and then meets the same gate. The accept response carries twoFactorRequired: true so the dashboard can send them straight to enrollment
Their CLI tokens and API keys in that workspaceRefused with 403 and auth.workspace_two_factor_required

Enrolling clears it. There is nothing to re-request and no admin to ask: the next request after the factor is on is answered normally, because the check reads the same twoFactorEnabled column the account page writes.

Machine credentials

Turning this on breaks CI for any member who has not enrolled. A CLI token and an ev_sk_ key are checked against their owner's account rather than against a session, so from the moment the requirement lands, every request either one makes into that workspace is refused with 403 auth.workspace_two_factor_required. Nothing is deleted or deactivated, and both start working again the moment their owner enrolls or the requirement is turned off. A CLI token is bound to the workspace it was approved for, so a blocked one is refused on every command, including envless workspace list. Check who owns the credentials your pipelines use before you switch this on.

Turning it off

Turning the requirement off stays possible for someone it is currently blocking. That one path checks your permission with the requirement deliberately skipped, so a workspace whose admins have all lost their authenticator is recoverable rather than dead forever. It still asks for a step-up code, which an emailed code answers. Access returns on the very next request, with nothing to re-enable and nobody to re-invite.

Every other member of the workspace is emailed when the requirement is turned on, and again when it is turned off. The person who flipped it is not mailed. It is a security notice rather than workspace news, so it is not one of the switches on Preferences and cannot be turned off. Flipping the setting to the value it already holds answers 200 with No changes detected. and mails nobody.

Your own factor stays yours. You can turn it off from Account settings while a workspace requires it, and nothing refuses the write. The only consequence is that the workspace stops answering until you turn it back on. An admin can close a workspace to you; they cannot reach into your account settings.

What the second factor does not cover

Be clear about the edges of this. Two-factor authentication guards interactive sign-in to the dashboard and the API endpoints behind it. It does not reach any of the following.

Not coveredWhy, and what changed instead
CLI tokens (env_cli_...)Not asked for the sign-in factor: a token has no sign-in. It is asked for a step-up code before a guarded command, exactly like a browser session, and envless prompts for it in the terminal; an authenticator or recovery code works there too. Minting a token is guarded as well: approving a device from the dashboard refuses CLI-token callers outright and requires a verification code. A token still lives 90 days. The one thing that stops a live token outright is the workspace requirement above, which refuses it on the owner's account rather than on the session
API keys (ev_sk_...)Same reason. Creating one requires a verification code, and the key itself then authenticates the public API with no second factor and no step-up on any endpoint. It is still refused with 403 auth.workspace_two_factor_required while its workspace requires the factor and its owner has not enrolled
The workspace passphraseA different secret entirely. It never reaches Envless, and two-factor protects account access, never the ciphertext
Enrolling your teammatesA workspace can now require the factor of every member, covered above, but requiring is not enrolling. Nobody can turn the factor on for someone else, scan their QR code, see their seed or read their recovery codes. All the requirement does is close one workspace to the members who have not done it themselves

A CLI token minted before you enrolled keeps working for the rest of its 90 days, second factor or not, unless the workspace it is bound to requires the factor. And once it expires, renewing it needs a dashboard session that can pass the factor and the verification code, so do not enroll on a device you are about to lose access to and expect the terminal to carry on indefinitely.

How it is enforced

This is the part worth reading if you are reviewing the security model rather than using it.

The gate is in the API, not the dashboard. Every authenticated request carrying a Firebase ID token is checked before the route runs: if the account has two-factor on and this sign-in has no verification recorded, the request is refused. A screen in the dashboard would have been decorative. Requesting an emailed sign-in code hands the underlying sign-in link back to an unauthenticated caller, and the ID token is then minted by Google rather than by Envless, so anyone willing to use curl would walk straight past a check that lived only in the browser.

The workspace requirement is a second, narrower gate. The account gate above already refuses every request from a sign-in that has not satisfied its factor, so any member whose factor is on has proved it for this sign-in. The workspace layer therefore only has to ask whether they have the factor at all, which it does inside the single row lookup every workspace endpoint already runs to resolve permissions. That is why it covers every controller that resolves a permission without one of them being changed, and why the only endpoint that opts out is the switch itself. CLI tokens and API keys never reach the account gate, so they are checked separately, in their own middleware, against the owner's account record.

Verification is bound to the sign-in, not the session. Firebase stamps each ID token with an auth_time claim, the moment that sign-in actually happened, and it stays put across the hourly token refreshes. A successful verification writes a row keyed by user and auth_time. That is why a verified browser is not asked again an hour later, and why a fresh sign-in is asked immediately: it has a new auth_time and no row.

DetailValue
Refusal403 with error code auth.two_factor_required
Why not 401The dashboard reads 401 as a dead session and signs you out, which would loop forever against a gate you are trying to clear
Session rowKeyed on user and auth_time, expires after 30 days, and an hourly job clears expired ones
Endpoints outside the gateinternal/users/auth, so the dashboard can be told it must ask; internal/two-factor/status; and internal/two-factor/verify-login, the endpoint that clears the gate
Workspace refusal403 with auth.workspace_two_factor_required for a CLI token or an API key, because those are checked in middleware. Inside the dashboard it is the endpoint's own 401, because the workspace check is folded into the permission lookup
Everything elseGated, including the endpoints that turn the factor off, so the factor cannot be removed from behind it

The endpoints are POST /internal/two-factor/status, /enroll, /confirm, /disable, /verify-login and /recovery-codes. Enrollment is deliberately two calls: /enroll writes a pending seed and does not switch anything on, and /confirm is what flips the flag, only after both the app code and the account challenge check out. /disable and /recovery-codes take no code of their own, the account challenge is the proof.