Skip to content

Commit 97a447a

Browse files
cstocktonChris Stockton
andauthored
docs(auth-hooks): clarify webhook status codes and document email_change quirk (supabase#38141)
* docs(auth-hooks): clarify webhook status codes and document email_change quirk - Clarify hook response handling: - Note that some hooks do not support 204 responses if they require a body - Update retry-able error behavior for 429/503 with explicit 5s budget - Add note that Retry-After header is only checked for non-empty value, not parsed - Expand send-email-hook docs: - Add detailed section on `email_change` behavior - Explain secure vs non-secure modes and when two emails must be sent - Document long-standing quirk where `token_hash` and `token_hash_new` are swapped relative to expected naming - Extend enum to include `reauthentication` - Improve confirmation URL example using URLSearchParams and projectRef * fix: fix linter error * fix: pr feedback * fix: pnpm format * fix: pr feedback --------- Co-authored-by: Chris Stockton <[email protected]>
1 parent db4ae7e commit 97a447a

File tree

2 files changed

+70
-14
lines changed

2 files changed

+70
-14
lines changed

apps/docs/content/guides/auth/auth-hooks.mdx

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -313,23 +313,37 @@ Hooks return status codes based on the nature of the response. These status code
313313
| HTTP Status Code | Description | Example Usage |
314314
| ---------------- | ------------------------------------------------------------- | ---------------------------------------------- |
315315
| 200, 202, 204 | Valid response, proceed | Successful processing of the request |
316-
| 429, 503 | Retry-able errors with Retry-after header supported | Temporary server overload or maintenance |
317316
| 403, 400 | Treated as Internal Server Errors and return a 500 Error Code | Malformed requests or insufficient permissions |
317+
| 429, 503 | Retry-able errors | Temporary server overload or maintenance |
318318

319-
Errors are responses which contain status codes 400 and above. On a retry-able error, such as an error with a `429` or `503` status code, HTTP Hooks will attempt up to three retries with a back-off of two seconds.
319+
<Admonition type="note">
320+
321+
`204` Status is not supported by the following hooks which require a response body:
322+
323+
- [Custom Access Token](/docs/guides/auth/auth-hooks/custom-access-token-hook)
324+
- [MFA Verification Attempt](/docs/guides/auth/auth-hooks/mfa-verification-hook)
325+
- [Password Verification Attempt](/docs/guides/auth/auth-hooks/password-verification-hook)
326+
327+
</Admonition>
328+
329+
Errors are responses which contain status codes 400 and above. On a retry-able error, such as an error with a `429` or `503` status code, HTTP Hooks will attempt up to three retries with a back-off of two seconds. We have a time budget of 5s for the entire webhook invocation, including retry requests.
320330

321331
Here's a sample HTTP retry schedule:
322332

323-
| Time Since Start (HH:MM:SS) | Event | Notes |
324-
| --------------------------- | ----------------------- | ------------------------------------------------- |
325-
| 00:00:00 | Initial Attempt | Initial invocation begins. |
326-
| 00:00:05 | Initial Attempt Timeout | Initial invocation must complete. |
327-
| 00:00:07 | Retry Start #1 | After 2 sec delay, first retry begins. |
328-
| 00:00:12 | Retry Timeout #1 | First retry timeout. |
329-
| 00:00:14 | Retry Start #2 | After 2 sec delay, second retry begins. |
330-
| 00:00:19 | Retry Timeout #2 | Second retry timeout. Returns an error on failure |
333+
| Time Since Start (HH:MM:SS) | Event | Notes |
334+
| --------------------------- | --------------------- | -------------------------------------------------------------------------------- |
335+
| 00:00:00 | Initial Attempt | Initial invocation begins. |
336+
| 00:00:02 | Initial Attempt Fails | Initial invocation returns `429` or `503` with non-empty `retry-after` header. |
337+
| 00:00:04 | Retry Start #1 | After 2 sec delay, first retry begins. |
338+
| 00:00:05 | Retry Timeout #1 | First retry times out, exceeded 5 second budget and invocation returns an error. |
331339

332-
Return a retry-able error by attaching a appropriate status code (`429`, `503` ) and a non-empty `retry-after` header
340+
Return a retry-able error by attaching a appropriate status code (`429`, `503`) and a non-empty `retry-after` header
341+
342+
<Admonition type="note">
343+
344+
`Retry-After` Supabase Auth does not fully support the `Retry-After` header as described in RFC7231, we only check if it is a non-empty value such as `true` or `10`. Setting this to your preferred value is fine as a future update may address this.
345+
346+
</Admonition>
333347

334348
```jsx
335349
return new Response(

apps/docs/content/guides/auth/auth-hooks/send-email-hook.mdx

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,33 @@ Email sending depends on two settings: Email Provider and Auth Hook status.
1717
| Disabled | Enabled | Email Signups Disabled |
1818
| Disabled | Disabled | Email Signups Disabled |
1919

20+
## Email change behavior and token hash mapping
21+
22+
When `email_action_type` is `email_change`, the hook payload can include one or two OTPs and their hashes. This depends on your [Secure Email Change](/dashboard/project/_/auth/providers?provider=Email) setting.
23+
24+
- Secure Email Change enabled: two OTPs are generated, one for the current email (`user.email`) and one for the new email (`user.email_new`). You must send two emails.
25+
- Secure Email Change disabled: only one OTP is generated for the new email. You send a single email.
26+
27+
<Admonition type="note">
28+
29+
Important quirk (backward compatibility):
30+
31+
- `email_data.token_hash_new` = Hash(`user.email`, `email_data.token`)
32+
- `email_data.token_hash` = Hash(`user.email_new`, `email_data.token_new`)
33+
34+
This naming is historical and kept for backward compatibility. Do not assume that the `_new` suffix refers to the new email.
35+
36+
</Admonition>
37+
38+
### What to send
39+
40+
If both `token_hash` and `token_hash_new` are present, send two messages:
41+
42+
- To the current email (`user.email`): use `token` with `token_hash_new`.
43+
- To the new email (`user.email_new`): use `token_new` with `token_hash`.
44+
45+
If only one token/hash pair is present, send a single email. In non-secure mode, this is typically the new email OTP. Use `token` with `token_hash` or `token_new` with `token_hash`, depending on which fields are present in the payload.
46+
2047
**Inputs**
2148

2249
| Field | Type | Description |
@@ -282,7 +309,15 @@ Email sending depends on two settings: Email Provider and Auth Hook status.
282309
},
283310
"email_action_type": {
284311
"type": "string",
285-
"enum": ["signup", "invite", "magiclink", "recovery", "email_change", "email"]
312+
"enum": [
313+
"signup",
314+
"invite",
315+
"magiclink",
316+
"recovery",
317+
"email_change",
318+
"email",
319+
"reauthentication"
320+
]
286321
},
287322
"site_url": {
288323
"type": "string",
@@ -578,6 +613,7 @@ import { readAll } from 'https://deno.land/std/io/read_all.ts'
578613
const postmarkEndpoint = 'https://api.postmarkapp.com/email'
579614
// Replace this with your email
580615
const FROM_EMAIL = '[email protected]'
616+
const PROJECT_REF = '<your-project-ref>'
581617

582618
// Email Subjects
583619
const subjects = {
@@ -642,8 +678,14 @@ const templates = {
642678
}
643679

644680
function generateConfirmationURL(email_data) {
645-
// TODO: replace the ref with your project ref
646-
return `https://<ref>.supabase.co/auth/v1/verify?token=${email_data.token_hash}&type=${email_data.email_action_type}&redirect_to=${email_data.redirect_to}`
681+
const baseUrl = `https://${PROJECT_REF}.supabase.co/auth/v1/verify`
682+
const params = new URLSearchParams({
683+
token: email_data.token_hash,
684+
type: email_data.email_action_type,
685+
redirect_to: email_data.redirect_to,
686+
})
687+
688+
return `${baseUrl}?${params.toString()}`
647689
}
648690

649691
Deno.serve(async (req) => {

0 commit comments

Comments
 (0)