Auth section

This commit is contained in:
Terry Sutton committed 2026-02-16 22:50:32 -03:30
1 parent 4da9865254
commit 17ca390c08
4 files changed
+554 -25

No files matched your search

@@ -3,12 +3,81 @@ title: Advanced Authentication
description: MFA, SSO, custom claims, and identity linking
---
The previous chapters covered the authentication methods you'll use in most apps. As your app grows — or as your users' security needs increase — Supabase offers several features that go beyond basic sign-in. You don't need any of these on day one, but it's worth knowing they exist so you can reach for them when the time comes.
## Topics Covered
## Multi-factor authentication (MFA)
- Multi-factor authentication (MFA/2FA)
- Single Sign-On (SSO) with SAML
- Custom claims and RBAC patterns
- Identity linking: connecting multiple providers
Multi-factor authentication adds a second step to sign-in. After entering their password, the user also provides a time-based code from an authenticator app like Google Authenticator or 1Password. This means a stolen password alone isn't enough to access an account.
_Content coming soon_
Supabase supports TOTP-based MFA (the kind where you scan a QR code and get a 6-digit code that rotates every 30 seconds). The flow looks like this:
1. **Enroll** — the user scans a QR code with their authenticator app. Your app gets this QR code from `supabase.auth.mfa.enroll()`.
2. **Challenge and verify** — at sign-in, after the password step, your app asks for the 6-digit code and verifies it with `supabase.auth.mfa.verify()`.
3. **Session upgrade** — once verified, the session is upgraded to **AAL2** (Authenticator Assurance Level 2). You can write RLS policies that require AAL2 for sensitive operations, so even a valid session token can't access protected data without completing MFA.
The key thing to understand is that MFA integrates with Row Level Security. You're not just adding a UI step — you're enforcing the second factor at the database level.
## Single Sign-On (SSO)
Single Sign-On lets users sign in through their organization's identity provider — like Okta, Microsoft Entra ID, or Google Workspace. Instead of entering a password in your app, they're redirected to their company's login page, authenticate there, and are redirected back with an active session.
This is most relevant for B2B apps selling to enterprise customers. The benefits for the customer are:
- **Centralized access** — their IT team controls who can access your app through their existing identity system
- **Automatic deprovisioning** — when an employee leaves and their corporate account is disabled, they lose access to your app too
- **Familiar experience** — employees sign in the same way they do for every other work tool
Setting up SSO involves coordinating with the customer's IT team to exchange SAML metadata. It's configured through the Supabase Management API or CLI. From the user's perspective, your app just calls `supabase.auth.signInWithSSO({ domain: 'company.com' })` and Supabase handles the rest.
## Role-based access control (RBAC)
Many apps need to know not just *who* a user is, but *what they're allowed to do*. Is this user an admin? A member of a specific team? Allowed to access billing?
The most straightforward approach is a table that maps users to roles:
```sql
create table public.user_roles (
id uuid primary key default gen_random_uuid(),
user_id uuid references auth.users(id) on delete cascade,
role text not null,
unique(user_id, role)
);
```
Then you reference it in RLS policies:
```sql
create policy "Admins can manage all posts"
on posts for all
using (
exists (
select 1 from public.user_roles
where user_id = auth.uid()
and role = 'admin'
)
);
```
This works well for most apps. For apps that need to optimize further, Supabase also supports **custom JWT claims** — embedding role information directly in the access token so it's available on every request without a database lookup. This is a more advanced pattern that involves a database hook function, so it's worth exploring once you're comfortable with the table-based approach.
## Identity linking
A single user might sign in with email and password on their laptop and Google on their phone. Identity linking connects multiple authentication methods to one user account, so they don't end up with separate accounts and separate data.
Supabase handles the most common case automatically — if a user signs up with email and later signs in with a social provider that has the same verified email, both identities are linked to the same user record.
You can also let users explicitly connect additional providers from within your app using `supabase.auth.linkIdentity()`. This is useful for account settings pages where a user might want to add Google sign-in to an existing email/password account, or disconnect a provider they no longer use.
The important thing is that regardless of how a user signs in, their UUID stays the same. All their data, RLS policies, and foreign keys continue to work as expected.
## When to adopt these features
These features layer on top of the basics. A typical progression looks like:
- **Start simple** — email/password and maybe one social provider
- **Add MFA** — when your app handles sensitive data or your users request it
- **Add SSO** — when you start selling to organizations that require it
- **Add RBAC** — when "who can do what" gets more complex than "is this their data?"
- **Support identity linking** — when your users want flexibility in how they sign in
Start with what your users need today. Everything here will be waiting when you're ready for it.
@@ -3,13 +3,199 @@ title: Authentication Methods
description: Email, magic links, social providers, and phone auth
---
Supabase Auth is flexible and supports all the common authentication methods that modern apps use today — email and password, magic links, one-time passwords, social logins, phone-based auth, and anonymous sessions. You can offer one method or combine several — a user might sign up with email and password today and later link their Google account for faster login.
## Topics Covered
This chapter walks through each authentication method, what it looks like from the user's perspective, and how to set it up with the Supabase client library.
- Email/password authentication
- Magic links and OTP
- Social providers: Google, GitHub, Apple, Microsoft, and others
- Phone authentication with SMS
- Anonymous authentication for guest experiences
## Email and password
_Content coming soon_
The most straightforward method. A user provides their email address and a password to create an account, then uses the same credentials to sign in.
### Sign up
```js
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'their-password',
})
```
By default, Supabase sends a confirmation email with a link the user must click before their account is active. You can disable email confirmation in the Dashboard under **Authentication > Providers > Email**, but it's recommended to keep it on for production apps.
### Sign in
```js
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'their-password',
})
```
If the credentials are valid, this returns a session with an access token and refresh token. The Supabase client library stores the session automatically.
### Password reset
When a user forgets their password, you can send them a reset email:
```js
const { error } = await supabase.auth.resetPasswordForEmail('user@example.com')
```
This sends an email with a link. When the user clicks it, they're redirected to your app where you can let them set a new password:
```js
const { error } = await supabase.auth.updateUser({
password: 'new-password',
})
```
## Magic links
Magic links let users sign in without a password. They enter their email address, receive a link in their inbox, and clicking the link signs them in. There's nothing to remember.
```js
const { error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
})
```
This sends an email with a sign-in link. When the user clicks it, they're redirected to your app with an active session. If the email doesn't belong to an existing account, one is created automatically.
Magic links are popular for apps where reducing friction matters more than having users remember a password. The tradeoff is that sign-in is only as fast as checking your email.
## One-time passwords (OTP)
Similar to magic links, but instead of clicking a link, the user enters a 6-digit code. This works over email or SMS.
### Email OTP
```js
const { error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
})
```
The user receives an email with a code. Your app collects the code and verifies it:
```js
const { data, error } = await supabase.auth.verifyOtp({
email: 'user@example.com',
token: '123456',
type: 'email',
})
```
### Phone / SMS OTP
```js
const { error } = await supabase.auth.signInWithOtp({
phone: '+15551234567',
})
```
The user receives an SMS with a code. Verify it the same way:
```js
const { data, error } = await supabase.auth.verifyOtp({
phone: '+15551234567',
token: '123456',
type: 'sms',
})
```
Phone auth requires a third-party SMS provider (like Twilio or MessageBird), which you configure in the Dashboard under **Authentication > Providers > Phone**.
## Social providers (OAuth)
Social login lets users sign in with an existing account from a provider like Google, GitHub, Apple, or Microsoft. The user clicks a button, is redirected to the provider to grant permission, and then is redirected back to your app with an active session.
```js
const { error } = await supabase.auth.signInWithOAuth({
provider: 'google',
})
```
This redirects the user to Google's sign-in page. After they approve, Google redirects them back to your app. Supabase handles the token exchange and creates (or updates) the user in `auth.users`.
### Supported providers
Supabase supports a wide range of OAuth providers, including:
- Google
- GitHub
- Apple
- Microsoft / Azure
- Discord
- Slack
- Twitter / X
- Facebook
- Spotify
- And many more
Each provider requires some setup — typically creating an OAuth app in the provider's developer console and adding the client ID and secret to the Supabase Dashboard under **Authentication > Providers**.
### Redirect URLs
OAuth requires a redirect URL — the page in your app that users land on after authenticating with the provider. You configure the allowed redirect URLs in the Dashboard under **Authentication > URL Configuration**.
For local development, this is usually something like `http://localhost:3000/auth/callback`. For production, it's your actual domain.
### Scopes and user data
When a user signs in with a provider, Supabase requests basic profile information (email, name, avatar). This data is stored in the user's `raw_user_meta_data` field in `auth.users`, which you can access in your app or in trigger functions.
You can request additional data by specifying scopes:
```js
const { error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
scopes: 'repo',
},
})
```
The available scopes depend on the provider.
## Anonymous authentication
Sometimes you want users to interact with your app before they create an account — like adding items to a cart, saving preferences, or trying out a feature. Anonymous auth creates a temporary user with no email or password, that can later be converted to a regular user.
```js
const { data, error } = await supabase.auth.signInAnonymously()
```
This creates a real user in `auth.users` with a UUID and a session, but no identifying information. The user can interact with your app normally, and RLS policies work the same way — `auth.uid()` returns their anonymous ID.
Later, when the user decides to create a full account, you can link their anonymous session to a permanent identity:
```js
const { error } = await supabase.auth.updateUser({
email: 'user@example.com',
password: 'their-password',
})
```
This converts the anonymous user into a regular user while keeping their existing data and UUID. No data migration needed.
## Signing out
Regardless of which method the user signed in with, signing out works the same way:
```js
const { error } = await supabase.auth.signOut()
```
This clears the local session. The user will need to sign in again to access protected data.
## Choosing an authentication method
There's no single right answer — it depends on your app and your users. Here are some general guidelines:
- **Email/password** — the most familiar option. Good default for most apps.
- **Magic links** — lower friction, no password to remember. Good for apps where ease of access matters.
- **Social providers** — fast sign-in with one click, and you get profile data. Good when your users are likely to have accounts with those providers.
- **Phone/SMS** — useful for apps where users are more reachable by phone than email, or as a second factor.
- **Anonymous** — useful when you want users to try your app before committing to an account.
You can combine multiple methods. Many apps offer email/password alongside one or two social providers, giving users a choice.
@@ -3,12 +3,102 @@ title: Security Considerations
description: Rate limiting, token storage, and audit logging
---
Authentication gives your app the ability to identify users, but there's more to security than just signing people in. This chapter covers the things that are easy to overlook — protecting against abuse, storing tokens safely, handling OAuth securely, and keeping a record of what's happening in your system.
## Topics Covered
## Rate limiting and bot protection
- Rate limiting and bot protection (CAPTCHA)
- Secure token storage patterns
- PKCE flow for mobile and SPA applications
- Audit logging and compliance
Authentication endpoints are a target for abuse. Bots can try to brute-force passwords, spam sign-up emails, or flood your magic link endpoint. Supabase applies built-in rate limits to auth endpoints to mitigate this, but there are additional steps you can take.
_Content coming soon_
### Built-in rate limits
Supabase rate limits authentication requests by default. If a single IP sends too many sign-in attempts, sign-up requests, or OTP requests in a short window, subsequent requests are temporarily blocked. You can view and adjust these limits in the Dashboard under **Authentication > Rate Limits**.
The defaults are sensible for most apps, but if you're seeing legitimate users get blocked (for example, in a corporate environment where many users share an IP), you may need to adjust them.
### CAPTCHA
For public-facing sign-up and sign-in forms, adding a CAPTCHA is one of the most effective ways to stop automated abuse. Supabase has built-in support for hCaptcha and Cloudflare Turnstile.
You enable CAPTCHA in the Dashboard under **Authentication > Bot and Abuse Protection**, then include the CAPTCHA token when calling auth methods:
```js
const { error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'their-password',
options: {
captchaToken: token, // from your CAPTCHA widget
},
})
```
CAPTCHA is especially worth enabling on sign-up and password reset endpoints, since those trigger emails — and email abuse can affect your sender reputation.
## Secure token storage
When a user signs in, Supabase returns an access token and a refresh token. How you store these matters, because a stolen token means someone else can act as that user.
### Browser apps
The Supabase client library stores the session in `localStorage` by default. This is simple and works across page refreshes, but `localStorage` is accessible to any JavaScript running on the page — including third-party scripts. For most apps this is fine, but if you're concerned about XSS (cross-site scripting) attacks, there are a couple of things to keep in mind:
- **Sanitize user input** — XSS is the real risk here. If an attacker can inject JavaScript into your page, they can read `localStorage`. The best defense is preventing XSS in the first place.
- **Use a server-side approach** — for apps with strict security requirements, you can handle authentication on the server and store tokens in HTTP-only cookies, which aren't accessible to client-side JavaScript. The Supabase SSR package (`@supabase/ssr`) supports this pattern.
### Server-rendered apps
If your app uses server-side rendering (Next.js, SvelteKit, Nuxt, etc.), the recommended approach is to use the `@supabase/ssr` package. This stores the session in cookies rather than `localStorage`, which means:
- The token is sent with every request to your server automatically
- You can use HTTP-only cookies to prevent client-side JavaScript from accessing the token
- The session is available during server-side rendering, so you can check authentication before the page loads
### Mobile apps
On mobile, the Supabase client libraries use the platform's secure storage by default — Keychain on iOS and Keystore on Android. These are encrypted storage systems managed by the operating system and are significantly more secure than plain storage.
## Audit logging
For apps that handle sensitive data or need to meet compliance requirements, keeping a record of authentication events is important. Supabase provides several levels of visibility into what's happening.
### Auth logs in the Dashboard
The Dashboard shows authentication logs under **Authentication > Logs**. You can see sign-in attempts (successful and failed), sign-ups, password resets, and other auth events. This is useful for debugging issues ("why can't this user sign in?") and for spotting suspicious patterns (many failed attempts for the same email).
### Postgres audit trails
Since Supabase Auth stores its data in your Postgres database, you can build your own audit trail using database triggers. For example, you could log every time a user's email changes or a new session is created:
```sql
create table public.auth_audit_log (
id bigint generated always as identity primary key,
event_type text not null,
user_id uuid references auth.users(id),
metadata jsonb,
created_at timestamptz default now()
);
```
Then create triggers on the relevant `auth` tables to capture changes. This gives you a permanent, queryable record of authentication events that you control.
### What to log
Not every auth event needs to be logged. Focus on the ones that matter for security and compliance:
- **Failed sign-in attempts** — can indicate brute-force attacks or a user who needs help
- **Password changes and resets** — important for account security audits
- **Email or phone changes** — could indicate account takeover if the user didn't initiate it
- **MFA enrollment and unenrollment** — changes to security posture
- **Admin actions** — any time the Secret Key is used to modify a user
## A security checklist
Here's a quick checklist of things to verify before going to production:
- **RLS is enabled** on every table that contains user data
- **The Secret Key** is only used in server-side code, never exposed to the client
- **Email confirmation** is turned on so users verify their email before gaining access
- **CAPTCHA** is enabled on sign-up and password reset forms
- **Redirect URLs** are configured to only allow your actual domains (not `localhost` in production)
- **Password minimum length** is set to something reasonable (8+ characters)
- **Rate limits** are reviewed and appropriate for your expected traffic
- **Custom SMTP** is configured so emails come from your domain and aren't rate-limited by the default sender
@@ -3,12 +3,196 @@ title: User Management
description: User profiles, email templates, and administration
---
Once users can sign up and sign in, the next question is: how do you manage them? This chapter covers the practical side of working with users — storing profile data, customizing the emails they receive, configuring password and security settings, and performing admin tasks like updating or deleting accounts.
## Topics Covered
## User profiles and metadata
- User profiles and metadata
- Email templates and customization
- Password policies and security settings
- User administration and impersonation
Supabase Auth stores a user record in `auth.users` when someone signs up, but that table is managed by the Auth service — you shouldn't write to it directly. For any app-specific data like display names, avatars, or preferences, the standard pattern is to create a `profiles` table in your `public` schema.
_Content coming soon_
### Creating a profiles table
```sql
create table public.profiles (
id uuid primary key references auth.users(id) on delete cascade,
display_name text,
avatar_url text,
bio text,
created_at timestamptz default now()
);
alter table public.profiles enable row level security;
create policy "Users can read any profile"
on public.profiles for select
using (true);
create policy "Users can update their own profile"
on public.profiles for update
using (id = auth.uid());
```
The `id` column references `auth.users(id)`, so each profile is tied to exactly one user. The `on delete cascade` means if a user is deleted from Auth, their profile is automatically cleaned up.
### Automatically creating a profile on sign-up
You don't want users to manually create their profile row. A database trigger can handle this automatically:
```sql
create or replace function public.handle_new_user()
returns trigger
language plpgsql
security definer
as $$
begin
insert into public.profiles (id, display_name, avatar_url)
values (
new.id,
new.raw_user_meta_data ->> 'full_name',
new.raw_user_meta_data ->> 'avatar_url'
);
return new;
end;
$$;
create trigger on_auth_user_created
after insert on auth.users
for each row execute function public.handle_new_user();
```
When a user signs up — whether with email, Google, GitHub, or any other method — this trigger fires and creates a profile row. If the sign-up method provides metadata (like a name and avatar from a social provider), it's pulled in automatically.
### User metadata
Supabase Auth stores two types of metadata on the user object:
- **`raw_user_meta_data`** — data that the user can update themselves (via `supabase.auth.updateUser()`). This is where social providers store profile info like name and avatar.
- **`raw_app_meta_data`** — data that only the server or admin API can update. Useful for things like roles or internal flags that users shouldn't be able to change.
You can update user metadata from your app:
```js
const { error } = await supabase.auth.updateUser({
data: {
display_name: 'Alice',
favorite_color: 'blue',
},
})
```
This updates `raw_user_meta_data`. The data is available on the user object and in the JWT, so you can access it without querying your profiles table. For most apps, a combination of metadata (for lightweight data) and a profiles table (for richer data) works well.
## Email templates
Supabase sends emails for several authentication events — confirmation, password reset, magic links, and email changes. You can customize these templates in the Dashboard under **Authentication > Email Templates**.
Each template supports variables that Supabase replaces with actual values:
- `{{ .ConfirmationURL }}` — the link the user should click
- `{{ .Token }}` — the OTP code (for code-based flows)
- `{{ .SiteURL }}` — your configured site URL
- `{{ .Email }}` — the user's email address
For example, a custom confirmation email might look like:
```html
<h2>Welcome!</h2>
<p>Click the link below to confirm your email address:</p>
<p><a href="{{ .ConfirmationURL }}">Confirm email</a></p>
```
### Redirect URLs in emails
The links in authentication emails redirect users back to your app. You configure the base URL in the Dashboard under **Authentication > URL Configuration**. For local development this is typically `http://localhost:3000`, and for production it's your actual domain.
You can also specify a redirect URL per request. For example, after a password reset, you might want to send the user to a specific page:
```js
const { error } = await supabase.auth.resetPasswordForEmail('user@example.com', {
redirectTo: 'https://yourapp.com/update-password',
})
```
### Custom SMTP
By default, Supabase sends emails from its built-in email service, which works for development but has rate limits. For production, you'll want to configure a custom SMTP provider (like Resend, Postmark, or SendGrid) in the Dashboard under **Project Settings > Auth**. This gives you higher send limits, better deliverability, and emails that come from your own domain.
## Password policies and security settings
Supabase provides several settings to control password and session behavior. These are configured in the Dashboard under **Authentication > Providers > Email** and **Authentication > Settings**.
### Password strength
You can set a minimum password length (the default is 6 characters). For production apps, a minimum of 8 characters is a reasonable starting point. Supabase also supports the HaveIBeenPwned integration, which checks passwords against a database of known breaches and rejects any that appear in it.
### Session lifetime
You can control how long sessions last:
- **JWT expiry** — how long an access token is valid before it needs to be refreshed (default: 1 hour)
- **Refresh token lifetime** — how long a refresh token is valid before the user needs to sign in again
Shorter lifetimes are more secure but require more frequent token refreshes. The defaults work well for most apps.
### Rate limiting
Supabase applies rate limits to authentication endpoints to prevent abuse — things like brute-force password attempts or spamming sign-up emails. The defaults are reasonable, but you can adjust them in the Dashboard if needed.
## User administration
As your app grows, you'll need to manage users from the admin side — viewing accounts, updating information, or removing users.
### The Dashboard
The simplest way to manage users is through the Supabase Dashboard under **Authentication > Users**. From there you can:
- Browse and search users
- View a user's metadata, identities, and sessions
- Manually confirm a user's email
- Delete a user
- Send a password reset email
### The Admin API
For programmatic access, the Supabase client library has admin methods that require the Secret Key (not the publishable anon key). These should only be used in server-side code — never expose the Secret Key to the browser.
```js
import { createClient } from '@supabase/supabase-js'
const supabaseAdmin = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SECRET_KEY
)
```
With the admin client, you can manage users programmatically:
```js
// List users
const { data: { users }, error } = await supabaseAdmin.auth.admin.listUsers()
// Get a specific user
const { data: { user }, error } = await supabaseAdmin.auth.admin.getUserById(userId)
// Update a user
const { error } = await supabaseAdmin.auth.admin.updateUserById(userId, {
email: 'newemail@example.com',
user_metadata: { role: 'admin' },
})
// Delete a user
const { error } = await supabaseAdmin.auth.admin.deleteUser(userId)
```
### Inviting users
If your app requires users to be invited rather than signing up on their own, you can send invite emails through the admin API:
```js
const { error } = await supabaseAdmin.auth.admin.inviteUserByEmail('newuser@example.com')
```
This sends an email with a sign-up link. When the user clicks it, they're taken to your app to set their password and complete their account.
### A note on the Secret Key
The Secret Key bypasses Row Level Security entirely. Any request made with it has full read and write access to your database. This is why it should only be used in trusted server environments — API routes, server actions, background jobs, or admin scripts. Never include it in client-side code or expose it in environment variables that are bundled into your frontend.