Reputation: 143
I have searched about how to provide an active directory/LDAP based login for my Java web application. In this forum are also approx. 100+ tickets, which I did not understand, or which did not have same target like I. I tried several configurations.
With following code I had the best result, but still not getting it completely working.
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
@SuppressWarnings("restriction")
public class LdapAuth
{
private final static String ldapURI = "ldap://XXX.XXX.XXX.X:389"; //LDAP Server IP
private final static String contextFactory = "com.sun.jndi.ldap.LdapCtxFactory";
private static String[] requiredAttributes = {"cn", "givenName", "sn", "displayName", "userPrincipalName",
"sAMAccountName", "objectSid", "userAccountControl"};
private static String[] ADSearchPaths =
{
"CN=Users"
};
public static void authenticateUserAndGetInfo(final String user, final String password) throws Exception
{
try
{
final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
final String fullPath = classLoader.getResource("/META-INF/krb5.conf").getPath();
System.out.println(fullPath);
System.setProperty("sun.security.krb5.debug", "true");
System.setProperty("sun.security.spnego.debug", "true");
System.setProperty("java.security.krb5.conf", fullPath);
System.setProperty("sun.security.krb5.principal", user);
System.setProperty("sun.security.krb5.PrincipalName", user + "@MY.DOMAIN");
System.setProperty("sun.security.krb5.Credentials", password);
System.setProperty("javax.security.auth.useSubjectCredsOnly", "false");
final Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, LdapAuth.contextFactory);
env.put(Context.REFERRAL, "ignore");
env.put(Context.PROVIDER_URL, LdapAuth.ldapURI);
env.put(Context.SECURITY_AUTHENTICATION, "GSSAPI");
env.put(Context.SECURITY_PRINCIPAL, user);
env.put(Context.SECURITY_CREDENTIALS, password);
env.put(Context.REFERRAL, "ignore");
env.put(Context.AUTHORITATIVE, "false");
final DirContext ctx = new InitialDirContext(env);
String filter = "(sAMAccountName=" + user + ")"; // default for search filter username
if(user.contains("@")) // if user name is a email then
{
// String parts[] = user.split("\\@");
// use different filter for email
filter = "(userPrincipalName=" + user + ")";
}
final SearchControls ctrl = new SearchControls();
ctrl.setSearchScope(SearchControls.SUBTREE_SCOPE);
ctrl.setReturningAttributes(LdapAuth.requiredAttributes);
NamingEnumeration userInfo = null;
Integer i = 0;
do
{
userInfo = ctx.search(LdapAuth.ADSearchPaths[i], filter, ctrl);
i++;
}
while(!userInfo.hasMore() && i < LdapAuth.ADSearchPaths.length);
if(userInfo.hasMore())
{
final SearchResult UserDetails = (SearchResult)userInfo.next();
final Attributes userAttr = UserDetails.getAttributes();
System.out.println("adEmail = " + userAttr.get("userPrincipalName").get(0).toString());
System.out.println("adFirstName = " + userAttr.get("givenName").get(0).toString());
System.out.println("adLastName = " + userAttr.get("sn").get(0).toString());
System.out.println("name = " + userAttr.get("cn").get(0).toString());
System.out.println("AdFullName = " + userAttr.get("cn").get(0).toString());
}
userInfo.close();
}
catch(final javax.naming.AuthenticationException e)
{
e.printStackTrace();
}
}
}
As AD-Controller I use a synology AD package. I tried out simple authentication, but this did not work.
I got an error:
javax.naming.AuthenticationNotSupportedException: [LDAP: error code 8 - BindSimple: Transport encryption required.]
at com.sun.jndi.ldap.LdapCtx.mapErrorCode(Unknown Source)
...
Additional Information. With klist on client console I got two tickets for my user. And I am able to login from client console by ssh on AD-Controller with success
After changing and setting System.setProperty .... 6 lines and setting instead of simple:
env.put(Context.SECURITY_AUTHENTICATION, "GSSSAPI");
I got following output and I was asked on console for the Kerberos Userid.
>>>KinitOptions cache name is C:\Users\myuser\krb5cc_myuser
>> Acquire default native Credentials
default etypes for default_tkt_enctypes: 23.
>>> Found no TGT's in LSA
Kerberos-Password für myUserId: mySecretPassword
########################################
>>> KdcAccessibility: reset
default etypes for default_tkt_enctypes: 23.
>>> KrbAsReq creating message
>>> KrbKdcReq send: kdc=XXX.XXX.XXX.X TCP:88, timeout=30000, number of retries =3, #bytes=126
>>> KDCCommunication: kdc=XXX.XXX.XXX.X TCP:88, timeout=30000,Attempt =1, #bytes=126
>>>DEBUG: TCPClient reading 248 bytes
>>> KrbKdcReq send: #bytes read=248
>>>Pre-Authentication Data:
PA-DATA type = 2
PA-ENC-TIMESTAMP
>>>Pre-Authentication Data:
PA-DATA type = 16
>>>Pre-Authentication Data:
PA-DATA type = 15
>>>Pre-Authentication Data:
PA-DATA type = 11
PA-ETYPE-INFO etype = 23, salt = null
>>>Pre-Authentication Data:
PA-DATA type = 19
PA-ETYPE-INFO2 etype = 23, salt = null, s2kparams = null
>>> KdcAccessibility: remove XXX.XXX.XXX.X:88
>>> KDCRep: init() encoding tag is 126 req type is 11
>>>KRBError:
sTime is Sat Mar 07 16:49:32 CET 2020 1583596172000
suSec is 184771
error code is 25
error Message is Additional pre-authentication required
cname is [email protected]
sname is krbrAdmin/[email protected]
eData provided.
msgType is 30
>>>Pre-Authentication Data:
PA-DATA type = 2
PA-ENC-TIMESTAMP
>>>Pre-Authentication Data:
PA-DATA type = 16
>>>Pre-Authentication Data:
PA-DATA type = 15
>>>Pre-Authentication Data:
PA-DATA type = 11
PA-ETYPE-INFO etype = 23, salt = null
>>>Pre-Authentication Data:
PA-DATA type = 19
PA-ETYPE-INFO2 etype = 23, salt = null, s2kparams = null
KRBError received: Need to use PA-ENC-TIMESTAMP/PA-PK-AS-REQ
KrbAsReqBuilder: PREAUTH FAILED/REQ, re-send AS-REQ
default etypes for default_tkt_enctypes: 23.
default etypes for default_tkt_enctypes: 23.
>>> EType: sun.security.krb5.internal.crypto.ArcFourHmacEType
>>> KrbAsReq creating message
>>> KrbKdcReq send: kdc=XXX.XXX.XXX.X TCP:88, timeout=30000, number of retries =3, #bytes=206
>>> KDCCommunication: kdc=XXX.XXX.XXX.X TCP:88, timeout=30000,Attempt =1, #bytes=206
>>>DEBUG: TCPClient reading 1186 bytes
>>> KrbKdcReq send: #bytes read=1186
>>> KdcAccessibility: remove XXX.XXX.XXX.X:88
>>> EType: sun.security.krb5.internal.crypto.ArcFourHmacEType
>>> KrbAsRep cons in KrbAsReq.getReply hri4wa1
Found ticket for [email protected] to go to krbrAdmin/[email protected] expiring on Sun Mar 08 02:49:32 CET 2020
Entered Krb5Context.initSecContext with state=STATE_NEW
Service ticket not found in the subject
>>> Credentials acquireServiceCreds: same realm
default etypes for default_tgs_enctypes: 23.
>>> CksumType: sun.security.krb5.internal.crypto.RsaMd5CksumType
>>> EType: sun.security.krb5.internal.crypto.ArcFourHmacEType
>>> KrbKdcReq send: kdc=XXX.XXX.XXX.X TCP:88, timeout=30000, number of retries =3, #bytes=1225
>>> KDCCommunication: kdc=XXX.XXX.XXX.X TCP:88, timeout=30000,Attempt =1, #bytes=1225
>>>DEBUG: TCPClient reading 107 bytes
>>> KrbKdcReq send: #bytes read=107
>>> KdcAccessibility: remove XXX.XXX.XXX.X:88
>>> KDCRep: init() encoding tag is 126 req type is 13
After this output I got following error:
java.lang.IllegalArgumentException: Empty nameStrings not allowed
at sun.security.krb5.PrincipalName.validateNameStrings(Unknown Source)
I tried to figure out how to prevent this: Also without success Also setting
System.setProperty("sun.security.krb5.PrincipalName", user + "@MY.DOMAIN");
did still not work.
Also additional information, my server supports:
{supportedsaslmechanisms=supportedSASLMechanisms: GSS-SPNEGO, GSSAPI, NTLM}
I feel that there are more than one issue to get it running. One what I am also not sure is, will it be required to configure my tomcat in any way?
Upvotes: 0
Views: 2023
Reputation: 290
Here is code to create users through java
private void addUser(final DirContext ctx, final LDAPPojo ldapPojo)
{
this.log.info(UI.getCurrent().getSession().getAttribute(ConfigProperties.SESSION_KEY)
+ "::LDAPCreateUserUtility::addUser::");
try
{
final Attributes attributes = new BasicAttributes();
final javax.naming.directory.BasicAttribute objClasses = new BasicAttribute("objectClass");
// WARNING: DO NOT TOUCH
objClasses.add("inetOrgPerson");
objClasses.add("posixAccount");
objClasses.add("top");
attributes.put(objClasses);
// Define User attributes
attributes.put(ConfigProperties.LDAP_KEY_LOGIN_SHELL, ldapPojo.getLoginShell());
attributes.put(ConfigProperties.LDAP_KEY_GIVEN_NAME, ldapPojo.getFirstName());
attributes.put(ConfigProperties.LDAP_KEY_HOME_DIRECTORY, ldapPojo.getHomeDirectory());
attributes.put(ConfigProperties.LDAP_KEY_UID, ldapPojo.getUid());
attributes.put(ConfigProperties.LDAP_KEY_UID_NUMBER, ldapPojo.getUidNumber());
attributes.put(ConfigProperties.LDAP_KEY_GID_NUMBER, ldapPojo.getGidNumber());
attributes.put(ConfigProperties.LDAP_KEY_SN, ldapPojo.getSn());
attributes.put(ConfigProperties.LDAP_KEY_CN, ldapPojo.getCn());
attributes.put(ConfigProperties.LDAP_KEY_PASSWORD, ldapPojo.getPassword());
this.log.info(UI.getCurrent().getSession().getAttribute(ConfigProperties.SESSION_KEY)
+ "::LDAPCreateUserUtility::addUser::Sub Context: " + ldapPojo.getSubContext());
ctx.createSubcontext(ldapPojo.getSubContext(), attributes);
this.log.info(UI.getCurrent().getSession().getAttribute(ConfigProperties.SESSION_KEY)
+ "::LDAPCreateUserUtility::addUser::success");
ldapPojo.setReturnFlag(ResourceProperty.configBundle.getString("RETURN_TRUE_FLAG"));
}
catch(final Exception e)
{
this.log.error(UI.getCurrent().getSession().getAttribute(ConfigProperties.SESSION_KEY)
+ "::LDAPCreateUserUtility::addUser::Exception: " + e.getStackTrace(), e);
this.log.error(UI.getCurrent().getSession().getAttribute(ConfigProperties.SESSION_KEY)
+ "::LDAPCreateUserUtility::addUser::exception_for_user_LDAP >> " + ldapPojo.getUid());
ldapPojo.setReturnFlag(ResourceProperty.messagesBundle.getString("LDAP_USER_CREATION_ERROR_MSG"));
}
finally
{
try
{
this.log.info(UI.getCurrent().getSession().getAttribute(ConfigProperties.SESSION_KEY)
+ "::LDAPCreateUserUtility::addUser::pstmt closed");
}
catch(final Exception ignore)
{
this.log.error(UI.getCurrent().getSession().getAttribute(ConfigProperties.SESSION_KEY)
+ "::LDAPCreateUserUtility::addUser::Exception: " + ignore.getStackTrace(), ignore);
ldapPojo.setReturnFlag(ResourceProperty.messagesBundle.getString("LDAP_USER_CREATION_ERROR_MSG"));
}
}
}
public void createLDAPUser(final LDAPPojo ldapPojo)
{
this.log.info(UI.getCurrent().getSession().getAttribute(ConfigProperties.SESSION_KEY)
+ "::LDAPCreateUserUtility::createLDAPUser::");
try
{
final Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, ConfigProperties.INITIAL_CONTEXT_FACTORY);
env.put(Context.PROVIDER_URL, this.contextSetPropertiesUtilityPOJO.getLDAP_PROVIDER_URL());
env.put(Context.SECURITY_PRINCIPAL, ConfigProperties.SECURITY_PRINCIPAL);
env.put(Context.SECURITY_AUTHENTICATION, ConfigProperties.SECURITY_AUTHENTICATION);
env.put(Context.SECURITY_PRINCIPAL, this.contextSetPropertiesUtilityPOJO.getLDAP_ROOT_LOGIN_USER());
env.put(Context.SECURITY_CREDENTIALS, this.contextSetPropertiesUtilityPOJO.getLDAP_ROOT_LOGIN_PASSWORD());
this.addUser((new InitialDirContext(env)), ldapPojo);
}
catch(final Exception exception)
{
this.log.error(UI.getCurrent().getSession().getAttribute(ConfigProperties.SESSION_KEY)
+ "::LDAPCreateUserUtility::addUser::Exception: " + exception.getStackTrace());
}
}
Upvotes: 1
Reputation: 290
Here is the working code for OpenLDAP login
public void checkUserCredential(final LDAPPojo ldapPojo)
{
this.contextSetPropertiesUtilityPOJO =
this.contextSetPropertiesUtility.ContextSetProperties(this.contextSetPropertiesUtilityPOJO);
this.log.info(UI.getCurrent().getSession().getAttribute(ConfigProperties.SESSION_KEY)
+ "::LDAPCreateUserUtility::checkUserCredential::");
this.log.info(UI.getCurrent().getSession().getAttribute(ConfigProperties.SESSION_KEY)
+ "::LDAPCreateUserUtility::checkUserCredential::get LDAP Provider URL: "
+ this.contextSetPropertiesUtilityPOJO.getLDAP_PROVIDER_URL());
try
{
final Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, ConfigProperties.INITIAL_CONTEXT_FACTORY);
env.put(Context.PROVIDER_URL, this.contextSetPropertiesUtilityPOJO.getLDAP_PROVIDER_URL());
env.put(Context.SECURITY_PRINCIPAL, ConfigProperties.SECURITY_PRINCIPAL);
env.put(Context.SECURITY_AUTHENTICATION, ConfigProperties.SECURITY_AUTHENTICATION);
env.put(Context.SECURITY_PRINCIPAL, ldapPojo.getUserName());
env.put(Context.SECURITY_CREDENTIALS, ldapPojo.getPassword());
new InitialDirContext(env).close();
ldapPojo.setReturnFlag(ResourceProperty.errorCodeBundle.getString("PASSWORD_MATCHES_CODE"));
}
catch(final NamingException e)
{
this.log.error(UI.getCurrent().getSession().getAttribute(ConfigProperties.SESSION_KEY)
+ "::LDAPCreateUserUtility::checkUserCredential::Exception: " + e.getStackTrace(), e);
ldapPojo.setReturnFlag(ResourceProperty.messagesBundle.getString("PASSWORD_DOES_NOT_MATCH_MSG"));
}
catch(final Exception ex)
{
this.log.error(UI.getCurrent().getSession().getAttribute(ConfigProperties.SESSION_KEY)
+ "::LDAPCreateUserUtility::checkUserCredential::Exception: " + ex.getStackTrace(), ex);
ldapPojo.setReturnFlag(ResourceProperty.messagesBundle.getString("PASSWORD_DOES_NOT_MATCH_MSG"));
}
Upvotes: 1
Reputation: 21
I have managed to solve it with oauth/azure ad. Its not LDAP - however it might be interesting anyway. Have a look: https://github.com/xware-gmbh/SeicentoBilling
Upvotes: 2