Выход из системы с помощью Keyclay REST API не работает

У меня проблема при вызове оконечной точки выхода Keycloak из (мобильного) приложения.

Этот сценарий поддерживается, как указано в его документации:

/realms/{realm-name}/protocol/openid-connect/logout

Конечная точка выхода из системы выходит из системы прошедшего проверку пользователя.

Пользовательский агент может быть перенаправлен на конечную точку, и в этом случае активный сеанс пользователя выходит из системы. После этого пользовательский агент перенаправляется обратно в приложение.

Конечная точка также может быть вызвана непосредственно приложением. Чтобы вызвать эту конечную точку напрямую, необходимо включить токен обновления, а также учетные данные, необходимые для аутентификации клиента.

Мой запрос имеет следующий формат:

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

Кажется, что Keycloak не может обнаружить текущее событие идентификации клиента, если я предоставил access_token. Я использовал тот же access_token для доступа к другим API Keycloak без каких-либо проблем, например userinfo  (/Авт/царств//Протокол /OpenID-соединения /UserInfo).

Мой запрос был основан на этой проблеме с клавиатурой. Автор проблемы получил это сработало, но это не мое дело.

Я использую Keycloak 3.2.1.Final.

У вас есть та же проблема? У вас есть идеи, как это решить?

Ответ 1

Наконец, я нашел решение, посмотрев на исходный код Keycloak: https://github.com/keycloak/keycloak/blob/9cbc335b68718443704854b1e758f8335b06c242/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java#L169. Это говорит:

Если клиент является общедоступным клиентом, необходимо включить параметр формы "client_id".

Так что я упустил это параметр формы 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>

Сессия должна быть уничтожена правильно.

Ответ 2

в версии 3.4 вам нужно как x-www-form-urlencoded body key client_id, client_secret и refresh_token.

Ответ 4

Я пробовал это с 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 в поле экземпляра объекта с помощью @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");

Это все

Ответ 5

Работает с Keycloak 6.0.

Просто для наглядности: у нас истекает refreshToken, но accessToken все еще действителен во время "срока действия Access Token". В следующий раз, когда пользователь пытается обновить токен доступа, передав токен обновления, 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
}