Passwordless login
Instead of a password, a user enters their email, receives a 6-digit code, and types it back. Two calls: one to send the code, one to spend it.
This is off by default. Turn it on per workspace with authPolicy:
authPolicy | What the sign-in screen accepts |
|---|---|
PasswordOnly | Password only. The default. |
PasswordAndPasswordless | Either. |
PasswordlessOnly | Emailed code only; passwords refused. |
curl -X PATCH https://api.passura.dev/v1/acme/tenant \
-H "Authorization: Bearer <adminAccessToken>" \
-H "content-type: application/json" \
-d '{ "authPolicy": "PasswordAndPasswordless" }'
Sending the code
curl -X POST https://api.passura.dev/v1/acme/auth/request-login-code \
-H "content-type: application/json" \
-d '{ "email": "[email protected]" }'
{ "ok": true }
This response is always { "ok": true } — for a real account, an unknown
address, and a suspended user alike. That is deliberate: an endpoint that
answered differently would let anyone test which email addresses have accounts
in your workspace. Your sign-in screen should say "if that address has an
account, a code is on its way" rather than confirming anything.
The code is valid for 10 minutes, survives 5 wrong guesses before it burns, and requesting a new one immediately invalidates the previous one.
Rate limits are enforced on two independent buckets: 3 requests per 15 minutes for a given email address, and 10 per minute from a given IP. The first stops one inbox from being flooded; the second stops one host from spraying many.
Spending the code
curl -X POST https://api.passura.dev/v1/acme/auth/verify-login-code \
-H "content-type: application/json" \
-d '{ "email": "[email protected]", "code": "482913" }'
On success this returns the same token envelope as
/auth/login. A wrong or expired code returns
401 with details.reason of invalid_code — the same answer an unknown
address gets, for the same reason as above.
Interaction with MFA
An emailed code proves the user controls the inbox, which makes it a first factor. Your workspace's MFA policy still applies on top of it.
In a workspace with mfaPolicy: "Required", a correct code comes back as:
{
"code": "UNAUTHORIZED",
"message": "MFA required",
"details": { "reason": "mfa_required", "methods": ["totp"] }
}
The code is not consumed by that response. Retry the same request with the authenticator's code alongside it:
curl -X POST https://api.passura.dev/v1/acme/auth/verify-login-code \
-H "content-type: application/json" \
-d '{ "email": "[email protected]", "code": "482913", "totpCode": "123456" }'
email is not offered as a second factor here — a second emailed code would
prove nothing the first one didn't.
A user who has never enrolled TOTP gets details.reason: "mfa_not_enrolled".
They can enroll using their login code in place of a password, which is the only
way for someone in a PasswordlessOnly workspace to get through:
curl -X POST https://api.passura.dev/v1/acme/auth/totp/initial-enroll \
-H "content-type: application/json" \
-d '{ "email": "[email protected]", "loginCode": "482913" }'
Then /auth/totp/initial-verify with the same loginCode plus the code from
the authenticator. Pass exactly one of password or loginCode to either call.
Accounts on first sign-in
By default only existing users can request a code; an unknown address silently
does nothing. Setting passwordlessJitSignup: true turns the sign-in screen
into a registration form — anyone who can reach it and receives a code at the
address they typed becomes a member.
curl -X PATCH https://api.passura.dev/v1/acme/tenant \
-H "Authorization: Bearer <adminAccessToken>" \
-H "content-type: application/json" \
-d '{ "passwordlessJitSignup": true }'
Users created this way start as Pending and become Active on their first
verified code. Leave this off unless open registration is what you want.
What your login screen needs to know
Which methods a workspace accepts is readable without a session:
curl https://api.passura.dev/v1/acme/auth/config
{ "authPolicy": "PasswordAndPasswordless", "googleOauthEnabled": true }
Audit trail
Passwordless sign-ins record PasswordlessCodeSent, PasswordlessCodeVerified,
and PasswordlessCodeFailed, plus the usual UserLogin with
metadata.via: "passwordless". They are kept separate from the MfaEmailOtp*
actions so the log doesn't file a first-factor sign-in under "MFA".