Logout user via Keycloak REST API doesn't work

后端 未结 6 735
余生分开走
余生分开走 2020-12-14 06:55

I have issue while calling Keycloak\'s logout endpoint from an (mobile) application.

This scenario is supported as stated in its documentation:

相关标签:
6条回答
  • 2020-12-14 07:23

    Works with Keycloak 6.0.

    Just for clarity: we do expire refreshToken, but accessToken IS STILL VALID while "Access Token Lifespan" time. Next time user tries to renew access token passing refresh token, Keycloak returns 400 Bad request, what should be catch and send as 401 Unauthorised response.

    public void logout(String refreshToken) {
        try {
            MultiValueMap<String, String> requestParams = new LinkedMultiValueMap<>();
            requestParams.add("client_id", "my-client-id");
            requestParams.add("client_secret", "my-client-id-secret");
            requestParams.add("refresh_token", refreshToken);
    
            logoutUserSession(requestParams);
    
        } catch (Exception e) {
            log.info(e.getMessage(), e);
            throw e;
        }
    }
    
    private void logoutUserSession(MultiValueMap<String, String> requestParams) {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
    
        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(requestParams, headers);
    
        String url = "/auth/realms/my-realm/protocol/openid-connect/logout";
    
        restTemplate.postForEntity(url, request, Object.class);
        // got response 204, no content
    }
    
    0 讨论(0)
  • 2020-12-14 07:25

    Finally, I've found the solution by looking at the Keycloak's source code: https://github.com/keycloak/keycloak/blob/9cbc335b68718443704854b1e758f8335b06c242/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java#L169. It says:

    If the client is a public client, then you must include a "client_id" form parameter.

    So what I was missing is the client_id form parameter. My request should have been:

    POST http://localhost:8080/auth/realms/<my_realm>/protocol/openid-connect/logout
    Authorization: Bearer <access_token>
    Content-Type: application/x-www-form-urlencoded
    
    client_id=<my_client_id>&refresh_token=<refresh_token>
    

    The session should be destroyed correctly.

    0 讨论(0)
  • 2020-12-14 07:27

    FYI: OIDC spec and Google's implementation has a token revocation endpoint but currently this is not implemented in Keycloak so you can vote for the feature in Keycloak JIRA

    0 讨论(0)
  • 2020-12-14 07:28

    in version 3.4 you need as x-www-form-urlencoded body key client_id, client_secret and refresh_token.

    0 讨论(0)
  • 2020-12-14 07:32

    According to the code: https://github.com/keycloak/keycloak/blob/master/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java#L106

    This is how it worked for my SpringBoot FX app

    GET http://loccalhost:8080/auth/realms/<my_realm>/protocol/openid-connect/logout?post_redirect_uri=your_encodedRedirectUri&id_token_hint=id_token

    0 讨论(0)
  • 2020-12-14 07:39

    I tried this with Keycloak 4.4.0.Final and 4.6.0.Final. I checked the keycloak server log and I saw the following warning messages in the console output.

    10:33:22,882 WARN  [org.keycloak.events] (default task-1) type=REFRESH_TOKEN_ERROR, realmId=master, clientId=security-admin-console, userId=null, ipAddress=127.0.0.1, error=invalid_token, grant_type=refresh_token, client_auth_method=client-secret
    10:40:41,376 WARN  [org.keycloak.events] (default task-5) type=LOGOUT_ERROR, realmId=demo, clientId=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJqYTBjX18xMHJXZi1KTEpYSGNqNEdSNWViczRmQlpGS3NpSHItbDlud2F3In0.eyJqdGkiOiI1ZTdhYzQ4Zi1mYjkyLTRkZTYtYjcxNC01MTRlMTZiMmJiNDYiLCJleHAiOjE1NDM0MDE2MDksIm5iZiI6MCwiaWF0IjoxNTQzNDAxMzA5LCJpc3MiOiJodHRwOi8vMTI3Lj, userId=null, ipAddress=127.0.0.1, error=invalid_client_credentials
    

    So how did build the HTTP request? First, I retrieved the user principal from the HttpSession and cast to the internal Keycloak instance types:

    KeycloakAuthenticationToken keycloakAuthenticationToken = (KeycloakAuthenticationToken) request.getUserPrincipal();
    final KeycloakPrincipal keycloakPrincipal = (KeycloakPrincipal)keycloakAuthenticationToken.getPrincipal();
    final RefreshableKeycloakSecurityContext context = (RefreshableKeycloakSecurityContext) keycloakPrincipal.getKeycloakSecurityContext();
    final AccessToken accessToken = context.getToken();
    final IDToken idToken = context.getIdToken();
    

    Second, I created the logout URL as in the top stack overflow answer (see above):

    final String logoutURI = idToken.getIssuer() +"/protocol/openid-connect/logout?"+
                "redirect_uri="+response.encodeRedirectURL(url.toString());
    

    And now I then build the rest of the HTTP request like so:

    KeycloakRestTemplate keycloakRestTemplate = new KeycloakRestTemplate(keycloakClientRequestFactory);
    HttpHeaders headers = new HttpHeaders();
    headers.put("Authorization", Collections.singletonList("Bearer "+idToken.getId()));
    headers.put("Content-Type", Collections.singletonList("application/x-www-form-urlencoded"));
    

    And also build the body content string:

    StringBuilder bodyContent = new StringBuilder();
    bodyContent.append("client_id=").append(context.getTokenString())
                .append("&")
                .append("client_secret=").append(keycloakCredentialsSecret)
                .append("&")
                .append("user_name=").append(keycloakPrincipal.getName())
                .append("&")
                .append("user_id=").append(idToken.getId())
                .append("&")
                .append("refresh_token=").append(context.getRefreshToken())
                .append("&")
                .append("token=").append(accessToken.getId());
    HttpEntity<String> entity = new HttpEntity<>(bodyContent.toString(), headers);
    //   ...
    ResponseEntity<String> forEntity = keycloakRestTemplate.exchange(logoutURI, HttpMethod.POST, entity, String.class); // *FAILURE*
    

    As you can observed, I attempted many variations of theme, but I kept getting invalid user authentication. Oh yeah. I injected the keycloak credentials secret from the application.properties into object instance field with @Value

    @Value("${keycloak.credentials.secret}")
    private String keycloakCredentialsSecret;
    

    Any ideas from Java Spring Security experienced engineers?

    ADDENDUM I created a realm in KC called 'demo' and a client called 'web-portal' with the following parameters:

    Client Protocol: openid-connect
    Access Type: public
    Standard Flow Enabled: On
    Implicit Flow Enabled: Off
    Direct Access Grants Enabled: On
    Authorization Enabled: Off
    

    Here is the code that rebuilds the redirect URI, I forgot to include it here.

    final String scheme = request.getScheme();             // http
    final String serverName = request.getServerName();     // hostname.com
    final int serverPort = request.getServerPort();        // 80
    final String contextPath = request.getContextPath();   // /mywebapp
    
    // Reconstruct original requesting URL
    StringBuilder url = new StringBuilder();
    url.append(scheme).append("://").append(serverName);
    
    if (serverPort != 80 && serverPort != 443) {
        url.append(":").append(serverPort);
    }
    
    url.append(contextPath).append("/offline-page.html");
    

    That's all

    0 讨论(0)
提交回复
热议问题