通过Keycloak REST API注销用户不起作用

时间:2017-10-11 13:07:02

标签: keycloak

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

支持此方案,如its documentation中所述:

  

/境界/ {领域名} /协议/ 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>

但始终会出现此错误:

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 /领域//协议/ OpenID的连接/用户信息)。

我的请求基于此Keycloak's issue。该问题的作者得到了它的工作,但这不是我的情况。

我使用的是Keycloak 3.2.1.Final

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

9 个答案:

答案 0 :(得分:15)

最后,我通过查看Keycloak的源代码https://github.com/keycloak/keycloak/blob/9cbc335b68718443704854b1e758f8335b06c242/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java#L169找到了解决方案。它说:

  

如果客户端是公共客户端,那么您必须包含&#34; client_id&#34;表格参数。

所以我缺少的是 client_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>

会话应该被正确销毁。

答案 1 :(得分:2)

在3.4版本中,您需要x-www-form-urlencoded正文密钥client_id, client_secret 和refresh_token。

答案 2 :(得分:2)

与Keycloak 6.0兼容。

为清楚起见:我们确实使refreshToken过期,但是accessToken仍然有效,而“ Access Token Lifespan”时间却没有。下次用户尝试通过传递令牌更新访问令牌时,Keycloak返回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
}

答案 3 :(得分:1)

仅供参考:OIDC规范和Google的实施有token revocation endpoint 但目前这在Keycloak中没有实现,因此您可以投票选择功能in Keycloak JIRA

答案 4 :(得分:1)

最后。它对我有用。我进行了 REST 调用,如下所示:

标题

{
 "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

我收到了 200 作为回复...如果你做错了什么,你会得到 401 或 400 错误。调试这个问题非常困难。顺便说一句,我的密钥斗篷版本是 12.0.4

如果帖子不清楚或者您需要更多信息,请告诉我。

答案 5 :(得分:0)

我使用Keycloak 4.4.0.Final和4.6.0.Final进行了尝试。我检查了keycloak服务器日志,并在控制台输出中看到以下警告消息。

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实例类型:

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

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

现在我像这样构建其余的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*

正如您所看到的,我尝试了多种主题,但是我一直在获得无效的用户身份验证。 哦耶。我将application.properties的keycloak凭据秘密通过@Value

注入到对象实例字段中
@Value("${keycloak.credentials.secret}")
private String keycloakCredentialsSecret;

Java Spring Security经验丰富的工程师有何想法?

附录 我在KC中创建了一个名为“ demo”的领域,并创建了一个名为“ web-portal”的客户端 具有以下参数:

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

仅此而已

答案 6 :(得分:0)

根据代码:https://github.com/keycloak/keycloak/blob/master/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java#L106

这就是我的SpringBoot FX应用程序的工作方式

获取http:// loccalhost:8080 / auth / realms / / protocol / openid-connect / logout?post_redirect_uri = your_encodedRedirectUri&id_token_hint = id_token

答案 7 :(得分:0)

在 JWT 中你有“session_state”

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

答案 8 :(得分:0)

这种方法不需要任何手动端点触发器。它依赖于 LogoutSuccessHandler,尤其是检查 OidcClientInitiatedLogoutSuccessHandler 是否存在于 end_session_endpoint bean 上的 ClientRegistration

在某些情况下,当与 Spring Security 配对时,大多数身份验证提供程序(Okta 除外)默认不使用 end_session_endpoint,我们需要手动将其注入 ClientRegistration。最简单的方法是将它放在 InMemoryClientRegistrationRepository 初始化之前,就在 application.propertiesapplication.yaml 加载之后。

package com.tb.ws.cscommon.config;

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientPropertiesRegistrationAdapter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Configuration
public class ClientRegistrationConfig {

  @Bean
  @ConditionalOnMissingBean({ClientRegistrationRepository.class})
  InMemoryClientRegistrationRepository clientRegistrationRepository(
      OAuth2ClientProperties properties) {
    List<ClientRegistration> registrations =
        OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties)
            .values()
            .stream()
            .map(
                o ->
                    ClientRegistration.withClientRegistration(o)
                        .providerConfigurationMetadata(
                            Map.of(
                                "end_session_endpoint",
                                "http://127.0.0.1:8080/auth/realms/OAuth2/protocol/openid-connect/logout"))
                        .build())
            .collect(Collectors.toList());

    return new InMemoryClientRegistrationRepository(registrations);
  }
}

WebSecurity中:

package com.tb.ws.cscommon.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

@Slf4j
@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
  private final InMemoryClientRegistrationRepository registrationRepository;

  public WebSecurity(InMemoryClientRegistrationRepository registrationRepository) {
    this.registrationRepository = registrationRepository;
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    String[] permitAccess = new String[] {"/", "/styles/**"};

    http.authorizeRequests()
        .antMatchers(permitAccess)
        .permitAll()
        .anyRequest()
        .authenticated()
        .and()
        .oauth2Login()
        .and()
        .logout(
            logout -> {
              logout.logoutSuccessHandler(logoutSuccessHandler());
              logout.invalidateHttpSession(true);
              logout.clearAuthentication(true);
              logout.deleteCookies("JSESSIONID");
            });
  }

  private LogoutSuccessHandler logoutSuccessHandler() {
    OidcClientInitiatedLogoutSuccessHandler handler =
        new OidcClientInitiatedLogoutSuccessHandler(registrationRepository);
    handler.setPostLogoutRedirectUri("http://127.0.0.1:8005/");

    return handler;
  }
}

默认情况下,Spring Security 将查询参数 id_token_hintpost_logout_redirect_uri 附加到 end_session_endpoint。这可以通过 OidcClientInitiatedLogoutSuccessHandler handler 更改。这可以与社交提供者一起使用。只需为每个提供程序提供一个相关的 end_session_endpoint

用于此示例的属性文件 application.yaml

spring:
  application:
    name: cs-common
  main:
    banner-mode: off
  security:
    oauth2:
      client:
        registration:
          cs-common-1:
            client_id: cs-common
            client-secret: 03e2f8e1-f150-449c-853d-4d8f51f66a29
            scope: openid, profile, roles
            authorization-grant-type: authorization_code
            redirect_uri: http://127.0.0.1:8005/login/oauth2/code/cs-common-1
        provider:
          cs-common-1:
            authorization-uri: http://127.0.0.1:8080/auth/realms/OAuth2/protocol/openid-connect/auth
            token-uri: http://127.0.0.1:8080/auth/realms/OAuth2/protocol/openid-connect/token
            jwk-set-uri: http://127.0.0.1:8080/auth/realms/OAuth2/protocol/openid-connect/certs
            user-info-uri: http://127.0.0.1:8080/auth/realms/OAuth2/protocol/openid-connect/userinfo
            user-name-attribute: preferred_username
server:
  port: 8005
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8004/eureka
  instance:
    instance-id: ${spring.application.name}:${instanceId:${random.value}}

为了测试,我们只是从 UI 中踢出 Spring Security 的默认 GET /logout 端点。

其他:

  • Spring Boot 2.5
  • Spring Cloud 2020.0.3
  • Java 11
  • Keycloak 服务器 13.0.1

客户端设置:

  • 标准流程已启用
  • 隐式流被禁用
  • 已启用直接访问授权

某人,某处可能会发现它有帮助。

附言该应用程序及其属性文件供学习