I am currently building a feign client manually and passing Interceptors to it for authorization. I would like to have a smarter Retryer for some Response code.
public class myErrorEncoder extends ErrorDecoder.Default {
#Override
public Exception decode(final String methodKey, final Response response) {
if (response.status() == 401) {
String token = refreshToken(); // I would like to refresh the token and Edit the client
return new RetryableException("Token Expired will retry it", null);
} else {
return super.decode(methodKey, response);
}
}
}
Interceptor
#Bean public CustomInterceptor getInterceptor(String token) {
return new CustomInterceptor(token);}
Feign builder
private <T> T feignBuild(final Class<T> clazz, final String uri, final String token) {
return Feign
.builder().client(new ApacheHttpClient())
.encoder(new GsonEncoder())
.decoder(new ResponseEntityDecoder(feignDecoder())
.retryer(new Retryer.Default(1,100,3))
.errorDecoder(new ErrorDecoder())
.requestInterceptor(getInterceptor(token))
.contract(new ClientContract())
.logger(new Slf4jLogger(clazz)).target(clazz, uri);
}
Now I would like to update feign client with the refreshed token and retry.
Is there a way get access to the client instance and configure it.
Your use of the interceptor is incorrect. Interceptors are re-applied during a retry, but they are instantiated only once and are expected to be thread safe. To achieve what you are looking for will need to separate the token generation from the interceptor and have the interceptor request a new token.
public class TokenInterceptor() {
TokenService tokenService;
public TokenInterceptor(TokenService tokenService) {
this.tokenService = tokenService;
}
public void apply(RequestTemplate template) {
/* getToken() should create a new token */
String token = this.tokenService.getToken();
template.header("Authorization", "Bearer " + token);
}
}
This will ensure that a new token is created each retry cycle.
Related
Background
My use case is to authenticate the calls using a Gateway. I assume that all the calls must have user_id and a token. I have an API that will take both user_id, token and return a boolean after checking that it's a valid request or not.
Current Work
I have written a Gateway configuration which is working fine in routing the APIs to concerned micro-service but it's not working fine for authentication.
working code,
#Configuration
public class RouteConfig {
#Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route", r -> r.path("app1/**")
.filters(f -> f.rewritePath("app1/(?<segment>.*)",
"app1/${segment}"))
.uri("http://localhost:8099"))
.build();
}
}
Firsly , I need to get the Headers variables and then make a POST call to an API which returns BOOLEAN response. If its true then the above code should call. Otherwise the call should be rejected.
Tried to get Headers value
#Configuration
public class RouteConfig {
#Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder,#RequestHeader("user_id")
String userId,#RequestHeader("token")
String token) {
System.out.println(userId);
System.out.println(token);
return builder.routes()
.route("path_route", r -> r.path("app1/**")
.filters(f -> f.rewritePath("app1/(?<segment>.*)",
"app1/${segment}"))
.uri("http://localhost:8099"))
.build();
}
}
But its not working and application crashed.
You can implement a GatewayFilter to check each request on your gateway.
#Component
public class AuthFilter implements GatewayFilter {
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
if (!headers.containsKey("user_id") || !headers.containsKey("token"))
return this.onError(exchange, HttpStatus.UNAUTHORIZED);
String userId = request.getHeaders().getOrEmpty("user_id").get(0);
String token = request.getHeaders().getOrEmpty("token").get(0);
Map<String, String> body = new HashMap<>();
body.put("user_id", userId);
body.put("token", token);
WebClient client = WebClient.builder().build();
boolean ok = client
.post()
.uri("your/auth/api")
.body(Mono.just(body), Map.class)
.retrieve()
.bodyToFlux(Boolean.class)
.blockFirst();
if (!ok)
return this.onError(exchange, HttpStatus.UNAUTHORIZED);
return chain.filter(exchange);
}
private Mono<Void> onError(ServerWebExchange exchange, HttpStatus httpStatus) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
return response.setComplete();
}
}
and apply the filter to your route configuration. Not sure if the configuration is correct and the rewrite is applied correctly.
#Configuration
public class RouteConfig {
#Autowired
private AuthFilter filter;
#Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route", r -> r.path("app1/**")
.filters(f -> f.filters(filter).rewritePath("app1/(?<segment>.*)",
"app1/${segment}"))
.uri("http://localhost:8099"))
.build();
}
}
Note that this code is untested, just to give you an idea how you may solve your issue.
I am using spring feign client for making http requests.
Fiegn configuration class
DefaultConfig.class
public class DefaultConfig {
#Bean
public OkHttpClient client() {
return new OkHttpClient();
}
#Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
#Bean
public Decoder feignDecoder() {
return new JacksonDecoder();
}
#Bean
public Encoder feignEncoder() {
return new JacksonEncoder();
}
#Bean
public RequestInterceptor requestInterceptor() {
return template -> {
template.header(Authorization, apiKey);
};
}
}
My client interface:
#FeignClient(name = "default", url = "${base-url}",
configuration = DefaultConfig.class)
public interface {
#PostMapping(value = "/users/")
Response createUser(#RequestBody Map<String, ?> requestBody);
#GetMapping(value = "/users/{id}")
Response getUserDetails(#PathVariable String id);
}
The thing is now I need to use different authorization key for GET and POST request. In the configuration class I need to differentiate the request by url called. How can we get the url the requester interceptor or any other way we can achieve this. We can create separate interceptor for this but I try to use the same interceptor for both cases.
Everything you need seems to be in the template object.
#Bean
public RequestInterceptor requestInterceptor() {
return template -> {
if ("GET".equals(template.method()) {
template.header(Authorization, apiKey);
} else {
template.header(Authorization, differentApiKey);
}
};
}
Everything you need seems to be in the code below.
#Bean.
public RequestInterceptor requestInterceptor() {
return template -> {
template.request().url() // this is request url which is from feign!
template.requsst().httpMethod() // this is request method!
};
}
I have Eureka and connected services Zuul:8090, AuthService:[any_port].
I send ../login request to Zuul he send to AuthSercice. Then AuthSerice put into Header JWT Authentication.
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
String token = Jwts.builder()
.setSubject( ((User) authResult.getPrincipal()).getUsername())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
response.addHeader("Authorization", "Bearer "+ token); // this is missing
response.addHeader("Authorization2", "Bearer " + token); // ok
}
I do request on Postman. Request result
First I tried to use JWT in Monoliths. There wasn't any problem, and Authorization Token can be added.
Why is Authorization Header missing?
It is because of a built-in mechanism in Zuul -- it automatically filters out sensitive headers, such as Authorization and Cookies, to protect sensitive information from being forwarded to downstream services.
That is why you can not get the header with the name Authorization.
if you want your downstream services to receive them anyway, just define the filter by yourself in your Zuul config file, instead of using default.
zuul:
routes:
users:
path: your url pattern
sensitiveHeaders: //put nothing here!! leave it blank, the filter will be off
url: downstream url
Here is spring official explanation on sensitive headers: document
You need set the option for forwarding headers in Eureka.
For Login I would suggest to have a custom ZuulFilter.
public abstract class AuthenticationZuulFilter extends ZuulFilter {
private static final Log logger = getLog(AuthenticationZuulFilter.class);
private static final String BEARER_TOKEN_TYPE = "Bearer ";
private static final String PRE_ZUUL_FILTER_TYPE = "pre";
private AuthTokenProvider tokenProvider;
public AuthenticationZuulFilter(AuthTokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
#Override
public Object run() {
RequestContext ctx = getCurrentContext();
ctx.addZuulRequestHeader(X_USER_INFO_HEADER_NAME, buildUserInfoHeaderFromAuthentication());
ctx.addZuulRequestHeader(AUTHORIZATION, BEARER_TOKEN_TYPE + tokenProvider.getToken());
return null;
}
#Override
public String filterType() {
return PRE_ZUUL_FILTER_TYPE;
}
#Override
public int filterOrder() {
return 1;
}
This is an implementation of it can be like this.
#Component
public class UserAuthenticationZuulFilter extends AuthenticationZuulFilter {
#Value("#{'${user.allowed.paths}'.split(',')}")
private List<String> allowedPathAntPatterns;
private PathMatcher pathMatcher = new AntPathMatcher();
#Autowired
public UserAuthenticationZuulFilter (AuthTokenProvider tokenProvider) {
super(tokenProvider);
}
#Override
public boolean shouldFilter() {
Authentication auth = getContext().getAuthentication();
HttpServletRequest request = getCurrentContext().getRequest();
String requestUri = request.getRequestURI();
String requestMethod = request.getMethod();
return auth instanceof UserAuthenticationZuulFilter && GET.matches(requestMethod) && isAllowedPath(requestUri);
}
}
I have a microservice architecture, both of them securized by spring security an JWT tokens.
So, when I call my first microservice, I want to take the JWT token and send a request to another service using those credentials.
How can I retrieve the token and sent again to the other service?
Basically your token should be located in the header of the request, like for example: Authorization: Bearer . For getting it you can retrieve any header value by #RequestHeader() in your controller:
#GetMapping("/someMapping")
public String someMethod(#RequestHeader("Authorization") String token) {
}
Now you can place the token within the header for the following request:
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", token);
HttpEntity<RestRequest> entityReq = new HttpEntity<RestRequest>(request, headers);
Now you can pass the HttpEntity to your rest template:
template.exchange("RestSvcUrl", HttpMethod.POST, entityReq, SomeResponse.class);
Hope I could help
I've accomplished the task, creating a custom Filter
public class RequestFilter implements Filter{
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(RequestContext.REQUEST_HEADER_NAME);
if (token == null || "".equals(token)) {
throw new IllegalArgumentException("Can't retrieve JWT Token");
}
RequestContext.getContext().setToken(token);
chain.doFilter(request, response);
}
#Override
public void destroy() { }
#Override
public void init(FilterConfig arg0) throws ServletException {}
}
Then, setting in my config
#Bean
public FilterRegistrationBean getPeticionFilter() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new RequestFilter());
registration.addUrlPatterns("/*");
registration.setName("requestFilter");
return registration;
}
With that in mind, I've create another class with a ThreadLocal variable to pass the JWT token from the Controller to the Rest Templace interceptor
public class RequestContext {
public static final String REQUEST_HEADER_NAME = "Authorization";
private static final ThreadLocal<RequestContext> CONTEXT = new ThreadLocal<>();
private String token;
public static RequestContext getContext() {
RequestContext result = CONTEXT.get();
if (result == null) {
result = new RequestContext();
CONTEXT.set(result);
}
return result;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}
public class RestTemplateInterceptor implements ClientHttpRequestInterceptor{
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
String token = RequestContext.getContext().getToken();
request.getHeaders().add(RequestContext.REQUEST_HEADER_NAME, token);
return execution.execute(request, body);
}
}
Add interceptor to the config
#PostConstruct
public void addInterceptors() {
List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
interceptors.add(new RestTemplateInterceptor());
restTemplate.setInterceptors(interceptors);
}
I think it is better to add the interceptor specifically to the RestTemplate, like this:
class RestTemplateHeaderModifierInterceptor(private val authenticationService: IAuthenticationService) : ClientHttpRequestInterceptor {
override fun intercept(request: org.springframework.http.HttpRequest, body: ByteArray, execution: ClientHttpRequestExecution): ClientHttpResponse {
if (!request.headers.containsKey("Authorization")) {
// don't overwrite, just add if not there.
val jwt = authenticationService.getCurrentUser()!!.jwt
request.headers.add("Authorization", "Bearer $jwt")
}
val response = execution.execute(request, body)
return response
}
}
And add it to the RestTemplate like so:
#Bean
fun restTemplate(): RestTemplate {
val restTemplate = RestTemplate()
restTemplate.interceptors.add(RestTemplateHeaderModifierInterceptor(authenticationService)) // add interceptor to send JWT along with requests.
return restTemplate
}
That way, every time you need a RestTemplate you can just use autowiring to get it. You do need to implement the AuthenticationService still to get the token from the TokenStore, like this:
val details = SecurityContextHolder.getContext().authentication.details
if (details is OAuth2AuthenticationDetails) {
val token = tokenStore.readAccessToken(details.tokenValue)
return token.value
}
May be a little bit late but I think this is a common question, regarding
Spring Security 6.0.0 for web client there is a class called ServletBearerExchangeFilterFunction that you can use to read the token from the security context and inject it.
#Bean
public WebClient rest() {
return WebClient.builder()
.filter(new ServletBearerExchangeFilterFunction())
.build();
For RestTemplate there is no automatic way and is recommended use a filter
#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 have an Java socket API application, that handles socket requests from users and sends responses.
I have a configurer:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
private static final Logger LOGGER = Logger.getLogger(WebSocketConfig.class);
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/queue");
config.setApplicationDestinationPrefixes("/server_in");
config.setUserDestinationPrefix("/user");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/websocket").withSockJS();
}
}
When i send response to user i do the following:
this.simpMessagingTemplate.convertAndSend("/queue/private/user_"+secret_key, socketResponse);
On client i have the following code:
sc.subscribe('/queue/private/user_'+secret_key, function (greeting) {
console.log(greeting.body);
});
And the response is handled successfully.
But the problem is that some other user can also subscribe to "/queue/private/*" destination and handle private messages.
sc.subscribe('/queue/private/*', function (greeting) {
console.log(greeting.body);
});
How can I privent that behaviour?
If you want each user to have a socket and only get him messages, what you can do is :
Subscribe as you do to the endPoint but with "/user" infront for example
sc.subscribe('/user/queue/websocket, function (greeting) {
console.log(greeting.body);
});
and at the server side you should have a rest method:
#RequestMapping(value = "/test", method = RequestMethod.POST)
public void test(Principal principal) throws Exception {
this.template.convertAndSendToUser(principal.getName(), "/queue/click", "");
}
With this every user subscibers to each channel and only the user is notified about, when a rest call is made.
The rest call should be authenticated so the Principal has the username.
The user channel is auto managed from Spring so you have to add it like this.
You can extend ChannelInterceptorAdapter and manage each event individually:
public class AuthorizedChannelInterceptorAdapter extends ChannelInterceptorAdapter {
#Override
public Message<?> preSend(Message<?> message, MessageChannel messageChannel) throws AuthenticationException {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT == accessor.getCommand())
setUserAuthenticationToken(accessor);
else if (StompCommand.SUBSCRIBE == accessor.getCommand())
validateSubscription((Authentication) accessor.getUser(), accessor.getDestination());
return message;
}
private void setUserAuthenticationToken(StompHeaderAccessor accessor) {
String token = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION);
accessor.setUser(loadAuthentication(token));
}
private Authentication loadAuthentication(String token){
return ....;
}
private void validateSubscription(Authentication authentication, String destination) {
if(...)
throw new AccessDeniedException("No permission to subscribe to this topic");
}
}
First of all you will need to store the authentication object provided by the client in connection event. After this, each event sent by the client will have this authentication object set so you can use it to validate if it is authorized to subscribe to a particular channel.