Propagate HTTP header (JWT Token) over services using spring rest template - java

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;
}

Related

Spring Rest Forbidden Access when hit Exception

I'm using Spring Boot 3.0. The authorization just works as expected but when it hit SomeException like MethodArgumentNotValidException it just only show 403 Forbidden Access with empty body. Before I'm using Spring Boot 3.0 everything just work as I'm expected when hitting Exception like they give me the exception JSON result.
SecurityConfiguration
#Configuration
#EnableWebSecurity
public class SecurityConfiguration {
#Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {
var secret = System.getProperty("app.secret");
var authorizationFilter = new AuthorizationFilter(secret);
var authenticationFilter = new AuthenticationFilter(secret, authenticationManager);
authenticationFilter.setFilterProcessesUrl("/login");
authenticationFilter.setPostOnly(true);
return http
.cors().and()
.csrf((csrf) -> csrf.disable())
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/login/**", "/trackers/camera/**").permitAll()
.requestMatchers("/sites/**").hasAnyRole(Role.OWNER.name())
.anyRequest().authenticated()
)
.addFilter(authenticationFilter)
.addFilterBefore(authorizationFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
#Bean
public CorsConfigurationSource corsConfigurationSource() {
var config = new CorsConfiguration();
var all = Arrays.asList("*");
config.setAllowedOrigins(all);
config.setAllowedHeaders(all);
config.setAllowedMethods(all);
config.setExposedHeaders(all);
var source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
AuthenticationFilter
#RequiredArgsConstructor
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final String secretToken;
private final AuthenticationManager authenticationManager;
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username, password;
try {
var requestMap = new ObjectMapper().readValue(request.getInputStream(), LoginRequest.class);
username = requestMap.getUsername();
password = requestMap.getPassword();
} catch (Exception e) {
throw new AuthenticationServiceException(e.getMessage(), e);
}
var token = new UsernamePasswordAuthenticationToken(username, password);
return authenticationManager.authenticate(token);
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
var user = (UserDetails) authResult.getPrincipal();
var algorithm = Algorithm.HMAC512(secretToken.getBytes());
var token = JWT.create()
.withSubject(user.getUsername())
.withIssuer(request.getRequestURL().toString())
.withClaim("roles", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
.sign(algorithm);
var jsonMap = new HashMap<String, String>();
jsonMap.put("token", token);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), jsonMap);
response.flushBuffer();
}
}
AuthorizationFilter
#RequiredArgsConstructor
public class AuthorizationFilter extends OncePerRequestFilter {
private final String secretToken;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
var authentication = request.getHeader(HttpHeaders.AUTHORIZATION);
if(authentication != null) {
if(authentication.startsWith("Bearer")) {
var token = authentication.substring("Bearer ".length());
var algorithm = Algorithm.HMAC512(secretToken.getBytes());
var verifier = JWT.require(algorithm).build();
var message = verifier.verify(token);
var subject = message.getSubject();
var roles = message.getClaim("roles").asArray(String.class);
var authorities = new ArrayList<SimpleGrantedAuthority>();
Arrays.stream(roles).forEach(role -> authorities.add(new SimpleGrantedAuthority(role)));
var authenticationToken = new UsernamePasswordAuthenticationToken(subject, token, authorities);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
} else if(authentication.startsWith("Basic")) {
var token = authentication.substring("Basic ".length());
var bundle = new String(Base64.getDecoder().decode(token)).split(":", 2);
if(bundle.length == 2 && bundle[0].equals(System.getProperty("app.camera.username")) && bundle[1].equals(System.getProperty("app.camera.password"))) {
var authenticationToken = new UsernamePasswordAuthenticationToken("camera1", null, Arrays.asList(new SimpleGrantedAuthority(Role.USER.getAuthority())));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
filterChain.doFilter(request, response);
}
}
If I understood you correctly, you want the exception-message shown in the return body of the request.
I solved this problem by implementing a (global) exception handler.
(Optional) Create a custom exception, extending some sort of other exception.
public class ApiException extends RuntimeException {
// Not really needed here, as Throwable.java has a message too
// I added it for better readability
#Getter
private String message;
public ApiException() {
super();
}
public ApiException(String message) {
this.message = message;
}
public ApiException(String message, Throwable cause) {
super(message, cause);
}
}
(Optional) A Wrapper, with custom information. (This is the object returned in the body).
// I've used a record, as the wrapper simply has to store data
public record ApiError(String message, HttpStatus status, Throwable cause, ZonedDateTime timestamp) {}
The handler
To create the handler, you simply have to create a custom class, which extends the ResponseEntityExceptionHandler.java
#ControllerAdvice
#Order(Ordered.HIGHEST_PRECEDENCE)
public class ApiExceptionHandler extends ResponseEntityExceptionHandler {
// The annotation's value can be replaced by any exception.
// Use Throwable.class to handle **all** exceptions.
// For this example I used the previously created exception.
#ExceptionHandler(value = { ApiException.class })
#ResponseBody
#ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<Object> handleApiRequestException(ApiException e) {
// At this point, you can create the exception wrapper to create a
// formatted JSON-response, but you could also just get the info
// required from the exception and return that.
ApiError error = new ApiError(
e.getMessage(),
HttpStatus.BAD_REQUEST,
null,
ZonedDateTime.now(ZoneId.of("Z"))
);
return new ResponseEntity<>(error, error.status());
}
}
Also: To handle different kinds of exceptions differently, like e.g. you want a ApiException to return a 403 and a FooException to return a 404, just create another method inside of the handler and adjust it to your likings.
I hope this helped!
Cheers

Upgraded Spring: org.springframework.web.client.UnknownContentTypeException: Could not extract response

So I upgraded Spring from 2.1.15 to 2.2.8
I have #SpringBootTests where I make a rest template and make requests to localhost.
Full error is
org.springframework.web.client.UnknownContentTypeException: Could not extract response: no suitable HttpMessageConverter found for response type [class MyResponseClass] and content type [text/html;charset=utf-8]
My RestTemplateFactory
public class RestTemplateFactory {
public static RestTemplate getRestTemplate(String baseUrl) {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(baseUrl));
restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
ResponseErrorHandler responseErrorHandler =
new ResponseErrorHandler() {
#Override
public boolean hasError(ClientHttpResponse httpResponse) throws IOException {
return (httpResponse.getStatusCode().series() == HttpStatus.Series.CLIENT_ERROR
|| httpResponse.getStatusCode().series() == HttpStatus.Series.SERVER_ERROR);
}
#Override
public void handleError(ClientHttpResponse httpResponse) throws IOException {}
};
restTemplate.setErrorHandler(responseErrorHandler);
return restTemplate;
}
public static RestTemplate getMultiTenantOAuthRestTemplate(
String baseUrl, MockKeycloakUserDetails userDetails) {
RestTemplate restTemplate = getRestTemplate(baseUrl);
ClientHttpRequestInterceptor oauthInterceptor =
(request, body, execution) -> {
HttpHeaders headers = request.getHeaders();
headers.add("Authorization", String.format("Bearer %s", getToken(baseUrl, userDetails)));
headers.
return execution.execute(request, body);
};
ClientHttpRequestInterceptor tenantInterceptor =
(request, body, execution) -> {
HttpHeaders headers = request.getHeaders();
headers.add("Tenant-ID", userDetails.getTenant());
return execution.execute(request, body);
};
restTemplate.setInterceptors(Arrays.asList(oauthInterceptor, tenantInterceptor));
return restTemplate;
}
How I use the rest template
def response = restTemplateForTenant(ACME_TENANT, acmeUser_1).getForEntity(url("ApiUrl"), MyResponseClass)
When I hit the API directly from my browser, the API seems to work fine. So it seems the configuration of my RestTemplate is a problem after upgrading?

Feign client Retryer with a new request interceptor?

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.

Why is Authorization Header missing?

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);
}
}

Spring security. How to log out user (revoke oauth2 token)

When I want to get logout I invoke this code:
request.getSession().invalidate();
SecurityContextHolder.getContext().setAuthentication(null);
But after it (in next request using old oauth token) I invoke
SecurityContextHolder.getContext().getAuthentication();
and I see my old user there.
How to fix it?
Here's my implementation (Spring OAuth2):
#Controller
public class OAuthController {
#Autowired
private TokenStore tokenStore;
#RequestMapping(value = "/oauth/revoke-token", method = RequestMethod.GET)
#ResponseStatus(HttpStatus.OK)
public void logout(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader != null) {
String tokenValue = authHeader.replace("Bearer", "").trim();
OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
tokenStore.removeAccessToken(accessToken);
}
}
}
For testing:
curl -X GET -H "Authorization: Bearer $TOKEN" http://localhost:8080/backend/oauth/revoke-token
The response by camposer can be improved using the API provided by Spring OAuth. In fact, it's not necessary to access directly to the HTTP headers, but the REST method which removes the access token can be implemented as follows:
#Autowired
private AuthorizationServerTokenServices authorizationServerTokenServices;
#Autowired
private ConsumerTokenServices consumerTokenServices;
#RequestMapping("/uaa/logout")
public void logout(Principal principal, HttpServletRequest request, HttpServletResponse response) throws IOException {
OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) principal;
OAuth2AccessToken accessToken = authorizationServerTokenServices.getAccessToken(oAuth2Authentication);
consumerTokenServices.revokeToken(accessToken.getValue());
String redirectUrl = getLocalContextPathUrl(request)+"/logout?myRedirect="+getRefererUrl(request);
log.debug("Redirect URL: {}",redirectUrl);
response.sendRedirect(redirectUrl);
return;
}
I also added a redirect to the endpoint of Spring Security logout filter, so the session is invalidated and the client must provide credentials again in order to access to the /oauth/authorize endpoint.
It depends on type of oauth2 'grant type' that you're using.
The most common if your have used spring's #EnableOAuth2Sso in your client app is 'Authorization Code'. In this case Spring security redirects login request to the 'Authorization Server' and creates a session in your client app with the data received from 'Authorization Server'.
You can easy destroy your session at the client app calling /logout endpoint, but then client app sends user again to 'authorization server' and returns logged again.
I propose to create a mechanism to intercept logout request at client app and from this server code, call "authorization server" to invalidate the token.
The first change that we need is create one endpoint at the authorization server, using the code proposed by Claudio Tasso, to invalidate the user's access_token.
#Controller
#Slf4j
public class InvalidateTokenController {
#Autowired
private ConsumerTokenServices consumerTokenServices;
#RequestMapping(value="/invalidateToken", method= RequestMethod.POST)
#ResponseBody
public Map<String, String> logout(#RequestParam(name = "access_token") String accessToken) {
LOGGER.debug("Invalidating token {}", accessToken);
consumerTokenServices.revokeToken(accessToken);
Map<String, String> ret = new HashMap<>();
ret.put("access_token", accessToken);
return ret;
}
}
Then at the client app, create a LogoutHandler:
#Slf4j
#Component
#Qualifier("mySsoLogoutHandler")
public class MySsoLogoutHandler implements LogoutHandler {
#Value("${my.oauth.server.schema}://${my.oauth.server.host}:${my.oauth.server.port}/oauth2AuthorizationServer/invalidateToken")
String logoutUrl;
#Override
public void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) {
LOGGER.debug("executing MySsoLogoutHandler.logout");
Object details = authentication.getDetails();
if (details.getClass().isAssignableFrom(OAuth2AuthenticationDetails.class)) {
String accessToken = ((OAuth2AuthenticationDetails)details).getTokenValue();
LOGGER.debug("token: {}",accessToken);
RestTemplate restTemplate = new RestTemplate();
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("access_token", accessToken);
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "bearer " + accessToken);
HttpEntity<String> request = new HttpEntity(params, headers);
HttpMessageConverter formHttpMessageConverter = new FormHttpMessageConverter();
HttpMessageConverter stringHttpMessageConverternew = new StringHttpMessageConverter();
restTemplate.setMessageConverters(Arrays.asList(new HttpMessageConverter[]{formHttpMessageConverter, stringHttpMessageConverternew}));
try {
ResponseEntity<String> response = restTemplate.exchange(logoutUrl, HttpMethod.POST, request, String.class);
} catch(HttpClientErrorException e) {
LOGGER.error("HttpClientErrorException invalidating token with SSO authorization server. response.status code: {}, server URL: {}", e.getStatusCode(), logoutUrl);
}
}
}
}
And register it at WebSecurityConfigurerAdapter:
#Autowired
MySsoLogoutHandler mySsoLogoutHandler;
#Override
public void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.logout()
.logoutSuccessUrl("/")
// using this antmatcher allows /logout from GET without csrf as indicated in
// https://docs.spring.io/spring-security/site/docs/current/reference/html/csrf.html#csrf-logout
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
// this LogoutHandler invalidate user token from SSO
.addLogoutHandler(mySsoLogoutHandler)
.and()
...
// #formatter:on
}
One note: If you're using JWT web tokens, you can't invalidate it, because the token is not managed by the authorization server.
Its up to your Token Store Implementation.
If you use JDBC token store then you just need to remove it from table...
Anyway you must add /logout endpoint manually then call this :
#RequestMapping(value = "/logmeout", method = RequestMethod.GET)
#ResponseBody
public void logmeout(HttpServletRequest request) {
String token = request.getHeader("bearer ");
if (token != null && token.startsWith("authorization")) {
OAuth2AccessToken oAuth2AccessToken = okenStore.readAccessToken(token.split(" ")[1]);
if (oAuth2AccessToken != null) {
tokenStore.removeAccessToken(oAuth2AccessToken);
}
}
Programmatically you can log out this way:
public void logout(HttpServletRequest request, HttpServletResponse response) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null){
new SecurityContextLogoutHandler().logout(request, response, auth);
}
SecurityContextHolder.getContext().setAuthentication(null);
}
Add following line in <http></http> tag.
<logout invalidate-session="true" logout-url="/logout" delete-cookies="JSESSIONID" />
This will delete JSESSIONID and invalidate session. And link to logout button or label would be something like:
Logout
EDIT:
You want to invalidate session from java code. I assume you have to do some task right before logging the user out, and then invalidate session. If this is the use case, you should use custome logout handlers. Visit this site for more information.
This works for Keycloak Confidential Client logout. I have no idea why the folks over at keycloak don't have more robust docs on java non-web clients and their endpoints in general, I guess that's the nature of the beast with open source libs. I had to spend a bit of time in their code:
//requires a Keycloak Client to be setup with Access Type of Confidential, then using the client secret
public void executeLogout(String url){
HttpHeaders requestHeaders = new HttpHeaders();
//not required but recommended for all components as this will help w/t'shooting and logging
requestHeaders.set( "User-Agent", "Keycloak Thick Client Test App Using Spring Security OAuth2 Framework");
//not required by undertow, but might be for tomcat, always set this header!
requestHeaders.set( "Accept", "application/x-www-form-urlencoded" );
//the keycloak logout endpoint uses standard OAuth2 Basic Authentication that inclues the
//Base64-encoded keycloak Client ID and keycloak Client Secret as the value for the Authorization header
createBasicAuthHeaders(requestHeaders);
//we need the keycloak refresh token in the body of the request, it can be had from the access token we got when we logged in:
MultiValueMap<String, String> postParams = new LinkedMultiValueMap<String, String>();
postParams.set( OAuth2Constants.REFRESH_TOKEN, accessToken.getRefreshToken().getValue() );
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<MultiValueMap<String, String>>(postParams, requestHeaders);
RestTemplate restTemplate = new RestTemplate();
try {
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
System.out.println(response.toString());
} catch (HttpClientErrorException e) {
System.out.println("We should get a 204 No Content - did we?\n" + e.getMessage());
}
}
//has a hard-coded client ID and secret, adjust accordingly
void createBasicAuthHeaders(HttpHeaders requestHeaders){
String auth = keycloakClientId + ":" + keycloakClientSecret;
byte[] encodedAuth = Base64.encodeBase64(
auth.getBytes(Charset.forName("US-ASCII")) );
String authHeaderValue = "Basic " + new String( encodedAuth );
requestHeaders.set( "Authorization", authHeaderValue );
}
Solution provided by user composer perfectly worked for me. I made some minor changes to the code as follows,
#Controller
public class RevokeTokenController {
#Autowired
private TokenStore tokenStore;
#RequestMapping(value = "/revoke-token", method = RequestMethod.GET)
public #ResponseBody ResponseEntity<HttpStatus> logout(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader != null) {
try {
String tokenValue = authHeader.replace("Bearer", "").trim();
OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
tokenStore.removeAccessToken(accessToken);
} catch (Exception e) {
return new ResponseEntity<HttpStatus>(HttpStatus.NOT_FOUND);
}
}
return new ResponseEntity<HttpStatus>(HttpStatus.OK);
}
}
I did this because If you try to invalidate same access token again, it throws Null Pointer exception.
At AuthServer
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
...
endpoints.addInterceptor(new HandlerInterceptorAdapter() {
#Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
if (modelAndView != null
&& modelAndView.getView() instanceof RedirectView) {
RedirectView redirect = (RedirectView) modelAndView.getView();
String url = redirect.getUrl();
if (url.contains("code=") || url.contains("error=")) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
}
}
}
});
}
At client site
.and()
.logout().logoutSuccessUrl("/").permitAll()
.and().csrf()
.ignoringAntMatchers("/login", "/logout")
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
Seems a better solutions for me. referred this link
for logout token with spring boot rest security and oauth2.0
user as follow
import org.springframework.security.oauth2.provider.token.ConsumerTokenServices;
#RestController
#RequestMapping("/v1/user/")
public class UserController {
#Autowired
private ConsumerTokenServices consumerTokenServices;
/**
* Logout. This method is responsible for logout user from application based on
* given accessToken.
*
* #param accessToken the access token
* #return the response entity
*/
#GetMapping(value = "/oauth/logout")
public ResponseEntity<Response> logout(#RequestParam(name = "access_token") String accessToken) {
consumerTokenServices.revokeToken(accessToken);
return new ResponseEntity<>(new Response(messageSource.getMessage("server.message.oauth.logout.successMessage", null, LocaleContextHolder.getLocale())), HttpStatus.OK);
}
}
You can remove both access token and refresh token from database to save the space.
#PostMapping("/oauth/logout")
public ResponseEntity<String> revoke(HttpServletRequest request) {
try {
String authorization = request.getHeader("Authorization");
if (authorization != null && authorization.contains("Bearer")) {
String tokenValue = authorization.replace("Bearer", "").trim();
OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
tokenStore.removeAccessToken(accessToken);
//OAuth2RefreshToken refreshToken = tokenStore.readRefreshToken(tokenValue);
OAuth2RefreshToken refreshToken = accessToken.getRefreshToken();
tokenStore.removeRefreshToken(refreshToken);
}
} catch (Exception e) {
return ResponseEntity.badRequest().body("Invalid access token");
}
return ResponseEntity.ok().body("Access token invalidated successfully");
}

Categories