Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring boot ClientHttpRequestInterceptor resend on 401

So i have below scenario to implement using Spring boot rest template to consume a REST-API (involves token authentication mechanism). To perform test i've created simple mock REST API in spring boot. Here's the process,

From my API consumer app,

  • sends a request using rest-template to consume a protected API, this API requires Authorization: Bearer <token> header to be present in request.
  • if something is wrong with this token (missing header, invalid token), protected API returns HTTP-Unauthorized (401).
  • when this happens, consumer API should send another request to another protected API that returns a valid access token, this protected API requires Authorization: Basic <token> header to be present. New access token will be stored in a static field and it will be used in all other requests to authenticate.

This can be achieved by simply catching 401-HttpClientErrorException in RestTemplate consumer methods (postForObject), but the idea was to decouple it from REST-API consumer classes. To achieve it, i tried to use ClientHttpRequestInterceptor

Here's the code, that i tried so far.

Interceptor class

public class AuthRequestInterceptor implements ClientHttpRequestInterceptor {

private static final Logger LOGGER = LoggerFactory.getLogger(AuthRequestInterceptor.class);
private static final String BASIC_AUTH_HEADER_PREFIX = "Basic ";
private static final String BEARER_AUTH_HEADER_PREFIX = "Bearer ";

//stores access token
private static String accessToken = null;

@Value("${app.mife.apiKey}")
private String apiKey;

@Autowired
private GenericResourceIntegration resourceIntegration; // contains methods of rest template

@Override
public ClientHttpResponse intercept(
        HttpRequest request,
        byte[] body,
        ClientHttpRequestExecution execution
) throws IOException {
    LOGGER.info("ReqOn|URI:[{}]{}, Headers|{}, Body|{}", request.getMethod(), request.getURI(), request.getHeaders(), new String(body));
    request.getHeaders().add(ACCEPT, APPLICATION_JSON_VALUE);
    request.getHeaders().add(CONTENT_TYPE, APPLICATION_JSON_VALUE);
    try {
        //URI is a token generate URI, request
        if (isBasicUri(request)) {
            request.getHeaders().remove(AUTHORIZATION);
            //sets BASIC auth header
            request.getHeaders().add(AUTHORIZATION, (BASIC_AUTH_HEADER_PREFIX + apiKey));
            ClientHttpResponse res = execution.execute(request, body);
            LOGGER.info("ClientResponse:[{}], status|{}", "BASIC", res.getStatusCode());
            return res;
        }

        //BEARER URI, protected API access
        ClientHttpResponse response = null;
        request.getHeaders().add(AUTHORIZATION, BEARER_AUTH_HEADER_PREFIX + getAccessToken());
        response = execution.execute(request, body);
        LOGGER.info("ClientResponse:[{}], status|{}", "BEARER", response.getStatusCode());

        if (unauthorized(response)) {
            LOGGER.info("GetToken Res|{}", response.getStatusCode());
            String newAccessToken = generateNewAccessCode();
            request.getHeaders().remove(AUTHORIZATION);
            request.getHeaders().add(AUTHORIZATION, (BEARER_AUTH_HEADER_PREFIX + newAccessToken));
            LOGGER.info("NewToken|{}", newAccessToken);
            return execution.execute(request, body);
        }

        if (isClientError(response) || isServerError(response)) {
            LOGGER.error("Error[Client]|statusCode|{}, body|{}", response.getStatusCode(), CommonUtills.streamToString(response.getBody()));
            throw new AccessException(response.getStatusText(),
                    ServiceMessage.error().code(90).payload(response.getRawStatusCode() + ":" + response.getStatusText()).build());
        }

        return response;
    } catch (IOException exception) {
        LOGGER.error("AccessError", exception);
        throw new AccessException("Internal service call error",
                ServiceMessage.error().code(90).payload("Internal service call error", exception.getMessage()).build()
        );
    } finally {
        LOGGER.info("ReqCompletedOn|{}", request.getURI());
    }
}

private String generateNewAccessCode() {
    Optional<String> accessToken = resourceIntegration.getAccessToken();
    setAccessToken(accessToken.get());
    return getAccessToken();
}

private static void setAccessToken(String token) {
    accessToken = token;
}

private static String getAccessToken() {
    return accessToken;
}

private boolean isClientError(ClientHttpResponse response) throws IOException {
    return (response.getRawStatusCode() / 100 == 4);
}

private boolean isServerError(ClientHttpResponse response) throws IOException {
    return (response.getRawStatusCode() / 100 == 5);
}

private boolean unauthorized(ClientHttpResponse response) throws IOException {
    return (response.getStatusCode().value() == HttpStatus.UNAUTHORIZED.value());
}

private boolean isBasicUri(HttpRequest request) {
    return Objects.equals(request.getURI().getRawPath(), "/apicall/token");
}

private boolean isMifeRequest(HttpRequest request) {
    return request.getURI().toString().startsWith("https://api.examplexx.com/");
}

}

Token generate method- In resourceIntegration

public Optional<String> getAccessToken() {
    ResponseEntity<AccessTokenResponse> res = getRestTemplate().exchange(
            getAccessTokenGenUrl(),
            HttpMethod.POST,
            null,
            AccessTokenResponse.class
    );
    if (res.hasBody()) {
        LOGGER.info(res.getBody().toString());
        return Optional.of(res.getBody().getAccess_token());
    } else {
        return Optional.empty();
    }
}

Another sample protected API call method

public Optional<String> getMobileNumberState(String msisdn) {
    try {
        String jsonString = getRestTemplate().getForObject(
                getQueryMobileSimImeiDetailsUrl(),
                String.class,
                msisdn
        );
        ObjectNode node = new ObjectMapper().readValue(jsonString, ObjectNode.class);
        if (node.has("PRE_POST")) {
            return Optional.of(node.get("PRE_POST").asText());
        }
        LOGGER.debug(jsonString);
    } catch (IOException ex) {
        java.util.logging.Logger.getLogger(RestApiConsumerService.class.getName()).log(Level.SEVERE, null, ex);
    }
    return Optional.empty();
}

Problem

Here's the log of mock API,

//first time no Bearer token, this returns 401 for API /simulate/unauthorized
accept:text/plain, application/json, application/*+json, */*
authorization:Bearer null
/simulate/unauthorized


//then it sends Basic request to get a token, this is the log
accept:application/json, application/*+json
authorization:Basic M3ZLYmZQbE1ERGhJZWRHVFNiTEd2Vlh3RThnYTp4NjJIa0QzakZUcmFkRkVOSEhpWHNkTFhsZllh
Generated Token:: 57f21374-1188-4c59-b5a7-370eac0a0aed
/apicall/token


//finally consumer API sends the previous request to access protected API and it contains newly generated token in bearer header
accept:text/plain, application/json, application/*+json, */*
authorization:Bearer 57f21374-1188-4c59-b5a7-370eac0a0aed
/simulate/unauthorized

The problem is even-though mock API log had the correct flow, consumer API does not get any response for third call, here's the log of it (unnecessary logs are omitted).

RequestInterceptor.intercept() - ReqOn|URI:[GET]http://localhost:8080/simulate/unauthorized?x=GlobGlob, Headers|{Accept=[text/plain, application/json, application/*+json, */*], Content-Length=[0]}, Body|
RequestInterceptor.intercept() - ClientResponse:[BEARER], status|401 UNAUTHORIZED

RequestInterceptor.intercept() - GetToken Res|401 UNAUTHORIZED
RequestInterceptor.intercept() - ReqOn|URI:[POST]http://localhost:8080/apicall/token?grant_type=client_credentials, Headers|{Accept=[application/json, application/*+json], Content-Length=[0]}, Body|
RequestInterceptor.intercept() - ClientResponse:[BASIC], status|200 OK
RequestInterceptor.intercept() - ReqCompletedOn|http://localhost:8080/apicall/token?grant_type=client_credentials

RestApiConsumerService.getAccessToken() - |access_token2163b0d4-8d00-4eba-92d0-7e0bb609b982,scopeam_application_scope default,token_typeBearer,expires_in34234|
RequestInterceptor.intercept() - NewToken|2163b0d4-8d00-4eba-92d0-7e0bb609b982
RequestInterceptor.intercept() - ReqCompletedOn|http://localhost:8080/simulate/unauthorized?x=GlobGlob

http://localhost:8080/simulate/unauthorized third time does not return any response, but mock API log says it hit the request. What did i do wrong ?, is it possible to achieve this task using this techniques ? or is there any other alternative way to do this ? any help is highly appreciated.

like image 215
benjamin c Avatar asked Mar 01 '19 08:03

benjamin c


People also ask

What is ClientHttpRequestInterceptor spring boot?

Interface ClientHttpRequestInterceptor This is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference. @FunctionalInterface public interface ClientHttpRequestInterceptor. Intercepts client-side HTTP requests.

How do I intercept a spring boot request?

To work with interceptor, you need to create @Component class that supports it and it should implement the HandlerInterceptor interface. preHandle() method − This is used to perform operations before sending the request to the controller. This method should return true to return the response to the client.

What is OAuth2RestTemplate?

The main goal of the OAuth2RestTemplate is to reduce the code needed to make OAuth2-based API calls. It basically meets two needs for our application: Handles the OAuth2 authentication flow. Extends Spring RestTemplate for making API calls.

Is spring rest template deprecated?

RestTemplate provides a synchronous way of consuming Rest services, which means it will block the thread until it receives a response. RestTemplate is deprecated since Spring 5 which means it's not really that future proof. First, we create a Spring Boot project with the spring-boot-starter-web dependency.


1 Answers

I have tried this:

Add an interceptor ClientHttpRequestInterceptor

    import java.io.IOException;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.http.HttpRequest;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.client.ClientHttpRequestExecution;
    import org.springframework.http.client.ClientHttpRequestInterceptor;
    import org.springframework.http.client.ClientHttpResponse;
    import org.springframework.util.StringUtils;






    public class RequestResponseHandlerInterceptor implements ClientHttpRequestInterceptor {


        @Autowired
        private TokenService tokenService;

        @Autowired
        private RedisTemplate<String, String> redisTemplate;

        private static final String AUTHORIZATION = "Authorization";

        /**
         * This method will intercept every request and response and based on response status code if its 401 then will retry 
         * once
         */

        @Override
        public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
            ClientHttpResponse response = execution.execute(request, body);
            if (HttpStatus.UNAUTHORIZED == response.getStatusCode()) {
                String accessToken = tokenService.getAccessToken();
                if (!StringUtils.isEmpty(accessToken)) {
                    request.getHeaders().remove(AUTHORIZATION);
                    request.getHeaders().add(AUTHORIZATION, accessToken);
//retry                    
response = execution.execute(request, body);
                }
            }
            return response;
        }

    }

Apart from this you need to override RestTemplate initialization as well.

@Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setInterceptors(Collections.singletonList(new RequestResponseHandlerInterceptor()));
        return restTemplate;
    }
like image 185
Swapnil Srivastav Avatar answered Oct 10 '22 11:10

Swapnil Srivastav