Spring reactive web client REST request with oauth token in case of 401 response - spring-webflux

I wanted to play around with Spring reactive web client and an actually simple example: Ask for a REST resource and in case of a 401 response get new OAuth access token.
The first part seemed to be easy:
return webClientBuilder
.baseUrl(targetInstance.getBaseUrl())
.build()
.get().uri(targetInstance.getItemEndpointUrl())
.retrieve()
.bodyToMono(ItemResponse.class)
....
But here the confusion already started. I tried something like
.onStatus(HttpStatus::is4xxClientError, (response) -> {
if(response.rawStatusCode() == 401) {
oAuthClient.initToken()
My token should then be saved within an instance JPA entity. But I have a lack of conceptual understanding here I guess. When the OAuth client receives the OAuth response I need to extract it first to persist it (as embedded object) within my instance entity. And therefore I need to block it, right?
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
OAuthResponse oauthResponse = response.bodyToMono(OAuthResponse.class).block();
}
Based on the response result of the OAuth client I need some kind of Mono to tell the actual REST client then if it should start a retry? And which way should be the preferred on: .retrieve() or .exchangeToMono()? So I'm a bit lost here if I'm on the right path or if something like that should better be done with the classic RestTemplate? But I've also read that the RestTemplate is no deprecated...
Thanks for sharing some thoughts with me.

Ok, in the meantime I've found a non-blocking way. Maybe not the best, but it works out well for me.
The client:
class ApiClient {
public Mono<MyResponse> getResponse(Tenant tenant) {
return webClientBuilder
.baseUrl(tenant.getUrl())
.clientConnector(getClientConnector())
.build()
.get().uri("/api/my-content-entpoint")
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(MyResponse.class);
} else if(response.statusCode().equals(HttpStatus.FORBIDDEN)) {
return Mono.error(new MyOAuthExcpetion());
} else {
return Mono.empty();
}
});
}
}
the service:
#Service
public class MyService {
private final ApiClient apiClient;
private final RetryStrategy retryStrategy;
private final TenantService tenantService;
public Mono<MyResponse> getResponse(String tenantId){
return tenantService.getTenant(tenantId)
.flatMap(tenant-> apiClient.getResponse(instance))
.retryWhen(Retry.from(signals -> signals
.flatMap(retrySignal -> retryStrategy.reconnect(retrySignal, tenantId))));
}
}
and the retry strategy
#Component
public class RetryStrategy {
private final TenantService tenantService;
public Publisher<? extends Long> reconnect(RetrySignal retrySignal, String tenantId) {
long count = retrySignal.totalRetriesInARow();
Throwable failure = retrySignal.failure();
if(count > 0) {
return Mono.error(new UnsupportedOperationException("Retry failed", failure));
}
Mono<Tenant> updatedTenant = null;
if(failure instanceof MyOAuthExcpetion) {
updatedTenant = tenantService.getTenant(tenantId)
.flatMap(tenant -> tenantService.refreshOAuth(tenant));
}
if(updatedTenant == null) {
return Mono.error(new UnsupportedOperationException("Retry failed", failure));
}
return updatedTenant.then(Mono.delay(Duration.ofSeconds(1)));
}
}
Happy for any feedback or improvements.

In my application I went with prechecking the token before requests are being made:
client.get()
.uri("...")
.header("Authorization", "Bearer " + authenticator.getToken(client,token))
.retrieve()
...
And in Authenticator Service I verify the validity of the token as follow:
String getToken(WebClient client, String token) {
if (token == null || isTokenExpired(token)) {
return this.fetchToken(client); // fetches a new token
}
return token;
}
private boolean isTokenExpired(String token) {
DecodedJWT jwt = JWT.decode(token);
return jwt.getExpiresAt().before(new Date());
}

Related

"retrieveUser returned null - a violation of the interface contract" in springboot security when authentication

I am new to springboot security and i am trying write the signup function.My approach is to save the user and then pass the data to the autheicationmanager,but the went in here and it return null and the above error occur.
token service:
public String signup(JpaUser jpaUser) {
System.out.println(jpaUserrepository.findByUsername(jpaUser.getUsername()));
if(jpaUserrepository.findByUsername(jpaUser.getUsername()).isPresent())
{return "repeated username";}
JpaUser saveuser=jpaUserrepository.save(new JpaUser(jpaUser.getUsername(),
passwordEncoder.encode(jpaUser.getPassword()),jpaUser.getEmail(),jpaUser.getRoles()));
Authentication authentication= authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
saveuser.getUsername(),saveuser.getPassword()));
System.out.println(authentication);
String token=generateToken(authentication);
System.out.println(token);
return token;
}
securityconfig:
#Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) {
try{System.out.println(authConfig.getAuthenticationManager());
return authConfig.getAuthenticationManager();}catch(Exception exception){
System.out.println(exception);
return null;
}
}
generate token method:
public String generateToken (Authentication authentication){
Instant now=Instant.now();
String scope=authentication.getAuthorities().stream() //Stream<capture of ? extends GrantedAuthority >
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(" "));
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer(authentication.getName())
.issuedAt(now)
.expiresAt(now.plus(1, ChronoUnit.HOURS))
.claim("scope",scope).build();
return this.encoder.encode(JwtEncoderParameters.from((claims))).getTokenValue();
}

How do I hook into micronaut server on error handling from a filter?

For any 4xx or 5xx response given out by my micronaut server, I'd like to log the response status code and endpoint it targeted. It looks like a filter would be a good place for this, but I can't seem to figure out how to plug into the onError handling
for instance, this filter
#Filter("/**")
class RequestLoggerFilter: OncePerRequestHttpServerFilter() {
companion object {
private val log = LogManager.getLogger(RequestLoggerFilter::class.java)
}
override fun doFilterOnce(request: HttpRequest<*>, chain: ServerFilterChain): Publisher<MutableHttpResponse<*>>? {
return Publishers.then(chain.proceed(request), ResponseLogger(request))
}
class ResponseLogger(private val request: HttpRequest<*>): Consumer<MutableHttpResponse<*>> {
override fun accept(response: MutableHttpResponse<*>) {
log.info("Status: ${response.status.code} Endpoint: ${request.path}")
}
}
}
only logs on a successful response and not on 4xx or 5xx responses.
How would i get this to hook into the onError handling?
You could do the following. Create your own ApplicationException ( extends RuntimeException), there you could handle your application errors and in particular how they result into http error codes. You exception could hold the status code as well.
Example:
class BadRequestException extends ApplicationException {
public HttpStatus getStatus() {
return HttpStatus.BAD_REQUEST;
}
}
You could have multiple of this ExceptionHandler for different purposes.
#Slf4j
#Produces
#Singleton
#Requires(classes = {ApplicationException.class, ExceptionHandler.class})
public class ApplicationExceptionHandler implements ExceptionHandler<ApplicationException, HttpResponse> {
#Override
public HttpResponse handle(final HttpRequest request, final ApplicationException exception) {
log.error("Application exception message={}, cause={}", exception.getMessage(), exception.getCause());
final String message = exception.getMessage();
final String code = exception.getClass().getSimpleName();
final ErrorCode error = new ErrorCode(message, code);
log.info("Status: ${exception.getStatus())} Endpoint: ${request.path}")
return HttpResponse.status(exception.getStatus()).body(error);
}
}
If you are trying to handle Micronaut native exceptions like 400 (Bad Request) produced by ConstraintExceptionHandler you will need to Replace the beans to do that.
I've posted example here how to handle ConstraintExceptionHandler.
If you want to only handle responses itself you could use this mapping each response code (example on #Controller so not sure if it works elsewhere even with global flag:
#Error(status = HttpStatus.NOT_FOUND, global = true)
public HttpResponse notFound(HttpRequest request) {
<...>
}
Example from Micronaut documentation.
Below code I used for adding custom cors headers in the error responses, in doOnError you can log errors
#Filter("/**")
public class ResponseCORSAdder implements HttpServerFilter {
#Override
public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, ServerFilterChain chain) {
return this.trace(request)
.switchMap(aBoolean -> chain.proceed(request))
.doOnError(error -> {
if (error instanceof MutableHttpResponse<?>) {
MutableHttpResponse<?> res = (MutableHttpResponse<?>) error;
addCorsHeaders(res);
}
})
.doOnNext(res -> addCorsHeaders(res));
}
private MutableHttpResponse<?> addCorsHeaders(MutableHttpResponse<?> res) {
return res
.header("Access-Control-Allow-Origin", "*")
.header("Access-Control-Allow-Methods", "OPTIONS,POST,GET")
.header("Access-Control-Allow-Credentials", "true");
}
private Flowable<Boolean> trace(HttpRequest<?> request) {
return Flowable.fromCallable(() -> {
// trace logic here, potentially performing I/O
return true;
}).subscribeOn(Schedulers.io());
}
}

Handling Global Scenarios in Spring WebFlux

I have a Rest Web Client todo an API call and I handle the exceptions as given below.
I want to handle 404, 401 and 400 errors in a global way rather than handling at the individual client level. How can we achieve the same.
public Mono<ProductResponse> getProductInformation(String productId) {
return webClient.get()
.uri("/v1/products/"+productId)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus( httpStatus -> HttpStatus.NOT_FOUND.equals(httpStatus), clientResponse -> {
Mono<NotFound> notFound = clientResponse.bodyToMono(NotFound.class);
return notFound.flatMap( msg -> {
log.info(" Error Message {}" , msg.getErrorMsg());
return Mono.error(new NotFoundException(msg.getErrorMsg()));
});
}).onStatus( httpStatus -> HttpStatus.UNAUTHORIZED.equals(httpStatus), clientResponse -> {
Mono<NotFound> notFound = clientResponse.bodyToMono(NotFound.class);
return Mono.error(new NotAuthorisedException("Unauthorised"));
}).bodyToMono(ProductResponse.class);
}
Two approaches:
Exceptions with webclients are all wrapped in WebClientResponseException class. You can handle that using Spring's ExceptionHandler annotation like this.
#ExceptionHandler(WebClientResponseException.class)
public ResponseEntity handleWebClientException(WebClientResponseException ex){
return ResponseEntity.badRequest().body(ex.getResponseBodyAsString());
}
Note - Here you can write complex conditional logic based on the response status, by using methods like getStatusCode(), getRawStatusCode(), getStatusText(), getHeaders() and getResponseBodyAsString(). Also you can get reference of the request that was sent using the method getRequest.
Using ExchangeFilterFunction while constructing the webclient bean.
#Bean
public WebClient buildWebClient() {
Function<ClientResponse, Mono<ClientResponse>> webclientResponseProcessor =
clientResponse -> {
HttpStatus responseStatus = clientResponse.statusCode();
if (responseStatus.is4xxClientError()) {
System.out.println("4xx error");
return Mono.error(new MyCustomClientException());
} else if (responseStatus.is5xxServerError()) {
System.out.println("5xx error");
return Mono.error(new MyCustomClientException());
}
return Mono.just(clientResponse);
};
return WebClient.builder()
.filter(ExchangeFilterFunction.ofResponseProcessor(webclientResponseProcessor)).build();
}
Then you can either handle the MyCustomClientException using #ExceptionHandler or leave it as it is.

Service Stack - Custom authentication on one route

In my current application, I am using Service Stack with JWT's for security. Security has been implemented and works perfectly. Trouble is, I would like to secure one route differently from the others. There is a document the logged in user retrieves, I want to make sure the document they are retrieving is theirs and not someone else's. It is very sensitive data. I would like to secure it differently because something like PostMan could be used with a valid token to retrieve any document, I want to prevent this. The users id is in the token, I would like to match it against the document that is being retrieved if possible. The current security is implemented like so:
public class AppHost: AppHostBase
{
public override void Configure(Funq.Container container)
{
Plugins.Add(new AuthFeature(() => new AuthUserSession(),
new IAuthProvider[] {
new JsonWebTokenAuthProvider("myKey", "myAudience"),
}));
}
}
JsonWebTokenAuthProvider is a custom class where security was implemented, this all works perfectly. Here is the code:
public override object Authenticate(IServiceBase authService, IAuthSession session, Authenticate request)
{
// first validate the token, then get roles from session
string header = request.oauth_token;
// if no auth header, 401
if (string.IsNullOrEmpty(header))
{
throw HttpError.Unauthorized(MissingAuthHeader);
}
string[] headerData = header.Split(' ');
// if header is missing bearer portion, 401
if (!string.Equals(headerData[0], "BEARER", StringComparison.OrdinalIgnoreCase))
{
throw HttpError.Unauthorized(InvalidAuthHeader);
}
// swap - and _ with their Base64 string equivalents
string secret = SymmetricKey.Replace('-', '+').Replace('_', '/');
string token = headerData[1].Replace("\"", "");
// set current principal to the validated token principal
Thread.CurrentPrincipal = JsonWebToken.ValidateToken(token, secret, Audience, true, Issuer);
string lanId = GetLanID(Thread.CurrentPrincipal.Identity.Name);
string proxyAsLanId = request.Meta.ContainsKey(META_PROXYID) ? request.Meta[META_PROXYID] : null;
if (HttpContext.Current != null)
{
// set the current request's user the the decoded principal
HttpContext.Current.User = Thread.CurrentPrincipal;
}
// set the session's username to the logged in user
session.UserName = Thread.CurrentPrincipal.Identity.Name;
session.Roles = GetApplicableRoles(lanId, proxyAsLanId);
authService.Request.SetItem("lanID", lanId);
authService.Request.SetItem("proxyAsLanId", proxyAsLanId);
return OnAuthenticated(authService, session, null, null);
}
I looked up RequestFilterAttribute found here, but I do not think that is what I want. Ideally, if the check fails I would like to return a 401 (unauthorized) if possible.
What is the best way to do this?
If you just want to handle one route differently than you can just add the validation in your single Service, e.g:
public object Any(MyRequest dto)
{
var lanId = base.Request.GetItem("lanId");
if (!MyIsValid(lanId))
throw HttpError.Unauthorized("Custom Auth Validation failed");
}
You could do the same in a RequestFilter, e.g:
public class CustomAuthValidationAttribute : RequestFilterAttribute
{
public override void Execute(IRequest req, IResponse res, object responseDto)
{
var lanId = req.GetItem("lanId");
if (!MyIsValid(lanId))
{
res.StatusCode = (int) HttpStatusCode.Unauthorized;
res.StatusDescription = "Custom Auth Validation failed";
res.EndRequest();
}
}
}
And apply it to a single Service:
[CustomAuthValidation]
public object Any(MyRequest dto)
{
//...
}
Or a collection of Services, e.g:
[CustomAuthValidation]
public class MyAuthServices : Service
{
public object Any(MyRequest1 dto)
{
//...
}
public object Any(MyRequest2 dto)
{
//...
}
}

nextToken() not consuming token from stream?

I am able to generate the following RuntimeException and am unsure why. My understanding is that nextToken() advances the underlying stream. The offending token is a normal token in the middle of a document I am parsing, not something strange like EOF.
class TokenStreamRelease implements TokenStream
{
private final MyLexer _lexer; // MyLexer extends org.antlr.v4.runtime.Lexer
Token _nextToken;
#Override
public Token next()
{
Token toReturn = _nextToken;
_nextToken = _lexer.nextToken();
if(_nextToken == _lexer.getToken())
{
throw new RuntimeException();
}
return toReturn;
}
}
Your code basically says: if the current token is equal to the current token, throw a RuntimeException.
Lexer.getToken is defined as:
public Token getToken() { return _token; }
And Lexer.nextToken updates and returns _token.
So calling getToken right after nextToken will get you the same token as a result.