diff --git a/content/docs/data-api/access-control.md b/content/docs/data-api/access-control.md index d7c9d941043..1a3ed0e8f49 100644 --- a/content/docs/data-api/access-control.md +++ b/content/docs/data-api/access-control.md @@ -5,9 +5,9 @@ subtitle: Understand how the Data API authenticates requests and enforces databa summary: >- The Neon Data API has no separate permission system. All access control is delegated to PostgreSQL through two layers: GRANT-based table privileges and - Row-Level Security (RLS) policies. The database role is selected from the - incoming JWT: `authenticated` for valid tokens, `anonymous` for unauthenticated - requests, or a custom role from the JWT `role` claim. Use this page to + Row-Level Security (RLS) policies. The database role always comes from the JWT + `role` claim: `authenticated` when the token carries `role: authenticated`, + `anonymous` when it has no role claim, or any other role the claim names. Use this page to configure GRANT statements, enable RLS, and write per-row policies with `auth.user_id()`, which extracts the `sub` claim from the request JWT. enableTableOfContents: true @@ -33,20 +33,20 @@ Securing your data involves two layers: ## API Roles -When the Data API receives an HTTP request, it switches to a specific PostgreSQL role before executing the query. The role chosen depends on the JWT sent in the `Authorization` header. +When the Data API receives an HTTP request, it switches to a specific PostgreSQL role before executing the query. That role is read from the `role` claim in the request's JWT: the API runs `SET ROLE` to the role the claim names. If the token has no `role` claim, the request falls back to the `anonymous` role. A valid signature on its own does not grant the `authenticated` role; the token must actually carry `"role": "authenticated"`. ### 1. The `authenticated` role -**Used for:** Requests with a valid JWT token. +**Used for:** Tokens that carry `"role": "authenticated"`. -When a client sends a valid Bearer token, the API switches to the `authenticated` role. This is the primary role for your application users. +When a token's `role` claim is `authenticated`, the API switches to the `authenticated` role. This is the primary role for your application users, and it's the role the default `GRANT`s target. [Neon Auth](/docs/auth/overview) issues this claim automatically; a custom provider must be configured to include it (see [Required JWT claims](/docs/data-api/custom-authentication-providers#required-jwt-claims)). - The JWT token identifies _who_ the user is (via the `sub` claim). - The `authenticated` role defines _what_ the application is allowed to touch. ### 2. The `anonymous` role -**Used for:** Requests from unauthenticated users. +**Used for:** Requests whose token carries no `role` claim, including unauthenticated or guest access. Anonymous access still uses a JWT, but no user sign-in is required. How you obtain that token depends on your auth setup: @@ -77,7 +77,7 @@ The single-URL form shown above, `createClient(url)`, requires a version of `@ne ### 3. Custom roles -The API determines the role based on the `role` claim in the JWT. If you issue your own tokens with a custom role claim (for example, `"role": "admin"`), the API will attempt to switch to a Postgres role named `admin`. You must ensure this role exists in your database and has the correct permissions. +`authenticated` and `anonymous` are the two roles Neon sets up for you, but the `role` claim can name any Postgres role. This is the general rule behind both roles above: the API always runs `SET ROLE` to whatever the claim says. If you issue tokens with a different role (for example, `"role": "admin"`), the API switches to a role named `admin`. That role must exist in your database and hold the right privileges, or the request fails: a claim naming a role that doesn't exist returns a "role does not exist" error, and a role that exists but lacks the table grant returns `42501 permission denied`. The sections below explain how to configure these roles. diff --git a/content/docs/data-api/custom-authentication-providers.md b/content/docs/data-api/custom-authentication-providers.md index 92170acd52d..0051f56249f 100644 --- a/content/docs/data-api/custom-authentication-providers.md +++ b/content/docs/data-api/custom-authentication-providers.md @@ -70,6 +70,15 @@ The key steps: 3. Neon validates the tokens using your provider's [JWKS (JSON Web Key Set)](https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-key-sets) URL. 4. The Data API enforces [Row-Level Security policies](/docs/guides/row-level-security) based on the user identity in the JWT. +## Required JWT claims + +Neon checks the token's signature against your JWKS URL, but a valid signature is only half the story. The database role the query runs as comes from the token's `role` claim, not from the fact that the token passed validation. This trips up custom providers most often, because they don't add a `role` claim unless you configure them to. + +- **`role`**: The Data API reads this claim and switches to the Postgres role it names. To use the default `authenticated` role (the role the [default `GRANT`s](/docs/data-api/access-control#layer-1-table-privileges) target), your provider must issue `"role": "authenticated"`. The role must exist in your database and hold the privileges the request needs. [Neon Auth](/docs/auth/overview) adds this claim for you; a custom provider does not. +- **`sub`**: Identifies the user for Row-Level Security. See [JWT token missing sub claim](/docs/data-api/troubleshooting#jwt-token-missing-sub-claim). + +If a token has no `role` claim, the request runs as the fallback `anonymous` role instead. `anonymous` can enter the schema but has no table grants by default, so the query returns `42501 permission denied for table ...` even when `authenticated` has access. For how these roles are chosen and granted, see [Access control & security](/docs/data-api/access-control#api-roles). + ## Add your authentication provider You can configure your authentication provider when you first enable the Data API, or add it later from the **Settings** tab. Select **Other Provider** from the dropdown and enter: diff --git a/content/docs/data-api/troubleshooting.md b/content/docs/data-api/troubleshooting.md index 8bee76b5fea..5204cd165c5 100644 --- a/content/docs/data-api/troubleshooting.md +++ b/content/docs/data-api/troubleshooting.md @@ -106,11 +106,12 @@ For step-by-step instructions, see [Testing with Managed Better Auth](/docs/data ### Why this happens -The `authenticated` role doesn't have GRANT permissions on the table. This commonly occurs when: +The role the request runs as doesn't have GRANT permissions on the table. This commonly occurs when: - The table was created before the Data API was enabled - The table was created after enabling the Data API, but default privileges weren't applied - You disabled the "Grant public schema access" option when enabling the Data API +- You're using a custom authentication provider and the token has no `role` claim (or a role claim that doesn't name `authenticated`). The request then runs as the `anonymous` role, which has no table grants by default, so it can't read the table even though `authenticated` can. ### Fix @@ -130,6 +131,8 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, UPDATE, INSERT, DELETE ON TABLES TO authenticated; ``` +If you use a custom authentication provider, also confirm the token carries a `role` claim that names a granted role. To use the `authenticated` role, issue `"role": "authenticated"`. If your provider puts the role under a different claim, point the Data API's JWT role claim key at that claim in the Data API settings. See [Required JWT claims](/docs/data-api/custom-authentication-providers#required-jwt-claims). + ## I can see all rows in my table If authenticated users can see all rows in a table regardless of ownership, Row-Level Security (RLS) is likely disabled on that table.