I have a Spring Boot REST API acting as an OAuth2 Resource Server, configured with Spring Security 6. Salesforce is my external authorization server (IdP). The access token issued by Salesforce is a JWT, and I can validate it with the standard JwtDecoder.
The issue is that Salesforce tokens do not contain any custom claims for my application roles.
I want to enforce role-based access control in my Spring API (e.g. @PreAuthorize("hasRole('ADMIN')")).
The common solution I’ve found is to load the user and roles from the database on each request (inside a JwtAuthenticationConverter), but that feels inefficient because it requires a DB lookup every single time, which defeats the purpose of having JWTs.
My setup:
Clients: Next.js and React Native apps authenticating directly with Salesforce.
They send the Salesforce access token with every request to my API.
My API must both proxy calls to Salesforce and enforce application-specific roles stored in my DB.
I’ve looked at solutions where the resource server fetches roles from the DB on each request, but I don’t like this because of the performance overhead.
I initially considered enriching the Salesforce JWT with my own roles at login time, but I know this is not possible, since Salesforce doesn’t allow custom application roles in its access tokens.
Another idea is to issue my own JWT (containing roles) after validating Salesforce’s token — basically a token exchange pattern. But this introduces a new problem: there are requests where my Spring backend would need to receive both tokens simultaneously —
my application JWT (to enforce application roles),
and the Salesforce token (to proxy requests to Salesforce).
This makes the overall flow more complex, because now the clients must send and the server must process two different access tokens for certain API calls.
The other approach I thought about is to use caching or synchronization so that roles are resolved once at login and reused, instead of hitting the DB on every request.
So my question is: what is the recommended approach in Spring Security when using Salesforce as IdP, to keep the API stateless, avoid constant DB lookups, and also avoid the complexity of sending two access tokens to the backend?
Here are a few patterns you can use, with varying levels of cost. The first of them is just code whereas the other two might require discussions with other stakeholders.
AUTHORIZATION VALUE CACHING
In your API, use a library like cache2k in your converter to cache extra values that you need for authorization with a time to live that does not exceed the access token expiry. This is a low cost way to avoid a database lookup on every API request.
cache.invoke(accessTokenHash, e -> e.setValue(values).setExpiryTime(expiryMilliseconds));
On subsequent requests with the same access token, if the extra values exist in the memory cache you avoid a database lookup.
var values = cache.get(accessTokenHash);
This technique is generally useful for secondary authorization values that you might prefer to not issue to access tokens.
EMBEDDED ACCESS TOKENS
The answer from ch4mp is the correct longer term solution, though it requires the cost of managing an extra system:
The authorization server (AS) should enable you to issue your preferred scopes and claims to access tokens.
An identity provider (IDP) is a different role. It gives you a login method, possibly access tokens for use against its resources, but does not allow you to create custom access tokens for your own resources.
In such a deployment, clients run a code flow at the AS, which runs a second code flow at the IDP. Your APIs receive AS access tokens, which include custom claims like the roles your API needs. The AS access token can include the IDP access token as a custom claim.
CONFIDENTIAL ACCESS TOKENS
Meanwhile you return a confidential access token to the client, that does not reveal sensitive claims that only APIs should be able to read.
A common option is for the AS to issue opaque access tokens (which look similar to a UUID) to internet clients. For browser based apps the best practice is to avoid exposing tokens directly to JavaScript and wrap the confidential token in a secure cookie.
Usually, an API gateway processes the confidential token (or cookie) and forwards a JWT access token to APIs.