package org.apereo.cas.authentication.handler.support;
import org.apereo.cas.authentication.HandlerResult;
import org.apereo.cas.authentication.PreventedException;
import org.apereo.cas.authentication.UsernamePasswordCredential;
import org.apereo.cas.authentication.principal.Principal;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.services.ServicesManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.Set;
/**
* JAAS Authentication Handler for CAAS. This is a simple bridge from CAS'
* authentication to JAAS.
* <p>
* Using the JAAS Authentication Handler requires you to configure the
* appropriate JAAS modules. You can specify the location of a jass.conf file
* using the following VM parameter:
* <pre>
* -Djava.security.auth.login.config=$PATH_TO_JAAS_CONF/jaas.conf
* </pre>
* <p>
* This example jaas.conf would try Kerberos based authentication, then try LDAP
* authentication:
* <pre>
* CAS {
* com.sun.security.auth.module.Krb5LoginModule sufficient
* client=TRUE
* debug=FALSE
* useTicketCache=FALSE;
* edu.uconn.netid.jaas.LDAPLoginModule sufficient
* java.naming.provider.url="ldap://ldapserver.my.edu:389/dc=my,dc=edu"
* java.naming.security.principal="uid=jaasauth,dc=my,dc=edu"
* java.naming.security.credentials="password"
* Attribute="uid"
* startTLS="true";
* };
* </pre>
*
* @author <a href="mailto:dotmatt@uconn.edu">Matthew J. Smith</a>
* @author Marvin S. Addison
* @author Misagh Moayyed
* @see javax.security.auth.callback.CallbackHandler
* @see javax.security.auth.callback.PasswordCallback
* @see javax.security.auth.callback.NameCallback
* @since 3.0.0
*/
public class JaasAuthenticationHandler extends AbstractUsernamePasswordAuthenticationHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(JaasAuthenticationHandler.class);
/**
* System property key to specify kerb5 realm.
*/
private static final String SYS_PROP_KRB5_REALM = "java.security.krb5.realm";
/**
* System property key to specify kerb5 kdc.
*/
private static final String SYS_PROP_KERB5_KDC = "java.security.krb5.kdc";
/**
* The realm that contains the login module information.
*/
private String realm = "CAS";
/**
* System property value to overwrite the realm in krb5 config.
*/
private String kerberosRealmSystemProperty;
/**
* System property value to overwrite the kdc in krb5 config.
*/
private String kerberosKdcSystemProperty;
/**
* Instantiates a new Jaas authentication handler,
* and attempts to load/verify the configuration.
*
* @param name the name
* @param servicesManager the services manager
* @param principalFactory the principal factory
* @param order the order
*/
public JaasAuthenticationHandler(final String name, final ServicesManager servicesManager, final PrincipalFactory principalFactory, final Integer order) {
super(name, servicesManager, principalFactory, order);
Assert.notNull(Configuration.getConfiguration(),
"Static Configuration cannot be null. Did you remember to specify \"java.security.auth.login.config\"?");
}
@Override
protected HandlerResult authenticateUsernamePasswordInternal(final UsernamePasswordCredential credential, final String originalPassword)
throws GeneralSecurityException, PreventedException {
if (this.kerberosKdcSystemProperty != null) {
LOGGER.debug("Configured kerberos system property [{}] to [{}]", SYS_PROP_KERB5_KDC, this.kerberosKdcSystemProperty);
System.setProperty(SYS_PROP_KERB5_KDC, this.kerberosKdcSystemProperty);
}
if (this.kerberosRealmSystemProperty != null) {
LOGGER.debug("Setting kerberos system property [{}] to [{}]", SYS_PROP_KRB5_REALM, this.kerberosRealmSystemProperty);
System.setProperty(SYS_PROP_KRB5_REALM, this.kerberosRealmSystemProperty);
}
final String username = credential.getUsername();
final String password = credential.getPassword();
final LoginContext lc = new LoginContext(this.realm, new UsernamePasswordCallbackHandler(username, password));
try {
LOGGER.debug("Attempting authentication for: [{}]", username);
lc.login();
} finally {
lc.logout();
}
Principal principal = null;
final Set<java.security.Principal> principals = lc.getSubject().getPrincipals();
if (principals != null && !principals.isEmpty()) {
final java.security.Principal secPrincipal = principals.iterator().next();
principal = this.principalFactory.createPrincipal(secPrincipal.getName());
}
return createHandlerResult(credential, principal, null);
}
public void setRealm(final String realm) {
this.realm = realm;
}
/**
* Typically, the default realm and the KDC for that realm are indicated in the Kerberos {@code krb5.conf} configuration file.
* However, if you like, you can instead specify the realm value by setting this following system property value.
* <p>If you set the realm property, you SHOULD also configure the {@link #setKerberosKdcSystemProperty(String)}.
* <p>Also note that if you set these properties, then no cross-realm authentication is possible unless
* a {@code krb5.conf} file is also provided from which the additional information required for cross-realm authentication
* may be obtained.
* <p>If you set values for these properties, then they override the default realm and KDC values specified
* in {@code krb5.conf} (if such a file is found). The {@code krb5.conf} file is still consulted if values for items
* other than the default realm and KDC are needed. If no {@code krb5.conf} file is found,
* then the default values used for these items are implementation-specific.
*
* @param kerberosRealmSystemProperty system property to indicate realm.
* @see <a href="http://docs.oracle.com/javase/7/docs/technotes/guides/security/jgss/tutorials/KerberosReq.html">
* Oracle documentation</a>
* @since 4.1.0
*/
public void setKerberosRealmSystemProperty(final String kerberosRealmSystemProperty) {
this.kerberosRealmSystemProperty = kerberosRealmSystemProperty;
}
/**
* Typically, the default realm and the KDC for that realm are indicated in the Kerberos {@code krb5.conf} configuration file.
* However, if you like, you can instead specify the kdc value by setting this system property value.
* <p>If you set the realm property, you SHOULD also configure the {@link #setKerberosRealmSystemProperty(String)}.
* <p>Also note that if you set these properties, then no cross-realm authentication is possible unless
* a {@code krb5.conf} file is also provided from which the additional information required for cross-realm authentication
* may be obtained.
* <p>If you set values for these properties, then they override the default realm and KDC values specified
* in {@code krb5.conf} (if such a file is found). The {@code krb5.conf} file is still consulted if values for items
* other than the default realm and KDC are needed. If no {@code krb5.conf} file is found,
* then the default values used for these items are implementation-specific.
*
* @param kerberosKdcSystemProperty system property to indicate kdc
* @see <a href="http://docs.oracle.com/javase/7/docs/technotes/guides/security/jgss/tutorials/KerberosReq.html">
* Oracle documentation</a>
* @since 4.1.0
*/
public void setKerberosKdcSystemProperty(final String kerberosKdcSystemProperty) {
this.kerberosKdcSystemProperty = kerberosKdcSystemProperty;
}
/**
* A simple JAAS CallbackHandler which accepts a Name String and Password
* String in the constructor. Only NameCallbacks and PasswordCallbacks are
* accepted in the callback array. This code based loosely on example given
* in Sun's javadoc for CallbackHandler interface.
*/
protected static class UsernamePasswordCallbackHandler implements CallbackHandler {
/**
* The username of the principal we are trying to authenticate.
*/
private String userName;
/**
* The password of the principal we are trying to authenticate.
*/
private String password;
/**
* Constructor accepts name and password to be used for authentication.
*
* @param userName name to be used for authentication
* @param password Password to be used for authentication
*/
protected UsernamePasswordCallbackHandler(final String userName, final String password) {
this.userName = userName;
this.password = password;
}
@Override
public void handle(final Callback[] callbacks) throws UnsupportedCallbackException {
Arrays.stream(callbacks).filter(callback -> {
if (callback.getClass().equals(NameCallback.class)) {
((NameCallback) callback).setName(this.userName);
return false;
}
if (callback.getClass().equals(PasswordCallback.class)) {
((PasswordCallback) callback).setPassword(this.password.toCharArray());
return false;
}
return true;
}).findFirst().ifPresent(callback -> {
throw new RuntimeException(new UnsupportedCallbackException(callback, "Unrecognized Callback"));
});
}
}
}