API authorization scopes are how you grant specific, granular permissions to your API clients. Instead of just saying "this client can access the API," scopes let you say "this client can read user profiles, but not update them."
Let’s see this in action with a simple Node.js Express API and a client making requests.
First, our Express API needs to be configured to understand scopes. We’ll use the express-oauth2-jwt-bearer middleware.
const express = require('express');
const jwt = require('express-oauth2-jwt-bearer');
const app = express();
const jwtCheck = jwt.requestJwt({
audience: 'https://my-api.example.com',
issuerBaseURL: 'https://your-auth0-domain.auth0.com/',
tokenSigningAlg: 'RS256'
});
// Apply the JWT check middleware to all routes
app.use(jwtCheck);
// Route that requires 'read:users' scope
app.get('/users', (req, res) => {
// The middleware will automatically check for the scope
// If the token doesn't have the scope, it will return a 403 Forbidden
res.json([{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]);
});
// Route that requires 'write:users' scope
app.post('/users', (req, res) => {
// Again, scope check is handled by middleware
res.status(201).json({ message: 'User created' });
});
const port = 3001;
app.listen(port, () => {
console.log(`API listening on port ${port}`);
});
In Auth0, you define these scopes under your API settings. Go to "APIs," select your API, then navigate to the "Scopes" tab. You’d add entries like:
- Name:
read:users- Description: Allows reading user information.
- Name:
write:users- Description: Allows creating and updating user information.
Now, when a client requests an access token from Auth0, they can specify the scopes they need using the scope parameter in their authorization request.
For example, a client wanting to read users would construct a token request URL like this:
https://your-auth0-domain.auth0.com/oauth/token
?grant_type=client_credentials
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&scope=read:users
When Auth0 issues the access token, the scope claim within the JWT will contain the granted scopes, e.g.:
{
"iss": "https://your-auth0-domain.auth0.com/",
"sub": "...",
"aud": "https://my-api.example.com",
"scope": "read:users", // <-- This is what the API checks
"iat": 1678886400,
"exp": 1678890000
}
Our express-oauth2-jwt-bearer middleware, configured with the correct audience and issuerBaseURL, automatically inspects this scope claim against the requested resource. If the token’s scope doesn’t include read:users for the /users endpoint, the middleware intercepts the request and returns a 403 Forbidden status.
The core problem scopes solve is the principle of least privilege. Without scopes, a client with an API token could do anything the API allows. With scopes, you can precisely define what actions a given client is authorized to perform, significantly reducing the blast radius of a compromised client credential. You can also dynamically grant different scopes based on user roles or other attributes during the authorization flow.
A common misconception is that scopes are checked by Auth0 during token issuance. While Auth0 enforces policies on which scopes can be requested (e.g., based on client type or user consent), the actual authorization decision for a specific API endpoint using a given token is made by the API itself, by inspecting the scope claim in the JWT.
The next step is often implementing refresh tokens or exploring different grant types like the Authorization Code flow for user-interactive applications.