Authentication
Forte provides authentication for your project's users. You choose the sign-in methods, and Forte handles user creation, sessions, and security. Your app uses the Forte SDK to authenticate users and manage sessions.
Forte supports Google OAuth and one-time-passcode (OTP) login over email or SMS. For password-based sign-in, see Passwords.
The authentication flows on this page are part of Forte's client-side API. Call the Users methods from your frontend — the response sets the Forte-User-Session-Token cookie automatically. Never call these methods from code that also holds FORTE_API_TOKEN.
If your project enables MFA, every sign-in response carries mfaStatus. When it's CHALLENGE_REQUIRED or ENROLLMENT_REQUIRED, the returned session token is a short-lived pending token and the user must complete a second factor before it grants access. A pending response includes the user's userId but omits the full userObject until the second factor is verified. Branch on mfaStatus after any login call.
Google OAuth
Google OAuth lets your users sign in with their Google Accounts. Forte manages the entire OAuth flow. Configure your Google OAuth Client ID on your project, and Forte handles the rest: token verification, user creation, deduplication, CAPTCHAs, and session management.
How it works
- Your app renders Google's Sign-In button using your project's Google OAuth Client ID.
- When a user completes the Google sign-in flow, Google returns a credential token to your app.
- Your app sends the credential token to Forte's callback endpoint, along with a reCAPTCHA token if bot protection is enabled.
- Forte verifies the token, creates or links the user, and returns a session token.
Add Google sign-in to your app
import { GoogleLogin } from "@react-oauth/google";
import { ForteClient } from "@forteplatforms/sdk";
const forte = new ForteClient();
function LoginButton({ projectId }: { projectId: string }) {
const handleSuccess = async (response) => {
const csrfToken = crypto.randomUUID();
// If reCAPTCHA is configured on your project, execute before calling the callback
const recaptchaToken = await grecaptcha.execute("YOUR_RECAPTCHA_SITE_KEY", {
action: "login",
});
const result = await forte.users.googleAuthLoginCallback({
projectId,
gCsrfToken: csrfToken,
credential: response.credential,
recaptchaToken, // optional — required if reCAPTCHA is configured
});
// result.userId — the signed-in user's id
// result.userObject — the authenticated user (omitted while MFA is pending)
// result.sessionToken.sessionToken — use for subsequent requests
// result.sessionToken.expirationTime — token expiry (default: 365 days)
console.log("Logged in as", result.userObject?.fullName);
};
return <GoogleLogin onSuccess={handleSuccess} />;
}Configure your Google client ID
Configure your Google OAuth Client ID on your project in the Console or with the CLI. Once configured, your app can use Google's Sign-In button and send the resulting credential to Forte.
User creation and linking
- If the user is signing in for the first time, Forte automatically creates a new user account with a verified Google contact method.
- If a user with the same email already exists in the project, Forte links the Google Account to the existing user.
- Google OAuth contact methods are verified by default since Google has already verified the user's email.
Response
A successful authentication returns:
- The user object with the user's profile and contact methods
- A session token for authenticating later requests
- The session token's expiry (defaults to 365 days)
Forte automatically sets a Forte-User-Session-Token cookie on the response. Your app can use either the cookie or the session token from the response body to authenticate later requests.
Signing in with an OAuth or OIDC provider other than Google — GitHub, Microsoft, or your own identity provider — is available in early access. Contact Forte support to get it enabled for your project.
Bot protection with reCAPTCHA
Forte supports Google reCAPTCHA v3 for automatic bot protection on your project's authentication endpoints. When configured, Forte validates a reCAPTCHA token on every sign-up and login request, rejecting requests that appear to come from bots or automated scripts.
reCAPTCHA v3 runs invisibly in the background — your users never see a challenge or checkbox. Instead, Google assigns a risk score to each request, and Forte rejects requests with scores that signal automated or suspicious activity.
reCAPTCHA is only enforced when a secret key is configured on your project. If no key is set, authentication endpoints work normally without reCAPTCHA validation.
Why use reCAPTCHA
- Prevent credential stuffing: Block automated login attempts that test stolen credentials against your app.
- Stop spam registrations: Prevent bots from creating fake user accounts in bulk.
- No user friction: reCAPTCHA v3 is invisible — legitimate users are never interrupted with challenges.
Setup
- Go to the Google reCAPTCHA administrator console and create a new site.
- Select Score based (v3) as the reCAPTCHA type.
- Add your app's domains to the allowed domains list.
- Google provides two keys:
- Site key — use this in your frontend to load the reCAPTCHA script and generate tokens.
- Secret key — set this in your Forte Project settings.
- In the Forte Console, go to Project Settings and enter your reCAPTCHA v3 secret key.
Add reCAPTCHA to your frontend
Load the reCAPTCHA v3 script in your app and execute it before calling Forte's authentication endpoints. Pass the resulting token as recaptchaToken in your SDK calls.
// Load reCAPTCHA v3 script in your HTML:
// <script src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY"></script>
// Execute reCAPTCHA before authentication
const recaptchaToken = await grecaptcha.execute("YOUR_SITE_KEY", {
action: "login", // use "login" or "signup" for Google OAuth
});
// Pass the token to the Forte SDK
const result = await forte.users.googleAuthLoginCallback({
projectId,
gCsrfToken: csrfToken,
credential: response.credential,
recaptchaToken,
});Error handling
If reCAPTCHA validation fails, Forte returns a 400 error with the error code RECAPTCHA_VALIDATION_FAILED. This can happen when:
- The reCAPTCHA token is missing or invalid.
- The request received a low risk score (indicating bot-like behavior).
- The token has expired (tokens are valid for a short time after generation).
reCAPTCHA actions
When executing reCAPTCHA on your frontend, use these action names to match what Forte expects:
| Endpoint | Action |
|---|---|
| Google OAuth login/signup | login or signup |
| User registration | register |
| OTP login request | login |
OTP login (email and SMS)
Forte supports passwordless sign-in via a 6-digit one-time passcode sent to the user's email or phone number. The same API powers both channels — the request body's email or phoneNumber field selects which one. Codes are time-limited (10 minutes), single-use, attempt-limited (5 tries), and hashed at rest.
The flow is SDK-driven so you can build whatever sign-in UI fits your app. The notification templates rendered to your users are customizable on a per-project basis — see Notification Templates in the Console.
OTP login uses the same emailLoginEnabled and phoneLoginEnabled flags as registration. Enable the channel you want in your project settings before calling createOtpLogin. A project with MFA can also switch OTP off as a sign-in factor entirely. When Disable passwordless one-time-code sign-in is on, createOtpLogin returns 400 OTP_LOGIN_DISABLED_UNDER_MFA.
How it works
- Your app collects an email address or phone number.
- Your app calls
createOtpLoginwith that contact. Forte returns apendingLoginIdand the code'sexpirationTime. - If a user owns that contact method, Forte sends a 6-digit code to it. A verified contact always receives a code; an unverified contact receives one only while the account has no verified contact method yet (see Edge Cases and Recovery). In every other case — no matching user, or an unverified contact on an account that already has a verified one — Forte returns the same response shape but sends nothing, which prevents account-existence enumeration.
- The user enters the code in your UI; your app calls
completeOtpLoginwith thependingLoginIdand code. Forte verifies it, marks the code consumed, and returns a session token. If the contact method wasn't already verified, completing the login also verifies it. The session cookie (Forte-User-Session-Token) is also set on the response. - If the user needs a new code, your app calls
resendLoginOtp. Forte throttles resends to once every 60 seconds and never extends the original 10-minute window.
Request an OTP
import { ForteClient } from "@forteplatforms/sdk";
const forte = new ForteClient();
// Email channel
const emailRequest = await forte.users.createOtpLogin({
projectId,
createOtpLoginRequest: {
email: "alice@example.com",
recaptchaToken, // optional — required if reCAPTCHA is configured
},
});
// SMS channel
const smsRequest = await forte.users.createOtpLogin({
projectId,
createOtpLoginRequest: {
phoneNumber: "+15551234567",
recaptchaToken,
},
});
// Stash pendingLoginId — you'll need it to verify or resend.
const { pendingLoginId, expirationTime } = emailRequest;Exactly one of email or phoneNumber must be set. Sending both, or neither, returns a 400 with NO_CONTACT_METHOD_PROVIDED.
Verify an OTP
const result = await forte.users.completeOtpLogin({
projectId,
pendingLoginId,
completeOtpLoginRequest: { code: "123456" },
});
if (result.mfaStatus && result.mfaStatus !== "SATISFIED") {
// The project enables MFA. result.sessionToken is a short-lived *pending* token, and
// result.availableMfaMethods lists the factors the user can use now — the exact contact
// they just signed in with is never offered (their other verified contacts are). Drive
// the challenge → verify loop (see MFA below) before the user is fully signed in.
} else {
// Fully signed in.
// result.userObject — the authenticated user
// result.sessionToken.sessionToken — use for subsequent requests
// result.sessionToken.expirationTime — 365 days from now
}A wrong code returns 400 INVALID_VERIFICATION_CODE and increments the pending login's attempt counter. After 5 failed attempts the pending login is locked — even the correct code is rejected, and the user must request a new one. Expired and already-consumed codes return the same error.
When MFA is enabled, a successful completeOtpLogin may return a pending token rather than a full session — branch on result.mfaStatus as shown earlier and run the MFA challenge → verify loop. Forte does not offer (or accept) an email/SMS second factor on the exact contact the user just signed in with — their other verified contacts, including another address on the same channel, remain usable.
Request another code
const resent = await forte.users.resendLoginOtp({
projectId,
pendingLoginId,
});
// Same response shape as createOtpLogin.A resend within 60 seconds of the last send returns 429 VERIFICATION_CODE_RATE_LIMITED. After the throttle window passes, Forte generates and sends a fresh 6-digit code; the 10-minute expiry window is not extended.
Example sign-in component (SMS)
By continuing, you agree to receive a one-time passcode via SMS.
Your sign-in UI must display the message "By continuing, you agree to receive a one-time passcode via SMS" before sending an OTP. SMS-based authentication requires this notice for compliance.
Customizing the message
The email subject, email HTML body, and SMS body are all editable per project under the Login OTP tab in your project's notification settings. Supported template variables in login-OTP templates: {{code}}, {{projectName}}, {{contactValue}}. ({{userFullName}} is intentionally unsupported here — at OTP send time, the user's identity has not yet been confirmed.)
Testing OTP login in sandbox
In sandbox projects, assign a fixed test code to a 555 test number or reserved-test-domain email, and every login OTP for that contact method uses your fixed code with nothing actually delivered. Your test still calls createOtpLogin/completeOtpLogin exactly as a real client would — expiry, resend throttling, and the 5-attempt limit all behave as in production.
Edge cases and recovery
Authentication, contact-method verification, and sessions interact in a few ways that aren't obvious from the individual flows on their own. This section covers the cases that come up most often.
One rule ties them together: an unverified contact method can act as a login or recovery channel only while the account has no verified contact method. Once an account has even one verified contact, it has an established owner — and its unverified contacts can then only be verified by a signed-in session, never through an unauthenticated OTP login or password reset.
A user registered but never verified, then lost their session
When a user registers, Forte sets a session cookie right away, so they can verify their contact method immediately. If they never verify and later lose that session — they signed out, cleared cookies, or moved to a different device — they can still get back in, because the account has no verified owner yet. Forte treats a completed login challenge as proof that the user controls the contact:
- OTP login works on the unverified contact.
createOtpLoginsends a code to it, and completingcompleteOtpLoginmarks it verified and signs the user in. - Password reset works on the unverified contact.
requestPasswordResetresolves it even though it isn't verified, and acting on the reset — entering a new password, or signing in with a generated one — verifies it.
Either way the user ends up signed in with a verified contact method, with no manual step from you or your support team.
Unverified contacts are not a back door on an established account
Once an account has a verified contact method, an unverified contact on that same account is not a login or recovery channel:
createOtpLogindoes not send an OTP to it — the request returns the normal response, but nothing is sent.requestPasswordResetdoes not send a reset to it — the request still returns its normal204.
This is a deliberate takeover protection. An unverified contact can land on an established account in ordinary ways — an administrator added one (see Administration), or the owner added a second contact and hasn't verified it yet. Without this rule, anyone who controlled that unverified contact could request a reset or OTP against it and seize an account that already has a real owner. Verifying another contact on an account that already has a verified owner always goes through the signed-in verification flow, which requires the owner's session and a code sent to the new contact.
Password login is the exception
passwordLogin always requires a verified contact method, even on an account with no verified owner. A password proves the user knows a secret; it does not prove they control the email or phone number on file. A user who set a password at sign-up but never verified cannot sign in with it yet. They recover through OTP login or password reset — both verify the contact as a side effect — and after that, password login works.
The contact is claimed by someone else first
If an unverified contact method is left untouched past its 10-minute verification window, it becomes stale, and another user can claim that email or phone number by registering with it. If that happens before the original user returns, the stale entry is reclaimed and removed — see Contact Methods → Reclaim behavior. The original user can recover at any point before the reclaim happens; recovering (via OTP or reset) verifies the contact and ends its staleness.
First verification clears other sessions
The first time an account gains a verified contact method — through the verification endpoint, OTP login, or password reset — Forte invalidates every other session for that account and keeps only the one that just authenticated. This closes the window where more than one party held a session for the same unverified account. See Sessions → Understand automatic invalidation.
Next steps
- Learn about Contact Methods and how verification works
- Understand Sessions and token management