从(移动)应用程序调用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 。
你有同样的问题吗?你知道如何解决它吗?
答案 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)
答案 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)
这就是我的SpringBoot FX应用程序的工作方式
获取http:// loccalhost:8080 / auth / realms /
答案 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.properties
或 application.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_hint
和 post_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
端点。
其他:
客户端设置:
某人,某处可能会发现它有帮助。
附言该应用程序及其属性文件供学习