Rate limit of the Rest api using spring cloud gateway does not work - spring-boot

I tried to run, but I don’t understand that why don’t show an error when to occur rate limit to rest api.
GatewaySecureRateLimiterTest - I only observe the success of requests, but I do not see errors when limiting requests.
In addition, I would like to clarify for myself (I did not find it in the documentation):
I would like to clarify for myself (I didn't find this in the documentation):
how to change the error code to the address to which the response was sent that the resource is currently busy
is it possible to collect statistics and see which IP generates more requests than our endpoint can handle
the speed limit can only be configured using *. yml, or it can also be configured using Java, while ?
I would like to see it in tests. what is the restriction-it triggers and gets detailed information(for example, from which IP and how many requests and in what unit of time).
I also didn't fully understand what the meaning of these parameters is:
key determinant: "#{#userRemoteAddressResolver}"
reuse rate limiter.Top-up rate: 1
reuse rate limiter.Bandwidth: 1
For example, I would like to know what is the number of requests in these parameters, and what is the unit of time during which this number of requests should work ?
server:
port: ${PORT:8085}
logging.pattern.console: "%clr(%d{HH:mm:ss.SSS}){blue} %clr(---){faint} %clr([%15.15t]){yellow} %clr(:){red} %clr(%m){faint}%n"
spring:
application:
name: gateway-service
redis:
host: 192.168.99.100
port: 6379
output.ansi.enabled: ALWAYS
cloud:
gateway:
routes:
- id: account-service
uri: http://localhost:8085
predicates:
- Path=/account/**
filters:
- RewritePath=/account/(?<path>.*), /$\{path}
- name: RequestRateLimiter
args:
key-resolver: "#{#userKeyResolver}"
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
config-security
#Configuration
//#ConditionalOnProperty("rateLimiter.secure")
#EnableWebFluxSecurity
public class SecurityConfig {
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange(exchanges ->
exchanges
.anyExchange()
.authenticated())
.httpBasic();
http.csrf().disable();
return http.build();
}
#Bean
public MapReactiveUserDetailsService users() {
UserDetails user1 = User.builder()
.username("user1")
.password("{noop}1234")
.roles("USER")
.build();
UserDetails user2 = User.builder()
.username("user2")
.password("{noop}1234")
.roles("USER")
.build();
UserDetails user3 = User.builder()
.username("user3")
.password("{noop}1234")
.roles("USER")
.build();
return new MapReactiveUserDetailsService(user1, user2, user3);
}
}
config
#Configuration
public class GatewayConfig {
#Bean
#Primary
#ConditionalOnProperty("rateLimiter.non-secure")
KeyResolver userKeyResolver() {
return exchange -> Mono.just("1");
}
// #Bean
// #ConditionalOnProperty("rateLimiter.secure")
KeyResolver authUserKeyResolver() {
return exchange -> ReactiveSecurityContextHolder.getContext()
.map(securityContext -> securityContext.getAuthentication()
.getPrincipal()
.toString()
);
}
}
test
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT,
properties = {"rateLimiter.non-secure=true"})
#RunWith(SpringRunner.class)
public class GatewayRateLimiterTest {
private static final Logger LOGGER =
LoggerFactory.getLogger(GatewayRateLimiterTest.class);
private Random random = new Random();
#Rule
public TestRule benchmarkRun = new BenchmarkRule();
private static final DockerImageName IMAGE_NAME_MOCK_SERVER =
DockerImageName.parse("jamesdbloom/mockserver:mockserver-5.11.2");
#ClassRule
public static MockServerContainer mockServer =
new MockServerContainer(IMAGE_NAME_MOCK_SERVER);
#ClassRule
public static GenericContainer redis =
new GenericContainer("redis:5.0.6")
.withExposedPorts(6379);
#Autowired
TestRestTemplate testRestTemplate;
#Test
#BenchmarkOptions(warmupRounds = 0, concurrency = 6, benchmarkRounds = 600)
public void testAccountService() {
String username = "user" + (random.nextInt(3) + 1);
HttpHeaders headers = createHttpHeaders(username,"1234");
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<Account> responseEntity =
testRestTemplate.exchange("/account/{id}",
HttpMethod.GET,
entity,
Account.class,
1);
LOGGER.info("Received: status->{}, payload->{}, remaining->{}",
responseEntity.getStatusCodeValue(),
responseEntity.getBody(),
responseEntity.getHeaders()
.get("X-RateLimit-Remaining"));
}
private HttpHeaders createHttpHeaders(String user, String password) {
String notEncoded = user + ":" + password;
String encodedAuth = Base64.getEncoder().encodeToString(notEncoded.getBytes());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.add("Authorization", "Basic " + encodedAuth);
return headers;
}
}
repository
here

Related

How to handle login failure with Spring API on server and Retrofit2 on Android?

On server side I have created simple Spring API with authentication. I have just added implementation("org.springframework.boot:spring-boot-starter-security") dependency and when I go to url with browser - it shows login page when I'm not logged in.
For now I'm using basic authentication, my username and password set in configuration file like this (resources/application.properties file):
spring.security.user.name=myusername
spring.security.user.password=mypassword
spring.security.user.roles=manager
I'm also using Spring Data REST, so Spring creates API automatically for JPA repositories that exist in my project. I had to set up my database, create JPA repositories for tables and add implementation("org.springframework.boot:spring-boot-starter-data-rest") to my dependencies to make it work.
On Android side I call my API with this Adapter and Client.
interface ApiClient {
#GET("inventoryItems/1")
suspend fun getFirstInventoryItem(): Response<InventoryItemDto>
}
object ApiAdapter {
private const val API_BASE_URL = "http://some.url/"
private const val API_USERNAME = "myusername"
private const val API_PASSWORD = "mypassword"
val apiClient: ApiClient = Retrofit.Builder()
.baseUrl(API_BASE_URL)
.client(getHttpClient(API_USERNAME, API_PASSWORD))
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiClient::class.java)
private fun getHttpClient(user: String, pass: String): OkHttpClient =
OkHttpClient
.Builder()
.authenticator(getBasicAuth(user,pass))
.build()
private fun getBasicAuth(username: String?, password: String?): Authenticator? =
object : Authenticator {
override fun authenticate(route: Route?, response: okhttp3.Response): Request? {
return response
.request()
.newBuilder()
.addHeader("Authorization", Credentials.basic(username, password))
.build()
}
}
}
And this is how I call my API on Android:
(I'm calling this from onViewCreated on my view Fragment)
lifecycleScope.launch {
val item: InventoryItemDto? = ApiAdapter.apiClient.getFirstInventoryItem().body()
binding?.tvTest?.text = item.toString()
}
When I provide correct password everything works.
But when I provide wrong password my Android app crashes because java.net.ProtocolException: Too many follow-up requests: 21 is thrown.
It looks like my Android client goes to requested url (inventoryItems/1) and then it is redirected to login page. Then my clients tries to authenticate on that page again, because I have .addHeader("Authorization", Credentials.basic(username, password)) added to every request (I assume). Login is failed again, so it is redirected again to login page where it sends wrong credentials again and again is redirected...
My question 1: how to deal with login failed properly on Android and/or Spring?
My question 2: how to handle other errors (like bad request) properly on Android and/or Spring?
What I have tried:
Disable followRedirects and followSslRedirects on Android side like this:
private fun getHttpClient(user: String, pass: String): OkHttpClient =
OkHttpClient
.Builder()
.followRedirects(false)
.followSslRedirects(talse)
.authenticator(getBasicAuth(user,pass))
.build()
Add .addHeader("X-Requested-With", "XMLHttpRequest") header, also on Android side:
private fun getBasicAuth(username: String?, password: String?):Authenticator? =
object : Authenticator {
override fun authenticate(route: Route?, response: okhttp3.Response): Request? {
return response
.request()
.newBuilder()
.addHeader("Authorization", Credentials.basic(username, password))
.addHeader("X-Requested-With", "XMLHttpRequest")
.build()
}
}
OK, I got this (answering my own question). Solution is based on this: link
I will not accept my answer, maybe someone will propose some solution without deprecated class, and maybe with good explanation.
I have created my own AuthenticationFailureHandler, like this:
class CustomAuthenticationFailureHandler : AuthenticationFailureHandler {
private val objectMapper = ObjectMapper()
#Throws(IOException::class, ServletException::class)
override fun onAuthenticationFailure(
request: HttpServletRequest?,
response: HttpServletResponse,
exception: AuthenticationException
) {
response.status = HttpStatus.UNAUTHORIZED.value()
val data: MutableMap<String, Any> = HashMap()
data["timestamp"] = Calendar.getInstance().time
data["exception"] = exception.message.toString()
response.outputStream.println(objectMapper.writeValueAsString(data))
}
}
I had to configure security manually by creating this class:
#Configuration
#EnableWebSecurity
class SecurityConfiguration : WebSecurityConfigurerAdapter() {
#Throws(Exception::class)
override fun configure(auth: AuthenticationManagerBuilder) {
auth.inMemoryAuthentication()
.withUser(API_USERNAME)
.password(passwordEncoder().encode(API_PASSWORD))
.roles(API_ROLE)
}
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
.anyRequest()
.authenticated()
}
#Bean
fun authenticationFailureHandler(): AuthenticationFailureHandler = CustomAuthenticationFailureHandler()
#Bean
fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
companion object {
private const val API_USERNAME = "user1"
private const val API_PASSWORD = "user1"
private const val API_ROLE = "USER"
}
}
Unfortunately WebSecurityConfigurerAdapter class is deprected. I will deal with that later.

Reuse existing token rather than requesting it on every request in spring boot + Retrofit app

I have a spring boot application that uses Retrofit to make requests to a secured server.
My endpoints:
public interface ServiceAPI {
#GET("/v1/isrcResource/{isrc}/summary")
Call<ResourceSummary> getResourceSummaryByIsrc(#Path("isrc") String isrc);
}
public interface TokenServiceAPI {
#FormUrlEncoded
#POST("/bbcb6b2f-8c7c-4e24-86e4-6c36fed00b78/oauth2/v2.0/token")
Call<Token> obtainToken(#Field("client_id") String clientId,
#Field("scope") String scope,
#Field("client_secret") String clientSecret,
#Field("grant_type") String grantType);
}
Configuration class:
#Bean
Retrofit tokenAPIFactory(#Value("${some.token.url}") String tokenUrl) {
Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl(tokenUrl)
.addConverterFactory(JacksonConverterFactory.create());
return builder.build();
}
#Bean
Retrofit serviceAPIFactory(#Value("${some.service.url}") String serviceUrl, TokenServiceAPI tokenAPI) {
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new ServiceInterceptor(clientId, scope, clientSecret, grantType, apiKey, tokenAPI))
.build();
Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl(repertoireUrl)
.client(okHttpClient)
.addConverterFactory(JacksonConverterFactory.create());
return builder.build();
}
Interceptor to add the Authorization header to every request
public class ServiceInterceptor implements Interceptor {
public ServiceInterceptor(String clientId,
String scope,
String clientSecret,
String grantType,
String apiKey,
TokenServiceAPI tokenAPI) {
this.clientId = clientId;
this.scope = scope;
this.clientSecret = clientSecret;
this.grantType = grantType;
this.apiKey = apiKey;
this.tokenAPI = tokenAPI;
}
#Override
public Response intercept(Chain chain) throws IOException {
Request newRequest = chain.request().newBuilder()
.addHeader(AUTHORIZATION_HEADER, getToken())
.addHeader(API_KEY_HEADER, this.apiKey)
.build();
return chain.proceed(newRequest);
}
private String getToken() throws IOException {
retrofit2.Response<Token> tokenResponse = repertoireTokenAPI.obtainToken(clientId, scope, clientSecret, grantType).execute();
String accessToken = "Bearer " + tokenAPI.body().getAccessToken();
return accessToken;
}
}
This is working as expected, the problem is that the token is being requested for every request rather than using the existing valid one. How can one store the token somewhere and re-use it? I was wondering if Retrofit had a built-in solution.
a possible option with caching:
add caffeiene
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
add #Cacheable("your-token-cache-name") on the method returning the token, looks like getToken above
add max cache size and expiration configuration in application.yml
e.g. 500 entries and 10 minutes for configuration below
spring.cache.cache-names=your-token-cache-name
spring.cache.caffeine.spec=maximumSize=500,expireAfterAccess=600s
example from: https://www.javadevjournal.com/spring-boot/spring-boot-with-caffeine-cache/

SpringBoot: LoadBalancer [server]: Error choosing server for key default

I'm creating a load balance feature on my project in which I have three server that will simultaneously ping for 15 seconds. However, when I already run my client-side, it always goes to the fallback page and received an error of "LoadBalancer [server]: Error choosing server for key default" even if the servers are already running.
Here are the codes in my project:
app.properties
server.port=8788
server.ribbon.eureka.enabled=false
server.ribbon.listOfServers=localhost:8787,localhost:8789,localhost:8790
#every 15 seconds
server.ribbon.ServerListRefreshInterval=15000
client service (wherein it is my fallback method)
private LoadBalancerClient loadBalancer;
private RestTemplate restTemplate;
public ClientService(RestTemplate rest) {
this.restTemplate = rest;
}
#HystrixCommand(fallbackMethod = "reliable")
public String login() {
ServiceInstance instance = loadBalancer.choose("server");
URI uri = URI.create(String.format("http://%s:%s/admin/ping", instance.getHost(), instance.getPort()));
//URI uri = URI.create("http://localhost:8787/admin/ping");
return this.restTemplate.getForObject(uri, String.class);
}
MainController
public class MainController{
private final static Logger LOGGER = LoggerFactory.getLogger(MainController.class);
#Autowired
private ClientService clientService;
#LoadBalanced
#Bean
public RestTemplate rest(RestTemplateBuilder builder) {
return builder.build();
}
#Autowired
RestTemplate restTemplate;
...
Client client = new Client();
WebResource resource = client.resource("http://%s:%s/auth/loginvalidate");
ClientResponse response = resource.type(MediaType.APPLICATION_JSON)
.header("Authorization", "Basic " + encodePw)
.get(ClientResponse.class);
I got rid of that error by doing two things:
1) Add the following properties to the remote service:
management.endpoints.web.exposure.include: "*"
management.endpoint.health.enabled: "true"
management.endpoint.restart.enabled: "true"
management.endpoint.info.enabled: "true"
2) Make sure that there is a ping endpoint in the remote service:
public class MainController{
#RequestMapping("/")
public String ribbonPing() {
return this.hostName;
}
}
I added a few amendments to the example provided by Kubernetes Circuit Breaker & Load Balancer Example to test this scenario and put in here.
I suggest that you follow those links as a kind of "best practises" guide in order to build your Hystrix/Ribbon solution. Pay special attention to:
the starters/dependencies added to the pom files
the structure of the Java classes (how and where each bean is declared and injected)
how you configure your (micro-)services (in this case with K8s ConfigMaps)

Webflux JWT Authorization not working fine

I am following a tutorial about JWT in a spring reactive context (webflux).
The token generation is working fine, however the authorization is not working when I use the Authorization with bearer
Here is what I have done:
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class WebSecurityConfig{
#Autowired private JWTReactiveAuthenticationManager authenticationManager;
#Autowired private SecurityContextRepository securityContext;
#Bean public SecurityWebFilterChain configure(ServerHttpSecurity http){
return http.exceptionHandling()
.authenticationEntryPoint((swe , e) -> {
return Mono.fromRunnable(()->{
System.out.println( "authenticationEntryPoint user trying to access unauthorized api end points : "+
swe.getRequest().getRemoteAddress()+
" in "+swe.getRequest().getPath());
swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
});
}).accessDeniedHandler((swe, e) -> {
return Mono.fromRunnable(()->{
System.out.println( "accessDeniedHandler user trying to access unauthorized api end points : "+
swe.getPrincipal().block().getName()+
" in "+swe.getRequest().getPath());
swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
});
})
.and()
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.authenticationManager(authenticationManager)
.securityContextRepository(securityContext)
.authorizeExchange()
.pathMatchers(HttpMethod.OPTIONS).permitAll()
.pathMatchers("/auth/login").permitAll()
.anyExchange().authenticated()
.and()
.build();
}
As you can see, I want to simply deny all not authorized requests other than login or options based ones.
The login is working fine and I'm getting a token.
But trying to logout (a tweak that I implemented my self to make it state-full since I m only learning) is not working.
Here is my logout controller:
#RestController
#RequestMapping(AuthController.AUTH)
public class AuthController {
static final String AUTH = "/auth";
#Autowired
private AuthenticationService authService;
#PostMapping("/login")
public Mono<ResponseEntity<?>> login(#RequestBody AuthRequestParam arp) {
String username = arp.getUsername();
String password = arp.getPassword();
return authService.authenticate(username, password);
}
#PostMapping("/logout")
public Mono<ResponseEntity<?>> logout(#RequestBody LogoutRequestParam lrp) {
String token = lrp.getToken();
return authService.logout(token);
}
}
The logout request is as below:
As stated in images above, I believe that I m doing fine, however I m getting the error log message:
authenticationEntryPoint user trying to access unauthorized api end points : /127.0.0.1:45776 in /auth/logout
Here is my security context content:
/**
* we use this class to handle the bearer token extraction
* and pass it to the JWTReactiveAuthentication manager so in the end
* we produce
*
* simply said we extract the authorization we authenticate and
* depending on our implementation we produce a security context
*/
#Component
public class SecurityContextRepository implements ServerSecurityContextRepository {
#Autowired
private JWTReactiveAuthenticationManager authenticationManager;
#Override
public Mono<SecurityContext> load(ServerWebExchange swe) {
ServerHttpRequest request = swe.getRequest();
String authorizationHeaderContent = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if( authorizationHeaderContent !=null && !authorizationHeaderContent.isEmpty() && authorizationHeaderContent.startsWith("Bearer ")){
String token = authorizationHeaderContent.substring(7);
Authentication authentication = new UsernamePasswordAuthenticationToken(token, token);
return this.authenticationManager.authenticate(authentication).map((auth) -> {
return new SecurityContextImpl(auth);
});
}
return Mono.empty();
}
#Override
public Mono<Void> save(ServerWebExchange arg0, SecurityContext arg1) {
throw new UnsupportedOperationException("Not supported yet.");
}
}
I'm unable to see or find any issue or error that I have made. Where is the mistake?
There's a difference in writing
//Wrong
Jwts.builder()
.setSubject(username)
.setClaims(claims)
and
//Correct
Jwts.builder()
.setClaims(claims)
.setSubject(username)
Indeed, look at setSubject method in the DefaultJwtBuilder class :
#Override
public JwtBuilder setSubject(String sub) {
if (Strings.hasText(sub)) {
ensureClaims().setSubject(sub);
} else {
if (this.claims != null) {
claims.setSubject(sub);
}
}
return this;
}
When setSubject(username) is called first, ensureClaims() creates a DefaultClaims without yours and if you call setClaims(claims) the precedent subject is lost ! This JWT builder is bogus.
Otherwise, you're importing the wrong Role class in JWTReactiveAuthenticationManager, you have to replace :
import org.springframework.context.support.BeanDefinitionDsl.Role;
by
import com.bridjitlearning.www.jwt.tutorial.domain.Role;
Last and not least, validateToken() will return always false because of the check(token). put call is coming too late, you have to be aware of that. Either you remove this check or you move the put execution before calling the check method.
I'am not sure about what you want to do with resignTokenMemory, so i'll let you fix it by your own:
public Boolean validateToken(String token) {
return !isTokenExpired(token) && resignTokenMemory.check(token);
}
Another thing, your token is valid only 28,8 second, for testing raison i recommend you to expiraiton * 1000.

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