How to add granular permissions to your API using OAuth scopes
Learn how to enhance your API's security with granular permissions using OAuth scopes, allowing you to control access precisely and protect user data effectively. This guide covers the basics of OAuth scopes, implementing fine-grained permissions, and best practices for secure API management.
Giving users precise control over what they share with your application is crucial. One powerful way to achieve this is by using OAuth scopes to implement granular permissions. By defining specific access levels for different resources, you can ensure that your API only accesses the data it needs, reducing risks and building user trust.
In this article, we’ll explore what OAuth scopes are, how you can use them to add granular permissions to your API, what to do when you need more fine-grained permissions, best practices, and common pitfalls.
What are OAuth scopes?
OAuth scopes are essentially permission levels that define the extent of access granted to a third-party application when it interacts with an API.
When a user authorizes an application, they’re presented with a list of scopes that specify what actions the application can perform on their behalf. These permissions can be broad (like read/write access to all data) or very specific (like access to only one specific part of the user’s data). When the authorization server generates an access token, it contains the scopes approved by the user. When the API receives the token, it checks if it includes the appropriate scopes for the task requested (e.g., if the API call was to delete a photo, then the access token must contain the scope photos:delete).
Another way to think about tokens and scopes is like access control cards. They let employees into a building, but not all cards are the same; some can let you access the executive floor while others don’t. The scopes are the equivalent of the information coded in each access control card that says what it’s allowed to do.
The primary reason for implementing granular permissions is to give users better control over their data. With OAuth scopes, users can limit the level of access a third-party application has to their information, reducing the risk of over-exposure.
Example of OAuth scopes
Let's say you’re building a service like a photo-sharing application, and your API allows users to upload, view, and delete their images. Here are some potential OAuth scopes you could define:
photos.read
: Allows the app to view the user’s photos.photos.upload
: Allows the app to upload new photos.photos.delete
: Allows the app to delete photos.
By using these scopes, you can offer users the ability to restrict the permissions of third-party applications according to their specific needs.
Another example is when you allow a third-party application access to your Google Calendar and you grant the privilege to create events on your behalf. You don’t access other privileges though, like the ability to delete existing events or list your contacts. The access token that would be issued in this scenario would include a scope like events:create but wouldn’t include events:delete.
Scopes are not the same as access control
It’s important to remember that scopes are not the same as the internal permissions system of an API.
OAuth scopes determine what parts of the API the app can access based on the user’s consent, but the scope is granted in the context of the user’s permissions. The API's internal permissions system controls what the user can do directly with the API based on their role or profile within the system.
For example, if you have a user in the “guest” group and the application requests the “admin” scope, the OAuth server will not create an access token with that scope because that user is not allowed to use it themselves.
In other words, OAuth scopes are part of the authorization flow to request specific permissions from a user, while the internal API permissions system defines what the user is actually allowed to do on the API. The two systems work together but address different aspects: scopes govern application access and the internal system manages user-level permissions.
How to add granular permissions to your API
To implement granular permissions via OAuth scopes in your API, you need to follow a few key steps. Let’s break them down.
1. Define scopes
Start by identifying the different types of access that your API supports. Consider the different resources your API handles and how you want to control access to those resources. For example, if you’re building a social media platform, your scopes might look like this:
user.read
: Read user profile information.user.update
: Update user profile information.post.create
: Create new posts.post.delete
: Delete posts.
Define your scopes based on the logical divisions within your application. The more specific your scopes, the more control you provide to the user. However, don’t go into too much detail here; balance is key. When granting consent, the user will be presented with a list of scope, and if the list is too long, what’s going to happen is a T&C situation (i.e., the user will just grant all permissions in order to get the app to work).
A good place to start is to define scopes by splitting between read and write. Like in the examples we saw, apps that need to create content on the behalf of the user (posts:create
) are different than the ones that just need to consume it (posts:read
).
Remember that the OAuth 2.0 specification specifically permits the authorization server to issue an access token with a more limited scope than what the application requests. Not many use this feature, but it is there and it can allow a user to choose which scopes to grant and which not.
2. Request scopes during authorization
When your application redirects the user to the authorization server, include the requested scopes as part of the authorization request. In OAuth 2.0, this is typically done using the scope parameter.
!!To learn the basic of OAuth see The complete guide to OAuth 2.0.!!
Here’s an example of an OAuth 2.0 authorization URL with specific scopes:
In this case, the application is requesting permission to read the user’s profile and create posts.
3. Check scopes in your API
Once the user authorizes the app and the OAuth provider returns an access token, your API needs to check the scopes included in the token to determine what actions are allowed.
Here’s an example of how you might verify the token and check for the required scopes in your API (using a pseudo-code example):
If the access token contains the correct scope, the API will allow the action. Otherwise, it will return an error (e.g., HTTP 403 Forbidden).
Implementing fine-grained permissions from scratch
If you want to implement more fine-grained permissions (e.g., ensure a user has the right to delete specific photos) this involves a level of fine-grained access control that typically goes beyond OAuth scopes alone. While scopes are useful for granting high-level permissions (e.g., read/write access to photos), Fine-Grained Access Control (FGAC) is the appropriate technique to implement more specific access control—like ensuring a user has the right to delete a particular photo.
Let’s see how this could be implemented.
1. Define scopes
First, you would define OAuth scopes that describe high-level access rights. For example, consider the following scopes:
photos.read
: View photos.photos.upload
: Upload photos.photos.delete
: Delete photos.
At this stage, the photos.delete scope grants permission to delete photos, but it doesn't specify which photos can be deleted.
2. Define the data model
Consider a basic data model where each photo has metadata, including an owner (i.e., the user who uploaded it):
In this case, each photo is associated with a user_id (the user who uploaded it). This user_id will be crucial in determining whether the current user has permission to delete the photo.
3. Implement the API endpoint
Let’s assume you have an endpoint for deleting photos like /api/photos/{photo_id}
. We will implement the authorization logic to support this flow when a user sends a request to delete a photo:
- The backend checks the access token for the
photos:delete
scope. - The backend then checks if the user is the owner of the specific photo they’re trying to delete.
- If both checks pass, the photo is deleted; otherwise, the user gets an error response.
Extract the token
When the user sends a request to delete a photo, their access token is included in the Authorization header.
Example request:
The server will extract the token from the Authorization header.
Decode and validate the token
You will decode and validate the token to get the user_id
and scopes associated with the request.
Check for the required scope
You’ll check that the user’s token includes the photos.delete scope. This grants them permission to delete photos, but it doesn't ensure they can delete any photo. They need ownership as well.
Check for ownership
Now, you retrieve the photo that the user is trying to delete by its photo_id
(e.g., 456
), and ensure that the user_id
from the token matches the user_id
of the photo.
Bring it all together
Now, we combine all of the checks: verify the scope and check if the user is the owner of the specific photo.
The final step would be to actually perform the deletion, which might look something like this:
Best practices for using OAuth scopes
When working with OAuth scopes, it's important to follow best practices to ensure the security and usability of your application.
- Use descriptive scope names: Ensure that your scope names are intuitive and descriptive so that users can easily understand what permissions they’re granting. For example, rather than using vague names like access_data, use more specific names like photos.read or user.update. Another recommended best practice is to use URNs (Uniform Resource Names) for scope names, such as
https://example.com/scopes/read
. - Use the principle of least privilege: Always follow the principle of least privilege when defining scopes. Only request the minimum permissions necessary to perform the desired functionality. This approach reduces the potential impact of any security vulnerabilities and enhances user trust.
- Use incremental authorization: Request scopes in context, based on the specific features or actions the user is about to perform, and just before they are needed, rather than at the initial login. This helps users understand why certain permissions are being requested.
- Handle consent for multiple scopes: Handle cases where users deny some scopes by disabling the relevant features. Provide clear explanations for why each scope is necessary.
- Allow user consent and revocation: Make sure users can review and revoke the permissions your application has at any time. This promotes trust and ensures that users have full control over their data.
- Follow the principle of consent transparency: Provide users with clear and concise explanations of the requested scopes. Users are more likely to consent to permissions if they understand what the application will do with the data. Instead of a vague request like access your data, explain specifically what the data is and why it’s needed: Access your calendar events to schedule meetings or Access your photos to create a gallery.
- Request refresh tokens only when necessary: Refresh tokens allow your app to maintain access to the API without requiring the user to reauthorize repeatedly. However, using refresh tokens can pose security risks if they are not managed properly. Only request a refresh token if your application requires long-term access to the user’s data. If your app needs to access a user’s email periodically without them having to log in again, request offline_access to get a refresh token. If the app only needs short-term access, don’t request a refresh token.
- Avoid overusing scopes like openid:
openid
is intended for authentication via OpenID Connect (OIDC). If your application only requires authorization (not authentication), requestingopenid
can be unnecessary. - Regularly review and update scopes: As your API evolves, your scope definitions may need to be updated. Regularly review the scopes you're offering to ensure they align with the latest functionality and best practices.
Common pitfalls when implementing OAuth scopes
Implementing OAuth 2.0 scopes can be challenging, and several common pitfalls should be avoided to ensure secure and effective authorization. Here are some of the key pitfalls and how to address them:
Conclusion
OAuth scopes are a powerful mechanism for adding granular permissions to your API, giving users control over what data they share with third-party applications.
By thoughtfully defining and enforcing scopes, you can offer more secure and flexible APIs, build user trust, and ensure that your applications follow best practices for data privacy.