Keycloakのlogoutエンドポイントを(モバイル)アプリケーションから呼び出すときに問題が発生します。
このシナリオは、 そのドキュメント に記載されているようにサポートされています:
/ 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"
}
access_tokenを指定した場合、Keycloakは現在のクライアントのIDイベントを検出できないようです。私は同じaccess_tokenを使用して、userinfo(/ auth/realms // protocol/openid-connect/userinfo)。
私のリクエストはこれに基づいていました Keycloakの問題 。問題の作者はそれを機能させましたが、それは私の場合ではありません。
Keycloak3.2.1.Finalを使用しています。
同じ問題がありますか?あなたはそれを解決する方法を知っていますか?
最後に、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>
セッションは正しく破棄されるはずです。
バージョン3.4では、x-www-form-urlencodedボディキーclient_id、client_secretおよびrefresh_tokenとして必要です。
参考までに、OIDCの仕様とGoogleの実装には トークン失効エンドポイント がありますが、現在これはKeycloakには実装されていないため、機能に投票できます Keycloak JIRA
これを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*
ご覧のとおり、さまざまなテーマを試しましたが、無効なユーザー認証を取得し続けました。そうそう。 @Valueを使用して、application.properties
からオブジェクトインスタンスフィールドにkeycloakクレデンシャルシークレットを挿入しました
@Value("${keycloak.credentials.secret}")
private String keycloakCredentialsSecret;
Java Spring Securityの経験豊富なエンジニアからのアイデアはありますか?
[〜#〜] addendum [〜#〜]「デモ」と呼ばれるKCのレルムと、「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");
それで全部です
Keycloak 6.0で動作します。
わかりやすくするために、refreshTokenを期限切れにしますが、accessToken IS「アクセストークンの有効期間」の間は有効です。次回ユーザーが更新トークンを渡すアクセストークンを更新しようとすると、Keycloakは400 Bad request、whatを返しますキャッシュして、401 Unauthorized応答として送信する必要があります。
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
}