Keycloak 通过KeyClope REST API注销的用户不';行不通

Keycloak 通过KeyClope REST API注销的用户不';行不通,keycloak,Keycloak,我在从(移动)应用程序调用KeyClope的注销端点时遇到问题 支持此方案,如中所述: /领域/{realm name}/protocol/openid连接/注销 注销端点注销经过身份验证的用户 用户代理可以重定向到端点,在这种情况下,活动用户会话将注销。之后,用户代理被重定向回应用程序 应用程序也可以直接调用端点。要直接调用此端点,需要包括刷新令牌以及验证客户端所需的凭据 我的请求格式如下: POST http://localhost:8080/auth/realms/<my_realm

我在从(移动)应用程序调用KeyClope的注销端点时遇到问题

支持此方案,如中所述:

/领域/{realm name}/protocol/openid连接/注销

注销端点注销经过身份验证的用户

用户代理可以重定向到端点,在这种情况下,活动用户会话将注销。之后,用户代理被重定向回应用程序

应用程序也可以直接调用端点。要直接调用此端点,需要包括刷新令牌以及验证客户端所需的凭据

我的请求格式如下:

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

refresh_token=<refresh_token>
如果我提供了访问\u令牌,那么keydove似乎无法检测当前客户端的身份事件。我使用了相同的access\u令牌来访问其他KeyClope的API,没有任何问题,比如userinfo (/auth/realms//protocol/openid connect/userinfo)

我的要求就是基于此。这篇文章的作者成功了,但这不是我的情况

我正在使用KeyClope3.2.1.Final


你也有同样的问题吗?你知道如何解决它吗?

最后,我通过查看KeyClope的源代码找到了解决方案:。它说:

如果客户端是公共客户端,则必须包含“client_id”表单参数

所以我缺少的是客户机id表单参数。我的要求应该是:

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>
POSThttp://localhost:8080/auth/realms//protocol/openid-连接/注销
授权:持票人
内容类型:application/x-www-form-urlencoded
客户端\u id=&刷新\u令牌=

会话应该被正确地销毁。

仅供参考:OIDC规范和Google的实现有一个
但目前这并没有在keydove中实现,所以你可以在3.4版中投票选择你需要的功能,比如
x-www-form-urlencoded
body key client\u id、
client\u secret
和refresh\u token。

我在keydove 4.4.0.Final和4.6.0.Final中尝试过这一功能。我检查了KeyClope服务器日志,在控制台输出中看到了以下警告消息

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中检索到用户主体,并将其转换为内部KeyClope实例类型:

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,如顶部堆栈溢出应答中所示(见上文):

现在我构建HTTP请求的其余部分,如下所示:

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"));
并构建正文内容字符串:

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*
有经验的Java Spring安全工程师有什么想法吗

附录 我在KC中创建了一个名为“demo”的领域和一个名为“WebPortal”的客户端 使用以下参数:

Client Protocol: openid-connect
Access Type: public
Standard Flow Enabled: On
Implicit Flow Enabled: Off
Direct Access Grants Enabled: On
Authorization Enabled: Off
这是重建重定向URI的代码,我忘了在这里包含它

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

这就是KeyClope 6.0的全部功能

为了清楚起见:我们确实让refreshToken过期,但accessToken在“访问令牌寿命”期间仍然有效。下一次用户试图通过刷新令牌更新访问令牌时,KeyClope将返回400个错误请求,该请求应捕获并作为401未授权响应发送

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
}
public void注销(字符串刷新令牌){
试一试{
MultiValueMap requestParams=新链接的MultiValueMap();
requestParams.add(“客户id”、“我的客户id”);
requestParams.add(“客户机密”、“我的客户id机密”);
添加(“刷新令牌”,刷新令牌);
logoutUserSession(requestParams);
}捕获(例外e){
log.info(e.getMessage(),e);
投掷e;
}
}
私有void logoutUserSession(多值映射请求参数){
HttpHeaders=新的HttpHeaders();
headers.setContentType(MediaType.APPLICATION\u FORM\u URLENCODED);
HttpEntity请求=新的HttpEntity(请求参数、头);
字符串url=“/auth/realms/my realm/protocol/openid connect/logout”;
postForEntity(url、请求、对象、类);
//得到回应204,没有内容
}
根据代码:

这就是我的SpringBoot FX应用程序的工作原理

得到http://loccalhost:8080/auth/realms//protocol/openid-连接/注销?post_redirect_uri=您的_encodedredredirecturi&id_token_hint=id_token

在JWT中,您有“会话状态”

之后


这种方法不需要任何手动端点触发器。它依赖于
LogoutSuccessHandler
,尤其是
OIDClientificatedlogoutsuccesshandler
,检查
ClientRegistration
bean上是否存在
end\u session\u endpoint

在某些情况下,当与Spring Security配对时,大多数身份验证提供程序(Okta除外)默认情况下不使用
end\u session\u endpoint
,我们只能手动将其注入
ClientRegistration
。最简单的方法是将它放在MemoryClientRegistrationRepository的
初始化之前,紧跟在
application.properties
application.yaml
加载之后

package com.tb.ws.cscommon.config;
导入org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
导入org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
导入org.springframework.boot.autoconfigure.security.oauth2.client.oauth2clientproperties注册适配器;
导入org.springframework.context.annotation.Bean;
导入org.springframework.context.annotation.Configuration;
导入org.springframework.security.oauth2.client.registration.ClientRegistration;
导入org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
导入org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
导入java.util.List;
导入java.util.Map;
导入java.util.stream.collector;
@配置
公共类客户端注册配置{
@豆子
@ConditionalOnMissingBean({ClientRegi
Client Protocol: openid-connect
Access Type: public
Standard Flow Enabled: On
Implicit Flow Enabled: Off
Direct Access Grants Enabled: On
Authorization Enabled: Off
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");
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
}
{
    "exp": 1616268254,
    "iat": 1616267954,
     ....
    "session_state": "c0e2cd7a-11ed-4537-b6a5-182db68eb00f",
    ...
}
public void testDeconnexion() {
        
        String serverUrl = "http://localhost:8080/auth";
        String realm = "master";
        String clientId = "admin-cli";
        String clientSecret = "1d911233-bfb3-452b-8186-ebb7cceb426c";
        
        String sessionState = "c0e2cd7a-11ed-4537-b6a5-182db68eb00f";

        Keycloak keycloak = KeycloakBuilder.builder()
                .serverUrl(serverUrl)
                .realm(realm)
                .grantType(OAuth2Constants.CLIENT_CREDENTIALS)
                .clientId(clientId)
                .clientSecret(clientSecret) 
                .build();

        String realmApp = "MeineSuperApp";      

        RealmResource realmResource = keycloak.realm(realmApp);
        realmResource.deleteSession(sessionState);      
        
    }
{
 "Authorization" : "Bearer <access_token>",
 "Content-Type" : "application/x-www-form-urlencoded"
}
{
    "client_id" : "<client_id>",
    "client_secret" : "<client_secret>",
    "refresh_token" : "<refresh_token>"
}
POST
<scheme>://<host>:<port>/auth/realms/<realmName>/protocol/openid-connect/logout