developers

A Developer's Guide to API Access Policies in Auth0

Explore how to properly configure Auth0 API Access Policies and client grants to draw a hard line between strict M2M security and user-delegated access.

We understand that securing API access is fundamental to building resilient applications. Auth0's API Access Policies for Applications provide a powerful mechanism to control precisely how your applications interact with the APIs registered within Auth0. These policies dictate whether an application can successfully obtain an access token for an API's resources, offering granular control over your security posture. The core idea is to define access policies for each API, which can be configured either through the Auth0 Dashboard or programmatically via the Auth0 Management API.

The FinFlow Scenario

Picture FinFlow, a fintech company with two APIs and two very different applications. One API moves money; the other serves public market data. One application is a browser app used by real people; the other is a backend service that never sees a human.

FinFlow's question is simple to ask and surprisingly easy to get wrong: How do you let the right application reach the right API and nothing else?

That is exactly what API Access Policies for Applications are built for. Policies let you control, per API, whether an application can obtain an access token at all. They let you draw a hard line between machine-to-machine traffic and access on behalf of a logged-in user.

We will use one concrete scenario throughout.

FinFlow has two APIs:

API Function Sensitivity Scopes
Payments API (https://api.finflow.io/payments) Handles fund transfers and balance reads. Sensitive; every application must be approved explicitly transfer:funds, read:balance, read:payment-aggregates
Market Data API (https://api.finflow.io/market-data) Serves public market pricing Less sensitive and open to any authenticated user read:market-data

And two applications:

  1. FinFlow Web App (WEB_APP_CLIENT_ID), a browser app for end users.
  2. Risk Engine (RISK_ENGINE_CLIENT_ID), a backend machine-to-machine (M2M) service that analyzes payment patterns.

FinFlow’s goal: Let the Risk Engine call the Payments API machine-to-machine but not the Market Data API, and let the web app reach both APIs when a user logs in.

What is the difference between User Access and Client Access

Auth0 lets you set a separate policy for each type of access, because the security implications of the two are not the same.

  • Client Access: This is designed for M2M access and primarily corresponds to the Client Credentials Flow. Common examples are daemons running recurrent tasks, cron jobs, and IoT devices that communicate directly with an API.

  • User Access: Applies whenever an access token is issued on behalf of an end-user. This covers flows like Authorization Code, Device Code, and Resource Owner Password. The application is acting with the user's authorization.

A subtle but important point: A backend service that calls an API on behalf of a logged-in user is doing user-delegated access, not client access.

Keeping these separate is what lets FinFlow say, "The Risk Engine may act on its own, but only against the Payments API."

The Three Levels of Application API Access Policies

Auth0 offers three distinct policies you can apply:

  1. Allow All: When an API is configured with this policy, any application within your tenant can obtain an access token for this API, as long as there is a successful user authentication and authorization. No specific grant is required for the app. It is important to note that allow_all can only be configured for user access. We do not recommend this policy, but provide it as an option to be used with non-sensitive public APIs. This policy behaves differently for third-party applications, explained later in this post.

  2. Allow via client-grant: This is the recommended policy, aligning with the principle of least privilege. With this setting, an application can only obtain an access token if a client grant has been explicitly defined for it. This grant establishes the maximum permissions (scopes) an application can request from the API.

  3. Deny: This policy completely restricts access. Regardless of any other settings or grants, no application can obtain an access token to the API.

In our FinFlow scenario:

  • Payments API uses require_client_grant for both user and client access (only approved apps, strict control).
  • Market Data API uses allow_all for user access (any application can request it on behalf of the user) but deny_all for client access (no M2M access allowed).

Here is how Auth0 evaluates a token request against those policies. Auth0 evaluates each token request by looking up the API's policy for that access type (user or client), then deciding:

  • deny_all: Returns 403 access_denied immediately. No grant or other configuration overrides this.
  • require_client_grant: Checks for a client grant matching this app and API. If a grant exists, returns 200 with the scopes defined on the grant. If no grant exists, returns 403.
  • allow_all (user access only): Skips the grant check and returns 200 with the requested scopes.
Auth0 Evaluating Token Request Against Policies Flow

Configuring the API Application Access Policies

You can configure an API's application access policy through the Auth0 Dashboard or programmatically through the Management API.

Setting Policies and Grants with the Auth0 Dashboard

In the Dashboard, the feature spans two tabs: you set the policies on the Settings tab and manage the per-application grants on the Application Access tab.

  1. Navigate to Applications > APIs and select your API.
  2. Open the Settings tab and scroll to Application Access Policy to configure the User-Delegated Access and Client Access policies.
    The application access policy configuration on the Settings tab for an API

  3. If you choose Per-app authorization, open the Application Access tab and select Edit to authorize User-Delegated Access, Client Access, or both for individual applications.

The per-application configuration on the Application Access tab for an APIConfiguring specific scopes that can be accessed by the Application for this API

Configuring the Management API

You configure API access policy on the resource-servers endpoint using the subject_type_authorization object.

Configuring the Payments API:

PATCH /api/v2/resource-servers/{payments-api-id}
Host: YOUR_AUTH0_DOMAIN
Authorization: Bearer YOUR_MGMT_API_ACCESS_TOKEN
Content-Type: application/json

{
  "subject_type_authorization": {
    "user": {
      "policy": "require_client_grant"
    },
    "client": {
      "policy": "require_client_grant"
    }
  }
}

Configuring the Market Data API:

PATCH /api/v2/resource-servers/{market-data-api-id}
Host: YOUR_AUTH0_DOMAIN
Authorization: Bearer YOUR_MGMT_API_ACCESS_TOKEN
Content-Type: application/json

{
  "subject_type_authorization": {
    "user": {
      "policy": "allow_all"
    },
    "client": {
      "policy": "deny_all"
    }
  }
}

Creating Client Grants

When you set a policy to require_client_grant, you must explicitly grant each application access via a client grant. This tells Auth0: "This application is allowed to request tokens for this API, and here are the scopes it can use."

FinFlow needs to create two grants:

Grant 1: Risk Engine -> Payments API (M2M)

POST /api/v2/client-grants
Host: YOUR_AUTH0_DOMAIN
Authorization: Bearer YOUR_MGMT_API_ACCESS_TOKEN
Content-Type: application/json

{
  "client_id": "RISK_ENGINE_CLIENT_ID",
  "audience": "https://api.finflow.io/payments",
  "scope": ["read:payment-aggregates"],
  "subject_type": "client"
}

This says: "Risk Engine can request an M2M token for the Payments API with read:payment-aggregates scope."

Grant 2: FinFlow Web App -> Payments API (User Login)

POST /api/v2/client-grants
Host: YOUR_AUTH0_DOMAIN
Authorization: Bearer YOUR_MGMT_API_ACCESS_TOKEN
Content-Type: application/json

{
  "client_id": "WEB_APP_CLIENT_ID",
  "audience": "https://api.finflow.io/payments",
  "scope": ["transfer:funds", "read:balance"],
  "subject_type": "user"
}

This says: "FinFlow Web App can request user-scoped tokens for the Payments API with transfer:funds and read:balance scopes."

No grant needed for Market Data API since it uses allow_all for user access, any application can request it.

Getting Tokens: Policies in Action

With the policies set and the grants in place, here is what FinFlow's applications actually get at the token endpoint. Two things in the request decide the outcome: the audience picks which API's policy is evaluated, and grant_type determines whether the user or client policy applies.

For the request mechanics themselves, see the Client Credentials Flow and Authorization Code Flow docs: policies do not change how you call /oauth/token endpoint, only whether you get a token back and the resulting scopes on the token.

Application → API Flow Policy in Play Result
Risk Engine → Payments Client Credentials client: requireclientgrant + Grant 1 200 - read:payment-aggregates
Web App → Payments Authorization Code user: requireclientgrant + Grant 2 200 - transfer:funds read:balance
Web App → Market Data Authorization Code user: allow_all 200 - read:market-data
Risk Engine → Market Data Client Credentials client: deny_all 403 access_denied

A Different Rule for Third-Party Applications

Everything so far quietly assumes FinFlow owns every application: the web app and the Risk Engine are both first-party apps living in FinFlow's own tenant. For those, the API's access policy is the whole story: set allow_all and a logged-in user gets a token, set require_client_grant and you add a grant.

Now FinFlow wants to open its Payments API to outside partners: fintech vendors that pull account balances to reconcile ledgers or score transactions, each calling FinFlow M2M and each registered as a third-party application. In this case, the rules are changed on purpose. Outside apps are untrusted by default. One rule overrides everything else:

A third-party application always requires an explicit client grant even when the API's policy is allow_all. There is no "open by default" for outside apps.

This rule is how we maintain secure-by-default policies: an allow_all policy is a statement about your own applications, not about the entire internet. The same policy that waves any first-party app through still rejects a third-party app that has no grant:

{
  "error": "access_denied",
  "error_description": "Client "tpc_PARTNER_CLIENT_ID" is not authorized to access resource server "https://api.finflow.io/payments". You need to create a "client-grant" associated to this API."
}

Auth0 gives third-party clients a tpc_ client-ID prefix making them easy to spot.

API access policy First-party application Third-party application
Allow All (allow_all) Access granted Requires client grant
Per-app authorization (require_client_grant) Requires client grant Requires client grant
No apps allowed (deny_all) Access denied Access denied

Handling Access for Newly Registered External Applications

The per-app grants FinFlow wrote above for the Risk Engine and the web app work when you know each application up front. But partner ecosystems do not work that way: new third-party apps register over time, and with Dynamic Client Registration, they can self-register with no admin in the loop. You cannot pre-write a grant for an app that does not exist yet.

This is what the default third-party client grant solves. Instead of a grant tied to one client_id, you create one grant keyed to default_for: "third_party_clients". It applies automatically to every third-party application in the tenant, including ones that register tomorrow. You open a deliberate, least-privilege baseline to the whole partner population without ever flipping the API to "trust everyone".

Creating a default third-party client grant

Like the API policies themselves, you can set the default third-party client grant in the Auth0 Dashboard or programmatically through the Management API.

Using the Auth0 Dashboard

On the API's settings, the Default Permissions for third-party applications panel sets the baseline every third-party app inherits. As the panel itself notes, "Third-party applications always require permissions to be explicitly selected" - this is that explicit selection, applied once to the whole group. It mirrors the two access types you configured earlier:

  • User-delegated Access: the default when a third-party app acts on behalf of a logged-in user.
  • Client Access: the default when a third-party app acts on its own behalf (M2M), which is FinFlow's partner case.

Each selector offers:

  • Unauthorized: no permissions allowed; third-party apps get no default for that access type and must be granted per-app.
  • All: includes all existing and future permissions on the API.
  • A specific subset of the API's permissions: the least-privilege choice.

For FinFlow's read-only partner baseline, set Client Access to just read:balance and leave User-delegated Access on Unauthorized. The panel warns that "Changes apply to both existing and new applications" — exactly the point: partners that register tomorrow inherit it too. Per-app overrides live on the Application Access tab, just as they do for first-party apps.

Default permissions for third-party applications

Using the Management API

It is a normal POST /client-grants, with one swap: drop client_id and add default_for. The two are mutually exclusive. A grant targets either one specific app or the whole third-party group, never both. (The Dashboard "permissions" above is the scope array below.)

# A baseline grant every third-party app inherits: read-only on the Payments API.
curl --request POST 
  --url 'https://YOUR_AUTH0_DOMAIN/api/v2/client-grants' 
  --header 'Authorization: Bearer YOUR_MGMT_API_ACCESS_TOKEN' 
  --header 'Content-Type: application/json' 
  --data '{
    "default_for": "third_party_clients",
    "audience": "https://api.finflow.io/payments",
    "scope": ["read:balance"],
    "subject_type": "client"
  }'
Field What it does
default_for Set to third_party_clients to apply this grant to all third-party apps. Mutually exclusive with client_id.
audience The API identifier the grant covers.
scope The baseline scopes every third-party app may request — the Dashboard's selected permissions. Keep it least-privilege. (To match the Dashboard's All, omit scope and send "allow_all_scopes": true instead.)
subject_type client for M2M (Client Credentials), user for user-delegated flows — same rule as any grant, and the two access selectors in the Dashboard. Set separate defaults for each.

With that one grant in place, the same third-party app that was rejected above now gets a token. No per-app configuration needed:

{
  "scope": "read:balance",
  "token_type": "Bearer",
  "expires_in": 86400
}

Per-App Grants Still Take Precedence

A default grant is a floor, not a ceiling. When a specific partner needs more (or less), add a per-app grant with their client_id. It takes precedence over the default for that app. Say FinFlow contracts one partner, a settlement provider, to also initiate transfers. Their per-app grant adds transfer:funds and overrides the read-only default. A deliberate elevation for one vetted partner, while everyone else stays read-only:

# The contracted settlement partner may also write. Their per-app grant overrides the default.
curl --request POST 
  --url 'https://YOUR_AUTH0_DOMAIN/api/v2/client-grants' 
  --header 'Authorization: Bearer YOUR_MGMT_API_ACCESS_TOKEN' 
  --header 'Content-Type: application/json' 
  --data '{
    "client_id": "SETTLEMENT_PARTNER_CLIENT_ID",
    "audience": "https://api.finflow.io/payments",
    "scope": ["read:balance", "transfer:funds"],
    "subject_type": "client"
  }'

So the model is two-tier: a default grant sets the baseline for the whole third-party population, and per-app grants tune individual partners up or down. Every other app keeps inheriting the default.

A Note on System APIs

For Auth0's own system APIs, such as the My Account API, policies come with pre-configured defaults that reflect each API's intended access model. Depending on the API, you may be able to adjust the user or client access policy within allowed boundaries, but some policies are fixed. What you always control is the scopes you enable per application via client grants: grant each app only the scopes it actually needs.

Default third-party client grants do not reach these APIs either: a default_for grant can't be created for an Auth0 system API at all. Manage access to system APIs with explicit per-application grants instead.

Wrapping Up

By pairing access policies with client grants, you decide exactly who can reach each API:

  • Sensitive APIs use Per-app authorization (require_client_grant) so only approved applications proceed.
  • Public APIs use All apps allowed (allow_all) for frictionless user access of the first-party apps you own.
  • No apps allowed (deny_all) shuts a door completely, as it did for the Risk Engine against the Market Data API.
  • Third-party applications always need a grant, even under allow_all, that's secure-by-default for outside apps. A default third-party client grant (default_for: "third_party_clients", no client_id) opens a least-privilege baseline to your whole partner population at once (the answer for partner ecosystems and Dynamic Client Registration) while per-app grants still take precedence when one partner needs more or less.

This tiered approach scales from simple to complex architectures without compromising security. To go deeper read: