Files
supabase/apps/docs/content/guides/auth/auth-email-passwordless.mdx
fadymak 0939174a92 feat(docs): use publishable keys instead of anon keys in Auth guides (#44851)
With the upcoming deprecation of the anonymous and service role keys,
this PR updates the Auth guides to use the publishable key instead of
the soon-to-be-deprecated anonymous key.

It also standardizes the example strings to be:
`'https://your-project-id.supabase.co'` and `'sb_publishable_...'` for
consistency.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Documentation**
* Standardized client initialization examples to use a consistent
publishable-key placeholder (`sb_publishable_...`) and full project URL
format.
* Replaced "anon key" wording with "publishable key" across auth and API
guides and examples.
* Minor formatting and import-order/whitespace improvements in code
samples for clarity and consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-15 14:13:23 +02:00

367 lines
10 KiB
Plaintext

---
title: 'Passwordless email logins'
subtitle: 'Email logins using Magic Links or One-Time Passwords (OTPs)'
---
Supabase Auth provides several passwordless login methods. Passwordless logins allow users to sign in without a password, by clicking a confirmation link or entering a verification code.
Passwordless login can:
- Improve the user experience by not requiring users to create and remember a password
- Increase security by reducing the risk of password-related security breaches
- Reduce support burden of dealing with password resets and other password-related flows
Supabase Auth offers two passwordless login methods that use the user's email address:
- [Magic Link](#with-magic-link)
- [OTP](#with-otp)
## With Magic Link
Magic Links are a form of passwordless login where users click on a link sent to their email address to log in to their accounts. Magic Links only work with email addresses and are one-time use only.
### Enabling Magic Link
Email authentication methods, including Magic Links, are enabled by default.
Configure the Site URL and any additional redirect URLs. These are the only URLs that are allowed as redirect destinations after the user clicks a Magic Link. You can change the URLs on the [URL Configuration page](/dashboard/project/_/auth/url-configuration) for hosted projects, or in the [configuration file](/docs/guides/cli/config#auth.additional_redirect_urls) for self-hosted projects.
By default, a user can only request a magic link once every <SharedData data="config">auth.rate_limits.magic_link.period</SharedData> and they expire after <SharedData data="config">auth.rate_limits.magic_link.validity</SharedData>.
### Signing in with Magic Link
Call the "sign in with OTP" method from the client library.
Though the method is labelled "OTP", it sends a Magic Link by default. The two methods differ only in the content of the confirmation email sent to the user.
If the user hasn't signed up yet, they are automatically signed up by default. To prevent this, set the `shouldCreateUser` option to `false`.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
```js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('https://your-project-id.supabase.co', 'sb_publishable_...')
// ---cut---
async function signInWithEmail() {
const { data, error } = await supabase.auth.signInWithOtp({
email: 'valid.email@supabase.io',
options: {
// set this to false if you do not want the user to be automatically signed up
shouldCreateUser: false,
emailRedirectTo: 'https://example.com/welcome',
},
})
}
```
</TabPanel>
<TabPanel id="react-native" label="Expo React Native">
```ts
import { makeRedirectUri } from 'expo-auth-session'
const redirectTo = makeRedirectUri()
const { error } = await supabase.auth.signInWithOtp({
email: 'valid.email@supabase.io',
options: {
emailRedirectTo: redirectTo,
},
})
```
Read the [Deep Linking Documentation](/docs/guides/auth/native-mobile-deep-linking) to learn how to handle deep linking.
</TabPanel>
<$Show if="sdk:dart">
<TabPanel id="dart" label="Dart">
```dart
Future<void> signInWithEmail() async {
final AuthResponse res = await supabase.auth.signinwithotp(email: 'valid.email@supabase.io');
}
```
</TabPanel>
</$Show>
<$Show if="sdk:swift">
<TabPanel id="swift" label="Swift">
```swift
try await supabase.auth.signInWithOTP(
email: "valid.email@supabase.io",
redirectTo: URL(string: "https://example.com/welcome"),
// set this to false if you do not want the user to be automatically signed up
shouldCreateUser: false
)
```
</TabPanel>
</$Show>
<$Show if="sdk:kotlin">
<TabPanel id="kotlin" label="Kotlin">
```kotlin
suspend fun signInWithEmail() {
supabase.auth.signInWith(OTP) {
email = "valid.email@supabase.io"
}
}
```
</TabPanel>
</$Show>
<$Show if="sdk:python">
<TabPanel id="python" label="Python">
```python
response = supabase.auth.sign_in_with_otp({
'email': 'valid.email@supabase.io',
'options': {
# set this to false if you do not want the user to be automatically signed up
'should_create_user': False,
'email_redirect_to': 'https://example.com/welcome',
},
})
```
</TabPanel>
</$Show>
</Tabs>
{/* supa-mdx-lint-disable-next-line Rule004ExcludeWords */}
That's it for the implicit flow.
If you're using PKCE flow, edit the Magic Link [email template](/docs/guides/auth/auth-email-templates) to send a token hash:
```html
<h2>Magic Link</h2>
<p>Follow this link to login:</p>
<p><a href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email">Log In</a></p>
```
At the `/auth/confirm` endpoint, exchange the hash for the session:
```js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('https://your-project-id.supabase.co', 'sb_publishable_...')
// ---cut---
const { error } = await supabase.auth.verifyOtp({
token_hash: 'hash',
type: 'email',
})
```
## With OTP
Email one-time passwords (OTP) are a form of passwordless login where users key in a six digit code sent to their email address to log in to their accounts.
### Enabling email OTP
Email authentication methods, including Email OTPs, are enabled by default.
Email OTPs share an implementation with Magic Links. To send an OTP instead of a Magic Link, alter the **Magic Link** email template. For a hosted Supabase project, go to [Email Templates](/dashboard/project/_/auth/templates) in the Dashboard. For a self-hosted project or local development, see the [Email Templates guide](/docs/guides/auth/auth-email-templates).
Modify the template to include the `{{ .Token }}` variable, for example:
```html
<h2>One time login code</h2>
<p>Please enter this code: {{ .Token }}</p>
```
By default, a user can only request an OTP once every <SharedData data="config">auth.rate_limits.otp.period</SharedData> and they expire after <SharedData data="config">auth.rate_limits.otp.validity</SharedData>. This is configurable via `Auth > Providers > Email > Email OTP Expiration`. An expiry duration of more than 86400 seconds (one day) is disallowed to guard against brute force attacks. The longer an OTP remains valid, the more time an attacker has to attempt brute force attacks. If the OTP is valid for several days, an attacker might have more opportunities to guess the correct OTP through repeated attempts.
### Signing in with email OTP
#### Step 1: Send the user an OTP code
Get the user's email and call the "sign in with OTP" method from your client library.
If the user hasn't signed up yet, they are automatically signed up by default. To prevent this, set the `shouldCreateUser` option to `false`.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
```js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('https://your-project-id.supabase.co', 'sb_publishable_...')
// ---cut---
const { data, error } = await supabase.auth.signInWithOtp({
email: 'valid.email@supabase.io',
options: {
// set this to false if you do not want the user to be automatically signed up
shouldCreateUser: false,
},
})
```
</TabPanel>
<$Show if="sdk:dart">
<TabPanel id="dart" label="Dart">
```dart
Future<void> signInWithEmailOtp() async {
final AuthResponse res = await supabase.auth.signInWithOtp(email: 'valid.email@supabase.io');
}
```
</TabPanel>
</$Show>
<$Show if="sdk:swift">
<TabPanel id="swift" label="Swift">
```swift
try await supabase.auth.signInWithOTP(
email: "valid.email@supabase.io",
// set this to false if you do not want the user to be automatically signed up
shouldCreateUser: false
)
```
</TabPanel>
</$Show>
<$Show if="sdk:kotlin">
<TabPanel id="kotlin" label="Kotlin">
```kotlin
suspend fun signInWithEmailOtp() {
supabase.auth.signInWith(OTP) {
email = "valid.email@supabase.io"
}
}
```
</TabPanel>
</$Show>
<$Show if="sdk:python">
<TabPanel id="python" label="Python">
```python
response = supabase.auth.sign_in_with_otp({
'email': 'valid.email@supabase.io',
'options': {
# set this to false if you do not want the user to be automatically signed up
'should_create_user': False,
},
})
```
</TabPanel>
</$Show>
</Tabs>
If the request is successful, you receive a response with `error: null` and a `data` object where both `user` and `session` are null. Let the user know to check their email inbox.
```json
{
"data": {
"user": null,
"session": null
},
"error": null
}
```
#### Step 2: Verify the OTP to create a session
Provide an input field for the user to enter their one-time code.
Call the "verify OTP" method from your client library with the user's email address, the code, and a type of `email`:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
```js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('https://your-project-id.supabase.co', 'sb_publishable_...')
// ---cut---
const {
data: { session },
error,
} = await supabase.auth.verifyOtp({
email: 'email@example.com',
token: '123456',
type: 'email',
})
```
</TabPanel>
<$Show if="sdk:swift">
<TabPanel id="swift" label="Swift">
```swift
try await supabase.auth.verifyOTP(
email: email,
token: "123456",
type: .email
)
```
</TabPanel>
</$Show>
<$Show if="sdk:kotlin">
<TabPanel id="kotlin" label="Kotlin">
```kotlin
supabase.auth.verifyEmailOtp(type = OtpType.Email.EMAIL, email = "email", token = "151345")
```
</TabPanel>
</$Show>
<$Show if="sdk:python">
<TabPanel id="python" label="Python">
```python
response = supabase.auth.verify_otp({
'email': email,
'token': '123456',
'type': 'email',
})
```
</TabPanel>
</$Show>
</Tabs>
If successful, the user is now logged in, and you receive a valid session that looks like:
```json
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjI3MjkxNTc3LCJzdWIiOiJmYTA2NTQ1Zi1kYmI1LTQxY2EtYjk1NC1kOGUyOTg4YzcxOTEiLCJlbWFpbCI6IiIsInBob25lIjoiNjU4NzUyMjAyOSIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6InBob25lIn0sInVzZXJfbWV0YWRhdGEiOnt9LCJyb2xlIjoiYXV0aGVudGljYXRlZCJ9.1BqRi0NbS_yr1f6hnr4q3s1ylMR3c1vkiJ4e_N55dhM",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "LSp8LglPPvf0DxGMSj-vaQ",
"user": {...}
}
```