Выход из системы с помощью 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.
Ответ 3
FYI: спецификация OIDC и реализация Google имеют конечную точку отзыва токена, но в настоящее время это не реализовано в Keycloak, поэтому вы можете проголосовать за эту функцию в Keycloak JIRA
Ответ 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
}