mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
docs: Reviewing 401 guide (#43990)
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? docs update ## What is the current behavior? There is a 401 guide ## What is the new behavior? This is an improved version of the same guide --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Chris Chinchilla <chris.ward@supabase.io>
This commit is contained in:
1 file changed
+170
-95
@@ -1,93 +1,139 @@
|
||||
---
|
||||
title = "Edge Function 401 error response"
|
||||
topics = [ "functions" ]
|
||||
keywords = [ "401", "error" ]
|
||||
database_id = "12e09c3f-a48b-4aea-9df3-90a25ef0021e"
|
||||
keywords = ["401", "error", "JWT", "authorization"]
|
||||
|
||||
[[errors]]
|
||||
http_status_code = 401
|
||||
message = "Invalid JWT"
|
||||
---
|
||||
|
||||
The function rejected the request for lacking the appropriate authorization headers.
|
||||
A 401 response from an Edge Function means either:
|
||||
|
||||
## Context for the error
|
||||
- The function failed the [legacy auth verification check](/docs/guides/functions/development-tips#skipping-authorization-checks)
|
||||
- Your function's logic deliberately returned a 401 response
|
||||
|
||||
#### The JWT verification check
|
||||
## Quick triage
|
||||
|
||||
By default, edge functions are configured to check requests for a valid [legacy key](/docs/guides/api/api-keys#overview).
|
||||
Check the response body returned by the request
|
||||
|
||||
#### How the check causes 401 errors
|
||||
|
||||
The check fails and return a 401 permission error if:
|
||||
|
||||
- The request includes an invalid legacy key, or
|
||||
- The request uses the newer asymmetric keys instead of a legacy key.
|
||||
|
||||
#### What this check actually does
|
||||
|
||||
This validation provides limited security value. It only confirms that the request includes a legacy token associated with your project, such as the anon key.
|
||||
|
||||
It does not validate the user's identity or permissions beyond that.
|
||||
|
||||
Because the security check is weak, it was deprecated when used with the new asymmetric keys.
|
||||
|
||||
## Solving the error
|
||||
|
||||
### Step 1: Identifying the error
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
If the tests return a 401 but don't match the criteria below, the error is coming from your app logic, not the JWT check.
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Inspecting the return message
|
||||
|
||||
When an edge function fails due to a platform 401 error, it returns the error:
|
||||
### Case 1: `"Invalid JWT"` or `"Missing authorization header"`
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 401,
|
||||
"message": "Invalid JWT"
|
||||
}
|
||||
{ "code": 401, "message": "Invalid JWT" }
|
||||
```
|
||||
|
||||
### Inspecting the logs
|
||||
```json
|
||||
{ "code": 401, "message": "Missing authorization header" }
|
||||
```
|
||||
|
||||
Your code may return a 401 error due to its own logic
|
||||
Both of these messages come from the [legacy auth verification check](/docs/guides/functions/development-tips#skipping-authorization-checks)
|
||||
|
||||
Go to: [Built-in JWT check failures](#built-in-jwt-check-failures)
|
||||
|
||||
### Case 2: Custom message or empty body
|
||||
|
||||
If the response body contains a message you coded, or nothing at all, then your function code _did_ execute and returned a 401 itself.
|
||||
|
||||
Go to: [Your function returned a 401](#your-function-returned-a-401)
|
||||
|
||||
### Case 3: Not sure
|
||||
|
||||
Run this query in [Log Explorer](/dashboard/project/_/logs/explorer?q=SELECT%0A++++cast%28timestamp+AS+datetime%29++AS+timestamp%2C%0A++++req.pathname+++++++++++++++++AS+function_name%2C%0A%0A++++CASE%0A++++++++WHEN+metadata.execution_id+IS+NOT+NULL%0A++++++++++++THEN+%27your_code_returned_401%27%0A++++++++WHEN+metadata.execution_id+IS+NULL%0A+++++++++AND+%28new_auth.prefix+IS+NOT+NULL+OR+legacy_payload.algorithm+<>+%27HS256%27%29%0A++++++++++++THEN+%27incompatible_keys%27%0A++++++++WHEN+metadata.execution_id+IS+NULL%0A++++++++AND+%0A++++++++++++%28%0A++++++++++++++++%28legacy_auth_data.invalid+IS+NOT+NULL+OR+new_auth.error+IS+NOT+NULL%29%0A++++++++++++++++++++OR%0A++++++++++++++++legacy_payload.algorithm+%3D+%27HS256%27%0A++++++++++++%29%0A++++++++++++THEN+%27invalid_key%27%0A++++++++WHEN+metadata.execution_id+IS+NULL%0A+++++++++AND+legacy_auth_data+++++++IS+NULL%0A+++++++++AND+new_auth.prefix+IS+NULL%0A++++++++++++THEN+%27missing_auth_header%27%0A++++END+AS+cause%0A%0AFROM+function_edge_logs%0A%0A++++--+unnesting+metadata%0A++++CROSS+JOIN+UNNEST%28metadata%29++++++++++AS+metadata%0A++++CROSS+JOIN+UNNEST%28metadata.request%29++AS+req%0A++++CROSS+JOIN+UNNEST%28metadata.response%29+AS+res%0A++++--+unnesting+auth+details%0A++++LEFT+JOIN+UNNEST%28req.sb%29++++++++++++++++++++AS+sb%0A++++LEFT+JOIN+UNNEST%28sb.apikey%29+++++++++++++++++AS+apikey%0A++++LEFT+JOIN+UNNEST%28apikey.authorization%29++++++AS+new_auth%0A++++LEFT+JOIN+UNNEST%28sb.jwt%29++++++++++++++++++++AS+legacy_jwt%0A++++LEFT+JOIN+UNNEST%28legacy_jwt.authorization%29++AS+legacy_auth_data%0A++++LEFT+JOIN+UNNEST%28legacy_auth_data.payload%29++AS+legacy_payload%0A%0AWHERE+res.status_code+%3D+401%0AORDER+BY+timestamp+DESC%0ALIMIT+200) to classify recent 401s:
|
||||
|
||||
```sql
|
||||
select
|
||||
cast(timestamp as datetime) as timestamp,
|
||||
req.pathname as function_name,
|
||||
case
|
||||
when metadata.execution_id is not null then 'your_code_returned_401'
|
||||
when metadata.execution_id is null
|
||||
and (
|
||||
new_auth.prefix is not null
|
||||
or legacy_payload.algorithm != 'HS256'
|
||||
) then 'incompatible_keys'
|
||||
when metadata.execution_id is null
|
||||
and (
|
||||
(legacy_auth_data.invalid is not null or new_auth.error is not null)
|
||||
or legacy_payload.algorithm = 'HS256'
|
||||
) then 'invalid_key'
|
||||
when metadata.execution_id is null
|
||||
and legacy_auth_data is null
|
||||
and new_auth.prefix is null then 'missing_auth_header'
|
||||
end as cause
|
||||
from
|
||||
function_edge_logs
|
||||
-- unnesting metadata
|
||||
cross join UNNEST(metadata) as metadata
|
||||
cross join UNNEST(metadata.request) as req
|
||||
cross join UNNEST(metadata.response) as res
|
||||
-- unnesting auth details
|
||||
left join UNNEST(req.sb) as sb
|
||||
left join UNNEST(sb.apikey) as apikey
|
||||
left join UNNEST(apikey.authorization) as new_auth
|
||||
left join UNNEST(sb.jwt) as legacy_jwt
|
||||
left join UNNEST(legacy_jwt.authorization) as legacy_auth_data
|
||||
left join UNNEST(legacy_auth_data.payload) as legacy_payload
|
||||
where res.status_code = 401
|
||||
order by timestamp desc
|
||||
limit 50;
|
||||
```
|
||||
|
||||
Depending on the output, you can use this table to find the appropriate debugging section:
|
||||
|
||||
| Value | Go to |
|
||||
| ------------------------ | ------------------------------------------------------------- |
|
||||
| `your_code_returned_401` | [Your function returned a 401](#your-function-returned-a-401) |
|
||||
| `incompatible_keys` | [Incompatible key format](#incompatible-key-format) |
|
||||
| `invalid_key` | [Invalid key](#invalid-key) |
|
||||
| `missing_auth_header` | [Missing Authorization header](#missing-authorization-header) |
|
||||
|
||||
---
|
||||
|
||||
## Your function returned a 401
|
||||
|
||||
Your function ran, and somewhere in your code, its logic returned a 401.
|
||||
|
||||
**Example:**
|
||||
|
||||
```js
|
||||
return new Response(JSON.stringify(data), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 401, // app logic returning a 401
|
||||
status: 401, // <-- you set this
|
||||
})
|
||||
```
|
||||
|
||||
A 401 status code alone doesn't confirm a JWT check failure by itself. The log must also lack an `execution_id`.
|
||||
**How to fix:**
|
||||
|
||||

|
||||
1. Search your function code for `401`. Look for explicit status codes on `Response` objects.
|
||||
2. Trace the condition that triggered it. If you're interacting with a third-party API in your code, that service may be returning 401 that you're forwarding in the response object.
|
||||
3. Add logging before the return so future occurrences leave a trace:
|
||||
|
||||
Instead of manually reviewing the logs, you can run the below query in the [log explorer](</dashboard/project/_/logs/explorer?q=SELECT+DISTINCT%0A++++req.pathname+AS+function_name,%0A++++res.status_code%0A++FROM+function_edge_logs%0A++CROSS+JOIN+UNNEST(metadata)+AS+metadata+%0A++CROSS+JOIN+UNNEST(metadata.request)+AS+req+%0A++CROSS+JOIN+UNNEST(metadata.response)+AS+res+%0AWHERE+status_code+=+401+AND+metadata.execution_id+IS+NULL%0ALIMIT+10;&its=&ite=%3E>) to get a list of functions that were impacted by the check:
|
||||
|
||||
```sql
|
||||
select distinct
|
||||
req.pathname as function_name,
|
||||
res.status_code
|
||||
from
|
||||
function_edge_logs
|
||||
cross join UNNEST(metadata) as metadata
|
||||
cross join UNNEST(metadata.request) as req
|
||||
cross join UNNEST(metadata.response) as res
|
||||
where status_code = 401 and metadata.execution_id is null
|
||||
limit 10;
|
||||
```js
|
||||
console.error('Returning 401 - reason:', reason)
|
||||
```
|
||||
|
||||
### Step 2: Disabling the JWT check
|
||||
See: [Error handling in Edge Functions](/docs/guides/functions/error-handling)
|
||||
|
||||
The JWT check provides minimal security benefits, so we now recommend handling authentication through app logic instead. See the [Edge Function Auth Doc](/docs/guides/functions/auth) for details.
|
||||
---
|
||||
|
||||
If you've migrated to [asymmetric keys (publishable/secret)](/dashboard/project/_/settings/api-keys) or no longer need the JWT check, you can disable it using one of the three options below:
|
||||
## Built-in JWT check failures
|
||||
|
||||
Supabase Edge Functions have a legacy auth verification check that runs before your code. When it fails, your function never executes, and you get a 401 with `"Invalid JWT"` or `"Missing authorization header"` directly from the platform.
|
||||
|
||||
<Admonition type="deprecation">
|
||||
|
||||
Supabase now recommends turning off this built-in check and managing authentication directly in your function code, giving you more control over access. See [Securing Edge Functions](/docs/guides/functions/auth).
|
||||
|
||||
</Admonition>
|
||||
|
||||
The subsections below cover specific failure modes.
|
||||
|
||||
### Incompatible key format
|
||||
|
||||
Your project uses the [new asymmetric keys](/blog/jwt-signing-keys) for authentication. However, the [legacy auth verification check](/docs/guides/functions/development-tips#skipping-authorization-checks) only understands the legacy format.
|
||||
|
||||
**Fix:** Disable the built-in JWT check using one of the below methods and optionally [handle auth in your function code](/docs/guides/functions/auth)
|
||||
|
||||
<Accordion
|
||||
type="default"
|
||||
@@ -99,51 +145,49 @@ If you've migrated to [asymmetric keys (publishable/secret)](/dashboard/project/
|
||||
>
|
||||
<div className="border-b mt-3 pb-3">
|
||||
<AccordionItem
|
||||
header="In the Dashboard"
|
||||
header="Method A: Dashboard"
|
||||
id="item-1"
|
||||
>
|
||||
In the [Function Dashboard](/dashboard/project/_/functions/), under the offending function's detail tab, you can toggle off the check:
|
||||
|
||||

|
||||
In the [Functions Dashboard](/dashboard/project/_/functions/), open the affected function's `detail tab` and toggle off JWT verification.
|
||||
|
||||

|
||||
|
||||
</AccordionItem>
|
||||
|
||||
</div>
|
||||
<div className="border-b mt-3 pb-3">
|
||||
<AccordionItem
|
||||
header="Method B: Supabase CLI"
|
||||
id="item-2"
|
||||
>
|
||||
|
||||
Redeploy the edge function from the [Supabase CLI](/docs/guides/functions/quickstart) with the `--no-verify-jwt` flag
|
||||
|
||||
```sh
|
||||
supabase functions deploy YOUR_FUNCTION_NAME --no-verify-jwt
|
||||
```
|
||||
|
||||
</AccordionItem>
|
||||
|
||||
</div>
|
||||
<div className="border-b mt-3 pb-3">
|
||||
<AccordionItem
|
||||
header="With the Management API"
|
||||
id="item-2"
|
||||
>
|
||||
|
||||
1. Generate a secret token in the [Account Preferences Dashboard](/dashboard/account/tokens)
|
||||
2. Get your project ID from the [General Settings](/dashboard/project/_/settings/general)
|
||||
3. Substitute in the `PROJECT_ID`, `FUNCTION_NAME`, and `SECRET_TOKEN` in below [management endpoint](/docs/reference/api/v1-update-a-function). Then execute the request from a terminal environment
|
||||
|
||||
```sh
|
||||
# remember to substitute in your PROJECT_ID, FUNCTION_NAME, and SECRET_TOKEN
|
||||
|
||||
curl 'https://api.supabase.com/v1/projects/PROJECT_ID/functions/FUNCTION_NAME?verify_jwt=false' \
|
||||
--request PATCH \
|
||||
--header 'Content-Type: application/vnd.denoland.eszip' \
|
||||
--header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
|
||||
--data '""'
|
||||
|
||||
````
|
||||
</AccordionItem>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="border-b mt-3 pb-3">
|
||||
<AccordionItem
|
||||
header="With the Supabase CLI"
|
||||
header="Method C: Management API"
|
||||
id="item-3"
|
||||
>
|
||||
Disable the legacy auth check with the [Supabase Management API](/docs/reference/api/introduction):
|
||||
|
||||
With the [Supabase CLI](/docs/guides/local-development/cli/getting-started), you can redeploy your edge function without the JWT check:
|
||||
1. Generate a token at [Account Preferences](/dashboard/account/tokens).
|
||||
2. Get your project ID from [General Settings](/dashboard/project/_/settings/general).
|
||||
3. Run:
|
||||
|
||||
```sh
|
||||
supabase functions deploy hello-world --no-verify-jwt
|
||||
curl 'https://api.supabase.com/v1/projects/PROJECT_ID/functions/FUNCTION_NAME' \
|
||||
--request PATCH \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
|
||||
--data '{"verify_jwt": false}'
|
||||
```
|
||||
|
||||
</AccordionItem>
|
||||
@@ -151,12 +195,43 @@ supabase functions deploy hello-world --no-verify-jwt
|
||||
</div>
|
||||
</Accordion>
|
||||
|
||||
If you would like to continue using the JWT check, make sure that your [Supabase Client](/docs/guides/api/rest/client-libs) only uses your [legacy keys](/dashboard/project/_/settings/api-keys/legacy).
|
||||
### Invalid key
|
||||
|
||||
The built-in check is enabled and the key you sent doesn't match your project's keys.
|
||||
|
||||
**Fix (recommended):** Disable the built-in check using the steps in [Incompatible key format](#incompatible-key-format).
|
||||
|
||||
**Fix (alternative):** If you want to keep the built-in check, ensure you're sending a valid key. Use one of your [legacy API keys](/dashboard/project/_/settings/api-keys/legacy) with the [Supabase client library](/docs/guides/api/rest/client-libs) when making your request.
|
||||
|
||||
```js
|
||||
const supabase = createClient('https://xyzcompany.supabase.co', 'anon-key-or-service_role-key')
|
||||
```
|
||||
|
||||
### Missing authorization header
|
||||
|
||||
The built-in check is enabled but your request has no `Authorization` header at all.
|
||||
|
||||
If you're using a [Supabase client library](/docs/guides/api/rest/client-libs), the header is added automatically. If you're calling the function from an external client (cURL, fetch, etc.), you need to supply it:
|
||||
|
||||
```sh
|
||||
curl -L -X POST 'https://PROJECT_REF.supabase.co/functions/v1/hello-world' \
|
||||
-H 'Authorization: Bearer YOUR_ANON_OR_SERVICE_ROLE_KEY' \
|
||||
--data '{"name":"Functions"}'
|
||||
```
|
||||
|
||||
Alternatively, you can disable the built-in check entirely (see [Incompatible key format](#incompatible-key-format)).
|
||||
|
||||
---
|
||||
|
||||
## Additional resources
|
||||
|
||||
- [Securing Edge Functions](/docs/guides/functions/auth)
|
||||
- [Debugging Edge Functions](/docs/guides/functions/logging)
|
||||
- [Quickstart Deployment: Dashboard](/docs/guides/functions/quickstart-dashboard)
|
||||
- [Quickstart Deployment: CLI](/docs/guides/functions/quickstart)
|
||||
````
|
||||
- [Securing Edge Functions with Auth](/docs/guides/functions/auth)
|
||||
- [Logging Edge Function Requests](/docs/guides/functions/logging)
|
||||
- [Error Handling Edge Functions](/docs/guides/functions/error-handling)
|
||||
- [Quickstart: Dashboard deployment](/docs/guides/functions/quickstart-dashboard)
|
||||
- [Quickstart: CLI deployment](/docs/guides/functions/quickstart)
|
||||
|
||||
## Still stuck?
|
||||
|
||||
- Check the [Discord](https://discord.com/channels/839993398554656828/1006358244786196510), [Supabase GitHub Discussions](https://github.com/orgs/supabase/discussions), and [Reddit page](https://www.reddit.com/r/Supabase/) for similar reports that can help with debugging
|
||||
- Open a [support ticket](/dashboard/support/new) for your project if the problem persists and you believe it is a platform issue
|
||||
Reference in new issue
Block a user