Logout user via Keycloak REST API doesn't work
从(移动)应用程序调用Keycloak的注销端点时出现问题。
如其文档中所述,支持此方案:
/realms/{realm-name}/protocol/openid-connect/logout
The logout endpoint logs out the authenticated user.
The user agent can be redirected to the endpoint, in which case the active user session is logged out. Afterward the user agent is redirected back to the application.
The endpoint can also be invoked directly by the application. To invoke this endpoint directly the refresh token needs to be included as well as the credentials required to authenticate the client.
我的请求具有以下格式:
1 2 3 4 5 | POST http://localhost:8080/auth/realms/<my_realm>/protocol/openid-connect/logout Authorization: Bearer Content-Type: application/x-www-form-urlencoded refresh_token=<refresh_token> |
但是总是会发生此错误:
1 2 3 4 5 6 7 8 9 10 11 12 | HTTP/1.1 400 Bad Request Connection: keep-alive X-Powered-By: Undertow/1 Server: WildFly/10 Content-Type: application/json Content-Length: 123 Date: Wed, 11 Oct 2017 12:47:08 GMT { "error":"unauthorized_client", "error_description":"UNKNOWN_CLIENT: Client was not identified by any client authenticator" } |
如果我提供了access_token,似乎Keycloak无法检测到当前客户端的身份事件。 我已经使用相同的access_token来访问其他Keycloak的API,而没有任何问题,例如userinfo
(/ auth / realms // protocol / openid-connect / userinfo)。
我的请求基于此Keycloak的问题。 问题的作者认为它起作用了,但这不是我的情况。
我正在使用Keycloak 3.2.1.Final。
你有同样的问题吗? 您知道如何解决吗?
最后,我通过查看Keycloak的源代码找到了解决方案:https://github.com/keycloak/keycloak/blob/9cbc335b68718443704854b1e758f8335b06c242/services/src/main/java/org/key/ak/keycloak/protocol/oidc/endpoints/ LogoutEndpoint.java#L169。它说:
If the client is a public client, then you must include a"client_id" form parameter.
所以我所缺少的是client_id表单参数。我的要求应该是:
1 2 3 4 5 | POST http://localhost:8080/auth/realms/<my_realm>/protocol/openid-connect/logout Authorization: Bearer Content-Type: application/x-www-form-urlencoded client_id=<my_client_id>&refresh_token=<refresh_token> |
该会话应正确销毁。
与Keycloak 6.0一起使用。
只是为了清楚起见:我们确实使refreshToken过期,但是accessToken仍然有效,而" Access Token Lifespan"时间。下次用户尝试通过刷新令牌更新访问令牌时,Keycloak返回400错误请求,应捕获并发送为401未经授权的响应。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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 } |
在版本3.4中,您需要作为
仅供参考:OIDC规范和Google的实现具有令牌撤销端点
但是目前在Keycloak中尚未实现,因此您可以在Keycloak JIRA中对该功能进行投票
根据代码:https://github.com/keycloak/keycloak/blob/master/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java#L106
这就是我的SpringBoot FX应用程序的工作方式
GET http:// loccalhost:8080 / auth / realms /
我用Keycloak 4.4.0.Final和4.6.0.Final进行了尝试。我检查了keycloak服务器日志,并在控制台输出中看到以下警告消息。
1 2 | 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 |
那么如何建立HTTP请求呢?首先,我从HttpSession中检索了用户主体,并将其转换为内部Keycloak实例类型:
1 2 3 4 5 | 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(); |
其次,我按照最上面的堆栈溢出答案(见上文)创建注销URL:
1 2 | final String logoutURI = idToken.getIssuer() +"/protocol/openid-connect/logout?"+ "redirect_uri="+response.encodeRedirectURL(url.toString()); |
现在,我将像下面这样构建其余的HTTP请求:
1 2 3 4 | 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")); |
并构建正文内容字符串:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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* |
如您所见,我尝试了多种主题更改,但是我一直在获得无效的用户身份验证。
哦耶。我将
1 2 | @Value("${keycloak.credentials.secret}") private String keycloakCredentialsSecret; |
Java Spring Security经验丰富的工程师有何想法?
附录
我在KC中创建了一个名为" demo"的领域,并创建了一个名为" web-portal"的客户端
具有以下参数:
1 2 3 4 5 6 | Client Protocol: openid-connect Access Type: public Standard Flow Enabled: On Implicit Flow Enabled: Off Direct Access Grants Enabled: On Authorization Enabled: Off |
这是重建重定向URI的代码,我忘了在此处包含它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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"); |
就这样