OAuth2 scopes for fine-grained access control lists (ACLs)
In OAuth2, the scope is a space-separated list of user permissions. There is no further restriction on its format and hence allows a lot of flexibility in its application. This article proposes a structure for using OAuth2 scopes to define fine-grained access control lists (ACLs).
What are others doing?
It is worth taking a look at how current implementations use scopes. Github's API is often cited as an example of good API practice. Examples of scopes you can request are
read:user. The former grants read and write permissions to the user account, while the latter is a subset of these permissions. Slack is another API that uses colons to indicate subsets. Examples from the Slack API are
chat:write:bot, where the latter is more restrictive.
Google and HubSpot utilize a similar convention but use a period to separate the parts. In Google's case, the scopes are prepended with a URI, for example:
Lastly, the OpenID specs define four scopes, namely
phone. These allow the token holder to retrieve these details for the user logged in (the resource owner in OAuth terms).
The scopes seen above are in the perspective of the resource owner. They are well suited to ask the user what permissions to assign to a connected app. However, a typical ACL is more fine-grained than the given examples.
Goals for the ACL
If we want to use OAuth2 scopes for ACL, we want to meet the following requirements:
- Control access A fine granularity is needed to meet the principle of least privilege. We want to provide access to specific entities without needing access to any broader scope.
- Understandable A proper security system must be well understood by its users.
- Ease of implementation Authentication is not the core business of most APIs; hence we don't want to take three extra steps to get it right. It is essential, but there isn't much budget to spend on this. Therefore, its implementation must be quick and easy to make the ACL effective.
How can OAuth scopes be used for fine-grained ACLs?
Before defining our scopes, we must look at the content structure and its associated actions. Let's take a simple blog platform as an example. Here, we have the User and Post entity types.
User Readable fields: name, mail Actions: create, read, update, delete, change password
Post Owned by: User entity referenced in the author field Readable fields: title, body, author
Post Owned by: User entity referenced in the author field Readable fields: title, body, author Actions: create, read, update, delete
Aside from the basic CRUD actions, we defined a particular "change password” action for users. We might want to separate access from other write actions. For example, we may only change the password after an additional user verification step.
Another noteworthy aspect is that posts are owned by their authors. Logically, we want to allow users to edit all their posts.
Using ids in scope
We can exactly define the actions listed above by using ids in the scope:
User:123:read User:123:update User:123:Post:45:read User:123:Post:45:update
However, listing all content items in the OAuth2 scope is impractical. Therefore, we need to use a wildcard. The scope for a typical user with id 123 would then become:
User:*:read User:123:write User:123:update User:123:delete User:123:changePassword User:*:Post:*:read User:123:Post:create User:123:Post:*:update User:123:Post:*:delete
A few notes on this:
- The hierarchy of users and posts is reflected in the scope. By doing so, we can easily grant write access to all posts owned by a given user.
- No id is available for the create action, so this is simply omitted.
- We have capitalized the entity names. This gives a clear distinction between entities and actions.
- Scopes are separated by line breaks for visibility but must be space separated in implementation to comply with the OAuth2 standard.
The scopes above provide direct access to parts of the system. Clearly, one can give access to another user profile as well. There are many ways to extend this model. For example, blogs may be posted in groups. Such use cases require you to make clear choices in advance over who owns the content and who has access to what. If posts are owned by both the group and the user, we might end up with a scope like
Group:67:User:123:Post:45:read. That is possible, but it becomes apparent that a clear content structure is key.
Excluding actions from the access list
What if we want to grant someone access to your house, but not to the safe inside that house? So, allow
House:34:*, but not
House:34:safe? It's a typical use case for an ACL. Many developers are known to the ".gitignore” file, which allows you to prepend a line with an exclamation mark to indicate an exclude. Apart from the technical implementation, there isn't much preventing us from adopting this convention in OAuth2 scopes.
Using scopes in Flowlet
Flowlet adopts the colon-for-subset style, wildcard matching, and exclusions described above. When defining HTTP endpoints, the developer can oblige a particular scope, which is not necessarily a fixed string. As seen above, it might contain ids used in the content fields. Therefore, the scope is a titbit that you can code in TypeScript. Flowlet is a low-code platform, after all. The scope construction is the only code required for implementing the authentication.
The scopes proposed in this article provide fine-grained access control. By specifying the specific actions a user is allowed to perform, you can ensure that only the authorized users have access to the data they need. The scopes are easily understood in terms of entities and actions. When using Flowlet, implementation in APIs is a breeze thanks to the build-in scope matching and OAuth2 support.
Can't wait to write secure APIs with Flowlet?