Java
authentication
Authorization
Keycloak

Keycloakで統合Windows認証(デスクトップSSO)を適用したときの実際

はじめに

Keycloakで統合Windows認証(以下、デスクトップSSO)を使用する案件があり、フィジビリティ検証を実施しました。今回はフィジビリティ検証の実施内容と実施結果を紹介したいと思います。

なお、Keycloakでの統合Windows認証については『Keycloakで統合Windows認証を試してみる』をご参照下さい。

検証したモジュールのバージョン

検証したモジュールのバージョンは以下になります。異なるバージョンでは、記事の内容が当てはまらない可能性がありますので、ご注意下さい。

  • Keycloakサーバ

    • Keycloak3.4.0.Final
    • OpenJDK 1.8.0_151-b12
    • CentOS 7.4
  • Active Directoryサーバ

    • Windows Server 2016

要件

検証内容の説明の前に、今回の案件の要件を説明します。

  • Active Directory(以下、AD)を使用して、デスクトップSSOがしたい。
  • ADは以下の図のように複数フォレストが信頼関係を結んでいる構成。フォレスト内には複数のドメインが存在。(マルチドメイン、マルチフォレスト)
  • フォレストによってはデスクトップSSOではなく、フォーム認証をしたい。具体的には下記図のフォレスト3のユーザーはデスクトップSSOではなく、フォーム認証をさせたい。さらに、フォーム認証の際はフォレスト1のユーザーでログインさせたい。
  • SSO対象のアプリにはヘッダ連携により、ADの項目を受け渡したい。
  • ユーザー管理はAD側で実施し、Keycloakからユーザー情報の更新は行わない。

AD.png

システム構成

検証は以下のシステム構成にて行いました。

system.png

また、Keycloakのサービスプリンシパル名HTTP/keycloak.example.comを以下のドメインにて登録しています。

ドメイン:ad101.example.com
ktpass -out keycloak.HTTP.keytab.ad101 -princ HTTP/keycloak.example.com@AD101.EXAMPLE.COM -ptype KRB5_NT_PRINCIPAL -mapuser keycloak -pass P@ssw0rd
ドメイン:ad201.example.com
ktpass -out keycloak.HTTP.keytab.ad201 -princ HTTP/keycloak.example.com@AD201.EXAMPLE.COM -ptype KRB5_NT_PRINCIPAL -mapuser keycloak -pass P@ssw0rd
ドメイン:ad301.example.com
ktpass -out keycloak.HTTP.keytab.ad301 -princ HTTP/keycloak.example.com@AD301.EXAMPLE.COM -ptype KRB5_NT_PRINCIPAL -mapuser keycloak -pass P@ssw0rd

:information_source:サービスプリンシパル名はフォレスト内で一意のため、ドメインad102.example.comでは実施しません。

Kerberos認証時の動作調査

早速検証に取り掛かりたいところですが、検証の前にKerberos認証時の動作内容を把握しておこうと思います。

KeycloakでデスクトップSSOをする場合の動作概要は、Keycloakで統合Windows認証を試してみるの内容になります。しかし、以下の点についてはよく分からなかったので調査してみました。

question.png

【疑問1】kerberosプロバイダとldapプロバイダによるkerberos認証の違い

疑問内容

Keycloakでユーザーフェデレーションを作成する際、プロバイダはkerberosldapを選択できます。どちらを選択してもkerberos認証ができるようですが、何が違うのでしょうか?

Q1.png

調査結果

Keycloakのドキュメントを見ると、以下の記述があります。

Keycloak validates token from the browser and authenticates the user. It provisions user data from LDAP (in case of LDAPFederationProvider with Kerberos authentication support) or let user to update his profile and prefill data (in case of KerberosFederationProvider).

どうやら、認証後の動作が異なるようです。

ldapプロバイダを使用した場合はLDAPからユーザーデータをプロビジョニングし、kerberosプロバイダを使用した場合はログイン時に自身によるプロフィール更新ができるという違いがあるようです。

今回は「SSO対象のアプリにはヘッダ連携により、ADの項目を受け渡したい」という要件があるため、LDAPからユーザーデータを取得する必要があります。そのためプロバイダは「ldap」を使用する必要がありますね。

【疑問2】マルチドメイン構成時のサービスチケット取得処理

疑問内容

ユーザーがログインしているドメインにKeycloakのサービスプリンシパル名が登録されていない場合(今回の例ではドメインad102.example.com)でも、クライアントとWindowsサーバ(KDC)とのやりとりは1回だけでしょうか?それとも各Windowsサーバ(KDC)とやりとりするのでしょうか?

調査結果

ユーザーがログインしているドメインにKeycloakのサービスプリンシパル名が登録されている場合(ドメイン:ad101.example.com)と、登録されていない場合(ドメイン:ad102.example.com)でパケットキャプチャを取り比較してみました。

結論から言うと、『各Windowsサーバ(KDC)とやりとりする』でした。

Keycloakのサービスプリンシパル名が登録されているドメインに対してサービスチケット取得を行った場合、Keycloakのサービスチケットを要求すると、Keycloakのサービスチケットが返却されるためクライアントとWindowsサーバ(KDC)のやりとりは1回になります。

TGS-sequence-ad101.example.com.png

しかし、Keycloakのサービスプリンシパル名が登録されていないドメインの場合、Keycloakのサービスチケットを要求すると、Keycloakのサービスプリンシパル名が登録されているドメインのTGTが返却され、そのドメインに対して再度Keycloakのサービスチケットを要求するという動作になります。つまり、クライアントとWindowsサーバ(KDC)とのやりとりは1回ではなく、各Windowsサーバ(KDC)とやりとりしていました。

TGS-sequence-ad102.example.com.png

Keycloakの動作に直接関係はない部分ですが、何か問題が発生した際にこのあたりの動きを理解しておくと役立ちますね。

【疑問3】サービスチケット取得後の処理

疑問内容

Keycloakはサービスチケット取得後、「HTTPサービスログイン」、「チケット検証」を行っているようですが、具体的に何をしているのでしょうか?

調査結果

Keycloakのソースコードから処理を確認しました。ドキュメントを見ても分からない場合はソースコードで確認できるというのはオープンソースのいいところですね。

ソースコードの該当箇所はorg.keycloak.federation.kerberos.impl.SPNEGOAuthenticator.javaauthenticateメソッドで、処理内容としては大きく2つに分けられます。

ServiceTicketProcessing.png

①の処理

javaのLoginModuleを実行して、LDAPプロバイダ設定で指定したサーバープリンシパル、keyTabをもとに、javax.security.auth.Subjectを作成します。

具体的には、LDAPプロバイダ設定で指定したサーバープリンシパルをSubjectの主体セットに、LDAPプロバイダ設定で指定したkeyTabファイルから秘密鍵をSubjectのprivate資格セットに設定します。

実行するLoginModuleは、使用しているJavaがIBM製かどうかで変わります。

■IBM製Javaではない場合
LoginModule
com.sun.security.auth.module.Krb5LoginModule

パラメータ

key value
storeKey true
doNotPrompt true
isInitiator false
useKeyTab true
keyTab プロバイダ設定で指定したKeytabファイル
principal プロバイダ設定で指定したサーバープリンシパル
debug プロバイダ設定で指定したデバッグ

パラメータの意味や処理の詳細についてはこちらをご参照下さい。

■IBM製Javaの場合
LoginModule
com.ibm.security.auth.module.Krb5LoginModule

パラメータ

key value
noAddress true
credsType acceptor
useKeyTab プロバイダ設定で指定したKeytabファイルのURL
principal プロバイダ設定で指定したサーバープリンシパル
debug プロバイダ設定で指定したデバッグ

パラメータの意味や処理の詳細についてはこちらをご参照下さい。

②の処理

GSS-API(Generic Security Service Application Program Interface)を使用して、サービスチケットからユーザープリンシパル(認証済みユーザー名)を取得します。①で作成したSubjectから秘密鍵を取得してサービスチケットを復号することによりユーザープリンシパルを取得します。

また、krb5.confのlibdefaultsセクションから設定clockskewを取得し、サービスチケットのタイムスタンプチェックを行っています。clockskewは、チケットのタイムスタンプとサーバーのシステム時刻が異なる場合の許容時間で、デフォルトは300秒になります。

読み込むkrb5.confファイルは以下のようにサーバーOSにより異なりますが、ファイルが存在しなくてもエラーにはならずにデフォルト値で動作します。

OS krb5.conf
Windows (Windowsディレクトリ)\krb5.ini
  または
c:\winnt\krb5.ini
SunOS /etc/krb5/krb5.conf
OS X (ユーザーホーム)/Library/Preferences/edu.mit.Kerberos
  または
/Library/Preferences/edu.mit.Kerberos
  または
/etc/krb5.conf
その他 /etc/krb5.conf

今回は特に設定は行わずに、デフォルトの300秒で動作させようと思います。

Keycloak設定内容

要件の内容とKerberos認証時の動作を調査した結果から、Keycloakの設定は以下のようにします。

ログインID

「マルチドメイン」かつ「フォーム認証を使用する」という要件のため、ログインIDにはドメイン情報が必要になります。そのためログインIDには、ADの「UserPrincipalName」を使用します。

ユーザー名のLDAP属性設定
UserNameSetting.png

LDAPマッパー「Username」設定
UserNameMapperSetting.png

ユーザーフェデレーション設定

アプリにADの項目を連携したいため、ユーザーの属性情報を取得する必要があります。ユーザーフェデレーション作成時に、kerberosプロバイダを選択すると属性情報の取得が行われないため、属性情報の取得を行うldapプロバイダを使用します。

LDAP接続設定

フォレスト内には複数のドメインが存在し、かつ、属性取得を行う必要があります。属性取得する際に、Keycloakから各ドメインコントローラに接続してしまうと、ユーザーフェデレーション設定をドメインコントローラの数分設定する必要があり大変です。ADにはフォレスト内にあるすべてのオブジェクトのコピーを保持する「グローバルカタログ」があるため、Keycloakからはグローバルカタログに接続するようにします。

LDAP設定
LDAPSetting.png

:warning: ユーザーDNはフォレスト内の全てのドメインが含まれる値を指定する必要があります。

設定まとめ

フォレスト3のユーザーではログインしないため、ドメイン「ad301.example.com」の接続設定は行いません。

keycloakSetting1.png

keycloakSetting2.png

keycloakSetting3.png

検証内容と結果

いよいよ検証内容と結果の説明になります。検証内容の説明の前に用語の説明をしたいと思います。

用語定義

KeycloakとADの接続構成を下記のように定義します。

用語 定義 対象ドメインコントローラ
Keycloak連携有りAD Keycloakと接続しているドメインコントローラと、そのドメインコントローラを含むフォレスト内のドメインコントローラ。 ad101.example.com
ad102.example.com
ad201.example.com
Keycloak連携無しAD Keycloakと接続しているドメインコントローラを含まないフォレスト内のドメインコントローラ ad301.example.com

testEnvironment.png

検証内容と結果

接続パターン毎に要件通りにログイン可能か検証しました。検証内容と結果は以下の通りです。

testResult.png

ADユーザーのパターンは全滅ですね。。。NGは4種類発生しました。それぞれのNG内容と対応方法を見ていきましょう。

NG内容と対応

NG①

画面

以下のエラー画面が出力されました。
NG1.png

エラーログ

以下のWARNログが出力されました。

2018-03-08 17:16:19,134 WARN  [org.keycloak.storage.ldap.LDAPStorageProvider] (default task-14) Kerberos/SPNEGO authentication succeeded with username [user101], but couldn't find or create user with federation provider [ad101]
2018-03-08 17:16:19,134 WARN  [org.keycloak.events] (default task-14) type=LOGIN_ERROR, realmId=demo, clientId=account, userId=null, ipAddress=172.26.22.198, error=invalid_user_credentials, auth_method=openid-connect, auth_type=code, response_type=code, redirect_uri=https://keycloak.example.com:8443/auth/realms/demo/account/login-redirect, code_id=cfad83fb-d620-42d6-bfdf-e9a3cad4027c, response_mode=query
2018-03-08 17:16:19,134 WARN  [org.keycloak.services] (default task-14) KC-SERVICES0013: Failed authentication: org.keycloak.authentication.AuthenticationFlowException
        at org.keycloak.authentication.DefaultAuthenticationFlow.processResult(DefaultAuthenticationFlow.java:224)
        at org.keycloak.authentication.DefaultAuthenticationFlow.processFlow(DefaultAuthenticationFlow.java:201)
        …

原因

ソースコードを見ると、Kerberos認証後のユーザー情報取得時に、チケットから取得したユーザープリンシパルから@より左側の文字列を抽出し、LDAP検索してしまっています。そのためUsernameのLDAP属性にuserPrincipalNameを指定していると「user101@ad101.example.com」で検索するところを「user101」で検索してしまい、ユーザー情報が取得できずエラーとなってしまいます。

org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator
    public String getAuthenticatedUsername() {
        String[] tokens = authenticatedKerberosPrincipal.split("@");
        String username = tokens[0];  // ★ここで@より左側の文字列のみを取得している
        if (!tokens[1].equalsIgnoreCase(kerberosConfig.getKerberosRealm())) {
            throw new IllegalStateException("Invalid kerberos realm. Realm from the ticket: " + tokens[1] + ", configured realm: " + kerberosConfig.getKerberosRealm());
        }
        return username;
    }

対応

チケットから取得したユーザープリンシパルをそのまま返却するよう上記ソースコードを変更します。そうすることにより、userPrincipalNameを「user101@ad101.example.com」で検索するようになるため、ユーザー情報を取得できるようになります。

しかし、この対応は完全ではありません。

チケットから取得したユーザープリンシパルの値は、ADの場合、userPrincipalNameの値ではなく、「sAMAccountNameの値@Kerberosレルムの値」です。デフォルトではuserPrincipalNameと「sAMAccountNameの値@Kerberosレルムの値」は同じ値になるのですが、異なる値に変更することが可能です。

WindowsServerのアカウント設定画面
sAMAccountName.png

上図の設定の場合、チケットから取得したユーザープリンシパルは「user101@ad101.example.com」ではなく「hoge@ad101.example.com」になってしまいます。

この問題に対応するには、Kerberos認証後のユーザー情報取得処理を以下のように変更します。

  • ADの場合は、UsernameのLDAP属性設定を使用せず、sAMAccountNameで検索する。
  • LDAPサーチDNに、チケットから取得したユーザープリンシパルの@より右側の文字列を指定する。

NG②

画面

NG①と同じです。

エラーログ

以下のWARNログが出力されました。

2018-03-09 13:40:47,013 WARN  [org.keycloak.services] (default task-9) KC-SERVICES0013: Failed authentication: java.lang.IllegalStateException: Invalid kerberos realm. Realm from the ticket: AD102.EXAMPLE.COM, configured realm: AD101.EXAMPLE.COM
        at org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator.getAuthenticatedUsername(SPNEGOAuthenticator.java:119)
        at org.keycloak.storage.ldap.LDAPStorageProvider.authenticate(LDAPStorageProvider.java:684)
        at org.keycloak.credential.UserCredentialStoreManager.authenticate(UserCredentialStoreManager.java:282)
        …

原因

Kerberos認証後にKeycloak内のチェック処理で、チケットから取得したユーザープリンシパルの@より右側の値と、LDAPプロバイダ設定の「kerberosレルム」に指定した値が異なる場合エラーにしてしまいます。

org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator
    public String getAuthenticatedUsername() {
        String[] tokens = authenticatedKerberosPrincipal.split("@");
        String username = tokens[0];
        if (!tokens[1].equalsIgnoreCase(kerberosConfig.getKerberosRealm())) {  // ★ここでエラーにしてしまう
            throw new IllegalStateException("Invalid kerberos realm. Realm from the ticket: " + tokens[1] + ", configured realm: " + kerberosConfig.getKerberosRealm());
        }
        return username;
    }

対応

マルチドメイン構成の場合、このチェック処理は不要のため、チェック処理を行わないようにソースコードを変更します。
:information_source: この対応は2017/12/21にリリースされた3.4.2.Finalで取り込まれました。

NG③

画面

NG①と同じです。

エラーログ

NG①と同じです。

原因

Kerberos認証後のユーザー情報取得時に、認証時に使用したLDAPプロバイダからユーザー情報を取得できない場合、他のLDAPプロバイダからユーザー情報を取得せずにエラーにしてしまいます。つまり、他フォレストのグローバルカタログは検索しないようになっています。

org.keycloak.storage.ldap.LDAPStorageProvider
    public CredentialValidationOutput authenticate(RealmModel realm, CredentialInput cred) {
        if (!(cred instanceof UserCredentialModel)) CredentialValidationOutput.failed();
        UserCredentialModel credential = (UserCredentialModel)cred;
        if (credential.getType().equals(UserCredentialModel.KERBEROS)) {
            if (kerberosConfig.isAllowKerberosAuthentication()) {
                String spnegoToken = credential.getValue();
                SPNEGOAuthenticator spnegoAuthenticator = factory.createSPNEGOAuthenticator(spnegoToken, kerberosConfig);

                spnegoAuthenticator.authenticate();

                Map<String, String> state = new HashMap<String, String>();
                if (spnegoAuthenticator.isAuthenticated()) {

                    // TODO: This assumes that LDAP "uid" is equal to kerberos principal name. Like uid "hnelson" and kerberos principal "hnelson@KEYCLOAK.ORG".
                    // Check if it's correct or if LDAP attribute for mapping kerberos principal should be available (For ApacheDS it seems to be attribute "krb5PrincipalName" but on MSAD it's likely different)
                    String username = spnegoAuthenticator.getAuthenticatedUsername();
                    UserModel user = findOrCreateAuthenticatedUser(realm, username);

                    if (user == null) {
                        logger.warnf("Kerberos/SPNEGO authentication succeeded with username [%s], but couldn't find or create user with federation provider [%s]", username, model.getName());
                        return CredentialValidationOutput.failed();  // ★ここでエラーにしてしまう
                    } else {

対応

Kerberos認証はOKにもかかわらずユーザー情報が取得できなかった場合は、他のLDAPプロバイダからユーザー情報を取得するようソースコードを変更します。

:information_source: この対応方法以外にも、ユーザー情報を取得するLDAPプロバイダを特定して実行するという方法も考えられます。

NG④

画面

NG①と同じです。

エラーログ

NG①と同じです。

原因

Kerberos認証後のユーザー情報取得時に、認証時に使用したLDAPプロバイダからユーザー情報を取得できない場合エラー画面を表示してしまい、フォーム認証が表示されません。
NG③は「ユーザー情報を取得できない場合に他のLDAPプロバイダで検索しない」という内容ですが、NG④は「ユーザー情報を取得できなかった場合にフォーム認証を表示しない」という違いがあります。

対応

この件はNG扱いにしていますが、今回の要件に対してはNGであり、本来の動作としては問題ないと考えます。

今回のように「AD側は信頼関係を結んでいるが、フォーム認証させたい」という要件への対処を考慮すると、画面上から「デスクトップSSOを除外するドメイン」を設定できるようにし、そのドメインの場合はデスクトップSSOを行わずにフォーム認証を行うようにするとよいですね。

まとめ

検証した結果、残念ながらKeycloak.3.4.0.FinalのLDAPプロバイダは、マルチドメイン・マルチフォレスト環境でのデスクトップSSOには対応できておりませんでした。そのため、マルチドメイン、マルチフォレスト環境でLDAPプロバイダを使用したデスクトップSSOを行う場合は、ソースコード修正が必要になりますのでご注意下さい。

今回の検証結果は、Keycloakコミュニティに報告しました。こちらで熱く議論中ですので興味のある方はご注目下さい!

mark.png