# 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.

```js
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.

```js
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.

```js
await supabase.auth.signUp({ email: 'user@example.com', password: '…' })
await supabaseAdmin.auth.admin.inviteUserByEmail('new@example.com', { 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.

```js
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`.

```json
{ "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.

---

https://end.blackall.ai/docs/auth?lang=en
