API Authorization Design – OAuth Architecture Guidance
Background
In our previous post we discussed User Data Management and in this post we will focus on data protection in APIs. Any real world system needs to apply business rules before allowing access to resources.
OAuth Capabilities
The OAuth family of specifications has many finer details, but I think of these as the two main capabilities:
Capability | Description |
---|---|
Authentication | Dealing with users and authentication, while externalizing this complexity from applications |
Data Protection | Design patterns that enable APIs to authorize access to resources based on tokens |
API clients may use different authentication flows, but APIs always protect data in the same way, by receiving access tokens:
This post will focus on OAuth data protection and how it maps to complex business rules, which is an area that is often not properly understood.
APIs and Business Rules
A request to get data from a UI might look like this, where a token containing the subject claim is sent and then business rules are applied:
The above example might include handling business rules like these, and we will show how to manage these types of rule in an OAuth secured API:
- User Bob has Write Access to orders for customers he manages
- User Bob has View Access to all orders for his own branch
- User Bob has No Access to orders for other branches
API Authorization Steps
Implementing authorization in done via three phases but the main work is always done in step 3:
Step | Description |
---|---|
Token Validation | JWT Access Token Validation, to ensure integrity of the message credential |
Scope Checks | Sanity checks to ensure that an access token is allowed to be used for a particular business area |
Claims Based Authorization | Detailed permission checks against resources, using domain specific data |
Step 1: Token Validation
The JWT Access Token Validation blog post described how an API verifies the JWT’s digital signature and if the token is expired. If the token is not valid a 401 error is returned:
"code": "unauthorized",
"message": "Missing, invalid or expired access token"
Think of token validation as an entry level check to authenticate the request to the API, after which the API can trust data in the access token’s payload.
Step 2: Scopes
Scopes can be included in access tokens to represent an Area of Data and Permissions on that Data.
Examples | Usage |
---|---|
orders | Indicates that an access token grants access to orders placed by a user |
orders_read | Indicates that an access token cannot be used to make data changes to orders |
When personal assets are involved, it is usual to show a consent screen that displays scopes, so that the user knows which data they are granting access to, and whether they are granting read or write access.
Built-In Scopes
OAuth also uses some built in scopes for Personally Identifiable Information (PII) that is stored by the Authorization Server:
Examples | Usage Scenario |
---|---|
openid | Indicates that the user’s identity is being used, via the OpenID Connect protocol |
profile | Indicates that the user’s name and possibly other information is being used |
Scope Limitations
Scopes are fixed at design time and you cannot use them for dynamic purposes, such as different scopes for different types of user. Whenever you need to perform dynamic authorization, claims must be used.
Audience Checks
APIs should also check the audience of received access tokens, and the most common setup is for a set of related APIs to use the same audience. This enables JWT access tokens to be forwarded between microservices:
API | Audience |
---|---|
Orders | api.mycompany.com |
Customers | api.mycompany.com |
Products | api.mycompany.com |
If you deal with different subdivisions of a large company, then it is usually recommended to use a different audience per subdivision.
Step 3: Claims
Our next code sample will authorize using the following custom claims. Note that none of this data is stored in the Authorization Server:
Claim | Represents |
---|---|
User ID | The user id with which transactions are stored in the domain specific data |
User Role | Two user roles are involved, for a normal user and an administrative user |
User Regions | An administrator grants users access to data for one or more regions, represented as an array |
The third of these is an array claim, to represent the type of authorization that must be done in many real world business systems:
- A doctor might only see data for particular surgeries
- A coverage banker might only see data for particular industry sectors
- A retail worker might only see data for particular branches
Claims Requirements
There are two main requirements that you need to consider when working with claims, and we will show two main ways to achieve this:
Requirement | Description |
---|---|
API Data | The API must receive the data it needs in order to implement its domain specific authorization |
Confidentiality | Access tokens returned to internet clients must not reveal all of this information |
Claims Architecture Option 1
The ideal way to meet the above requirements is for the Authorization Server to reach out to the API at the time of token issuance to get custom claims, then to include the custom claims in the access token.
This state is then stored in the Authorization Server, and the access token returned to clients uses a confidential reference token format, which is typically a UUID or something similar:
When a client calls the API, the reference token is introspected to get a JWT access token, which is then forwarded to the API. The introspection is usually done in an API gateway that is placed in front of the API:
This is a great solution when supported, since all claims issued are audited by the Authorization Server and it scales very well if the JWT needs to be forwarded between microservices.
Claims Architecture Option 2
Option 1 may require a specialist Authorization Server, whereas this blog is using AWS Cognito by default, which does not support the above features.
We will therefore also demonstrate an alternative approach, where custom claims are looked up when an access token is first received, then cached: