In this article
February 28, 2025
February 28, 2025

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:

	
https://authorization-server.com/authorize?
response_type=code&
client_id=your-client-id&
redirect_uri=https://yourapp.com/callback&
scope=user.read%20post.create
	

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):

  
def check_access_token(token, required_scope):
    # Decode and validate the token
    # See https://workos.com/blog/jwt-validation
    decoded_token = decode_token(token)
    
    # Check if the token contains the required scope
    if required_scope in decoded_token['scopes']:
        return True
    else:
        return False
  

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):

  
class Photo:
    def __init__(self, id, user_id, file_name, uploaded_at):
        self.id = id
        self.user_id = user_id
        self.file_name = file_name
        self.uploaded_at = uploaded_at
  

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:

  1. The backend checks the access token for the photos:delete scope.
  2. The backend then checks if the user is the owner of the specific photo they’re trying to delete.
  3. 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:

  
DELETE /api/photos/456
Authorization: Bearer <access_token>
  

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.

  
import jwt

private_key = b"-----BEGIN PRIVATE KEY-----\nMIGEAgEAMBAGByqGSM49AgEGBS..."
public_key = b"-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEAC..."
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg"

try:
    decoded_token = jwt.decode(token, public_key, algorithms=['RS256'], options={"verify_exp": True, "verify_iss": True, "verify_aud": True})
    user_id = decoded_token['user_id']
		scopes = decoded_token['scopes']
except jwt.ExpiredSignatureError:
    print("Token has expired")
except jwt.InvalidIssuerError:
    print("Invalid issuer")
except jwt.InvalidAudienceError:
    print("Invalid audience")
except jwt.InvalidTokenError:
    print("Invalid token")
  

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.

  
def has_permission_to_delete(scopes):
    if "photos.delete" in scopes:
        return True
    return False
  

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.

  
def get_photo_by_id(photo_id):
    # This is where you would query your database to retrieve the photo record
    photo = Photo(id=456, user_id=1234, file_name="beach.jpg", uploaded_at="2025-02-01")
    return photo

def can_delete_photo(user_id, photo_id):
    photo = get_photo_by_id(photo_id)
    if photo.user_id == user_id:
        return True
    return False
  

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.

  
def authorize_delete_photo(token, photo_id):
    decoded_token = decode_token(token)
    user_id = decoded_token['user_id']
    scopes = decoded_token['scopes']
    
    # Check if the user has permission to delete photos
    if not has_permission_to_delete(scopes):
        return "Error: Insufficient permissions (missing photos.delete scope)"
    
    # Check if the user owns the specific photo
    if not can_delete_photo(user_id, photo_id):
        return "Error: You can only delete photos that you own"
    
    # Proceed with deleting the photo
    delete_photo(photo_id)
    return "Success: Photo deleted"
  

The final step would be to actually perform the deletion, which might look something like this:

  
def delete_photo(photo_id):
    # Perform the deletion in the database
    print(f"Photo {photo_id} has been deleted.")
  

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), requesting openid 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:

Pitfall Solution
Granting too many permissions can lead to unauthorized data access if an application is compromised. Implement the Principle of Least Privilege by requesting only the necessary scopes for the application's functionality. Regularly review and update scope permissions to ensure they remain relevant and minimal.
OAuth scopes often lack fine-grained control, making it difficult to manage permissions precisely. Use external authorization services to manage permissions more granularly. This allows for more precise control over user privileges and easier revocation of permissions.
Creating too many scopes can lead to complexity and maintenance issues. Keep scopes simple and stable. Avoid creating client-specific scopes that can lead to duplication. Only add new scopes when necessary, such as when expanding APIs to new business areas.
Users may not fully understand the implications of granting scopes. Clearly inform users about the data access they are granting. Provide options for users to review or limit scopes. Implement a scope approval process for third-party apps to ensure transparency.
Failing to properly manage and validate access tokens can lead to unauthorized actions. Ensure rigorous token validation and secure storage of tokens. Use secure connections (HTTPS) for token transmission and storage.
Embedded scopes in access tokens can be difficult to invalidate once issued. Use an external authorization system to manage permissions more dynamically. This allows for immediate revocation of privileges without waiting for token expiration.

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.

This site uses cookies to improve your experience. Please accept the use of cookies on this site. You can review our cookie policy here and our privacy policy here. If you choose to refuse, functionality of this site will be limited.