Webclient - refresh API key on unauthorized - java

I'm looking for solution how to properly implement token refreshing mechanism.
A token should be changed after we got 401, or 403 http status code from external API. I'd like to call login endpoint to obtain a valid token, save the token into database and try again with the new token.
Currently, I'm having problem with reactor and streams. Below are two methods that contains core functionality.
register method (external call)
public Mono<Void> register(UserConfiguration configuration) {
return externalClient.post()
.uri("http://example.com")
.body(Mono.just(new SomeRequest()), SomeRequest.class)
.header(AUTHORIZATION_TOKEN_HEADER, token)
.exchangeToMono(clientResponse -> handleResponse(clientResponse, Void.class, configuration))
.retryWhen(Retry.max(5)
.doBeforeRetry(retrySignal -> log.info("Unauthorized request"))
.filter(UnauthorizedException.class::isInstance))
.then();
}
handleResponse method
private <R> Mono<R> handleResponse(ClientResponse clientResponse, Class<R> clazz, Configuration configuration, ) {
if (clientResponse.statusCode().equals(HttpStatus.OK)) {
return clientResponse.bodyToMono(clazz);
} else if (clientResponse.statusCode().equals(HttpStatus.FORBIDDEN) || clientResponse.statusCode().equals(HttpStatus.UNAUTHORIZED)) {
return authenticationService.refreshToken(configuration).flatMap(conf -> Mono.error(new UnauthorizedException()));
} else {
return clientResponse.createException()
.flatMap(Mono::error);
}
}
register is responsible for calling external API. handleResponse take care of handling response status, and when there is unauthorized response, the method invokes authenticationService in order to obtain a new token and save the token into db (UserConfiguration). Retry detects an exception instance and when there is problem with a token, it enforces the stream going again. Unfortunately, register method uses the same instance of UserConfiguration so values there are outdated as well as token is not valid anymore.
What would be the better approach? Now I can see the only workaround to start a stream from getting UserConfiguration, so on retry we will have the latest state of db. It seems to work, but it's not perfect from a performance view.

Might be late, but in case it helps someone else: you could use the onErrorResume method to filter for 401 and 403 HTTP responses and in those cases, renew the token, save it in the database and call the register method with the new user configuration.

Related

Reactive Programming - Webflux Webfilter not behaving properly

I'm a bit new to reactive programming, and I'm trying to assemble the following: using Java, Springboot 2, Webflux, and reactor core, I want to handle very specific requests that need extra authentication. So I'm implementing a WebFilter with a series of steps:
Capture the path and the method of the request. Check if the combination exists and needs specific authentication with the accessPointService.getAccessPointAuthorizationRequirement method (returns a Mono with a Boolean).
Since I have CSRF and Spring security configured, I need both csrf token and springsession credentials. I make a GET and a POST request for the credentials.
Then with the credentials, I simply make a POST request to a service (authcheck) that can do a series of security checks (the service is OK, works fine from Postman and Angular).
After that, I need to retrieve the body, convert it to String, and inspect it. Right now this does not happen.
The filter
#Override
public Mono<Void> filter(final ServerWebExchange serverWebExchange, final WebFilterChain webFilterChain) {
//client for specific requests.
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:8080")
.build();
//get request for the CSRF cookie.
WebClient.RequestHeadersSpec<?> getRequest = webClient.get()
.uri("/login");
//post request for the spring security session cookie.
WebClient.RequestHeadersSpec<?> postRequest = webClient.post()
.uri("/login")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.body(BodyInserters.fromFormData("username", "username")
.with("password", "password"));
//services that checks if the given request needs extra authentication
return accessPointService.getAccessPointAuthorizationRequirement(serverWebExchange.getRequest().getMethod().toString().toUpperCase(), serverWebExchange.getRequest().getPath().toString())
.log()
//gets the csrf token from the GET request
.flatMap(isRequired -> getRequest.exchangeToMono(response -> Mono.just(response.cookies().getFirst("XSRF-TOKEN").getValue())))
//combines the previous token with the POST request SESSION cookie,
//THEN secures the last request with both credentials
.zipWith(postRequest.exchangeToMono(resp -> Mono.just(resp.cookies().getFirst("SESSION").getValue())),
AuthenticationFilter::secureAuthRequest)
//gets the exchange from the request and converts the body into a String
.flatMap(AuthenticationFilter::getRequestExchange)
//code to validate if it's doing something. Not implemented yet because it never executes.
.flatMap(s -> Mono.just(s.equals("")))
.onErrorResume(e -> {
throw (CustomException) e;//breaks the execution
})
.then(webFilterChain.filter(serverWebExchange));//continues the execution
}
The secureAuthRequest and getRequestExchange methods invoked
//adds the springsession cookie and csrf cookie to the request
private static WebClient.RequestHeadersSpec<?> secureAuthRequest(String csrf, String spring) {
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:8080")
.build();
WebClient.RequestHeadersSpec<?> request = webClient.post()
.uri("/authcheck")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
request.header("X-XSRF-TOKEN", csrf);
request.cookies( cookies -> cookies.add( "XSRF-TOKEN", csrf) );
request.header("Authorization", spring);
return request;
}
//gets the body as string.
private static Mono<String> getRequestExchange(WebClient.RequestHeadersSpec<?> securedReq) {
return securedReq.exchangeToMono(clientResponse -> clientResponse.bodyToMono(String.class));
}
However, when a request is bound to be authenticated, the log is the following:
2021-10-26 23:57:18.760 INFO 6860 --- [ctor-http-nio-4] reactor.Mono.Just.4 : | onSubscribe([Synchronous Fuseable] Operators.ScalarSubscription)
2021-10-26 23:57:18.761 INFO 6860 --- [ctor-http-nio-4] reactor.Mono.Just.4 : | request(unbounded)
2021-10-26 23:57:18.761 INFO 6860 --- [ctor-http-nio-4] reactor.Mono.Just.4 : | onNext(true)
2021-10-26 23:57:18.762 INFO 6860 --- [ctor-http-nio-4] reactor.Mono.Just.4 : | onComplete()
As far as I know, the stream of data starts with a subscription and a posterior request (which I think returns a TRUE from the accessPointService.getAccessPointAuthorizationRequirement method Mono value, if I'm wrong please correct me), but then the 'onComplete()' log shows up. I don't know exactly what the onComplete() log means, since it's being shown before the execution of the getRequestExchange method (which is invoked). The Mono.just(s.equals("")) piece of code never executes.
I've read a lot about how 'nothing happens until you subscribe', but I still don't know why the reactive flow is being invoked at all if I never explicitly subscribe to the stream, and neither I know how to implement it, since it only returns a Disposable (I guess I can throw exceptions from within?). Also, I hear about decoupling when multiple subscribers are being invoked, so I tried to avoid them as possible.
Any help regarding reactive programming, reactor-core, or the specific flow and how to improve it it's appreciated.
Cheers.
So after some research and thanks to #Toerktumlare 's comments, and figured what was happening and what I changed/applied to this.
So for the 'onComplete()' log, it marks the end of a producer of data. So to see the full stack of the operation, I needed to chain each producer with its own log. For example:
Mono.just(Boolean.FALSE)
.log()
.flatMap(booleanVal -> Mono.just(booleanVal.toString()))
.log()
.subscribe(stringVal -> System.out.println("This is the boolean value " + stringVal));
That will produce the trace for the initial producer and the flatMap operation.
Now, onto the main problem, the issue was within the getRequestExchange method:
//gets the body as string.
private static Mono<String> getRequestExchange(WebClient.RequestHeadersSpec<?> securedReq) {
return securedReq.exchangeToMono(clientResponse -> clientResponse.bodyToMono(String.class));
}
The problem was hidden in the bodyToMono method. According to this site https://medium.com/#jeevjyotsinghchhabda/dont-let-webclient-s-bodytomono-trick-you-645123b3e0a9 , if the response to this request has no body for whatever reason, will not throw any error, but just return a Mono.empty(). Since that the flow was not prepared for such a producer, it ended right there.
In my case, the problem was spring cloud security. I provided the Authorization credential, but not the associated SESSION cookie in the request. So the request returned a 302 (Found) without body. That was the problem (not the reactive flow itself).
So, after that, I modified the request, and #Toerktumlare 's comments helped me develop a working solution:
//service that returns if certain resource needs authentication or not, or if it's not even configured
return accessPointService.getAccessPointAuthorizationRequirement(serverWebExchange.getRequest().getMethod().toString().toUpperCase(), FWKUtils.translateAccessPointPath(serverWebExchange.getRequest().getPath().pathWithinApplication().elements()))
//if the response is a Mono Empty, then returns a not acceptable exception
.switchIfEmpty(Mono.defer(() -> throwNotAcceptable(serverWebExchange)))
//takes the boolean value to check if extra auth is needed.
.flatMap(isRequired -> validateAuthenticationRequirement(isRequired))
//gets the access token - the extra auth credential
.flatMap(isRequired -> getHeaderToken(serverWebExchange))
//from this access generates a WebClient to the specific authentication service - from a webClientProvider to not create too many WebClients.
.flatMap(accessToken -> generateAuthenticationRequest(webClientProvider.getInstance(), accessToken))
//gets the CRSF token credential and secures the request (adds it to the header and the cookies)
.zipWith(getCredential(webClientProvider.getInstance(), "csrf"), (securedRequest, csrfToken) -> secureAuthenticationRequest(securedRequest, csrfToken, "X-XSRF-TOKEN", "XSRF-TOKEN"))
//gets the SESSION (spring cloud security) token credential and secures the request (adds it to the header and the cookies)
.zipWith(getCredential(webClientProvider.getInstance(), "spring-cloud"), (securedRequest, sessionToken) -> secureAuthenticationRequest(securedRequest, sessionToken, "Authorization", "SESSION"))
//does the request and gets the response
.map(requestBodySpecs -> requestBodySpecs.retrieve())
//from the response, maps it to a specific DTO. The single() clause is to validate that a body is present.
.flatMap(clientResponse -> clientResponse.bodyToMono(SecurityCredentialResponseDTO.class).single())
//checks the authentication and throws a Unauthorizedstatus if its not valid.
.flatMap(responseDTO -> checkTokenAuthentication(serverWebExchange, responseDTO))
//if an error is present, then throws it
.onErrorResume(e -> {
if (e instanceof FWKException.GenericException) {
throw (FWKException.GenericException) e;
}
throw (RuntimeException) e;
})
//finally, continues the execution if no exception was thrown.
.then(webFilterChain.filter(serverWebExchange));
There's a bit more that I implemented in this solution (storing the CSRF and spring-cloud credential to avoid innecesary calls).

Spring Security filters for JWT-based authentication, verification and authorization scheme, by example

Java + Spring (and Spring Security) here, interested in implementing a JWT-based auth mechanism for my web service using bearer tokens. My understanding of the proper way of using Spring Security for authentication and authorization is through the use of provided (or custom) filters as follows:
you specify which URLs in your app are authenticated (and thus require authenticated requests to access)
this is typically done in an #EnableWebSecurity-annotated web security class that extends WebSecurityConfigurerAdapter
for any unauthenticated URLs, no filters should block access to the resources being requested
an authentication filter effectively provides a "sign in" endpoint
request clients should hit this signin endpoint (authn filter) initially to obtain an auth token that can be used for making subsequent API calls
this filter should receive a type of "sign in request" object that contains a principal (e.g. username) and credential (e.g. password)
this authn filter should use the principal/credential contained in the sign in request to determine if they represents a valid user in the system
if so, an auth token (JWT, etc.) is generated and sent back to the requesters in the response somehow
else, if the principal/credential don't match a valid user in the system, an error response is returned and authentication fails
for authenticated URLs, a verification filter verifies that the request contains an auth token and that the auth token is valid (was signed correctly, contains user information such as JWT claims, is not expired, etc.)
if the auth token is valid, the request continues on to the authorization filter (see below)
else if the auth token is not valid, verification fails and the filter sends an error response back to the client
finally, an authorization filter verifies that the user associated with the valid auth token has the ability/permission to make such a request
if they do, then the request is allowed to continue on to whatever resources/controller was written to handle it, and that resource/controller provides the response back to the requester
if they don't, an error response is returned to the client
ideally the logic (code) inside this authz filter would have access to the permission annotations added to the resource method, so that I can add endpoints and specify permissions on them without having to modify the code of the authz filter
So to begin with, if anything I have stated above is a Spring Security (or web security in general) anti-pattern or is misled, please begin by providing course correction and steering me in the right direction!
Assuming I'm more or less understanding the "auth flow" above correctly...
Are there any specific Spring Security filters that take care of all of this for me already, or that can be extended and have a few methods overridden to behave this way? Or anything that comes really close? Looking at the list of authentication-specific Spring Security filters I see:
UsernamePasswordAuthenticationFilter -> looks like a decent candidate for the authn filter but expects a username and password parameter on the query string which is strange to me, and most importantly, does not generate a JWT
CasAuthenticationFilter -> looks like its used for CAS-based SSO and is not appropriate for use in non-SSO contexts
BasicAuthenticationFilter -> for HTTP basic authentication-based auth, not appropriate for more sophisticated setups
As for token verification and authorization, I (much to my surprise) don't see anything in the Spring Security landscape that could qualify.
Unless anyone knows of JWT-specific filters that I can use or subclass easily, I think I need to implement my own custom filters, in which case I'm wondering how to conigure Spring Security to use them and not use any of these other authentication filters (such as UsernamePasswordAuthenticationFilter) as part of the filter chain.
As I understand it, you want to:
Authenticate users via a username and password and respond with a JWT
On subsequent requests, authenticate users using that JWT
username/password -> JWT isn't an established authentication mechanism on its own, which is why Spring Security doesn't yet have direct support.
You can get it on your own pretty easily, though.
First, create a /token endpoint that produces a JWT:
#RestController
public class TokenController {
#Value("${jwt.private.key}")
RSAPrivateKey key;
#PostMapping("/token")
public String token(Authentication authentication) {
Instant now = Instant.now();
long expiry = 36000L;
// #formatter:off
String scope = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(" "));
JWTClaimsSet claims = new JWTClaimsSet.Builder()
.issuer("self")
.issueTime(new Date(now.toEpochMilli()))
.expirationTime(new Date(now.plusSeconds(expiry).toEpochMilli()))
.subject(authentication.getName())
.claim("scope", scope)
.build();
// #formatter:on
JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256).build();
SignedJWT jwt = new SignedJWT(header, claims);
return sign(jwt).serialize();
}
SignedJWT sign(SignedJWT jwt) {
try {
jwt.sign(new RSASSASigner(this.key));
return jwt;
}
catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
}
}
Second, configure Spring Security to allow HTTP Basic (for the /token endpoint) and JWT (for the rest):
#Configuration
public class RestConfig extends WebSecurityConfigurerAdapter {
#Value("${jwt.public.key}")
RSAPublicKey key;
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.authorizeRequests((authz) -> authz.anyRequest().authenticated())
.csrf((csrf) -> csrf.ignoringAntMatchers("/token"))
.httpBasic(Customizer.withDefaults())
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling((exceptions) -> exceptions
.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
.accessDeniedHandler(new BearerTokenAccessDeniedHandler())
);
// #formatter:on
}
#Bean
UserDetailsService users() {
// #formatter:off
return new InMemoryUserDetailsManager(
User.withUsername("user")
.password("{noop}password")
.authorities("app")
.build());
// #formatter:on
}
#Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(this.key).build();
}
}
I think there's appetite to add support for something like this in spring-authorization-server to reduce the /token boilerplate, if you're interested in contributing your efforts!

How to have JWT authentication between micro services in kubernetes cluster

We have 8 java microservices talking to each other in kubeneters cluster. Each microservice is bundled with auth library which intercepts and validates/renews JWT token for each REST request to controllers.
Scenario:
From Frontend, we get access token for the first time, Authentication gets successful. Lets say
Frontend hit 'Microservice A' with access token - Successful
'Microservice A' internally hits 'Microservice B' via restTemplate.
My 'Microservice B' also needs logged in user details.
Issue: I have to pass same access token from 'A' to 'B' but I am not able to get access token in Controller/Service logic but can get only in filters where token is being validated. I can get token in Rest Controllers by adding following argument in all rest methods in controller:
#RequestHeader (name="Authorization") String token
But I dont want to go with this approach as I have to pass this token to everywhere till end and have to declare this argument in all APIS.
I want to get token from TokenStore by passing authentication object. We are using Oauth2 and I checked the code in library, There are many tokenStore providers.
In DefaultTokenServices.java class, I am calling
Authentication auth = SecurityContextHolder.getContext().getAuthentication() // Passed this auth to tokenStore
String token = tokenStore.getAccessToken(auth).getValue(); // NullPointerException
My code is going through JWTTokenStore provider which is returning null. I checked, there is a provider called InMemoryTokenStore.class which actually extrActs token from store. But my flow is not going into in memory implementation.
Is there any way I can get token afterwards without grabbing it in controller via arguments? or how can I enable/use inMemoryTokenStore?
Also recommend something better for kubernetes intercommunication authentication?
TIA
It looks like you're using Spring (and Spring Security), so I believe the relevant part of the docs is the part on Bearer Token Propagation.
Its recommendation is to use a WebClient (the recommended replacement for RestTemplate as of Spring 5) that uses the provided ServletBearerExchangeFilterFunction to automagically propagate the JWT token from the incoming request into the outgoing request:
#Bean
public WebClient rest() {
return WebClient.builder()
.filter(new ServletBearerExchangeFilterFunction())
.build();
}
On RestTemplate, the docs say:
"There is no dedicated support for RestTemplate at the moment, but you can achieve propagation quite simply with your own interceptor"
and the following example is provided:
#Bean
RestTemplate rest() {
RestTemplate rest = new RestTemplate();
rest.getInterceptors().add((request, body, execution) -> {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return execution.execute(request, body);
}
if (!(authentication.getCredentials() instanceof AbstractOAuth2Token)) {
return execution.execute(request, body);
}
AbstractOAuth2Token token = (AbstractOAuth2Token) authentication.getCredentials();
request.getHeaders().setBearerAuth(token.getTokenValue());
return execution.execute(request, body);
});
return rest;
}
I don't believe you need to be looking at TokenStores if all you're trying to do is propagate the token. Remember everything relevant about a JWT should be inside the token itself. (Which is why the doc for the JwtTokenStore explains that it doesn't actually store anything, but just pulls info out of the token, and will return null for some methods, including the getAccessToken() method you're calling.)

How to correctly handle pre-authentication failures in Spring Security?

I'm implementing an application that has a variety of ways to login, most importantly via a request header for pre-auth purposes and normal username/password. Currently everything works, but I've been requested to redirect the user to an error page if they attempted to use the pre-auth filter and it "failed".
From what I can tell, if pre-auth returns null it just assumes non-authed and continues the flow. I'm looking for a way to redirect or raise an exception, but I can't seem to hit my AuthenticationFailureHandler from the pre-auth filter. Is there a better way to do this? Do I need to hack something together that optionally treats the principal as an Exception. Should I be using a different authentication mechanism?
I'm by no means am Spring Security expert, so any help is welcome.
I cannot share the code due to contractual agreements, but here's an example:
#Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
Object principal = null;
// Try login stuff if token exists...
String token = request.getHeader("the-token-key");
if (token != null && !token.empty()) {
...
// If successful, correctly set principal
principal = ...;
// Else login failed, redirect or throw exception??
}
return principal;
}

Getting exception when requesting XSRF-TOKEN using Spring RestTemplate

I'm trying to call a service which has CSRF enabled and all it's endpoints are configured to request authentication header from the user.
I'm using Spring RestTemplate as follows:
ResponseEntity<String> responseEntity = getRestTemplate().exchange(
"localhost:9090/",
"HEAD",
entity,
String.class);
return responseEntity.getBody();
However, I'm not able to read the Headers from the response as I'm getting HTTP 401 error.
My workaround is to read the token from the exception that RestTemplate throws HttpClientErrorException. Like this:
exception.getResponseHeaders().get("Set-Cookie");
for (String header : headers) {
if (header.startsWith("XSRF-TOKEN")) {
token = header.split("=")[1];
break;
}
}
Is there any way to get XSRF-TOKEN token with out relying on reading it from the exception?
You are not getting an exception when accessing with GET method. Hence, I would create a get endpoint for retrieving the token and then use it for next POST calls.
Hope that approach makes sense.
the csrf only blocks requests of type post, put, delete ... that is, the get is free, therefore in order to obtain the token, first you have to make a request to a get method and extract the token from there that you would use to the next requests.
in case the token is not generated, add this to the configure of your security configuration:
http.csrf (). csrfTokenRepository (CookieCrsfTokenRepository.withHttpOnlyFalse) .any () ........
XSRF-TOKEN following spring specification is marker for header by default. So you should try get it in this way:
List tokenList = responseEntity.getHeaders().get("XSRF-TOKEN");
This collection consist of single element as usual, so first element should be your token.

Categories

Resources