Federated Authentication / Claims Injection Pattern for GraphQL APIs
Building distributed applications by itself is hard, but if you add authentication & authorization to a heterogenous set of services, it becomes even more challenging.
The Problem
Using the API Aggregation Pattern together with the Backend for Frontend Pattern is super powerful. It allows you to unify all your APIs and Services into a single unified API. This is great for the developer experience, because you don't have to think about the underlying services or differences in API styles anymore.
At the same time, this paradigm shift also introduces new challenges, like authentication & authorization. If you have a single unified API, how do you authenticate users? More specifically, how do you authenticate users not just against your unified API, but also against the underlying services?
A user should be able to log into your system once, and then be able to gradually access more and more services. If these services require different authentication mechanisms, this becomes a challenge.
This is where the Federated Authentication Pattern comes in.
The Solution
In the first step, you need to authenticate the user against your unified API. Ideally, you delegate this to a dedicated authentication service, e.g. by using OpenID Connect. This way, we can decouple the authentication logic from the unified API.
Once the user is authenticated against the unified API / the API Gateway, we can use two different approaches to authenticate the user against sub-systems of our aggregated API.
Injecting Claims into trusted services we own
In case we've got a secure connection to a trusted service, usually a service we own, we can inject claims into the request.
Claims are name-value pairs of information about the authenticated user, like name, email, roles, etc.
Here's how this could look like:
mutation (
$project: cloud_CreateProjectInput!
$organizationID: ID!
$userEmail: String! @fromClaim(name: EMAIL)
$actorID: ID! @fromClaim(name: USERID)
) {
createProject: cloud_createProject(
input: $project
organizationID: $organizationID
actorID: $actorID
userEmail: $userEmail
) {
__typename
... on cloud_CreateProjectSuccess {
project {
id
slug
}
}
}
}
This is an example from WunderGraph Cloud. The GraphQL API is not publicly available, only via an internal network.
The createProject
mutation requires the organizationID
,
the actorID
and the userEmail
.
The actorID
and userEmail
in this case is injected by the API Gateway.
Separating them would allow us to create an admin user that can impersonate regular users
and create projects on their behalf.
At the same time, you can see that this pattern only really works when we have trust established between the API Gateway and the backend. The backend must rely on the API Gateway to authenticate users, inject the correct claims, and not allow users to inject claims themselves.
That said, this pattern is extremely powerful because we can decouple the authentication logic from the backend. The backend can simply expose the business logic, which can be used in different ways.
Federated authentication with public / untrusted services
In case we don't have a trusted connection between the API Gateway and the origin service, meaning that we need to authenticate every request between the API Gateway and the origin service, we can use a different approach.
This approach is also called "Token Handler Pattern". Our API Gateway will act as a secure token handler, which means that it will facilitate the process of securely acquiring, storing and renewing tokens for 3rd party services on behalf of the user. Let's break this down into more tangible steps.
- The user authenticates against the API Gateway
- The user wants to access a resource from a 3rd party service
- The API Gateway will recognize that the user is not authenticated against the 3rd party service
- The API Gateway will redirect the user to the 3rd party service to start the authentication flow
- The user authenticates against the 3rd party service
- The 3rd party service will redirect the user back to the API Gateway
- The API Gateway will exchange the authorization code for an access token and an optional refresh token for offline access
- The API Gateway will store the access token and the refresh token in a secure storage
- The API Gateway will use the access token to access the 3rd party service on behalf of the user
With this flow, the user only really has to authenticate against the API Gateway and we're never leaking any tokens to the user. Whenever we make a request to a protected resource on the 3rd party service, we will automatically inject the access token into the request. If the stored access token doesn't have sufficient permissions for the requested resource, we can ask the user to re-authenticate against the 3rd party service and elevate the permissions. This way, the token handler only has as many permissions as the user has granted it.
In addition to the "frontend" flow that is enabled through implementing the token handler pattern, we can also leverage it to implement asynchronous backend flows. The user can authenticate against the API Gateway and grant it permissions to act on their behalf on the 3rd party service. If the user also grants the API Gateway offline access, we're able to store a refresh token and retain access to the 3rd party service, even if the user is not actively using the API Gateway. This way, we can implement asynchronous backend flows, like syncing data, CRON jobs, queue workers and more.
Conclusion
As you can see, both the "Claims Injection Pattern" and the "Token Handler Pattern" can help you to implement Authentication for federated and aggregated APIs. Especially when it comes to "offline" access with background processing, the "Token Handler Pattern" plays a crucial role.
In addition, this pattern helps you to build more secure applications because we're keeping the tokens away from the "front channel". When implementing the flows correctly, we're only exposing an auth code to the front channel, which is useless without the client secret, which is also only known to the API Gateway / Token Handler. Exchanging the auth code for an access token and a refresh token is done on the back channel as well, so we're keeping the tokens away from the front channel.