User's OAuth2 Token into RestTemplate - java

How to correctly get the users's session oauth2 token ?
I implemented an OAuth2 Authorization/Resource server using spring-security-oauth2-autoconfigure.
I implemented a client app, that uses the authorization server to login the user and gets his access token. The login phase is working perfectly and so the retreive of the login data (using the access token by the oauth2 filters). The Principal in the client app requests correctly shows all authorities filled by the authorization server.
I'd like to use the client app as a proxy to send Rest Request using the given Access Token of the user that requested the call.
I already tried to use #EnableOAuth2Client but that does not work. The OAuth2RestTemplate is null when tried to be autowired.
I had to reimplement a request scoped bean of a RestTemplate which get the tokenValue from the SecurityContext. This works, but I do not find this clean. This behavior is quite common, so I should miss something.
application.yml
spring:
application.name: client
security:
oauth2:
client:
registration:
myclient:
client-id: client-id
client-secret: client-secret
redirect-uri: http://localhost:8081/login/oauth2/code/
authorization-grant-type: authorization_code
provider:
myclient:
authorization-uri: http://localhost:8090/oauth/authorize
token-uri: http://localhost:8090/oauth/token
user-info-uri: http://localhost:8090/me
user-name-attribute: name
SecurityConfiguration
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.authorizeRequests()
.antMatchers("/", "/login").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and().oauth2Login()
.and().oauth2Client()
;
// #formatter:on
}
#Bean
#Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
#Qualifier("oauth2RestTemplate")
public RestTemplate oauth2RestTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(new ClientHttpRequestInterceptor() {
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated() && auth instanceof OAuth2AuthenticationToken) {
#SuppressWarnings("unchecked")
Map<String, Object> details = (Map<String, Object>) ((OAuth2AuthenticationToken) auth).getPrincipal().getAttributes().get("details");
String tokenValue = (String) details.get("tokenValue");
if (tokenValue != null) {
request.getHeaders().add("Authorization", "Bearer " + tokenValue);
}
}
return execution.execute(request, body);
}
});
return restTemplate;
}
}
In WebController
private #Autowired #Qualifier("oauth2RestTemplate") RestTemplate oauth2RestTemplate;
#GetMapping("/remote")
public Map<String, Object> remote() {
#SuppressWarnings("unchecked")
Map<String, Object> resp = oauth2RestTemplate.getForObject(URI.create("http://localhost:8090/api/test"), Map.class);
return resp;
}
It works, but I do not think I should configure the RestTemplate myself.

Unfortunately, you have to define OAuth2RestTemplate. However, this is a more clean implementation.
#Bean
public OAuth2RestTemplate oauth2RestTemplate() {
ClientCredentialsResourceDetails resourceDetails = new ClientCredentialsResourceDetails();
resourceDetails.setAccessTokenUri(format("%s/oauth/token", authServerUrl));
resourceDetails.setClientId("client_id");
resourceDetails.setClientSecret("client_secret");
resourceDetails.setGrantType("client_credentials");
resourceDetails.setScope(asList("read", "write"));
DefaultOAuth2ClientContext clientContext = new DefaultOAuth2ClientContext();
return new OAuth2RestTemplate(resourceDetails, clientContext);
}
In this case, your Resource server will communicate with the authorization server on your behalf using its own credentials.

Related

Unable to set logged user from SecurityContextHolder in Spring Boot

I am trying to implement authentication using JWT in Spring Boot. In the login function I am setting the authentication in the SecurityContextHolder in order to be able to get it when requested. The login functionality works, but when I try to get the current logged user, I am getting unathorized. I debugged and the SecurityContextHolder gives anonymous user. Why is this happening?
UserController class:
#RestController
#CrossOrigin(origins = "http://localhost:3000")
#RequestMapping("/api")
public class UserController {
#Autowired
private UserService userService;
#Autowired
private CustomAuthenticationManager authenticationManager;
#Autowired
private JwtEncoder jwtEncoder;
#PostMapping("/user/login")
public ResponseEntity<User> login(#RequestBody #Valid AuthDto request) {
try {
Authentication authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()));
String userEmail = (String) authentication.getPrincipal();
User user = userService.findUserByEmail(userEmail);
Instant now = Instant.now();
long expiry = 36000L;
String scope = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(joining(" "));
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("uni.pu")
.issuedAt(now)
.expiresAt(now.plusSeconds(expiry))
.subject(format("%s,%s", user.getId(), user.getEmail()))
.claim("roles", scope)
.build();
String token = this.jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
SecurityContextHolder.getContext().setAuthentication(authentication);
return ResponseEntity.ok()
.header(HttpHeaders.AUTHORIZATION, token)
.body(user);
} catch (BadCredentialsException ex) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
#GetMapping("/user/current")
public ResponseEntity<User> getLoggedUser(){
try{
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return ResponseEntity.ok()
.body((User)auth.getPrincipal());
}
catch(Exception e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
}
WebSecurityConfig:
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true)
public class WebSecurityConfig {
private static final String[] WHITE_LIST_URLS = {"/api/user/login", "/api/user/current"};
#Autowired
private MyUserDetailsService userDetailsService;
#Value("${jwt.public.key}")
private RSAPublicKey rsaPublicKey;
#Value("${jwt.private.key}")
private RSAPrivateKey rsaPrivateKey;
#Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder(10);
}
#Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(encoder());
return authProvider;
}
#Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// Enable CORS and disable CSRF
http = http.cors().and().csrf().disable();
// Set session management to stateless
http = http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and();
// Set unauthorized requests exception handler
http = http.exceptionHandling(
(exceptions) -> exceptions.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
.accessDeniedHandler(new BearerTokenAccessDeniedHandler()));
http = http.authenticationProvider(authenticationProvider());
// Set permissions on endpoints
http.authorizeHttpRequests().antMatchers(WHITE_LIST_URLS).permitAll().antMatchers("/api/**").authenticated()
// Our private endpoints
.anyRequest().authenticated()
// Set up oauth2 resource server
.and().httpBasic(Customizer.withDefaults()).oauth2ResourceServer().jwt();
return http.build();
}
#Bean
public JwtEncoder jwtEncoder() {
JWK jwk = new RSAKey.Builder(this.rsaPublicKey).privateKey(this.rsaPrivateKey).build();
JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
// Used by JwtAuthenticationProvider to decode and validate JWT tokens
#Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(this.rsaPublicKey).build();
}
// Extract authorities from the roles claim
#Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
#Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
#Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
#Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String hierarchy = "ROLE_ADMIN > ROLE_INSPECTOR \n ROLE_INSPECTOR > ROLE_STUDENT";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}
}
In Spring documentation, section Storing the SecurityContext between requests says :
Depending on the type of application, there may need to be a strategy
in place to store the security context between user operations. In a
typical web application, a user logs in once and is subsequently
identified by their session Id. The server caches the principal
information for the duration session. In Spring Security, the
responsibility for storing the SecurityContext between requests falls
to the SecurityContextPersistenceFilter, which by default stores the
context as an HttpSession attribute between HTTP requests. It restores
the context to the SecurityContextHolder for each request and,
crucially, clears the SecurityContextHolder when the request completes
So basically, when you create the security context manually no session object is created. Only when the request finishes processing does the Spring Security mechanism realize that the session object is null (when it tries to store the security context to the session after the request has been processed).
At the end of the request Spring Security creates a new session object and session ID. However this new session ID never makes it to the browser because it occurs at the end of the request, after the response to the browser has been made. This causes the new session ID (and hence the Security context containing my manually logged on user) to be lost when the next request contains the previous session ID.
I found two solutions to hande this situation:
1.First solution : Save SecurityContext object in session and then extract it from session when needed :
HttpSession session = request.getSession(true);
session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
and then, extract it from session.
Second solution according to this answer would be to refactor your login function like this:
private void doAutoLogin(String username, String password, HttpServletRequest request) {
try {
// Must be called from request filtered by Spring Security, otherwise SecurityContextHolder is not updated
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
token.setDetails(new WebAuthenticationDetails(request));
Authentication authentication = this.authenticationProvider.authenticate(token);
logger.debug("Logging in with [{}]", authentication.getPrincipal());
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (Exception e) {
SecurityContextHolder.getContext().setAuthentication(null);
logger.error("Failure in autoLogin", e);
}
};
This is how you shoud get authenticationProvider :
#Configuration public class WebConfig extends WebSecurityConfigurerAdapter {
#Bean
public AuthenticationManager authenticationProvider() throws Exception{
return super.authenticationManagerBean();
}
}

Getting 403 Forbidden for WebFluxTest in Oauth2 Secured (Client Credentials) Resource Server Application

I have a reactive(Spring WebFlux) web-application where I am having few REST APIs which are protected resources.(Oauth2) . To access them manually, I need to get an authorization token with client credentials grant type and use that token in the request.
Now, I need to write tests where I can invoke the APIs by making a call through Spring's WebTestClient. I am getting 403 forbidden on trying to access the API. Where am I doing wrong when writing the test case.
Below is my security configuration:
#EnableWebFluxSecurity
public class WebSecurityConfiguration {
#Bean
SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeExchange()
.pathMatchers(ACTUATOR_ENDPOINT_PATTERN)
.permitAll()
.pathMatchers("/my/api/*")
.hasAuthority("SCOPE_myApi")
.anyExchange().authenticated()
.and()
.oauth2ResourceServer()
.jwt();
http.addFilterAfter(new SomeFilter(), SecurityWebFiltersOrder.AUTHORIZATION);
return http.build();
}
#Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ReactiveOAuth2AuthorizedClientService authorizedClientService) {
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
#Bean
public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder().filter(oauth).build();
}
}
Note:- I need this webclient bean because inside that filter (which I added to the SecurityWebFilterChain) I am calling another protected resource/API and the response of that API is being set in the reactive context
My application yaml:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: ${oidc-issuer-uri}
client:
provider:
myProvider:
issuer-uri: ${oidc-issuer-uri}
registration:
myProvider:
client-id: another-service-client
client-secret: ${another-service-clientSecret}
scope: anotherServiceScope
authorization-grant-type: client_credentials
My Controller:
#RestController
public class MyController {
#GetMapping(value = "/my/api/greet")
public Mono<String> greet() {
return Mono.subscriberContext()
.flatMap(context -> {
String someVal = context.get("MY_CONTEXT"); //This context is being set inside the filter 'SomeFilter'
//Use this someVal
return Mono.just("Hello World");
});
}
}
My Test Case:
#RunWith(SpringRunner.class)
#WebFluxTest(controllers = {MyController.class})
#Import({WebSecurityConfiguration.class})
#WithMockUser
public class MyControllerTest {
#Autowired
private WebTestClient webTestClient;
#Test
public void test_greet() throws Exception {
webTestClient.mutateWith(csrf()).get()
.uri("/my/api/greet")
.exchange()
.expectStatus().isOk();
}
}
Note:- I cannot bypass by not using my WebSecurityConfiguration class. Because the reactive context is being set in the filter which is added in the websecurityconfiguration.
2 things are required here:
First to access the /my/api/greet, the webTestClient needs SCOPE_myApi and since no "user" is involved here so we dont need #WithMockUser
#Test
public void test_greet() {
webTestClient
.mutateWith(mockOidcLogin().authorities(new SimpleGrantedAuthority("SCOPE_myApi")))
.get()
.uri("/my/api/greet")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("mockSasToken");
}
Next we need a wiremock server to mock the response of the "another service"
For this one option is to use spring boot #AutoConfigureWireMock(port = 0) to automatically boot up a wiremock server and shutdown for us at a random port.
Next we stub the response for the "another service" and the Oauth2 token endpoint in the test method.
Lastly, we need a "test" spring profile and a corresponding application-test.yaml where we tell spring to use the wiremock endpoints to fetch token:
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: http://localhost:${wiremock.server.port}/.well-known/jwks_uri
client:
provider:
myProvider:
token-uri: http://localhost:${wiremock.server.port}/.well-known/token
registration:
myProvider:
client-id: mockClient
client-secret: mockSecret

Spring 5 LDAP Authentication and JWT Token as response

Hello i have been trying to configure spring to have it return JWT token if user/pass is authenticated to LDAP Server; Consider the use case below ;
On the above diagram, i have configured WebSecurity to check/filter out requests with Bearer. See code below
WebSecurityConfig.java
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
#Autowired
JwtAuthorizationTokenFilter authenticationTokenFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
// Configure Web Security
// Allow only /auth/
// Disallow all others
http
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(HttpMethod.POST,
"/auth/**")
.permitAll()
.anyRequest().authenticated();
//Custom JWT
http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// disable page caching
http.headers().cacheControl();
}
}
AuthCtrl.java
#RestController
#RequestMapping("auth")
public class AuthCtrl {
private static final Logger logger = LoggerFactory.getLogger(AuthCtrl.class);
#Autowired
#Qualifier("authenticationManagerImpl")
private AuthenticationManager authenticationManager;
#Autowired
private JwtTokenUtil jwtTokenUtil;
#Autowired
#Qualifier("userDetailsServiceImpl")
private UserDetailsService userDetailsService;
#PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
public #ResponseBody String post(#RequestBody Map<String, String> credentials) {
logger.info("POST: {} | {} ",credentials.get("username"), credentials.get("password"));
String username = credentials.get("username");
String password = credentials.get("password");
Objects.requireNonNull(username);
Objects.requireNonNull(password);
try {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
// Reload password post-security so we can generate the token
final UserDetails userDetails = userDetailsService.loadUserByUsername(username);
final String token = jwtTokenUtil.generateToken(userDetails);
return token;
} catch (DisabledException e) {
throw new AuthenticationException("User is disabled!", e);
} catch (BadCredentialsException e) {
throw new AuthenticationException("Bad credentials!", e);
}
}
#ExceptionHandler({AuthenticationException.class})
public ResponseEntity<String> handleAuthenticationException(AuthenticationException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());
}
}
Above configuration was based on a youtube guide i've seen and also a pull from a demo source in git. Great help!, credits to the owners. Got to understand how filters work somehow.
The above source can already filter out all protected API and sends out unauthorized back as a response when it is not authorized. The only api i allowed to be accessed anonymously is the authentication api /auth. It can already receive the request and passed through the web filters.
But i can't quite figure out how to authenticate the said request to LDAP server and sends out a JWT token. On the guide i've read they are getting the user information on a database.
I've read some documentation on LDAP configuration in WebConfiguration, but i can't relate it to my current filters.
Please check the below link I have created it using spring 4.
Instead of .ldif file on classpath configure your own ldap server.
https://github.com/merugu/springsecurity/tree/master/ldapauthenticationjwttoken
The only differences is for Spring 5 you should use
advance password encoding algorithm like Bcryptpasswordencoder.As the LDAPpasswordEncoder is deprecated.
Happy coding!

spring security 4 RestController with ldap provider

I am trying to build a #Restcontroller with some basic function and authenticate/authorize thru spring security 4 with an ldap backend, all in java config. I have some questions:
storing passwords in ldap are realworld scenarios or they should be somewhere in a database and that involves a custom userDetailService implementation with dao functions?
If passwords should be stored in ldap then PasswordComparisonAuthenticator is how performance optimized(speed, constant connection toward ldap)?
When calling the #RestController how should the caller authenticate and what java config needs to be done there?
My current implementation works with simple httpBasic authentication against the #RestController but when changing the authenticationProvider to ldap I get some error(I see the bad credentials error but i don't know why I gets it), am I missing some conceptual thing?:
Basic Authentication Authorization header found for user 'jfryer'
[DEBUG] [http-bio-8080-exec-3 04:06:02] (ProviderManager.java:authenticate:162) Authentication attempt using org.springframework.security.ldap.authentication.LdapAuthenticationProvider
[DEBUG] [http-bio-8080-exec-3 04:06:02] (AbstractLdapAuthenticationProvider.java:authenticate:67) Processing authentication request for user: jfryer
[DEBUG] [http-bio-8080-exec-3 04:06:02] (BasicAuthenticationFilter.java:doFilterInternal:196) Authentication request for failed: org.springframework.security.authentication.BadCredentialsException: Bad credentials
[DEBUG] [http-bio-8080-exec-3 04:06:02] (DelegatingAuthenticationEntryPoint.java:commence:78) Trying to match using RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]
[DEBUG] [http-bio-8080-exec-3 04:06:02] (DelegatingAuthenticationEntryPoint.java:commence:91) No match found. Using default entry point org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint#56f05ca6
[DEBUG] [http-bio-8080-exec-3 04:06:02] (SecurityContextPersistenceFilter.java:doFilter:105) SecurityContextHolder now cleared, as request processing completed
And the java config:
#Configuration
#EnableWebSecurity
#ComponentScan("my packages")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public DefaultSpringSecurityContextSource contextSource() {
DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource("ldap://localhost:11389/o=sevenSeas");
contextSource.setUserDn("uid=admin,ou=system");
contextSource.setPassword("admin");
return contextSource;
}
#Bean
public PasswordComparisonAuthenticator ldapAuthenticator(DefaultSpringSecurityContextSource contextSource) {
PasswordComparisonAuthenticator authenticator = new PasswordComparisonAuthenticator(contextSource);
String[] userDn = {"cn={0},ou=people"};
authenticator.setUserDnPatterns(userDn);
authenticator.setPasswordAttributeName("userPassword");
authenticator.setPasswordEncoder(new LdapShaPasswordEncoder());
return authenticator;
}
#Bean
public DefaultLdapAuthoritiesPopulator authoritiesPopulator(DefaultSpringSecurityContextSource contextSource) {
DefaultLdapAuthoritiesPopulator populator = new DefaultLdapAuthoritiesPopulator(contextSource, "ou=groups");
populator.setGroupRoleAttribute("cn");
return populator;
}
#Bean
public LdapAuthenticationProvider ldapAuthenticationProvider(DefaultSpringSecurityContextSource contextSource) {
return new LdapAuthenticationProvider(this.ldapAuthenticator(contextSource), authoritiesPopulator(contextSource));
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth, DefaultSpringSecurityContextSource contextSource) throws Exception {
//auth.userDetailsService(userDetailsService);
auth.authenticationProvider(ldapAuthenticationProvider(contextSource));
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.httpBasic().and().authorizeRequests().antMatchers("/hello/**").hasRole("ADMIN").antMatchers("/date/**").hasRole("USER").and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().csrf().disable().exceptionHandling()
.authenticationEntryPoint(new AuthenticationHandler()).accessDeniedHandler(new PermissionHandler());
}
}
The calling code:
final String helloUri = "http://localhost:8080/Security/hello/Zoltan";
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
String auth = "jfryer:alma";
byte[] encodedAuth = Base64.encodeBase64(auth.getBytes(Charset.forName("US-ASCII")));
String authHeader = "Basic " + new String(encodedAuth);
headers.add("Authorization", authHeader);
HttpEntity<String> entity = new HttpEntity<String>("parameters", headers);
ResponseEntity<String> result = restTemplate.exchange(helloUri, HttpMethod.POST, entity, String.class);

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