From 700eeb075c7bd4139958b3b955cb29f22c9c6caa Mon Sep 17 00:00:00 2001 From: TheOtherBrian1 <91111415+TheOtherBrian1@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:32:03 -0400 Subject: [PATCH] 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 --- .../edge-function-401-error-response.mdx | 265 +++++++++++------- 1 file changed, 170 insertions(+), 95 deletions(-) diff --git a/apps/docs/content/troubleshooting/edge-function-401-error-response.mdx b/apps/docs/content/troubleshooting/edge-function-401-error-response.mdx index e65029fe643..83a8d58c76a 100644 --- a/apps/docs/content/troubleshooting/edge-function-401-error-response.mdx +++ b/apps/docs/content/troubleshooting/edge-function-401-error-response.mdx @@ -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 - - - -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. - - - -### 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:** -![image](/docs/img/troubleshooting/401_in_the_logs.png) +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]() 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. + + + +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). + + + +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)
- In the [Function Dashboard](/dashboard/project/_/functions/), under the offending function's detail tab, you can toggle off the check: - ![image](/docs/img/troubleshooting/401_edge_functions_toggle_off_JWT_check.png) +In the [Functions Dashboard](/dashboard/project/_/functions/), open the affected function's `detail tab` and toggle off JWT verification. +![image](/docs/img/troubleshooting/401_edge_functions_toggle_off_JWT_check.png) + + + +
+
+ + +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 +```
- - 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 '""' - -```` - - -
- -
- +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}' ``` @@ -151,12 +195,43 @@ supabase functions deploy hello-world --no-verify-jwt
-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