Avoid basic auth when using x.509 authentication - spring

I have created an REST API based on Spring WebFlux that is protected through X.509 authentication. I followed this guide https://www.baeldung.com/x-509-authentication-in-spring-security to create all the certificates.
The router implementation:
#Configuration
class LogRouter {
#Bean
fun functionalRoutes(handler: LogHandler): RouterFunction<ServerResponse> =
route()
.route(RequestPredicates.path("/")) {
ServerResponse.ok().body(Mono.just("I am alive"))
}
.nest(RequestPredicates.path("/api").and(RequestPredicates.accept(MediaType.APPLICATION_JSON))) { builder ->
builder.GET("/fn/mono", handler::monoMessage)
.POST("/fn/mono", handler::monoPostMessage)
}
.build()
}
and app implementation:
#SpringBootApplication
#EnableWebFluxSecurity
class RestplayApplication {
#Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain? {
val principalExtractor = SubjectDnX509PrincipalExtractor()
principalExtractor.setSubjectDnRegex("OU=(.*?)(?:,|$)")
val authenticationManager = ReactiveAuthenticationManager { authentication: Authentication ->
authentication.isAuthenticated = "Trusted Org Unit" == authentication.name
Mono.just(authentication)
}
http
.x509 { x509 ->
x509
.principalExtractor(principalExtractor)
.authenticationManager(authenticationManager)
}
.authorizeExchange { exchanges ->
exchanges
.anyExchange().authenticated()
}
return http.build()
}
}
fun main(args: Array<String>) {
runApplication<RestplayApplication>( *args)
}
I use Firefox browser to test the x.509 authentication and I have added the self signed certificate(rootCA.crt) to the Firefox:
included client certificate(clientBob.p12).
When calling the link in the browser it shows basic authentication form:
However, I expect the authentication form not to be appeared because I have provided a valid client certificate in the browser.
Why the basic form appears every time?
The code is hosted on https://github.com/softshipper/restplay. The password for certificates are always changeit.

I debugged it and the problem seems to be that your client certificate clientBob.crt does not contain the field for Organization Unit of the subject and your principalExtractor is set to extract this field. As a result, your principalExtractor fails, and so it calls authenticationFailureHandler, which is set to prompt for basic authentication by default.
Possible solutions could be:
Use a client certificate that includes the Organization Unit of the subject, and set it to "Trusted Org Unit".
Alter the principalExtractor regex so that it uses a different field. The default one uses the common name (CN). If you do edit this, then remember to also update your authenticationManager to check for "Bob" instead of "Trusted Org Unit"

Related

Changing a Spring oauth2ResourceServer JWT ex. to use a single secret instead of a Keypair throws an exception “Failed to select a JWK signing key”

I'm trying to implement a Spring Boot Rest backend with JWT-security, based on Springs new authorization server and this example: https://github.com/spring-projects/spring-security-samples/tree/main/servlet/spring-boot/java/jwt/login
It uses Asymmetric keys to sign and verify tokens which seems like an overkill, since both authentication (where the token is generated) and authorization (verified) happens on the same server. So, to simplify deployment (just pass in a single secret via an environment variable), I have been trying to rewrite it to use a single shared secret.
The example code implements two Bean-components, one to create the JwtEncoder (using a private RSA key) and one for the JWTDecoder (using the matching public key).
I have rewritten the Decoder as explained in chapter 15 in the book “Spring Security in Action” so I assume this should work, since the NimbusJwtDecoder offers a withSecretKey method.
//Will eventually come via an environment variable
static byte[] secret = "j8IoV1jF67".getBytes();
#Bean
JwtDecoder jwtDecoder() {
// return NimbusJwtDecoder.withPublicKey(this.key).build();
SecretKey theKey = new SecretKeySpec(secret, 0, secret.length, "AES");
return NimbusJwtDecoder.withSecretKey(theKey).build();
}
I have implemented the Encoder, which is coursing the problem, like so (code commented out, is the original code using the private RSA Key:
#Bean
JwtEncoder jwtEncoder() {
// JWK jwk = new RSAKey.Builder(this.key).privateKey(this.priv).build();
// JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
// return new NimbusJwtEncoder(jwks);
SecretKey originalKey = new SecretKeySpec(secret, 0, secret.length, "AES");
JWKSource<SecurityContext> immutableSecret = new ImmutableSecret<SecurityContext>(originalKey);
return new NimbusJwtEncoder(immutableSecret);
}
When I login (via the POST /token endpoint) the line that uses the encoder:
return this.encoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
Throws this exception
org.springframework.security.oauth2.jwt.JwtEncodingException: An error occurred while attempting to encode the Jwt: Failed to select a JWK signing key
at org.springframework.security.oauth2.jwt.NimbusJwtEncoder.selectJwk(NimbusJwtEncoder.java:134)
at org.springframework.security.oauth2.jwt.NimbusJwtEncoder.encode(NimbusJwtEncoder.java:108)
Any suggestions to how to implement this example with a simple shared secret, instead of asymmetric keys?

Migrating away from Spring Security OAuth 2

I'm having a Spring Boot Auth Microservice. It uses the Oauth2 spring cloud starter dependency which is deprecated nowadays.
buildscript {
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.1.9.RELEASE"
}
}
dependencies {
implementation "org.springframework.boot:spring-boot-starter-actuator"
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
implementation "org.springframework.boot:spring-boot-starter-web"
implementation "org.springframework.cloud:spring-cloud-starter-oauth2:2.1.5.RELEASE"
}
The Schema was taken from here: https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
It also has a custom user_details table. The JPA class is implementing UserDetails. I've also provided an implementation for UserDetailsService which looks up the user in my custom table.
OAuth Configuration is quite forward:
AuthorizationServerConfiguration - where oauth is configured:
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
#EnableAuthorizationServer
class AuthorizationServerConfiguration : AuthorizationServerConfigurerAdapter() {
#Autowired private lateinit var authenticationManager: AuthenticationManager
#Autowired private lateinit var dataSource: DataSource
#Autowired
#Qualifier("customUserDetailsService")
internal lateinit var userDetailsService: UserDetailsService
#Autowired
private lateinit var passwordEncoder: BCryptPasswordEncoder
override fun configure(endpoints: AuthorizationServerEndpointsConfigurer) {
endpoints
.tokenStore(JdbcTokenStore(dataSource))
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
}
override fun configure(clients: ClientDetailsServiceConfigurer) {
// This one is used in conjunction with oauth_client_details. So like there's one app client and a few backend clients.
clients.jdbc(dataSource)
}
override fun configure(oauthServer: AuthorizationServerSecurityConfigurer) {
oauthServer.passwordEncoder(passwordEncoder)
}
}
WebSecurityConfiguration - needed for class above:
#Configuration
class WebSecurityConfiguration : WebSecurityConfigurerAdapter() {
#Bean // We need this as a Bean. Otherwise the entire OAuth service won't work.
override fun authenticationManagerBean(): AuthenticationManager {
return super.authenticationManagerBean()
}
override fun configure(http: HttpSecurity) {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
}
ResourceServerConfiguration - to configure access for endpoints:
#Configuration
#EnableResourceServer
class ResourceServerConfiguration : ResourceServerConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().cors().disable().csrf().disable()
.authorizeRequests()
.antMatchers("/oauth/token").authenticated()
.antMatchers("/oauth/user/**").authenticated()
.antMatchers("/oauth/custom_end_points/**").hasAuthority("my-authority")
// Deny everything else.
.anyRequest().denyAll()
}
}
These few lines give me a lot.
User Info endpoint (used by microservices)
Client's such as Mobile frontends can authenticate using: POST oauth/token and providing a grant_type=password together with a username and a password.
Servers can authorize using 'oauth/authorize'
Basic Auth support with different authorities is also available as I can fill username + password into the oauth_client_details table:
select client_id, access_token_validity, authorities, authorized_grant_types, refresh_token_validity, scope from oauth_client_details;
client_id | access_token_validity | authorities | authorized_grant_types | refresh_token_validity | scope
-------------------+-----------------------+-------------------------------+-------------------------------------------+------------------------+---------
backend | 864000 | mail,push,app-register | mail,push,client_credentials | 864000 | backend
app | 864000 | grant | client_credentials,password,refresh_token | 0 | app
This is used by the app if there's no oauth token yet.
Other microservices also use this to protect their endpoints - such as in this example:
#Configuration #EnableResourceServer class ResourceServerConfig : ResourceServerConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http.authorizeRequests()
// Coach.
.antMatchers("/api/my-api/**").hasRole("my-role")
.antMatchers("/registration/**").hasAuthority("my-authority")
}
}
Their set up is quite easy:
security.oauth2.client.accessTokenUri=http://localhost:20200/oauth/token
security.oauth2.client.userAuthorizationUri=http://localhost:20200/oauth/authorize
security.oauth2.resource.userInfoUri=http://localhost:20200/oauth/user/me
security.oauth2.client.clientId=coach_client
security.oauth2.client.clientSecret=coach_client
The first three properties just go to my authorization server. The last two properties are the actual username + password that I've also inserted inside the oauth_client_details table. When my microservice wants to talk to another microservice it uses:
val details = ClientCredentialsResourceDetails()
details.clientId = "" // Values from the properties file.
details.clientSecret = "" // Values from the properties file.
details.accessTokenUri = "" // Values from the properties file.
val template = OAuth2RestTemplate(details)
template.exchange(...)
Now my question is - how can I get all of this with the built in Support from Spring Security using Spring Boot? I'd like to migrate away from the deprecated packages and retain all tokens so that users are still logged in afterwards.
We are also running a spring security authorization server and looked into this. Right now there is no replacement for the authorization server component in spring and there does not seem to be a timeline to implement one. Your best option would be to look into an existing auth component like keycloak or nimbus. alternatively there are hosted service like okta or auth0.
Keeping your existing tokens will be a bit of a challange as you would need to import them into your new solution. Our current tokens are opaque while newer auth-solutions tend to use some version of jwt, so depending on your tokens, keeping them may not even be an option.
Right now we consider accepting both old and new tokens for a time until the livetime of our old tokens ends, at wich point we would move fully to the new infrastukture.
So I've ended up developing my own authentication system with a migration API from the old Spring Security OAuth 2 to my system. That way you are not logged out and need to re-login.
I'll describe how I did it in case anyone is interested.
In my scenario it is 2 'microservices'. One being the deprecated auth and the other leveraging it.
Legacy Authentication System
To either get a token as a user you'd send a request to /oauth/token with your username + password.
To refresh a token another request to /oauth/token with your refresh token.
Both cases return your access token + refresh token. You can execute this multiple times per devices and you'd always end up with the same tokens. This is important later.
Tokens are stored as MD5 hashed.
Spring Security OAuth has these tables defined:
oauth_access_token (access tokens)
oauth_approvals (don't know what for, is always empty in my case)
oauth_client_details (contains a basic authorization method when you're not authorized)
oauth_client_token (empty in my case)
oauth_code (empty in my case)
oauth_refresh_token (refresh tokens)
user_details (contains the user data)
user_details_user_role (association between user + roles)
user_role (your roles)
I really didn't use the multi roles functionality, but in any case it's trivial to take that into consideration as well.
New Authentication System
Access token & refresh tokens are uuid4's that I SHA256 into my table.
I can query them easily and check for expiration and throw appropriate HTTP status codes.
I ended up doing a per device (it's just a UUID generated once in the frontend) system. That way I can distinguish when a user has multiple devices (AFAIK, this isn't possible with the old system).
We need these new endpoints
Login with email + password to get an authentication
Migration call from the old tokens to your new ones
Logout call which deletes your authentication
Refresh access token call
Thoughts
I can keep using the user_details table since only my code interacted with it and I expose it via Springs UserDetailsService.
I'll create a new authentication table that has a n:1 relationship to user_details where I store a device id, access token, access token expiry & refresh token per user.
To migrate from the old to the new system, my frontend will send a one time migration request, where I check for the given access token if it's valid and if it is, I generate new tokens in my system.
I'll handle both systems in parallel by distinguishing at the header level Authorization: Bearer ... for the old system & Authorization: Token ... for the new system
Code snippets
I use Kotlin, so in order to have type safety and not accidentally mix up my old / new token I ended up using a sealed inline classes:
sealed interface AccessToken
/** The token from the old mechanism. */
#JvmInline value class BearerAccessToken(val hashed: String) : AccessToken
/** The token from the new mechanism. */
#JvmInline value class TokenAccessToken(val hashed: String) : AccessToken
To get my token from an Authorization header String:
private fun getAccessToken(authorization: String?, language: Language) = when {
authorization?.startsWith("Bearer ") == true -> BearerAccessToken(hashed = hashTokenOld(authorization.removePrefix("Bearer ")))
authorization?.startsWith("Token ") == true -> TokenAccessToken(hashed = hashTokenNew(authorization.removePrefix("Token ")))
else -> throw BackendException(Status.UNAUTHORIZED, language.errorUnauthorized())
}
internal fun hashTokenOld(token: String) = MessageDigest.getInstance("MD5").digest(token.toByteArray(Charsets.UTF_8)).hex()
internal fun hashTokenNew(token: String) = MessageDigest.getInstance("SHA-256").digest(token.toByteArray(Charsets.UTF_8)).hex()
Verifying the tokens with type safety gets pretty easy:
when (accessToken) {
is BearerAccessToken -> validateViaDeprecatedAuthServer(role)
is TokenAccessToken -> {
// Query your table for the given accessToken = accessToken.hashed
// Ensure it's still valid and exists. Otherwise throw appropriate Status Code like Unauthorized.
// From your authentication table you can then also get the user id and work with your current user & return it from this method.
}
}
The validateViaDeprecatedAuthServer is using the old authentication sytem via the Spring APIs and returns the user id:
fun validateViaDeprecatedAuthServer(): String {
val principal = SecurityContextHolder.getContext().authentication as OAuth2Authentication
requireElseUnauthorized(principal.authorities.map { it.authority }.contains("YOUR_ROLE_NAME"))
return (principal.principal as Map<*, *>)["id"] as? String ?: throw IllegalArgumentException("Cant find id in principal")
}
Now we can verify if a given access token from a frontend is valid. The endpoint which generates a new token from the old one is also quite simple:
fun migrateAuthentication(accessToken: AccessToken) when (origin.accessToken(language)) {
is BearerAccessToken -> {
val userId = validateViaDeprecatedAuthServer(role)
// Now, create that new authentication in your new system and return it.
createAuthenticationFor()
}
is TokenAccessToken -> error("You're already migrated")
}
Creating authentication in your new system might look like this:
fun createAuthenticationFor() {
val refreshToken = UUID.randomUUID().toString()
val accessToken = UUID.randomUUID().toString()
// SHA256 both of them and save them into your table.
return refreshToken to accessToken
}
Then you only need some glue for your new 'login' endpoint where you need to check that the email / password matches a given user in your table, create an authentication & return it.
Logout just deletes the given authentication for your user id + device id.
Afterthoughts
I've been using this system now for the last few days and so far it's working nicely. Users are migrating. No one seems to be logged out which is exactly what I've wanted.
One downside is that since the old authentication system didn't distinguish between devices, I have no way of knowing when a user has successfully migrated. He could be using 1 device or 10. I simply don't know. So both systems will need to live side by side for a rather long time and slowly I'll phase out the old system. In which case, I'll force logout you and you need to re-login (and potentially install a new App version if you haven't updated).
Note that the new system is limited to my own needs, which is exactly what I want. I'd prefer it to be simple and maintainable than the Spring Blackbox authentication system.

How to create custom claims in JWT using spring-authorization-server

I'm building an OAuth2 authorization server based on the experimental Spring project Spring Authorization Server
My use case is quite simple, fetch users from a DB, and based on some properties of the user, set some custom claims in the JWT being produced.
I haven't found a way to do so with Spring Authorization Server, the only way I could work out is to inject a jwtCustomizer object as part of the JwtEncoder bean definition:
#Bean
public JwtEncoder jwtEncoder(CryptoKeySource keySource) {
NimbusJwsEncoder jwtEncoder = new NimbusJwsEncoder(keySource);
jwtEncoder.setJwtCustomizer((headersBuilder, claimsBuilder) -> {
// Inject some headers and claims...
});
return jwtEncoder;
}
This obviously doesn't give me access to users information, therefore I can't set the claims I need at this point.
Did anyone manage to solve this problem?
The solution for this is in a test of the library
#Bean
OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return context -> {
if (context.getTokenType().getValue().equals(OidcParameterNames.ID_TOKEN)) {
Authentication principal = context.getPrincipal();
Set<String> authorities = principal.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());
context.getClaims().claim(AUTHORITIES_CLAIM, authorities);
}
};
}
You can try following way. Though it is Kotlin code, not Java, but approach should be clear:
import org.springframework.security.oauth2.provider.token.TokenEnhancer
class UserTokenEnhancer : TokenEnhancer {
override fun enhance(accessToken: OAuth2AccessToken,
authentication: OAuth2Authentication): OAuth2AccessToken {
val username = authentication.userAuthentication.name
val additionalInfo = mapOf( /* populate with some data for given username */ )
(accessToken as DefaultOAuth2AccessToken).additionalInformation = additionalInfo
return accessToken
}
}
Then just register bean:
#Bean
fun userTokenEnhancer(): TokenEnhancer {
return UserTokenEnhancer()
}

Invalid JWToken: kid is a required JOSE Header

I am trying to implement an Oauth2 Authorization Server with SpringBoot using this guide as a reference.
My keystore has a single key. I have successfully managed to create a JWToken (I can check it at jwt.io).
I have also a test Resource Server. When I try to access any endpoint I receive the following message:
{
"error": "invalid_token",
"error_description": "Invalid JWT/JWS: kid is a required JOSE Header"
}
The token really does not have a kid header but I can not figure out how to add it. I can only add data to its payload, using a TokenEnchancer. It also seems that I am not the first one with this issue.
Is there any way to add this header or, at least, ignore it at the resource server?
I've been working on an article that might help you out here:
https://www.baeldung.com/spring-security-oauth2-jws-jwk
So, to configure a Spring Security OAuth Authorization Server to add a JWT kid header, you can follow the steps of section 4.9:
create a new class extending the JwtAccessTokenConverter
In the constructor:
configure the parent class using the same approach you've been using
obtain a Signer object using the signing key you're using
override the encode method. The implementation will be the same as the parent one, with the only difference that you’ll also pass the custom headers when creating the String token
public class JwtCustomHeadersAccessTokenConverter extends JwtAccessTokenConverter {
private JsonParser objectMapper = JsonParserFactory.create();
final RsaSigner signer;
public JwtCustomHeadersAccessTokenConverter(KeyPair keyPair) {
super();
super.setKeyPair(keyPair);
this.signer = new RsaSigner((RSAPrivateKey) keyPair.getPrivate());
}
#Override
protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
String content;
try {
content = this.objectMapper.formatMap(getAccessTokenConverter().convertAccessToken(accessToken, authentication));
} catch (Exception ex) {
throw new IllegalStateException("Cannot convert access token to JSON", ex);
}
Map<String, String> customHeaders = Collections.singletonMap("kid", "my_kid");
String token = JwtHelper.encode(content, this.signer, this.customHeaders)
.getEncoded();
return token;
}
}
Then, of course, create a bean using this converter:
#Bean
public JwtAccessTokenConverter accessTokenConverter(KeyPair keyPair) {
return new JwtCustomHeadersAccessTokenConverter(keyPair);
}
Here I used a KeyPair instance to obtain the signing key and configure the converter (based on the example of the article), but you might adapt that to your configuration.
In the article I also explain the relevant endpoints provided by the Spring Security OAuth Authentication Server.
Also, regarding #Ortomala Lokni's comment, I wouldn't expect Spring Security OAuth to add any new features at this point. As an alternative, you probably can wait to have a look at Spring Security's Authorization Server features, planned to be released in 5.3.0
I managed to solve it by changing the parameter used to identify the URL where the clients will retrieve the pubkey.
On application.properties, instead of:
security.oauth2.resource.jwk.key-set-uri=http://{auth_server}/.well-known/jwks.json
I used:
security.oauth2.resource.jwt.key-uri=http://{auth_server}/oauth/token_key
If I understood correctly, the key-set-uri config points to an endpoint that presents a set of keys and there is the need for a kid. On the other side key-uri config points to an endpoint with a single key.

Spring Boot add additional attribute to WebClient request in ServerOAuth2AuthorizedClientExchangeFilterFunction

I am trying to implement the client_credentials grant to get a token in my spring boot resource server.
I am using Auth0 as an Authorization server. They seem to require an extra parameter in the request body to be added called audience.
I have tried to do the request through postman and it works. I am now trying to reproduce it within Spring. Here is the working postman request
curl -X POST \
https://XXX.auth0.com/oauth/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=client_credentials&audience=https%3A%2F%2Fxxxxx.auth0.com%2Fapi%2Fv2%2F&client_id=SOME_CLIENT_ID&client_secret=SOME_CLIENT_SECRET'
The problem I am facing is that i have no way to add the missing audience parameter to the token request.
I have a configuration defined in my application.yml
client:
provider:
auth0:
issuer-uri: https://XXXX.auth0.com//
registration:
auth0-client:
provider: auth0
client-id: Client
client-secret: Secret
authorization_grant_type: client_credentials
auth0:
client-id: Client
client-secret: Secret
I have the web client filter configured like this.
#Bean
WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations,
ServerOAuth2AuthorizedClientRepository authorizedClients) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2 = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
clientRegistrations, authorizedClients);
oauth2.setDefaultClientRegistrationId("auth0");
return WebClient.builder()
.filter(oauth2)
.build();
}
I am injecting the instance and trying to do a request to get the user by email
return this.webClient.get()
.uri(this.usersUrl + "/api/v2/users-by-email?email={email}", email)
.attributes(auth0ClientCredentials())
.retrieve()
.bodyToMono(User.class);
The way i understand it, the filter intercepts this userByEmail request and before it executes it it tries to execute the /oauth/token request to get JWT Bearer token which it can append to the first one and execute it.
Is there a way to add a parameter to the filter? It has been extremely difficult to step through it and figure out where exactly the parameters are being appended since its reactive and am quite new at this. Even some pointers to where to look would be helpful.
I was having the same problem where access token response and request for it wasn't following oAuth2 standards. Here's my code (it's in kotlin but should be understandable also for java devs) for spring boot version 2.3.6.RELEASE.
Gradle dependencies:
implementation(enforcedPlatform("org.springframework.boot:spring-boot-dependencies:${springBootVersion}"))
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
After adding them you have to firstly create your custom token request/response client which will implement ReactiveOAuth2AccessTokenResponseClient interface:
class CustomTokenResponseClient : ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
private val webClient = WebClient.builder().build()
override fun getTokenResponse(
authorizationGrantRequest: OAuth2ClientCredentialsGrantRequest
): Mono<OAuth2AccessTokenResponse> =
webClient.post()
.uri(authorizationGrantRequest.clientRegistration.providerDetails.tokenUri)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.bodyValue(CustomTokenRequest(
clientId = authorizationGrantRequest.clientRegistration.clientId,
clientSecret = authorizationGrantRequest.clientRegistration.clientSecret
))
.exchange()
.flatMap { it.bodyToMono<NotStandardTokenResponse>() }
.map { it.toOAuth2AccessTokenResponse() }
private fun NotStandardTokenResponse.toOAuth2AccessTokenResponse() = OAuth2AccessTokenResponse
.withToken(this.accessToken)
.refreshToken(this.refreshToken)
.expiresIn(convertExpirationDateToDuration(this.data.expires).toSeconds())
.tokenType(OAuth2AccessToken.TokenType.BEARER)
.build()
}
As you can see above, in this class you can adjust token request/response handling to your specific needs.
Note: authorizationGrantRequest param inside getTokenResponse method. Spring is passing here data from you application properties, so follow the standards when defining them, e.g. they may look like this:
spring:
security:
oauth2:
client:
registration:
name-for-oauth-integration:
authorization-grant-type: client_credentials
client-id: id
client-secret: secret
provider:
name-for-oauth-integration:
token-uri: https://oauth.com/token
The last step is to use your CustomTokenResponseClient inside oAuth2 configuration, it may look like this:
#Configuration
class CustomOAuth2Configuration {
#Bean
fun customOAuth2WebWebClient(clientRegistrations: ReactiveClientRegistrationRepository): WebClient {
val clientRegistryRepo = InMemoryReactiveClientRegistrationRepository(
clientRegistrations.findByRegistrationId("name-for-oauth-integration").block()
)
val clientService = InMemoryReactiveOAuth2AuthorizedClientService(clientRegistryRepo)
val authorizedClientManager =
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistryRepo, clientService)
val authorizedClientProvider = ClientCredentialsReactiveOAuth2AuthorizedClientProvider()
authorizedClientProvider.setAccessTokenResponseClient(CustomTokenResponseClient())
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
val oauthFilter = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
oauthFilter.setDefaultClientRegistrationId("name-for-oauth-integration")
return WebClient.builder()
.filter(oauthFilter)
.build()
}
}
Right now, this is possible, but not elegant.
Note that you can provide a custom ReactiveOAuth2AccessTokenResponseClient to ServerOAuth2AuthorizedClientExchangeFilterFunction.
You can create your own implementation of this - and thereby add any other parameters you need - by copying the contents of WebClientReactiveClientCredentialsTokenResponseClient.
That said, it would be better if there were a setter to make that more convenient. You can follow the corresponding issue in Spring Security's backlog.
Here is what i found out after further investigation. The code described in my question was never going to call the client_credentials and fit my use-case. I think (not 100% sure on this) it will be very useful in the future if i am trying to propagate the user submitted token around multiple services in a micro-service architecture. A chain of actions like this comes to mind:
User calls Service A -> Service A calls Service B -> Service B responds -> Service A responds back to user request.
And using the same token to begin with through the whole process.
My solution to my use-case:
What i did was create a new Filter class largely based on the original and implement a step before executing the request where i check if i have a JWT token stored that can be used for the Auth0 Management API. If i don't i build up the client_credentials grant request and get one, then attach this token as a bearer to the initial request and execute that one. I also added a small token in-memory caching mechanism so that if the token is valid any other requests at a later date will just use it. Here is my code.
Filter
public class Auth0ClientCredentialsGrantFilterFunction implements ExchangeFilterFunction {
private ReactiveClientRegistrationRepository clientRegistrationRepository;
/**
* Required by auth0 when requesting a client credentials token
*/
private String audience;
private String clientRegistrationId;
private Auth0InMemoryAccessTokenStore auth0InMemoryAccessTokenStore;
public Auth0ClientCredentialsGrantFilterFunction(ReactiveClientRegistrationRepository clientRegistrationRepository,
String clientRegistrationId,
String audience) {
this.clientRegistrationRepository = clientRegistrationRepository;
this.audience = audience;
this.clientRegistrationId = clientRegistrationId;
this.auth0InMemoryAccessTokenStore = new Auth0InMemoryAccessTokenStore();
}
public void setAuth0InMemoryAccessTokenStore(Auth0InMemoryAccessTokenStore auth0InMemoryAccessTokenStore) {
this.auth0InMemoryAccessTokenStore = auth0InMemoryAccessTokenStore;
}
#Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
return auth0ClientCredentialsToken(next)
.map(token -> bearer(request, token.getTokenValue()))
.flatMap(next::exchange)
.switchIfEmpty(next.exchange(request));
}
private Mono<OAuth2AccessToken> auth0ClientCredentialsToken(ExchangeFunction next) {
return Mono.defer(this::loadClientRegistration)
.map(clientRegistration -> new ClientCredentialsRequest(clientRegistration, audience))
.flatMap(request -> this.auth0InMemoryAccessTokenStore.retrieveToken()
.switchIfEmpty(refreshAuth0Token(request, next)));
}
private Mono<OAuth2AccessToken> refreshAuth0Token(ClientCredentialsRequest clientCredentialsRequest, ExchangeFunction next) {
ClientRegistration clientRegistration = clientCredentialsRequest.getClientRegistration();
String tokenUri = clientRegistration
.getProviderDetails().getTokenUri();
ClientRequest clientCredentialsTokenRequest = ClientRequest.create(HttpMethod.POST, URI.create(tokenUri))
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.body(clientCredentialsTokenBody(clientCredentialsRequest))
.build();
return next.exchange(clientCredentialsTokenRequest)
.flatMap(response -> response.body(oauth2AccessTokenResponse()))
.map(OAuth2AccessTokenResponse::getAccessToken)
.doOnNext(token -> this.auth0InMemoryAccessTokenStore.storeToken(token));
}
private static BodyInserters.FormInserter<String> clientCredentialsTokenBody(ClientCredentialsRequest clientCredentialsRequest) {
ClientRegistration clientRegistration = clientCredentialsRequest.getClientRegistration();
return BodyInserters
.fromFormData("grant_type", AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.with("client_id", clientRegistration.getClientId())
.with("client_secret", clientRegistration.getClientSecret())
.with("audience", clientCredentialsRequest.getAudience());
}
private Mono<ClientRegistration> loadClientRegistration() {
return Mono.just(clientRegistrationId)
.flatMap(r -> clientRegistrationRepository.findByRegistrationId(r));
}
private ClientRequest bearer(ClientRequest request, String token) {
return ClientRequest.from(request)
.headers(headers -> headers.setBearerAuth(token))
.build();
}
static class ClientCredentialsRequest {
private final ClientRegistration clientRegistration;
private final String audience;
public ClientCredentialsRequest(ClientRegistration clientRegistration, String audience) {
this.clientRegistration = clientRegistration;
this.audience = audience;
}
public ClientRegistration getClientRegistration() {
return clientRegistration;
}
public String getAudience() {
return audience;
}
}
}
Token Store
public class Auth0InMemoryAccessTokenStore implements ReactiveInMemoryAccessTokenStore {
private AtomicReference<OAuth2AccessToken> token = new AtomicReference<>();
private Clock clock = Clock.systemUTC();
private Duration accessTokenExpiresSkew = Duration.ofMinutes(1);
public Auth0InMemoryAccessTokenStore() {
}
#Override
public Mono<OAuth2AccessToken> retrieveToken() {
return Mono.justOrEmpty(token.get())
.filter(Objects::nonNull)
.filter(token -> token.getExpiresAt() != null)
.filter(token -> {
Instant now = this.clock.instant();
Instant expiresAt = token.getExpiresAt();
if (now.isBefore(expiresAt.minus(this.accessTokenExpiresSkew))) {
return true;
}
return false;
});
}
#Override
public Mono<Void> storeToken(OAuth2AccessToken token) {
this.token.set(token);
return Mono.empty();
}
}
Token Store Interface
public interface ReactiveInMemoryAccessTokenStore {
Mono<OAuth2AccessToken> retrieveToken();
Mono<Void> storeToken(OAuth2AccessToken token);
}
And finally defining the beans and using it.
#Bean
public Auth0ClientCredentialsGrantFilterFunction auth0FilterFunction(ReactiveClientRegistrationRepository clientRegistrations,
#Value("${auth0.client-registration-id}") String clientRegistrationId,
#Value("${auth0.audience}") String audience) {
return new Auth0ClientCredentialsGrantFilterFunction(clientRegistrations, clientRegistrationId, audience);
}
#Bean(name = "auth0-webclient")
WebClient webClient(Auth0ClientCredentialsGrantFilterFunction filter) {
return WebClient.builder()
.filter(filter)
.build();
}
There is a slight problem with the token store at this time as the client_credentials token request will be executed multiple on parallel requests that come at the same time, but i can live with that for the foreseeable future.
Your application.yml is missing one variable:
client-authentication-method: post
it should be like this:
spring:
security:
oauth2:
client:
provider:
auth0-client:
token-uri: https://XXXX.auth0.com//
registration:
auth0-client:
client-id: Client
client-secret: Secret
authorization_grant_type: client_credentials
client-authentication-method: post
Without it I was getting "invalid_client" response all the time.
Tested in spring-boot 2.7.2

Resources