The passwordless authentication feature provides a secure and user-friendly method for users to sign in to your application without needing a password. This feature leverages magic links sent via email, along with the option for a fallback OTP (One-Time Password) mechanism, to enhance security and usability. Users can seamlessly log in by simply clicking a link in their email, or if they open the link in a different browser or device, they can use a client-generated OTP to complete the login process.
Flow Overview
- Configuration:
- Administrators must first configure the passwordless authentication settings via the GraphQL API.
- The configuration includes enabling the feature, setting the expiration time for magic links, and specifying the URL that will be included in the email.
- There is also an option to override the default email template with a new
PASSWORDLESS_SIGN_INmail type.
- Initiating Sign-In:
- The user begins the sign-in process by entering their email address.
- The system sends a magic link to the user's email, which contains a unique token with a default expiration of 5 minutes.
- The user is prompted to check their inbox for the magic link.
- Handling the Magic Link:
- If the user clicks the magic link in the same browser where they initiated the request, they are automatically logged in.
- If the link is opened in a different browser or device, an OTP flow is activated:
- A 6-digit (configurable) OTP is generated on the client-side and displayed to the user.
- The original token becomes invalid once the OTP is generated, meaning it cannot be reused for automatic sign-in or for generating a new OTP.
- The user copies the OTP and enters it into the original browser where they started the login process.
- Users are allowed a maximum of 3 attempts to enter the correct OTP.
- Multi-Factor Authentication (MFA):
- If MFA is enabled for the user, they will be prompted to complete MFA authentication after the magic link or OTP validation.
- Rate Limits and Security:
- Token validity period (default 5 minutes) and a limit of 3 OTP attempts per magic link.
- (since 2.2) Per-IP rate limit on
initSignInPasswordlessviarateLimits.passwordlessInitPerIp. See anti-abuse. - (since 2.2) Per-email exponential backoff on
initSignInPasswordlessreusing thelogin.baseBackoff/login.maxBackoff/login.attemptWindowknobs — a single inbox cannot be flooded with magic-link mails. The mutation still returnsok: truebut the mail is suppressed until the next allowed attempt. - (since 2.2) Optional captcha verification on
initSignInPasswordlesswhencaptchais configured. Pass the token via thecaptchaTokenargument. - (since 2.2) When
login.revealUserExists: false, aninitSignInPasswordlessfor an unknown email returnsPASSWORDLESS_DISABLEDrather thanPERSON_NOT_FOUND.
GraphQL API
Below is an overview of the key mutations and types involved:
Configuration
Mutation: configure
- Purpose: Configure passwordless authentication settings.
- Input:
ConfigInputcontaining passwordless settings such as enabling the feature, specifying the email URL, and setting the expiration time. - Response:
ConfigureResponseindicating success or failure (in case of invalid input).
mutation {
configure(config: {
passwordless: {
enabled: always,
url: "https://example.com/auth",
expiration: "PT5M"
}
}) {
ok
error {
code
developerMessage
}
}
}
Initiating Sign-In
Mutation: initSignInPasswordless
- Purpose: Initiate the passwordless sign-in process by sending a magic link to the user's email.
- Input: The user's email and optional settings such as email template variants.
- Response:
InitSignInPasswordlessResponsecontaining a request ID and expiration time.
mutation {
initSignInPasswordless(
email: "[email protected]",
captchaToken: "0.aXR…"
) {
ok
error {
code
developerMessage
}
result {
requestId
expiresAt
}
}
}
Error codes: PERSON_NOT_FOUND (or PASSWORDLESS_DISABLED when revealUserExists: false), INVALID_CAPTCHA (since 2.2), RATE_LIMIT_EXCEEDED (since 2.2).
Signing In with Magic Link or OTP
Mutation: signInPasswordless
- Purpose: Complete the sign-in process using the magic link or OTP.
- Input: The
requestId,validationType(eitherotportoken), thetokenfrom the magic link or OTP code, and optionally the MFA OTP. - Response:
SignInPasswordlessResponseindicating success or failure, with details of the signed-in user if successful.
mutation {
signInPasswordless(requestId: "abcd1234", validationType: token, token: "xyz789", expiration: 5) {
ok
error {
code
developerMessage
}
result {
token
person {
id
email
}
}
}
}
Activating Passwordless OTP
Mutation: activatePasswordlessOtp
- Purpose: Exchange a long token for a short OTP, which will be shown to the user for manual entry.
- Input: The
requestId, the longtoken, and theotpHashgenerated from the client-side OTP. - Response:
ActivatePasswordlessOtpResponseindicating success or failure.
mutation {
activatePasswordlessOtp(requestId: "abcd1234", token: "xyz789", otpHash: "hashedOtpValue") {
ok
error {
code
developerMessage
}
}
}
Completing sign-in with MFA
If 2FA is enabled for the person, signInPasswordless requires the current TOTP code via the mfaOtp argument. A missing or wrong code is reported the same way as on signIn:
mutation {
signInPasswordless(
requestId: "abcd1234",
validationType: token,
token: "xyz789",
mfaOtp: "123456"
) {
ok
error { code }
result { token person { id email } }
}
}
Error codes added in this case: OTP_REQUIRED (no mfaOtp supplied), INVALID_OTP_TOKEN (wrong code). React to OTP_REQUIRED by prompting the user and retrying with mfaOtp populated. See two-factor for the broader 2FA reference.
Per-person opt-in / opt-out
When passwordless.enabled is optIn or optOut (see configuration), each person can flip their own preference with two self-service mutations:
mutation { enableMyPasswordless { ok error { code } } }
mutation { disableMyPasswordless { ok error { code } } }
| Code | Cause |
|---|---|
NOT_A_PERSON | Caller is authenticated via a permanent API key, not a person. |
CANNOT_TOGGLE | passwordless.enabled is always or never — the tenant has forced the setting globally and individual persons cannot override it. |
The mutations are gated by the PERSON_TOGGLE_PASSWORDLESS permission, which is held by PERSON (any authenticated person) by default. The current state is observable through me { person { passwordlessEnabled } }.