One Server

Docs

Adding sign-in

Email and password, and social sign-in. Where the token comes back.

Adding sign-in

Email and password

POST /auth/v1/signup          {"email":"...","password":"..."}
POST /auth/v1/token?grant_type=password   {"email":"...","password":"..."}
GET  /auth/v1/user            Authorization: Bearer <access_token>
POST /auth/v1/logout

Passwords must be at least 10 characters, and passwords that have appeared in a breach are refused.

Social sign-in

Eleven providers are supported: Google, Kakao, Naver, Apple, GitHub, GitLab, Microsoft, Facebook, LINE, Discord, X. list_auth_providers returns all of them: what is configured now and what is not configured yet.

Setup is done entirely with tools. You don't need the console.

list_auth_providers   current state and callback addresses
set_auth_provider     save a client ID and secret, and turn it on
set_auth_provider_enabled   turn off and on, keeping the credentials
remove_auth_provider  delete, credentials included
list_redirect_urls    addresses sign-in may return to
add_redirect_url      allow a return address
remove_redirect_url   remove a return address

Order matters. Register the callback address in the provider's console first. That address is the callback_url from list_auth_providers. If it differs by a single character, the provider refuses the whole sign-in, and the error it shows names neither this platform nor what was wrong.

And if you don't register a return address, sign-in succeeds but goes nowhere. Add it with add_redirect_url. Scheme, host and port must all match and the path is ignored, so adding https://myapp/done stores https://myapp, and sign-in can return to any path under it. The whole origin is compared, because comparing only the beginning would let https://myapp.evil.test through.

If sign-in succeeds but goes nowhere, start with list_redirect_urls. Usually the address is not on it. Development entries such as http://localhost are better removed with remove_redirect_url before you ship.

GET /auth/v1/authorize?provider=kakao&redirect_to=https://myapp/done

When it finishes, it returns to redirect_to, and the token arrives after the # in the address (not in the query string). The query string ends up in proxy logs and the Referer header, but the part after # is never sent to a server.

const token = new URLSearchParams(location.hash.slice(1)).get('access_token')

Providers you register yourself (custom:)

Beyond the eleven above, an OAuth 2.0 / OpenID Connect provider can be registered as custom:<name> (the same name as on Supabase). It is also how a second Kakao app is kept, such as custom:kakao-xts, when the project's built-in Kakao slot belongs to another app.

set_auth_provider  provider: "custom:kakao-xts", client_id, client_secret,
                   authorize_url, token_url, userinfo_url (or only issuer),
                   scopes, claim_map: {"id":"id","email":"kakao_account.email",
                   "email_verified":"kakao_account.is_email_verified","phone":"kakao_account.phone_number"}
  • Only public https addresses are accepted. Without claim_map (its first name, claims, is taken too), OpenID's standard claims are read (sub, email, email_verified, name, picture, phone_number).
  • With provider_type: "oidc" the id_token is read only after its signature, issuer, audience (the client_id), expiry and nonce are checked against the keys the issuer publishes (jwks_uri, or found in the issuer's discovery document). An app holding such an id_token can also signInWithIdToken({ provider: 'custom:<name>', token }). Left out, it is oauth2 (the default), read through claim_map.
  • token_auth sends the secret in the body (body, the default) or as HTTP Basic (basic); pkce is on by default. email_optional: true lets people without an email address in.
  • The console's custom providers on Sign In / Providers, list_custom_providers and create_custom_provider (Supabase's shape), set_auth_provider and the CLI all see and change the same providers. Each plan allows a number of them (Free 3, Pro 10).
  • In the app it is the usual supabase.auth.signInWithOAuth({ provider: 'custom:kakao-xts', options: { scopes: 'phone_number' } }). scopes are added to the registered ones.
  • The session carries the provider's own token once as provider_token (and provider_refresh_token), to call the provider's API with (Kakao's phone number, for example). This platform keeps it encrypted only until the PKCE exchange finishes (ten minutes at most), then deletes it.
  • An existing account is joined only when the provider says it verified the address (the email_verified mapping).
  • As on Supabase, it is not listed in /auth/v1/settings. CLI: oneend auth providers set custom:<name> ....

Accounts without an email

Kakao, Naver, GitHub, GitLab, Facebook, LINE and X can return without an email. Code that requires an email refuses most sign-ins in Korea.

Linking to the same person's existing account also happens only when the provider says it verified the email. Linking on an unverified email lets anyone who types in someone else's email take over that account.

Anonymous sign-in

Turn it on with set_anonymous_sign_in and people get a session right away, with no email. The current state is anonymous_sign_in in list_auth_providers. Anonymous users also have the <ref>_authenticated role, so to tell them apart in access rules, look at is_anonymous in the token.

Sign-in by text message

Signing up and signing in with a phone number needs an SMS account. See the current settings with get_sms_settings and set them with set_sms_settings. Use your own Solapi (CoolSMS), NHN Cloud, Aligo or Twilio account, or, on a paid plan, the platform account (platform, ₩100 a message, Korea only). remove_sms_settings removes it.

await supabase.auth.signInWithOtp({ phone: '010-1234-5678' })
await supabase.auth.verifyOtp({ phone: '010-1234-5678', token: '123456', type: 'sms' })

As in GoTrue, a number is stored as digits from the country code (821012345678), and one starting with 010 is read as Korean. One number gets at most one message every 60 seconds and 5 an hour, and one IP address 20 an hour. Five wrong codes spend a code. Asking too often is refused with over_sms_send_rate_limit (429). allowed_countries is only 82 by default. Fraud that makes you text premium-rate numbers abroad is common, so leave out countries you do not serve.

Auth emails: the sender (SMTP) and templates

Sign-up confirmation, password reset, sign-in link, email change and invitation emails go out from the platform's address by default, in the default wording for the request's language (Korean, English or Japanese). To send from your own address, set set_smtp_settings (the same fields as Supabase's SMTP Settings: sender_email, sender_name, host, port, username, password, minimum_interval_seconds) and send a test with test_smtp_settings. get_smtp_settings never shows the password, only its last four characters. If that server refuses, the email goes out from the platform's address and the reason is kept in last_error. remove_smtp_settings removes it. Past the hourly limit (rate_limit_per_hour) or the per-address interval, requests are refused with over_email_send_rate_limit (429).

Read the wording with get_email_templates and set the subject and HTML body per type (confirmation, invite, magic_link, recovery, email_change, reauthentication) with set_email_template. Templates from Supabase paste in unchanged: {{ .ConfirmationURL }}, {{ .Token }}, {{ .TokenHash }}, {{ .SiteURL }}, {{ .Email }}, {{ .NewEmail }}, {{ .RedirectTo }}, {{ .Data.<key> }}. Check one with preview_email_template before saving, and go back to the default wording with remove_email_template. {{ .SiteURL }} is the origin of the first redirect address. reauthentication is not sent yet.

await supabase.auth.signUp({ email: '[email protected]', password: '…' })
await supabaseAdmin.auth.admin.inviteUserByEmail('[email protected]', { redirectTo: 'https://shop.example/welcome' })

SAML SSO

Lets people sign in through a company IdP (Okta, Microsoft Entra ID, Google Workspace and others). list_sso_providers gives the acs_url and entity_id to enter in the IdP. add_sso_provider registers one from the IdP's metadata (a URL or the XML) and its email domains; update_sso_provider changes it and remove_sso_provider removes it. A new IdP can be added on the Enterprise plan only (new_providers_allowed in list_sso_providers). IdPs already added keep working, and can be changed, on any plan.

const { data } = await supabase.auth.signInWithSSO({ domain: 'example.com' })
location.href = data.url

The response or the assertion has to be signed with the IdP's certificate, and only answers to requests this project made are accepted. Sign-ins started at the IdP are not. A person who signs in through SSO for the first time gets a new account, not merged with an existing one at the same email address.

The whole sign-in configuration (Sign In / Providers)

The settings on the console's Sign In / Providers page, under Supabase's auth config keys. get_auth_config returns all of them in lower case (external_google_client_id, sms_provider, saml_enabled and so on), and update_auth_config changes only the keys given. If a provider switched on is missing what it needs, or a value is malformed, nothing is saved and each key's reason comes back. A secret shows as its last four characters, like ••••abcd, and handing that back changes nothing. A project's own OAuth and OIDC providers are list_custom_providers, create_custom_provider, update_custom_provider, set_custom_provider_enabled and remove_custom_provider; Firebase, Clerk, WorkOS, Auth0 and Cognito tokens are list_third_party_auth, add_third_party_auth and remove_third_party_auth.

{ "external_kakao_enabled": true, "external_kakao_client_id": "REST_API_KEY", "external_kakao_secret": "CLIENT_SECRET",
  "external_kakao_scopes": "profile_nickname,account_email,gender", "external_kakao_email_optional": true }

Kakao's consent items (external_kakao_scopes) are sent as the scope, and what comes back is saved in the user's user_metadata. Naver decides what an application may read in Naver Developers, so external_naver_scopes chooses which fields are kept.

Removing sign-ups

remove_user { ref, id, confirm } permanently deletes one sign-up. The id is the UUID list_users returned, and confirm takes that user's email (or id) exactly. It cannot be undone, so use it only when the user has clearly asked. The deletion is kept in the audit log (access).

Importing sign-ups in bulk

import_users { ref, users: [{email, id?, encrypted_password?, email_confirmed?, user_metadata?, app_metadata?}], dry_run? } imports up to 500 sign-ups at once from another service (for example a Supabase auth.users export). It keeps id and the bcrypt hash ($2a$, $2b$), so nobody has to choose a new password. Plain passwords are not accepted. The whole list is checked first: a wrong row is named by number and nothing is imported (dry_run: true only checks). An account that already exists (same id or email) is skipped and counted in skipped, so repeating the same call is safe. REST is POST /v1/projects/<ref>/users/import {users, dry_run}.

Changing a sign-up, and sign-in links

update_user { ref, id, ... } changes one sign-up's email, password (password), whether the email is confirmed (email_confirm), a ban (ban_duration, such as 24h; none lifts it) and metadata. A new password or a ban signs the person out everywhere. The password rule is the same as at sign-up. generate_link makes an invitation (invite), a sign-in without a password (magiclink), a password reset (recovery) or a sign-up confirmation (signup) link without mailing it, and returns it as action_link. Whoever opens it is signed in as that user, so give it only to the person who asked. redirect_to must be an address added with add_redirect_url. Over REST it is PATCH /v1/projects/<ref>/users/<id> and POST /v1/projects/<ref>/users/<email>/links {type, password, data, redirect_to}.

The signed-in user, in a table

auth_<ref>.uid() returns the user's identifier. Use it inside access rules.

Passkeys, second factors, OAuth apps

get_passkey_settings     users' passkey sign-in settings
set_passkey_settings     on or off, RP name, RP ID, allowed origins
list_user_factors        one user's second factors and passkeys
remove_user_factors      remove all their second factors (email in confirm)
remove_user_passkey      remove one passkey
list_oauth_apps          other apps that sign in with this project, and the addresses
create_oauth_app         register an app (client_secret shown once)
reset_oauth_app_secret   make a new secret
remove_oauth_app         remove an app (its name in confirm)

Turning the OAuth server on and off, allowing signups and banning are update_auth_config (OAUTH_SERVER_ENABLED, DISABLE_SIGNUP) and update_user (ban_duration). Instead of a password reset mail, hand over generate_link's recovery link yourself.