説明
Keycloak 12.0.1 を ID プロバイダーとして使用するアプリケーションを作成しました。シングル サインオンは正常に機能し、「ローカル ログアウト」も同様です。
問題はシングルサインアウトです。
Web でドキュメントと問題を検索しましたが、何も見つかりませんでした。以下のログで説明されている 3 つの失敗シナリオがあります。
最後に質問は次のとおりです。
- 私は何を間違っていますか?
- アプリでバックチャネル ログアウトを実装するにはどうすればよいですか?
SSOut が機能することを理解する方法の例:
- ユーザーがアプリ A で「ログアウト」をクリックする
- アプリ A がセッションを終了する
- アプリAがKeycloakに通知
- キークロークは、バックチャネル ログアウトを介してアプリ B に通知します
- アプリ B がセッションを終了する
セキュリティ構成
メソッド keycloakCsrfRequestMatcher() は、「k_logout」などのライブラリ所有のエンドポイントを csrf 保護から解放しますが、自分の URL「/sso/logout」は解放しません。独自のマッチャーを作成することは可能かもしれませんが、これは開発者としての私の経験ではありません。
import java.util.Arrays;
import java.util.List;
import javax.annotation.PostConstruct;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.KeycloakSecurityComponents;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.keycloak.adapters.springsecurity.filter.KeycloakSecurityContextRequestFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
@Profile("KC")
@Configuration
@EnableWebSecurity
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
class SecurityConfigurationKeycloak extends KeycloakWebSecurityConfigurerAdapter implements EnvironmentAware {
private static final Logger LOG = LoggerFactory.getLogger(SecurityConfigurationKeycloak.class);
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
// SimpleAuthorityMapper is used to remove the ROLE_* conventions defined by
// Java so we can use only admin or user instead of ROLE_ADMIN and ROLE_USER
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
@Bean
public KeycloakSpringBootConfigResolver KeycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http);
http
.csrf()
.requireCsrfProtectionMatcher(keycloakCsrfRequestMatcher())
.and()
.sessionManagement()
.sessionAuthenticationStrategy(sessionAuthenticationStrategy())
.and()
.addFilterBefore(keycloakPreAuthActionsFilter(), LogoutFilter.class)
.addFilterBefore(keycloakAuthenticationProcessingFilter(), LogoutFilter.class)
.addFilterAfter(keycloakSecurityContextRequestFilter(), SecurityContextHolderAwareRequestFilter.class)
.addFilterAfter(keycloakAuthenticatedActionsRequestFilter(), KeycloakSecurityContextRequestFilter.class)
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint())
/*
* LOGOUT
*/
.and()
.logout()
.addLogoutHandler(keycloakLogoutHandler())
.logoutUrl("/sso/logout").permitAll()
.logoutSuccessUrl("/")
.and()
.authorizeRequests()
/*
* ADMIN
*/
.antMatchers(
"/admin/**"
)
.hasRole("ADMIN")
/*
* PUBLIC
*/
.antMatchers(
"/webjars/**",
"/css/**",
"/img/**",
"/favicon.ico",
"/**")
.permitAll();
}
@Override
public void setEnvironment(Environment environment) {
// TODO Auto-generated method stub
}
}
ログA
ご覧のとおり、KC が「/sso/logout」URL にアクセスしようとすると、CSRF エラーが発生します。しかし、これが KC で使用する適切なエンドポイントであるかどうかはわかりません。使用されているライブラリに「/k_logout」が見つかりました。これは、「内部リダイレクト」URL のようです。
(便宜上、日付などを削除しました。)
o.k.adapters.PreAuthActionsHandler : adminRequest http://domain.tld/sso/logout
.k.a.t.AbstractAuthenticatedActionsValve : AuthenticatedActionsValve.invoke /sso/logout
o.k.a.AuthenticatedActionsHandler : AuthenticatedActionsValve.invoke http://domain.tld/sso/logout
o.k.a.AuthenticatedActionsHandler : Policy enforcement is disabled.
o.s.security.web.FilterChainProxy : Securing POST /sso/logout
s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for https://domain.tld/sso/logout
o.s.s.w.access.AccessDeniedHandlerImpl : Responding with 403 status code
w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
o.s.security.web.FilterChainProxy : Securing POST /error
s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
o.k.adapters.PreAuthActionsHandler : adminRequest https://domain.tld/error
o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext
o.s.s.w.a.i.FilterSecurityInterceptor : Authorized filter invocation [POST /error] with attributes [permitAll]
o.s.security.web.FilterChainProxy : Secured POST /error
e.p.p.controller.CustomErrorController : User was not authorized for requested site: /error
w.c.HttpSessionSecurityContextRepository : Did not store anonymous SecurityContext
s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
ログB
代わりに KC で「/k_logout」エンドポイントを使用すると、アプリで JWT 解析エラーが発生します。私はそれをデバッグしようとしましたが、org.keycloak.jose.jws.JWSInput では、encodedHeader の前に「logout_token=」が付けられているようで、これが問題のようです。少なくとも私には。:-)
o.k.adapters.PreAuthActionsHandler : adminRequest http://domain.tld/k_logout
o.k.adapters.PreAuthActionsHandler : admin request failed, unable to verify token: Failed to parse JWT
o.k.adapters.PreAuthActionsHandler : Failed to parse JWT
org.keycloak.common.VerificationException: Failed to parse JWT
at org.keycloak.TokenVerifier.parse(TokenVerifier.java:402) ~[keycloak-core-12.0.1.jar:12.0.1]
at org.keycloak.TokenVerifier.getHeader(TokenVerifier.java:423) ~[keycloak-core-12.0.1.jar:12.0.1]
at org.keycloak.adapters.rotation.AdapterTokenVerifier.createVerifier(AdapterTokenVerifier.java:110) ~[keycloak-adapter-core-12.0.1.jar:12.0.1]
at org.keycloak.adapters.PreAuthActionsHandler.verifyAdminRequest(PreAuthActionsHandler.java:210) ~[keycloak-adapter-core-12.0.1.jar:12.0.1]
at org.keycloak.adapters.PreAuthActionsHandler.handleLogout(PreAuthActionsHandler.java:140) ~[keycloak-adapter-core-12.0.1.jar:12.0.1]
at org.keycloak.adapters.PreAuthActionsHandler.handleRequest(PreAuthActionsHandler.java:80) ~[keycloak-adapter-core-12.0.1.jar:12.0.1]
at org.keycloak.adapters.tomcat.AbstractKeycloakAuthenticatorValve.invoke(AbstractKeycloakAuthenticatorValve.java:177) ~[spring-boot-container-bundle-12.0.1.jar:12.0.1]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:888) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1597) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]
Caused by: org.keycloak.jose.jws.JWSInputException: com.fasterxml.jackson.core.JsonParseException: Unexpected character ((CTRL-CHAR, code 150)): expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false')
at [Source: (byte[])"��(���G��쉅����IL��؈�������耉)]P��������耉�
8�UL�ōYY����}=�!]�! �1]�}���1i���1]ጉ�"; line: 1, column: 2]
at org.keycloak.jose.jws.JWSInput.<init>(JWSInput.java:58) ~[keycloak-core-12.0.1.jar:12.0.1]
at org.keycloak.TokenVerifier.parse(TokenVerifier.java:400) ~[keycloak-core-12.0.1.jar:12.0.1]
... 19 common frames omitted
Caused by: com.fasterxml.jackson.core.JsonParseException: Unexpected character ((CTRL-CHAR, code 150)): expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false')
at [Source: (byte[])"��(���G��쉅����IL��؈�������耉)]P��������耉�
8�UL�ōYY����}=�!]�! �1]�}���1i���1]ጉ�"; line: 1, column: 2]
at com.fasterxml.jackson.core.JsonParser._constructError(JsonParser.java:1851) ~[jackson-core-2.11.3.jar:2.11.3]
at com.fasterxml.jackson.core.base.ParserMinimalBase._reportError(ParserMinimalBase.java:707) ~[jackson-core-2.11.3.jar:2.11.3]
at com.fasterxml.jackson.core.base.ParserMinimalBase._reportUnexpectedChar(ParserMinimalBase.java:632) ~[jackson-core-2.11.3.jar:2.11.3]
at com.fasterxml.jackson.core.json.UTF8StreamJsonParser._handleUnexpectedValue(UTF8StreamJsonParser.java:2686) ~[jackson-core-2.11.3.jar:2.11.3]
at com.fasterxml.jackson.core.json.UTF8StreamJsonParser._nextTokenNotInObject(UTF8StreamJsonParser.java:865) ~[jackson-core-2.11.3.jar:2.11.3]
at com.fasterxml.jackson.core.json.UTF8StreamJsonParser.nextToken(UTF8StreamJsonParser.java:757) ~[jackson-core-2.11.3.jar:2.11.3]
at com.fasterxml.jackson.databind.ObjectMapper._initForReading(ObjectMapper.java:4664) ~[jackson-databind-2.11.3.jar:2.11.3]
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4513) ~[jackson-databind-2.11.3.jar:2.11.3]
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3529) ~[jackson-databind-2.11.3.jar:2.11.3]
at org.keycloak.util.JsonSerialization.readValue(JsonSerialization.java:71) ~[keycloak-core-12.0.1.jar:12.0.1]
at org.keycloak.jose.jws.JWSInput.<init>(JWSInput.java:56) ~[keycloak-core-12.0.1.jar:12.0.1]
... 20 common frames omitted
ログC
.csrf().disable() を使用してアプリの csrf 保護を完全に無効にすると、上記のエラーは明らかになくなります。代わりに、アプリはログアウト要求をユーザーにマップできません。
o.k.adapters.PreAuthActionsHandler : adminRequest http://192.168.178.31:8090/sso/logout
.k.a.t.AbstractAuthenticatedActionsValve : AuthenticatedActionsValve.invoke /sso/logout
o.k.a.AuthenticatedActionsHandler : AuthenticatedActionsValve.invoke http://192.168.178.31:8090/sso/logout
o.k.a.AuthenticatedActionsHandler : Policy enforcement is disabled.
o.s.security.web.FilterChainProxy : Securing POST /sso/logout
s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
o.k.adapters.PreAuthActionsHandler : adminRequest http://192.168.178.31:8090/sso/logout
o.s.s.w.a.logout.LogoutFilter : Logging out [null]
o.k.a.s.a.KeycloakLogoutHandler : Cannot log out without authentication
o.s.s.web.DefaultRedirectStrategy : Redirecting to /
w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
バックチャネル ログアウトのキークローク構成
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>tld.domain</groupId>
<artifactId>artifact</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>name</name>
<description></description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.keycloak.bom</groupId>
<artifactId>keycloak-adapter-bom</artifactId>
<version>12.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>