📖
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認証する場合
- LDAP認証時の設定で、
PROVIDER_URL
をldaps://
に変更し、SECURITY_PROTOCOL
をssl
に指定します。
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.PROVIDER_URL, "ldaps://ldap.example.com:636");
(..省略..)
env.put(Context.SECURITY_PROTOCOL, "ssl");
- 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