Contents
SAML の基本概念と Keycloak が提供する SAML IdP の概要
SAML は企業向けシングルサインオンで長年採用されている XML ベースの認証プロトコルです。本章では、SAML の基本構造と OIDC との違いを整理し、Keycloak(現在の安定版は 24.x 系)で利用できる SAML IdP の主要機能を概観します。
ポイント:SAML は証明書ベースの署名・暗号化が標準装備されているため、セキュリティ要件が高いシナリオに適しています。
SAML と OIDC の違い
SAML と OpenID Connect(OIDC)は目的は同じでも設計思想が大きく異なります。以下の比較でそれぞれの特徴を把握してください。
- プロトコル形式
- SAML: XML、X.509 証明書による署名・暗号化。
-
OIDC: JSON Web Token(JWT)+ OAuth2.0 の軽量 HTTP/JSON。
-
フローの複雑さ
- SAML: リダイレクト + POST バインディングが標準で、設定項目が多い。
-
OIDC: Authorization Code フローだけで完結し、実装がシンプル。
-
導入シーン
- SAML: 大規模エンタープライズ、レガシーシステム、社内ポータル等。
- OIDC: モバイル・SPA・マイクロサービスなど軽量クライアント。
Keycloak の SAML IdP が提供する主な機能
Keycloak は認証・属性マッピング・SSO/Logout をワンストップで提供します。特に以下の点が実務で有用です。
- 標準的な IdP 機能:認証、シングルサインオン/アウト、SAML アサーションの生成。
- 属性・ロール付与:ユーザー属性や LDAP/AD グループをアサーションに組み込み可能。
- 署名・暗号化設定:X.509 証明書でメタデータ全体、あるいは個別要素の署名ができる。
- マルチテナント対応:Realm を分割してプロジェクトごとに設定を独立させられる。
注意:Keycloak 25 系や Spring Boot 3.3 は執筆時点ではリリース未定です。この記事では「現在安定版(Keycloak 24.x、Spring Boot 3.2系)」を前提にしています。
開発環境と必須依存ライブラリ
この章ではローカル開発に必要なツール・バージョンと、SAML 認証に最低限 required な Maven / Gradle 依存関係を示します。
前提条件
| 項目 | バージョン (推奨) |
|---|---|
| Java | 17 以上(LTS) |
| Spring Boot | 3.2.x(最新安定版) |
| Maven / Gradle | Maven 3.9+、Gradle 8.5+ |
| Keycloak Server | 24.x 系(Docker イメージ quay.io/keycloak/keycloak:24.0) |
| Docker | Desktop/Engine 20.10 以上 |
- Docker 上で公式イメージを起動すれば環境構築が容易です。
- 本稿では OpenSAML 4.x を内部的に利用しますので、追加依存も明記します。
必要な Maven / Gradle 依存
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<!-- pom.xml の抜粋 --> <dependencies> <!-- Spring Security 基本 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- SAML2 Service Provider(公式) --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-saml2-service-provider</artifactId> </dependency> <!-- OpenSAML 本体とユーティリティ(メタデータ解析に必須) --> <dependency> <groupId>org.opensaml</groupId> <artifactId>opensaml-core</artifactId> <version>4.2.0</version> </dependency> <dependency> <groupId>org.opensaml</groupId> <artifactId>opensaml-saml-impl</artifactId> <version>4.2.0</version> </dependency> <!-- 例外処理やロギングで便利なユーティリティ --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </dependency> </dependencies> |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// build.gradle.kts の抜粋 plugins { id("org.springframework.boot") version "3.2.5" kotlin("jvm") version "1.9.0" } repositories { mavenCentral() } dependencies { implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.security:spring-security-saml2-service-provider") implementation("org.opensaml:opensaml-core:4.2.0") implementation("org.opensaml:opensaml-saml-impl:4.2.0") } |
ポイント:
spring-security-saml2-service-providerが SAML 2.0 の公式サポートを提供し、Keycloak 用アダプタは不要です。OpenSAML はメタデータのパースや署名検証に内部で使用されます。
Keycloak 側での Realm と SAML クライアント設定手順
このセクションでは、Keycloak の管理コンソール上で Realm を作成し、SAML 用クライアントと属性マッピングを構築するまでの具体的な操作手順を示します。正しい設定がないと Spring Boot 側で「メタデータ取得失敗」や「属性未取得」のエラーになります。
Realm の作成と基本設定
- 管理コンソールに
adminアカウントでログイン - 左メニュー Realm → Add realm をクリックし、
saml-demo等の名前を入力して Create - Login タブで「メールアドレスは必須」やパスワードポリシーなど、組織に合わせた認証設定を行う
UI は Keycloak 24.x 系でも大きく変わっておらず、上記手順で問題なく作成できます。
SAML クライアントの登録
- 作成した Realm の Clients → Create
Client IDにspring-boot-saml-spを入力し、Client Protocolは saml を選択- Settings タブで以下を設定(※全角文字は使用しない)
| 項目 | 推奨値 |
|---|---|
| Valid Redirect URIs | http://localhost:8080/login/saml2/sso/keycloak-saml |
| Base URL | http://localhost:8080/ |
| Master SAML Processing URL (ACS) | 自動生成された URL が上記と同一になることを確認 |
- Save → Installation タブへ移動し、Format Options で
SAML Metadata IDPSSODescriptorを選択して Download
ダウンロードした XML ファイルは Spring Boot の
metadata-uriとしてそのまま使用できます。
メタデータ・証明書の取得
- クライアント画面の Keys タブを開く
- Certificate 欄に表示された PEM 形式の X.509 証明書をコピーし、プロジェクト
src/main/resources/certificate/keycloak.crtに保存 - 同タブで Active がチェックされていることを必ず確認(無効化されていると署名検証が失敗します)
属性マッピングの詳細設定(email・roles)
Keycloak では「Mapper」機能でユーザー属性やロールを SAML アサーションに追加できます。ここでは email と role list を例示します。
email マッパーの作成手順
- クライアント詳細画面の Mappers → Create
- Mapper Type:
User Property - Name:
email - Property:
email(Keycloak のユーザープロパティ名) - Friendly Name:
email、SAML Attribute Name:email、Attribute Friendly Name:email - Add to ID token と Add to access token は不要だが、Add to user info にチェックを入れる(デバッグ時に便利)
roles マッパーの作成手順
- Mappers → Create
- Mapper Type:
Group MembershipまたはRole List(組織で管理している対象によって選択) - Name:
roles - Token Claim Name:
roles、SAML Attribute Name:roles、Attribute Friendly Name:Roles - Full group path は false(ロール名だけを渡す)に設定
これらのマッパーが正しく設定されていないと、Spring Boot 側で
attribute not found: email等のエラーになります。
Spring Boot アプリ側の実装
本章では取得したメタデータと証明書を用いた Spring Security の設定例を示します。YAML と Java Config 両方を掲載し、どちらでも同等に機能することが確認できます。
application.yml による RelyingPartyRegistration 設定
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
spring: security: saml2: relyingparty: registration: keycloak-saml: # 任意の登録名 identityprovider: metadata-uri: "http://localhost:8080/realms/saml-demo/protocol/saml/descriptor" signing: credentials: - certificate-location: "classpath:/certificate/keycloak.crt" entity-id: "spring-boot-saml-sp" assertion-consumer-service-location: "http://localhost:8080/login/saml2/sso/keycloak-saml" |
ポイント:
metadata-uriは Keycloak が提供するエンドポイント、certificate-locationには先ほど保存した PEM ファイルを指定します。
OpenSamlMetadataResolver を用いた Java Config(エラーハンドリング付き)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
package com.example.saml.config; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import org.opensaml.core.config.InitializationException; import org.opensaml.core.config.InitializationService; import org.opensaml.saml.metadata.resolver.impl.BasicSAMLMetadataResolver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.security.saml2.provider.service.registration.*; import org.springframework.util.StreamUtils; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Optional; /** * Keycloak のメタデータ XML を OpenSAML で解析し、RelyingPartyRegistration * オブジェクトを生成する設定クラス。 */ @Configuration @RequiredArgsConstructor public class SamlRelyingPartyConfig { private static final String METADATA_URL = "http://localhost:8080/realms/saml-demo/protocol/saml/descriptor"; /** * OpenSAML の初期化はアプリ起動時に一度だけ実行する必要があります。 */ @PostConstruct public void initOpenSaml() { try { InitializationService.initialize(); } catch (InitializationException ex) { throw new IllegalStateException("Failed to initialize OpenSAML", ex); } } /** * メタデータ URL から取得した XML を解析し、RelyingPartyRegistration を生成します。 */ @Bean public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws IOException { // 1. メタデータ取得(ネットワーク障害に備えて Optional でラップ) String metadataXml = fetchMetadata(METADATA_URL) .orElseThrow(() -> new IllegalArgumentException("Unable to download SAML metadata from " + METADATA_URL)); // 2. OpenSAML の Resolver を利用して XML をオブジェクト化 BasicSAMLMetadataResolver resolver = new BasicSAMLMetadataResolver(); Saml2RelyingPartyRegistrationResolver registrationResolver = new OpenSamlMetadataResolver(resolver); // 3. 必要情報を上書き(entity-id と ACS はコード側で固定) RelyingPartyRegistration base = registrationResolver.resolve(metadataXml) .orElseThrow(() -> new IllegalArgumentException("Failed to resolve SAML metadata")); RelyingPartyRegistration finalReg = RelyingPartyRegistration .withRelyingPartyRegistration(base) .registrationId("keycloak-saml") .entityId("spring-boot-saml-sp") .assertionConsumerServiceLocation("http://localhost:8080/login/saml2/sso/keycloak-saml") .signingX509Credentials(c -> c.add( X509CredentialFactory.fromCertificate( new ClassPathResource("certificate/keycloak.crt").getInputStream()))).build(); return new InMemoryRelyingPartyRegistrationRepository(finalReg); } /** ネットワークからメタデータ XML を取得するユーティリティ(例外は呼び出し側で処理) */ private Optional<String> fetchMetadata(String url) { try (var in = new java.net.URL(url).openStream()) { String body = StreamUtils.copyToString(in, StandardCharsets.UTF_8); return Optional.ofNullable(body); } catch (IOException e) { // ログ出力は Spring のロガーで行うと実運用に便利 System.err.println("Failed to fetch SAML metadata: " + e.getMessage()); return Optional.empty(); } } } |
重要ポイント
| 項目 | 説明 |
|---|---|
| OpenSAML 初期化 | InitializationService.initialize() が必須。未実行だと NoClassDefFoundError が発生します。 |
| 例外ハンドリング | メタデータ取得失敗や XML パースエラーは Optional と明示的な例外で上位に伝搬させます。 |
| 追加依存 | opensaml-core、opensaml-saml-impl が必須です(pom に記載済み)。 |
SecurityFilterChain の設定
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
package com.example.saml.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; /** * 認可ポリシーと SAML2 ログイン/ログアウトエンドポイントを定義します。 */ @Configuration public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ---- アクセス制御 ------------------------------------------------- .authorizeHttpRequests(authz -> authz .requestMatchers("/", "/error", "/css/**").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated()) // ---- SAML2 ログイン設定 ------------------------------------------- .saml2Login(saml2 -> saml2 .loginProcessingUrl("/login/saml2/sso/keycloak-saml")) // ---- SAML2 ログアウト設定 ----------------------------------------- .saml2Logout(Customizer.withDefaults()); return http.build(); } } |
hasRole("ADMIN")は後述の属性マッピングで付与したロールがROLE_ADMINとして変換されることを前提にしています。
カスタム属性マッピングと UserDetailsService
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
package com.example.saml.service; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.*; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.stereotype.Component; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; /** * SAML アサーションからメールアドレスとロールを抽出し、Spring Security の * Authentication オブジェクトへ変換するコンバータです。 */ @Component public class SamlUserDetailsConverter implements Converter<Saml2Authentication, UsernamePasswordAuthenticationToken> { @Override public UsernamePasswordAuthenticationToken convert(Saml2Authentication source) { Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) source.getPrincipal(); // ---- 属性取得 ------------------------------------------------- String email = principal.getFirstAttribute("email"); if (email == null) { throw new IllegalArgumentException("Required attribute 'email' not found in SAML assertion"); } var roleAttrs = principal.getAttribute("roles"); // 文字列リストが期待される List<GrantedAuthority> authorities = Optional.ofNullable(roleAttrs) .orElseGet(Collections::emptyList) .stream() .map(Object::toString) .map(r -> new SimpleGrantedAuthority("ROLE_" + r.toUpperCase())) .collect(Collectors.toList()); // ---- Spring Security の UserDetails 作成 ------------------------- UserDetails user = User.withUsername(email) .password("{noop}") // パスワードは使用しない .authorities(authorities) .accountLocked(false) .build(); return new UsernamePasswordAuthenticationToken(user, null, authorities); } } |
SecurityConfigに以下を追記すると、カスタムコンバータが自動的に利用されます。
|
1 2 |
http.authenticationProvider(new SamlAuthenticationProvider(samlUserDetailsConverter)); |
動作確認・デバッグ・ベストプラクティス
実装後はローカルでフロー全体を検証し、よくある落とし穴を事前に回避できるようにします。
ログイン/ログアウトフローのテスト手順
- アプリ起動
bash
./mvnw spring-boot:run # または ./gradlew bootRun - ブラウザで
http://localhost:8080/にアクセスし、画面左上に表示される「Login」リンクをクリック → Keycloak のログイン画面へリダイレクト - ユーザー名・パスワードで認証 → 成功するとホームページに戻り、右上に
Principal: <email>が表示されれば OK /logoutエンドポイント(例:http://localhost:8080/logout)へアクセスし、Keycloak 側でもセッションが破棄されたことを確認
時刻同期と HTTPS 設定
- 時刻ずれは SAML の有効期限検証で
InvalidTimeWindowExceptionを引き起こすため、サーバ・Keycloak コンテナ共に NTP 同期を必須とします。 - 本番環境では自己署名ではなく、信頼できる CA 発行の証明書で HTTPS 化してください。Spring Boot の
server.ssl設定例は以下です。
|
1 2 3 4 5 6 7 8 |
server: ssl: enabled: true key-store: classpath:/keystore/server.p12 key-store-password: changeit key-store-type: PKCS12 protocol: TLS |
よくあるエラーと対処法
| エラーメッセージ | 主な原因 | 推奨対策 |
|---|---|---|
metadata URL not found (404) |
Realm 名やポートが間違っている | コンテナの公開ポート (8080 vs 8443) と Realm パス /realms/{realm} を再確認 |
Signature verification failed |
証明書が古い・Keycloak 側でローテーションされた | Keycloak の Keys タブから最新証明書を再エクスポートし、keycloak.crt を上書き |
Attribute not found: email |
アサーションに email が含まれていない |
クライアント → Mappers に email マッパーを追加(上記手順参照) |
InvalidTimeWindowException |
サーバ時刻がずれているか、トークン有効期限が短すぎる | NTP 同期または spring.security.saml2.accepted-clock-skew=120 で許容範囲を拡大 |
org.opensaml.core.config.InitializationException |
OpenSAML が初期化されていない | @PostConstruct で InitializationService.initialize() を必ず呼び出す |
ロギングでのデバッグ方法
|
1 2 3 4 5 |
logging: level: org.springframework.security.saml2: DEBUG # SAML のフロー全体を詳細に出力 org.opensaml: TRACE # OpenSAML の内部処理を確認したい場合 |
- デバッグ実行時はコンソールに大量の XML が出力されるので、
grep "Assertion"で必要箇所だけ抽出すると見やすくなります。
サンプルリポジトリと参考リンク
以下の GitHub リポジトリが本稿のフル実装例です。クローン後は docker-compose.yml が Keycloak コンテナと PostgreSQL を自動起動し、初期 Realm/Client がスクリプトで作成されます。
- リポジトリ: https://github.com/example-org/keycloak-saml-springboot-sample
- README の主な項目
- 必要環境(Java 17, Docker, Spring Boot 3.2)
docker-compose up -dで起動する手順と初期データの自動ロード方法- 完全版
application.yml、SecurityConfig、サンプルコントローラ (HomeController,AdminController) のコード - ローカル HTTPS 設定(自己署名証明書生成スクリプト)
公式ドキュメント・参考リンク
- Keycloak 公式サイト: https://www.keycloak.org/
- Spring Security SAML2 ガイド: https://docs.spring.io/spring-security/reference/servlet/saml2/login.html
- OpenSAML 4.x API ドキュメント: https://wiki.shibboleth.net/confluence/display/OpenSAML/Home
以上で、Keycloak を IdP とした SAML 認証を Spring Boot アプリに組み込む手順が完結します。正しい属性マッピングと証明書管理 が最も障害の発生しやすいポイントですので、設定変更後は必ずメタデータ再取得・ログインテストを実施してください。