We have a Spring project which is both Spring Security OAuth2 Client and Resource Server. So it has all the integration part with our identity provider (AWS Cognito) and the authorization part is implemented by our team.
When logging in, Cognito response back three tokens: ID, access and refresh token. We store as cookies the two last tokens.
Then, we also are auditing creation data and last update date (for both use cases - author user and datetime).
For that, we are using a custom AuditorAware
. The current version is something like this:
class PrincipalIdExtractor: AuditorAware<UUID> {
override fun getCurrentAuditor(): Optional<UUID> {
val authentication = SecurityContextHolder.getContext().authentication
return if (authentication == null || !authentication.isAuthenticated)
Optional.empty()
else
Optional.ofNullable(UUID.fromString(authentication.name))
}
}
The thing is we would like to avoid a new request to database (we store the ID from identity provider and e-mail), or a new request to Cognito's userinfo endpoint.
authentication.principal
is a Jwt, which does not bring the email
claim attribute.
I cannot think about any viable solution right now. Is it possible to customize Cognito's access token and add custom claims to it? (Cognito's documentation is a bite messy) Even storing the id token in the cookie means I will have to inject the data from the cookie and pass it throught the Spring's filters/interceptor.
What is the best approach to solve this?
Is it possible to customize Cognito's access token and add custom claims to it?
I don't think so (couldn't find a way to do it). But ID tokens can be customized.
As a preambule, if you are not sure about the very different security requirements for OAuth2 clients and OAuth2 resource server, please take time to read the OAuth2 essentials section in the introduction to my tutorials.
I wouldn't store ID token, and ever more the refresh token, in cookies. The refresh token is something really secret that should not leave your servers.
Tokens are persisted in an OAuth2AuthorizedClientRepository
. The default implementation is AuthenticatedPrincipalOAuth2AuthorizedClientRepository
, which uses a delegate: HttpSessionOAuth2AuthorizedClientRepository
. This means that by default, tokens (access, ID and refresh) are stored in session. This, off course, only applies to clients. Resource servers usually being stateless, do not store tokens.
If you are using K8s or any other distributed system, you might already be using Spring Session or something to share session between client instances. If so, OAuth2AuthenticationToken
in the security context of a request should be the same whatever the client instance and contain the ID token (it is built behind the scene using OAuth2AuthorizedClientRepository
and as so, session data)
It is then trivial to read the ID token from the OAuth2AuthenticationToken
in the security context and then add it in a custom header (let's say X-ID-TOKEN
) before issuing a request to the resource server side of your app. You could even write a filter for that: add this custom header to each and every request send to resource servers you trust (and only those servers).
On the resource server side of your app, you can configure a custom jwtAuthenticationConverter
. Such an authentication converter can return something else than JwtAuthenticationToken
. This means you can define your own AbstractAuthenticationToken
implementation containing the ID token claims (retrieved from the custom header using RequestContextHolder.getRequestAttributes()
) in addition to access token ones.
I configure such a resource server in this tutorial. This project is using one of my custom Spring Boot starters, but everything is open-source and in the same Github repo: if you clone it you can browse and debug the code to observe what happens.
Regarding your AuditorAware
, it would fetch ID claims from a different place depending on where it is used:
OAuth2AuthenticationToken
in the security context (it contains ID token already, no need to query the OAuth2AuthorizedClientRepository
)Authentication
instance would be my preferred way to go