Quand je veux me déconnecter, j'appelle ce code:
request.getSession().invalidate();
SecurityContextHolder.getContext().setAuthentication(null);
Mais après cela (dans la prochaine requête utilisant l'ancien jeton oauth), j'appelle
SecurityContextHolder.getContext().getAuthentication();
et je vois mon ancien utilisateur là-bas.
Comment le réparer?
Voici ma mise en œuvre (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);
}
}
}
Pour tester:
curl -X GET -H "Authorization: Bearer $TOKEN" http://localhost:8080/backend/oauth/revoke-token
La réponse de camposer peut être améliorée à l'aide de l'API fournie par Spring OAuth. En fait, il n'est pas nécessaire d'accéder directement aux en-têtes HTTP, mais la méthode REST, qui supprime le jeton d'accès, peut être implémentée comme suit:
@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;
}
J'ai également ajouté une redirection au noeud final du filtre de déconnexion Spring Security. La session est donc invalidée et le client doit à nouveau fournir les informations d'identification pour pouvoir accéder au noeud final/oauth/authorize.
Cela dépend du type d'oauth2 'type de subvention' que vous utilisez.
Le plus commun si vous avez utilisé le @EnableOAuth2Sso
de spring dans votre application client est le «code d'autorisation». Dans ce cas, la sécurité de Spring redirige la demande de connexion vers le 'Authorization Server' et crée une session dans votre application client avec les données reçues de 'Authorization Server'.
Vous pouvez facilement détruire votre session sur l’application cliente appelant le point de terminaison /logout
, mais l’application client renvoie l’utilisateur à nouveau au «serveur d’autorisation» et le renvoie à nouveau.
Je propose de créer un mécanisme pour intercepter la demande de déconnexion sur l'application cliente et, à partir de ce code de serveur, appelez "serveur d'autorisation" pour invalider le jeton.
Le premier changement dont nous avons besoin est de créer un noeud final sur le serveur d'autorisations, en utilisant le code proposé par Claudio Tasso , pour invalider le code d'accès de l'utilisateur.
@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;
}
}
Ensuite, dans l'application cliente, créez une 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);
}
}
}
}
Et enregistrez-le à 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
}
Remarque: si vous utilisez des jetons Web JWT, vous ne pouvez pas l'invalider, car le jeton n'est pas géré par le serveur d'autorisation.
Son à l'implémentation de votre magasin de jetons.
Si vous utilisezJDBCjeton, il vous suffit de le supprimer de la table ... Quoi qu'il en soit, vous devez ajouter/déconnecter le point de terminaison manuellement, puis appeler ceci:
@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);
}
}
Ajouter la ligne suivante dans la balise <http></http>
.
<logout invalidate-session="true" logout-url="/logout" delete-cookies="JSESSIONID" />
Cela supprimera JSESSIONID et invalidera la session. Et un lien vers un bouton ou une étiquette de déconnexion ressemblerait à ceci:
<a href="${pageContext.request.contextPath}/logout">Logout</a>
EDIT: Vous souhaitez invalider une session à partir de code Java. Je suppose que vous devez effectuer une tâche juste avant de déconnecter l'utilisateur, puis d'invalider la session. Si tel est le cas, vous devez utiliser des gestionnaires de déconnexion custome. Visitez this site pour plus d’informations.
Cela fonctionne pour la déconnexion du client confidentiel Keycloak. Je ne comprends pas pourquoi les gens de KeyCloak n'ont pas de documents plus robustes sur les clients non Web Java et leurs points de terminaison en général. Je suppose que c'est la nature de la bête avec des bibliothèques open source. Je devais passer un peu de temps dans leur 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 );
}
La solution fournie par l'utilisateur compositeur a parfaitement fonctionné pour moi. J'ai apporté quelques modifications mineures au code comme suit,
@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);
}
}
Je l'ai fait parce que si vous essayez d'invalider le même jeton d'accès à nouveau, il lève l'exception Null Pointer.