📖

LDAP/LDAPSのクライアント認証(Java)

に公開

はじめに

LDAP・LDAPSのクライアント認証のJavaの実装を行ったことのメモ記録です。

環境

  • LDAPサーバ:OpenLDAP
  • Java:OpenJDK17(Eclipse Temurin)

LDAPの事前知識

ベースDN

LDAPディレクトリの住所の出発点(ルート)に当たるものです。
どのディレクトリを起点に認証を進めるかの起点になります。

例えば、以下のような構成の場合は「dc=example,dc=com」になります。

  dc=example
    └─dc=com  ← ここ
        ├─ou=people
        │   ├─cn=yamada,ou=people,dc=example,dc=com
        │   └─cn=sato,ou=people,dc=example,dc=com
        │
        ├─ou=groups
        │   ├─cn=admin,ou=groups,dc=example,dc=com
        │   │

認証方式

LDAPの認証方式には以下のように色々と種類があります。
SASL認証はメカニズムを選択する必要があり、それぞれに必要な設定は変わってきます。

  • Anonymous(匿名認証)
    認証なしでの接続を行う。
    読み取り専用操作に使用することが一般的。
  • Simple
    平文でパスワード認証を行う。
    パスワードの盗聴リスクがあるため、LDAPS認証を推奨。
  • SASL
    認証メカニズムが複数ある。
    OSやLDAPの種類により対応有無がある。
    • DIGEST-MD5: MD5ハッシュベース、チャレンジ・レスポンス方式
    • CRAM-MD5: より軽量なMD5ベース認証
    • PLAIN: 平文(SASLフレームワーク内)
    • EXTERNAL: クライアント証明書による認証
    • GSSAPI: Kerberos認証(Active Directoryならこれが一般的)

クライアント側の実装

Java標準のライブラリである、javax.namingパッケージを使った実装です。
認証方式はSimpleです。
今回は、ユーザ認証を行う部分のみにフォーカスします。

DNを直接指定してユーザ認証

ユーザのDNを明示的に指定して認証する方法です。

    /**
     * ユーザ認証
     * @param username ユーザ名
     * @param password パスワード
     */
    public boolean authenticate(String username, String password) {
        // LDAPの接続先URL
        String ldapUrl = "ldap://ldap.example.com:389"
        // ユーザのDN
        String userPrincipal = "uid=" + username +",ou=people,dc=example,dc=com"
        // 認証方式
        String authenticationType = "simple";

        Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.PROVIDER_URL, ldapUrl);
        env.put(Context.SECURITY_AUTHENTICATION, authenticationType);
        env.put(Context.SECURITY_PRINCIPAL, userPrincipal); 
        env.put(Context.SECURITY_CREDENTIALS, password);

        try {
            DirContext ctx = new InitialDirContext(env);
            ctx.close();

            return true; 
        } catch (NamingException e) {
            // 認証失敗
            return false; 
        }
    }

ベースDN配下からユーザーを検索して認証

匿名認証を行い、認証対象のユーザをエントリ検索します。
検索で取得したエントリ情報をもとに、認証する方法です。
※匿名認証が禁止されているLDAPサーバでは利用できません。

/**
  * ユーザ認証
  * @param username ユーザ名
  * @param password パスワード
  */
public boolean authenticate(String username, String password) {
     // LDAPの接続先URL
     String ldapUrl = "ldap://ldap.example.com:389";
     // 認証対象のDN
     String userPrincipal = "ou=people,dc=example,dc=com";
     // 認証方式
     String authenticationType = "simple";
     // 匿名認証の設定
     Hashtable<String, String> env = new Hashtable<>();
     env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
     env.put(Context.PROVIDER_URL, ldapUrl);
     try {
         // 匿名認証の実行
         DirContext ctx = new InitialDirContext(env);
         ctx.close();
     } catch (NamingException e) {
         // 認証失敗
         return false;
     }
     // ユーザIDでの検索のためのフィルター
     String searchFilter = String.format("(uid=%s)", username);
     // 検索する階層範囲を指定
     SearchControls searchControls = new SearchControls();
     searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
     try {
         DirContext context = new InitialDirContext(env);
         NamingEnumeration<SearchResult> results = context.search(
             userPrincipal, searchFilter, searchControls);
         // 検索対象のDNにユーザが存在するか
         if (results.hasMoreElements()) {
             SearchResult entry = results.next();
             // 認証設定
             Hashtable<String, String> authEnv = new Hashtable<>();
             authEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
             authEnv.put(Context.PROVIDER_URL, ldapUrl);
             authEnv.put(Context.SECURITY_AUTHENTICATION, authenticationType);
             authEnv.put(Context.SECURITY_PRINCIPAL, entry.getNameInNamespace());
             authEnv.put(Context.SECURITY_CREDENTIALS, password);
                 DirContext authCtx = new InitialDirContext(authEnv);
                 authCtx.close();
                 return true;
         }
         return false;
     } catch (NamingException e) {
         return false;
     }
}

LDAPS認証する場合

  1. LDAP認証時の設定で、PROVIDER_URLldaps://に変更し、SECURITY_PROTOCOLssl に指定します。
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.PROVIDER_URL, "ldaps://ldap.example.com:636");
(..省略..)
env.put(Context.SECURITY_PROTOCOL, "ssl");
  1. JavaのキーストアにLDAPS用のサーバ証明書を登録します。
    以下のコマンドを実行します。
keytool -importcert \
  -alias {任意のエイリアス名} \
  -file {証明書ファイル(絶対パス)} \
  -keystore {Javaのキーストアパス} \
  -storepass {キーストアのパスワード}

デフォルトのキーストア($JAVA_HOME/lib/security/cacerts)を使用する場合、
-storepassはchangeitです。

補足

LDAP認証時に発生する例外

認証に失敗するのはInitialDirContextの生成時に発生する例外を元に、原因を特定するのが一般的です。
下記に代表的な例外と対応方法をまとめます。

例外クラス 主な原因 対処方法・備考
AuthenticationException DN またはパスワードが間違っている
アカウントが無効、ロック、期限切れ
ユーザーDNとパスワードを再確認
CommunicationException サーバが存在しない(ホスト名 or ポートミス)
ネットワーク切断
LDAP URL/ポートを確認
サーバが起動中か確認
ファイアウォールやDNS設定を見直す
NamingException LDAP認証に関わる全ての例外 e.getClass() で具体例外を特定して調査
SSLHandshakeException サーバ証明書が Java に信頼されていない
証明書チェーンの不備
クライアントとサーバのTLSバージョン非互換
Java truststore に証明書をインポート
-Djavax.net.debug=ssl で詳細ログ確認

Discussion