Overview
Add sign-in, sign-out, and session management to your React app.
BuildBase handles authentication end-to-end. Wrap your app in SaaSOSProvider, use useSaaSAuth() for sign-in/sign-out, and WhenAuthenticated/WhenUnauthenticated to control what users see.
Step by step, with screenshots: Sign-in setup guides.
import { useSaaSAuth } from '@buildbase/sdk/react';
function App() {
const { user, isAuthenticated, signIn, signOut } = useSaaSAuth();
if (!isAuthenticated)
return <button onClick={() => signIn()}>Sign In</button>;
return <p>Welcome, {user.name}</p>;
}Before you start
Enable at least one auth method in the BuildBase dashboard before integrating.
Supported auth methods
| Method | Description |
|---|---|
| Email/Password | Traditional credential-based sign-in |
| Magic Link | Passwordless email link - one click to authenticate |
| Email Code | A 6-digit code sent by email and typed on the sign-in page, on any device |
| Phone Code | A 6-digit code by text message, WhatsApp or voice call, sent through your own provider (Twilio or a signed webhook) |
| Passkeys | Phishing-resistant WebAuthn sign-in with device biometrics or security keys |
| Sign in with Google accounts (OAuth) | |
| Sign in with LinkedIn profiles (OpenID Connect) | |
| GitHub | Sign in with GitHub accounts (OAuth) |
| Microsoft | Sign in with Microsoft work, school or personal accounts (OpenID Connect) |
| Apple | Sign in with Apple, with the option to hide the email address |
| Custom OIDC | One OpenID Connect or OAuth 2.0 provider of your own, such as Keycloak or Okta |
| Ethereum Wallet | Sign a free message with an Ethereum wallet (EIP-4361) |
| Solana Wallet | Sign a free message with a Solana wallet |
| API Tokens | Server-to-server authentication for backend integrations |
Social Providers covers what Google, LinkedIn, GitHub, Microsoft, Apple and the custom OpenID Connect or OAuth 2.0 provider each need. Phone codes go out by text message, WhatsApp or a voice call through your own SMS provider, which Phone Sign-in covers connecting. Wallet sign-in covers Ethereum and Solana wallets in the browser.
Four more ways to use those methods, each off until you turn it on. They are not sign-in methods of their own:
- Usernames beside the email, for password sign-in.
- Google One Tap, a setting on the Google method.
- Guest users: Continue as guest, and an email added later to keep the same account.
- Device code sign-in for a CLI or a TV, approved from another screen.
Enable or disable any method from the dashboard - no code changes needed. Passkeys, step by step with screenshots: Set up passkeys.
Email and password
Step by step, with screenshots: Set up email and password.
Email/Password is a login method of its own. Add it under User Management → Access → Authentication → Methods, then tick it on your client in the Settings tab. That client's hosted login page then asks for the email and the password in one form, with a Forgot password? link under the password. If the client also has Magic Link, the same form carries Email me a sign-in code or Email me a link under the sign-in button, or both, depending on what the method sends, and each of them uses the email already typed. The register page asks for a password.
- Forgot password asks for the email and sends a 6-digit code. The answer is the same whether or not an account exists for the address, so the page cannot be used to find out who has an account.
- Reset password takes that code and a new password. The code lasts 10 minutes and allows 5 tries.
- Breached passwords are refused on register, reset and change. Only the first five characters of the password's SHA-1 hash leave the server, through the Pwned Passwords range API. If that service cannot be reached the password is allowed, so an outage never blocks a sign-up.
- Ten wrong passwords within 15 minutes lock the account's password sign-in, from any number of IP addresses, until 15 minutes after the first of them. A successful reset lifts the lock.
- A refused sign-in carries a
reasonbeside its sentence, for a page in another language or your own screens to match on:invalid_credentialsfor a wrong password or an address nobody here has (one answer for both),lockedwhile the lock above holds, andblockedfor a blocked account. Blocked is said only after the right password, so an address alone does not tell anyone the account exists. - Only through a client that offers it.
POST /api/v1/auth/loginanswers403with reasonmethod_unavailableand "Password sign-in is not turned on for this app." when the client signing in, the one in the hosted page's auth state, has no Email/Password method, however right the password. A call that names only the organization (?org=) needs the organization to have an Email/Password method. The check comes before the address or the password is looked at, so its answer is the same for every account. Sign-up through a client is held to the same rule:POST /api/v1/auth/registerwith a password, from a client without the method, answers403with reasonmethod_unavailableand "Password sign-up is not turned on for this app.", and makes no account. - Changing or resetting the password ends the user's other sessions, and a sign-in that was waiting on the hosted page can no longer reach your app. It also ends the access of every agent the person connected: each asks for their consent again. A change from a signed-in session keeps that session. The SDK's Security screen offers that change.
A client with Magic Link and no Email/Password method registers people with a name and email only; they sign in with a magic link. An account created through a social provider has no password of its own until the person sets one with Forgot password?.
Your own settings screen can tell which case it is looking at. GET /api/v1/public/profile carries passwordSet, whether the person has a password they know, and passwordSignIn, whether the app their session signed in to offers password sign-in. passwordSignIn is left out when the session names no client. Changing the password through POST /api/v1/public/profile/password answers 409 password_not_set for an account made without a password, with a sentence pointing the person at Forgot password?, and that attempt does not count toward the lockout.
Passwords are hashed exactly as typed, <, > and & included. Earlier, the request sanitiser changed those characters before the password was hashed. A password stored that way still signs in, and its hash is replaced with one of the password as typed at that sign-in.
Email sign-in codes
Step by step, with screenshots: Set up sign-in links and email codes.
A Magic Link method can send a 6-digit code instead of a link, or offer both. Choose what it sends under User Management → Access → Authentication → Methods, on the method itself. A code is the better choice when people read their email on their phone and sign in on a laptop: a link opens on the device that tapped it, while a code can be typed anywhere.
- Sign-in asks for the email, sends a code and takes it on the next screen. An address with no account gets the same answer and no mail, so the page cannot be used to find out who has an account.
- Sign-up asks for a name and email, sends a code, and creates the account when the code is entered, with the email already verified. Closing public sign-up closes this door too.
- Each code works once, lasts 10 minutes and allows 5 tries. A new code can be sent after 30 seconds, and each new code replaces the last.
- Codes count with magic links toward your plan's magic-link sends, since each one is an email we send for you.
- The second step still applies. If the account has an authenticator app, the code is followed by the app's code, as after any other first step.
Passkeys
Step by step, with screenshots: Set up passkeys.
A person adds a passkey after signing in another way, then signs in with it from Continue with passkey on the hosted page. The page asks POST /api/v1/auth/passkey/login/options for a challenge and sends the device's answer to POST /api/v1/auth/passkey/login/verify; each challenge works once.
- Only through a client that offers it. Both calls answer
403with reasonmethod_unavailableand "Passkey sign-in is not turned on for this app." when the client signing in has no Passkey method, or, for a call that names only the organization (?org=), when the organization has none. The check comes before any passkey or account is looked at, so its answer is the same for every passkey, and it spends no challenge. - It can count as both steps. A passkey that checked the person with a fingerprint, a face or a PIN satisfies two-step sign-in on its own.
Changing an email address
Step by step, with screenshots: The account security screens.
The SDK's Profile screen lets a signed-in user change their email address:
- A 6-digit code goes to the new address, and the change happens only once it is entered. The new address is then verified.
- The old address is mailed a notice saying the address was changed and where from.
- The user stays signed in. An address another account already uses is refused, with the same sentence whether it is taken or not allowed.
- An account that signs in with Google, Apple or another provider can change its address too. Those sign-ins find the account by the provider's own user ID before they look at the email, so the next one after a change reaches the same account. See How sign-in finds an account.
Two-step sign-in
Step by step, with screenshots: Set up two-step sign-in.
People can add an authenticator app (Google Authenticator, 1Password, Authy and the like) as a second step at sign-in, and, where you allow it, a code texted to their phone. Set the policy under User Management → Access → Authentication → Security:
| Policy | What happens |
|---|---|
| Optional (default) | People add an authenticator app from the Security screen of your app's settings. Whoever has one is asked for a code each time they sign in, apart from the exemptions below. Nobody else is asked anything. |
| Required | Every sign-in needs a second step, apart from the exemptions below. Someone without an app sets one up before they reach your app, at their next sign-in that is not exempt. |
| Off | Nobody is asked, and nobody can add one. |
- Backup codes. Setting up an app issues ten single-use backup codes, shown once. Any of them works in place of a code; new ones can be generated from the Security screen, which invalidates the old set. A wrong one is answered "That backup code did not match, or it was already used." A sign-in finished with one says how many are left, as
backupCodesRemainingin the answer toPOST /api/v1/auth/mfa/verify, and the hosted page warns when three or fewer remain. - Passkeys count as two steps when the device verified the person with a PIN or biometric, so they are not asked again.
- Social sign-ins are not asked by default, because the provider runs its own checks. Switch on Also ask social sign-ins to change that.
- Agent accounts are never asked, under any policy. An account made through agent sign-up has nobody to read a code.
- Guests are not asked either. A guest has no factor and no address to set one up against; the second step starts once they create an account.
- Trusted devices. When someone passes the second step and chooses Trust this device, that device skips the code until the Trust a device for window on the same tab runs out. Turning off Ask to trust new devices makes every device ask again, and a device trusted before the person set up their current app is asked once.
- Limits. A sign-in ends after five wrong codes. Ten wrong codes within 15 minutes, across any number of sign-ins, stop the account accepting codes until 15 minutes after the first of them. Each code works once.
- Lost phone. An admin can reset a user's second step from the user's page in the console. It removes the app and the backup codes, and every trusted device asks for a code again. A reset, or the person removing their own app, also means a sign-in waiting on the hosted page can no longer reach your app; it has to start again.
Removing the app, or generating new backup codes, needs a current code. While the policy is Required, people cannot remove their app.
Text message codes
Tick Text message codes in the same card to let people get their second-step code by text message instead of from an app. It is the weaker second step: a phone number can be moved to another SIM, and a text can be intercepted. It is there for people who would otherwise have no second step, not to replace the authenticator app.
- Never the only one. The list of allowed second steps cannot be text messages alone. Saving that is refused with
sms_only_factor, and the authenticator app stays allowed beside it. - It goes through your own SMS provider, so it needs one connected on the SMS tab, and a plan that includes phone sign-in codes. See Phone Sign-in.
- People turn it on from the Security screen of your app's settings, with a phone number they have verified on their account.
- At sign-in, the hosted page offers "Text a code to" the masked number beside the authenticator app, labelled as the weaker option, and sends nothing until the person asks. A new code can be sent after 30 seconds. The limits on wrong codes are the ones above.
- Never after a phone code. Someone who signed in with a code sent to their phone is not offered a text message as the second step: the same phone would be both steps.
- The number stays while it is in use. A person cannot remove the phone that their text-message second step uses until they turn that off.
A second step by text message puts sms, otp and mfa in the token's amr claim.
Two-step sign-in has the whole of it in one place: what your users see, trusted devices, a lost phone, and the API.
OAuth 2.0 authorization server
Step by step, with screenshots: Set up agent access.
Beyond first-party sign-in, every BuildBase app is a full OAuth 2.0 authorization server. Third-party apps and AI agents request scoped access to user accounts through the standard authorization code flow:
- Dynamic client registration - clients register programmatically (RFC 7591/7592)
- Token introspection - resource servers validate tokens (RFC 7662)
- Token revocation - clients invalidate tokens (RFC 7009)
- PKCE - proof key for code exchange, including public clients (CLIs, agents, SPAs)
- User consent - users approve a client's requested scopes before it gets access
- Device code - a CLI or a TV signs in with a code the person approves on another device (RFC 8628). See Device code sign-in
Scopes and resources are defined by your app. The authorization endpoints are live for every BuildBase app. Dynamic client registration is the exception: it is off until you enable it under Agent readiness in the console, and until then a registration request is refused.
Available hooks and components
Hook:
useSaaSAuth()- Current user, sign-in, sign-out, session status, andopenWorkspaceSettings().
Components:
WhenAuthenticated- Render children only when signed in.WhenUnauthenticated- Render children only when not signed in.
import { WhenAuthenticated, WhenUnauthenticated } from '@buildbase/sdk/react';
function Page() {
return (
<>
<WhenAuthenticated>
<Dashboard />
</WhenAuthenticated>
<WhenUnauthenticated>
<LoginPage />
</WhenUnauthenticated>
</>
);
}Closing public sign-up
By default anyone can create an account in your app. The registration endpoint takes your org id from the query string, and org ids are public - they sit in every page that embeds the SDK - so a stranger can create a real user, then a workspace, and draw on any trial or credit grant you give new workspaces. That is what an auth SDK is for, until your app is invite-only or you have finished onboarding the people you meant to.
The org setting allowPublicSignup turns it off. It defaults to true, and only an explicit false closes sign-up, so no existing org changes behaviour until someone sets it.
Switch it in the console under Authentication › Sign-up with Allow public sign-up. The change applies straight away. You can also set it through the org settings API:
curl -X PATCH https://api.console.buildbase.app/api/organizations/settings/config \
-H "Authorization: $BUILDBASE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"allowPublicSignup": false}'Send true to open sign-up again.
Off means invite-only, not shut. What changes:
- Email and password registration is refused.
POST /api/v1/auth/registeranswers403with reasonsignup_closedand "Sign-up is not enabled for this app." - Social sign-in cannot create accounts either. A first sign-in through any social provider is sent back to your app with
reason=signup_disabled, and the hosted pages show that sign-up is invite-only. - The hosted register page shows an invite-only state. Instead of the form, a visitor sees that sign-up is by invitation, with a link to log in. The login page stops offering "Register here".
- Invitations still let people in. Someone who follows a workspace invitation link can create an account with the address it was sent to, by email and password or with a social provider. The account is marked verified, because the link proves the mailbox. A link forwarded to a different address does not work, and neither does an expired or revoked one. This needs the token to reach the hosted pages, which the SDK does from the version named below.
- Existing users are untouched. Password, magic link, passkey and social sign-in keep working for accounts that already exist, and so do existing sessions.
- Agents are separate. Agent sign-up has its own opt-in,
agentReadiness.allowAgentSignup, under Authentication › Agent readiness.allowPublicSignupdoes not read it or change it.
Your plan's end-user limit still applies with sign-up off. An invitation does not create an account the plan has no room for.
Invitations reach the hosted pages through the SDK from 0.0.75. Earlier versions park the invitation but do not send it on, so an invited person on an organization with sign-up closed meets the invite-only notice instead of the form. Upgrade before you close sign-up, or keep it open until you have.
If you build your own sign-up form rather than using the hosted pages, pass the invitation link's token as invitationToken in the body of POST /api/v1/auth/register. That path has no SDK version requirement: the server has accepted the field since this release.
To keep sign-up open to some people and not others, with an allowlist, a blocklist or a waitlist you approve, see Who can sign up.
Next Steps
- Sign In & Sign Out -
useSaaSAuth()return values and usage patterns. - Protected Routes - Guard routes based on auth state.
- Social Providers - Set up Google, LinkedIn, GitHub, Microsoft, Apple or a provider of your own.