Java tutorial
package net.java.jaspicoil; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.security.Principal; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.security.auth.Subject; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.UnsupportedCallbackException; import javax.security.auth.kerberos.KerberosKey; import javax.security.auth.kerberos.KerberosPrincipal; import javax.security.auth.login.AppConfigurationEntry; import javax.security.auth.login.Configuration; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import javax.security.auth.message.AuthException; import javax.security.auth.message.AuthStatus; import javax.security.auth.message.MessageInfo; import javax.security.auth.message.MessagePolicy; import javax.security.auth.message.callback.CallerPrincipalCallback; import javax.security.auth.message.callback.GroupPrincipalCallback; import javax.security.auth.message.module.ServerAuthModule; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import net.java.jaspicoil.util.ADUtil; import org.apache.commons.codec.binary.Base64; import org.ietf.jgss.GSSContext; import org.ietf.jgss.GSSCredential; import org.ietf.jgss.GSSException; import org.ietf.jgss.GSSManager; import org.ietf.jgss.GSSName; import org.ietf.jgss.Oid; import org.jaaslounge.decoding.kerberos.KerberosAuthData; import org.jaaslounge.decoding.kerberos.KerberosPacAuthData; import org.jaaslounge.decoding.kerberos.KerberosToken; import org.jaaslounge.decoding.pac.Pac; import org.jaaslounge.decoding.pac.PacLogonInfo; import org.jaaslounge.decoding.pac.PacSid; import org.jaaslounge.decoding.spnego.SpnegoConstants; import org.jaaslounge.decoding.spnego.SpnegoToken; /** * <p> * This JASPIC Module supports the Kerberos SPNEGO mechanism and leverages * MS-PAC to provide advanced features: * <ul> * <li>Secured SSO between client session (Windows) and server webapp</li> * <li>REST compatible SSO that enables facade integration strategies between * webapps</li> * <li>Support Qualified SSO to segregate between Smarcard authenticated users * and regular ones</li> * <li>Compatible with JSR-196 containers : Glassfish, JBoss ...</li> * <li>Multi-OS Server Support (Windows, Linux ...)</li> * <li>Limit usage of LDAP to increase performance and stability</li> * <li>Supports Sun and IBM branches of JVM</li> * <li>...</li> * </ul> * </p> * <p> * Authentication is base on the Kerberos protocol using the underlying * </p> * The users will be added groups as contained in the MS-PAC plus if Smartcard * Logon authenticated the syntetic group SMARTCARD_AUTHENTICATED. * * Various parameters can be set : <dd> * <dt>principalName</dt> * <dl> * REQUIRED: The Service Principal Name to be used to fetch. * </dl> * </dd> <dd> * <dt>keyTabLocation</dt> * <dl> * REQUIRED: The location of the keytab containing the keys for the SPN * indicated in the "principalName" parameter * </dl> * </dd> <dd> * <dt>debug</dt> * <dl> * OPTIONAL : If true, it will print to the default logger some debug * information. By default debug is set to false. * </dl> * </dd> <dd> * <dt>jaasContext</dt> * <dl> * OPTIONAL : The name of the JAAS LoginModule configuration to use for the * internal usage. If none is provided, it will use defaults on the related * calls. * </dl> * </dd> <dd> * <dt>secureGroupSids</dt> * <dl> * OPTIONAL: A comma separated list of canonical string representation of AD * SIDs. When found on a user, any of these SIDS will be granted the synthetic * SMARTCARD_AUTHENTICATED group. * </dl> * </dd> <dd> * <dt>mandatoryGroups</dt> * <dl> * OPTIONAL: A comma separated list of group string (SID, CN ...) that will be * checked before granting access to the application. * </dl> * </dd> <dd> * <dt>smartcardSecuredUsersOnly</dt> * <dl> * OPTIONAL: When set to true, this boolean flag indicates that users not * authenticated thru smartcard logon will be denied access to the protected * context. When not set or set to another value, the default value is false. * </dl> * </dd> <dd> * <dt>delegatedSecuredUsersOnly</dt> * <dl> * OPTIONAL: When set to true, this boolean flag indicates that users not * authenticated thru KCD will be denied access to the protected context. When * not set or set to another value, the default value is false * </dl> * </dd> <dd> * <dt>groupMapping</dt> * <dl> * OPTIONAL: An URL to an accessible & valid Java properties file container * extra group mapping. It might be used to map SID for logical group names or * to provide an alternative solution to container managed mapping. * </dl> * </dd> <dd> * <dt>sessionAttributes</dt> * <dl> * OPTIONAL: A list of comma separated values representing a pair (key, value) * to store in the user session one authenticated. * </dl> * </dd> <dd> * <dt>userHeader</dt> * <dl> * OPTIONAL: Name of HTTP Header where to store the user name that was * authenticated. This feature is only supported thru the Tomcat Valve. * </dl> * </dd> <dd> * <dt>administratorGroups</dt> * <dl> * FUTURE / OPTIONAL: A list of groups that indicate the user is an * administrator. By default, no administrator group is set * </dl> * </dd> <dd> * <dt>administratorOnlyURIs</dt> * <dl> * FUTURE / OPTIONAL: A list of comma separated URI patterns that are blocked * unless member of administrator group. Warning : this feature is working on * container that invoke the SAM on all resources including the non-protected * ones. * </dl> * </dd> <dd> * <dt>javax.security.jacc.PolicyContext</dt> * <dl> * INTERNAL : send by the container to indicate the PolicyContext to use. * </dl> * </dd> <dd> * <dt>javax.security.auth.message.MessagePolicy.isMandatory</dt> * <dl> * INTERNAL : sent by the container according to JSR-196 to indicate if the * policy is mandatory (value true) or not (value false). * </dl> * </dd> * <p> * The parameters principalName and keyTabLocation are <b>only required</b> if * there was no JAAS delegated context found (from jaasContext associated * parameter value or thru JASPIC/JAAS internal context). * </p> * <p> * When sartcardSecuredUsersOnly and delegatedSecuredUsersOnly flags are both * set to true, users not authenticated thru smartcard logon or KCD will be * denied access to the protected context. * </p> * <p> * A tag FUTURE on a parameter indicates that this feature is not yet supported * and/or available in this version. * * @author bugeaud at gmail dot com * @license CDDL1 http://www.opensource.org/licenses/cddl1.txt * @license LGPL http://www.gnu.org/copyleft/lesser.html */ public class MSPacSpnegoServerAuthModule implements ServerAuthModule { public static final String AUTH_TYPE_INFO_KEY = "javax.servlet.http.authType"; public static final String MAGIC_SESSION_STATE_KEY = "net.java.spnego.jaspic.MagicSessionState"; public static final String USERNAME_SESSION_KEY = "net.java.jaspic.user.name"; public static final String REALM_SESSION_KEY = "net.java.jaspic.user.realm"; public static final String DEBUG_OPTIONS_KEY = "debug"; public static final String POLICY_CONTEXT_OPTIONS_KEY = "javax.security.jacc.PolicyContext"; public static final String IS_MANDATORY_INFO_KEY = "javax.security.auth.message.MessagePolicy.isMandatory"; public static final String SERVICE_PRINCIPAL_NAME_KEY = "principalName"; public static final String KEYTAB_LOCATION_KEY = "keyTabLocation"; public static final String JAAS_CONTEXT_KEY = "jaasContext"; public static final String SECURE_GROUP_SIDS_KEY = "secureGroupSids"; public static final String MANDATORY_GROUPS_KEY = "mandatoryGroups"; public static final String GROUP_MAPPING_KEY = "groupMapping"; public static final String SESSION_ATTRIBUTES_KEY = "sessionAttributes"; public static final String USER_HEADER_KEY = "userHeader"; public static final String ADMINISTRATOR_GROUPS_KEY = "administratorGroups"; public static final String ADMINISTRATOR_ONLY_URIS_KEY = "administratorOnlyURIs"; public static final String SMARTCARD_SECURED_USERS_ONLY = "smartcardSecuredUsersOnly"; public static final String DELEGATED_SECURED_USERS_ONLY = "delegatedSecuredUsersOnly"; private static final String AUTHORIZATION_HEADER = "authorization"; private static final String AUTHENTICATION_HEADER = "WWW-Authenticate"; private static final String NEGOTIATE = "Negotiate"; private static final String NTLM_INITIAL_TOKEN = "NTLMSSP"; /** * Role indicating the user has authenticated thru smarcard */ public static final String GROUP_SMARTCARD_AUTHENTICATED = "SMARTCARD_AUTHENTICATED"; /** * Role indicating the user has authenticated thru KCD authentication */ public static final String GROUP_DELEGATED_AUTHENTICATED = "DELEGATED_AUTHENTICATED"; /** * Role indicating the user was accessing thru a secured channel */ public static final String GROUP_SECURED_CHANNEL = "SECURED_CHANNEL"; /** * Role indicating the user was accessing thru an unsecured secured channel */ public static final String GROUP_UNSECURED_CHANNEL = "UNSECURED_CHANNEL"; private static final Logger LOG = Logger.getLogger(MSPacSpnegoServerAuthModule.class.getName()); private static Class<?>[] supportedMessageTypes = new Class<?>[] { javax.servlet.http.HttpServletRequest.class, javax.servlet.http.HttpServletResponse.class }; /** * The Kerberos OID */ private static Oid GSS_KRB5_MECH_OID = null; static { try { GSS_KRB5_MECH_OID = new Oid("1.2.840.113554.1.2.2"); } catch (final GSSException e) { LOG.log(Level.SEVERE, "Unkown GSS_KRB5_MECH_OID", e); } } /** * State of the AuthModule is kept using this status */ public enum SessionState { // The SPNEGO was started STARTED, // The SPNEGO was accepted a first time but more data is needed IN_PROGRESS, // The SPNEGO was fully ESTABLISHED ESTABLISHED } private MessagePolicy requestPolicy; private MessagePolicy responsePolicy; private CallbackHandler handler; private Map<String, ?> options; private boolean debug; private String servicePrincipal; private URL keyTabLocation; private String jaasContextName; private Subject serviceSubject; private Set<String> secureGroups; private Set<String> mandatoryGroups; private Set<String> administratorGroups; private Set<String> administratorOnlyUris; private Properties groupMapping; private Map<String, String> sessionAttributes; private String userHeader; private Level debugLevel; private String policyContextID; private boolean mandatory; private boolean smartcardSecuredUsersOnly = false; private boolean delegatedSecuredUsersOnly = false; /** * This stores the login module name */ private String loginModuleName; public MSPacSpnegoServerAuthModule() { } /** * Create a Kerberos MS-PAC JASPIC module indicating the login module * context * * @param loginModuleName */ public MSPacSpnegoServerAuthModule(String loginModuleName) { this.loginModuleName = loginModuleName; } /** * Initialize this module with request and response message policies to * enforce, a CallbackHandler, and any module-specific configuration * properties. The request policy and the response policy must not both be * null. * * @param requestPolicy * The request policy this module must enforce, or null. * @param responsePolicy * The response policy this module must enforce, or null. * @param handler * CallbackHandler used to request information. * @param options * A Map of module-specific configuration properties. * @throws AuthException * If module initialization fails, including for the case where * the options argument contains elements that are not supported * by the module. */ @SuppressWarnings({ "rawtypes", "unchecked" }) public void initialize(MessagePolicy requestPolicy, MessagePolicy responsePolicy, CallbackHandler handler, Map options) throws AuthException { boolean useDelegatedLoginModule = false; // If no options or empty options was provided get away and display some // usage log // Please note that some AS such as JBoss 7 call this method twice. if (options == null || options.isEmpty()) { LOG.warning(String.format( "Options is either empty or null this time. Please make sure that " + "at least the parameter %s and %s are set in the JASPIC provider configuration.", SERVICE_PRINCIPAL_NAME_KEY, KEYTAB_LOCATION_KEY)); return; } this.requestPolicy = requestPolicy; this.responsePolicy = responsePolicy; // If none policy was provided, we assume this provider is mandatory. // This is unfortunately required as some container issue workaround this.mandatory = requestPolicy != null ? requestPolicy.isMandatory() : true; this.handler = handler; this.options = options; this.debug = options.containsKey(DEBUG_OPTIONS_KEY); // Set the debug level according to the logger config and the JASPIC // config this.debugLevel = LOG.isLoggable(Level.FINE) && !this.debug ? Level.FINE : Level.INFO; debug("Debug was set ({0},{1})", this.debug, this.debugLevel); this.jaasContextName = (String) options.get(JAAS_CONTEXT_KEY); debug("Jaas context name was set {0}", this.jaasContextName); if (this.jaasContextName == null && this.loginModuleName != null) { debug("There was no JAAS context parameter set on the JASPIC so using the default passed from the application server"); this.jaasContextName = this.loginModuleName; useDelegatedLoginModule = true; } this.policyContextID = (String) options.get(POLICY_CONTEXT_OPTIONS_KEY); debug("Policy context set to {0}", this.policyContextID); this.servicePrincipal = (String) options.get(SERVICE_PRINCIPAL_NAME_KEY); debug("Principal set to {0}", this.servicePrincipal); // If there was no delegated LoginModule found we must set a SPN if (!useDelegatedLoginModule && this.servicePrincipal == null) { LOG.severe("A valid SPN must be configured for JASPIC connector with the property " + SERVICE_PRINCIPAL_NAME_KEY); return; } // Get the secure groups final String groupList = (String) options.get(SECURE_GROUP_SIDS_KEY); if (groupList != null && !"".equals(groupList.trim())) { this.secureGroups = Collections .unmodifiableSet(new HashSet<String>(Arrays.asList(groupList.trim().split(",")))); debug("Secure groups set as: {0}", this.secureGroups); } // Get the mandatory groups final String mandatoryGroupList = (String) options.get(MANDATORY_GROUPS_KEY); if (mandatoryGroupList != null && !"".equals(mandatoryGroupList.trim())) { this.mandatoryGroups = Collections .unmodifiableSet(new HashSet<String>(Arrays.asList(mandatoryGroupList.trim().split(",")))); debug("Mandatory groups set as: {0}", this.mandatoryGroups); } final String administratorGroupList = (String) options.get(ADMINISTRATOR_GROUPS_KEY); if (administratorGroupList != null && !"".equals(administratorGroupList.trim())) { this.administratorGroups = Collections .unmodifiableSet(new HashSet<String>(Arrays.asList(administratorGroupList.trim().split(",")))); debug("Administrator groups set as: {0}", this.mandatoryGroups); } final String administratorOnlyUriList = (String) options.get(ADMINISTRATOR_ONLY_URIS_KEY); if (administratorOnlyUriList != null && !"".equals(administratorOnlyUriList.trim())) { this.administratorOnlyUris = Collections.unmodifiableSet( new HashSet<String>(Arrays.asList(administratorOnlyUriList.trim().split(",")))); debug("Administrator restricted URIs set as: {0}", this.mandatoryGroups); } final String sessionAttributeList = (String) options.get(SESSION_ATTRIBUTES_KEY); if (sessionAttributeList != null && !"".equals(sessionAttributeList.trim())) { final String[] pairs = sessionAttributeList.trim().split(","); final Map<String, String> attributes = new HashMap<String, String>(); for (final String pair : pairs) { final String[] vals = pair.split("="); attributes.put(vals[0], vals[1]); } this.sessionAttributes = Collections.unmodifiableMap(attributes); debug("Session attributes was set to {0}", this.sessionAttributes); } final String keyTabLocationString = (String) options.get(KEYTAB_LOCATION_KEY); if (!useDelegatedLoginModule && keyTabLocationString == null) { LOG.severe("A valid key tab location must be configured for JASPIC connector with the property " + KEYTAB_LOCATION_KEY); return; } if (keyTabLocationString != null) { // Try to reference the indicated keytab try { this.keyTabLocation = new URL(keyTabLocationString); debug("Keytab location was set to {0}", this.keyTabLocation); } catch (final MalformedURLException e) { LOG.log(Level.WARNING, "Unable to build " + KEYTAB_LOCATION_KEY + " from the parameter value " + options.get(KEYTAB_LOCATION_KEY), e); } } URL groupMappingSource = null; try { final String groupParam = (String) options.get(GROUP_MAPPING_KEY); if (groupParam != null && !"".equals(groupParam.trim())) { groupMappingSource = new URL(groupParam); } } catch (final MalformedURLException e) { LOG.log(Level.WARNING, "Unable to build " + GROUP_MAPPING_KEY + " from the parameter value " + options.get(GROUP_MAPPING_KEY), e); } if (groupMappingSource != null) { final Properties groupMapping = new Properties(); try { InputStream groupMappingInputstream = null; try { groupMappingInputstream = groupMappingSource.openStream(); groupMapping.load(groupMappingInputstream); } finally { if (groupMappingInputstream != null) { groupMappingInputstream.close(); } } this.groupMapping = groupMapping; debug("Group mapping was set to {0}", this.groupMapping); } catch (final IOException ioex) { LOG.log(Level.WARNING, "Unable to load " + GROUP_MAPPING_KEY + " from the indicated ressource " + options.get(GROUP_MAPPING_KEY), ioex); } } try { // Load the serviceSubject from a LoginModule, this is required to // get the Kerberos Key this.serviceSubject = initializeKerberosServerContext(this.jaasContextName, this.servicePrincipal, this.keyTabLocation, this.debug); debug("Service subject was set to {0}", this.serviceSubject); } catch (final LoginException e) { final AuthException aex = new AuthException("Kerberos service context initialization failed"); aex.initCause(e); throw aex; } this.smartcardSecuredUsersOnly = Boolean.parseBoolean((String) options.get(SMARTCARD_SECURED_USERS_ONLY)); debug("Only accept smartcard secured users ? {0}", this.smartcardSecuredUsersOnly); this.delegatedSecuredUsersOnly = Boolean.parseBoolean((String) options.get(DELEGATED_SECURED_USERS_ONLY)); debug("Only accept delegated (KCD) users ? {0}", this.delegatedSecuredUsersOnly); this.userHeader = (String) options.get(USER_HEADER_KEY); debug("User header set to {0}", this.userHeader); // TODO Add some fetchExtraGroupsScript handling that will use // javax.script to execute // an idea is to use the first line as //#!!mime/type the mimetype will // be extracted out to known which engine to call } private Subject initializeKerberosServerContext(String jaasContextName, String servicePrincipal, URL keyTabLocation, boolean debug) throws LoginException { if (jaasContextName == null) { // Get the subject from a new LoginContext // WARNING: Here we assumes that the URL will be file: based, hence // local only. final Krb5LoginConfig loginConfig = new Krb5LoginConfig(keyTabLocation.getFile(), servicePrincipal, debug); return fetchSubjectFromLoginModuleWithPrincipal("", servicePrincipal, loginConfig); } else if (servicePrincipal == null) { // If there was no default service principal name specify then // default to the JAAS configuration setup return fetchSubjectFromLoginModule(jaasContextName, null, null); } else { // Get the subject from the indicated JAAS configuration selecting // with the indicated principal return fetchSubjectFromLoginModuleWithPrincipal(jaasContextName, servicePrincipal, null); } } private Subject fetchSubjectFromLoginModuleWithPrincipal(String jaasContextName, String servicePrincipal, Krb5LoginConfig loginConfig) throws LoginException { final Set<Principal> princ = new HashSet<Principal>(1); princ.add(new KerberosPrincipal(servicePrincipal)); // Create a new editable Subject final Subject sub = new Subject(false, princ, new HashSet<Object>(), new HashSet<Object>()); return fetchSubjectFromLoginModule(jaasContextName, sub, loginConfig); } private Subject fetchSubjectFromLoginModule(String jaasContextName, Subject subject, Krb5LoginConfig loginConfig) throws LoginException { debug("Try to create a context LM for jassname={0}, subject={1}, config={2}", jaasContextName, subject, loginConfig); final LoginContext lc = new LoginContext(jaasContextName, subject, null, loginConfig); lc.login(); return lc.getSubject(); } /** * Get the one or more Class objects representing the message types * supported by the module. * * @return An array of Class objects, with at least one element defining a * message type supported by the module. */ @SuppressWarnings("rawtypes") public Class[] getSupportedMessageTypes() { return supportedMessageTypes; } /** * Authenticate a received service request. * <p/> * This method is called to transform the mechanism-specific request message * acquired by calling getRequestMessage (on messageInfo) into the validated * application message to be returned to the message processing runtime. If * the received message is a (mechanism-specific) meta-message, the method * implementation must attempt to transform the meta-message into a * corresponding mechanism-specific response message, or to the validated * application request message. The runtime will bind a validated * application message into the the corresponding service invocation. * <p> * This method conveys the outcome of its message processing either by * returning an AuthStatus value or by throwing an AuthException. * <p/> * From a performance point of view this method will be called twice for * each resource with a security constraint on it. Resources with no * security constraint do not result in a call to this method. * * @param messageInfo * A contextual object that encapsulates the client request and * server response objects, and that may be used to save state * across a sequence of calls made to the methods of this * interface for the purpose of completing a secure message * exchange. * @param clientSubject * A Subject that represents the source of the service request. * It is used by the method implementation to store Principals * and credentials validated in the request. * @param serviceSubject * A Subject that represents the recipient of the service * request, or null. It may be used by the method implementation * as the source of Principals or credentials to be used to * validate the request. If the Subject is not null, the method * implementation may add additional Principals or credentials * (pertaining to the recipient of the service request) to the * Subject. * @return An AuthStatus object representing the completion status of the * processing performed by the method. The AuthStatus values that * may be returned by this method are defined as follows: * <p/> * <ul> * <li>AuthStatus.SUCCESS when the application request message was * successfully validated. The validated request message is * available by calling getRequestMessage on messageInfo. * <p/> * <li>AuthStatus.SEND_SUCCESS to indicate that * validation/processing of the request message successfully * produced the secured application response message (in * messageInfo). The secured response message is available by * calling getResponseMessage on messageInfo. * <p/> * <li>AuthStatus.SEND_CONTINUE to indicate that message validation * is incomplete, and that a preliminary response was returned as * the response message in messageInfo. * <p/> * When this status value is returned to challenge an application * request message, the challenged request must be saved by the * authentication module such that it can be recovered when the * module's validateRequest message is called to process the request * returned for the challenge. * <p/> * <li>AuthStatus.SEND_FAILURE to indicate that message validation * failed and that an appropriate failure response message is * available by calling getResponseMessage on messageInfo. * </ul> * @throws AuthException When the message processing failed without * establishing a failure response message (in messageInfo). */ @SuppressWarnings("unchecked") public AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject, Subject serviceSubject) throws AuthException { // Extra check (disabled withour -ea) if mandatory value is consistent // with initialize phase assert messageInfo.getMap().containsKey(IS_MANDATORY_INFO_KEY) == this.mandatory; // Get the servlet context final HttpServletRequest request = (HttpServletRequest) messageInfo.getRequestMessage(); final HttpServletResponse response = (HttpServletResponse) messageInfo.getResponseMessage(); // Invalidate any existing session to prevent session fixture attempt HttpSession session = request.getSession(false); if (session != null) { final SessionState state = (SessionState) session.getAttribute(MAGIC_SESSION_STATE_KEY); if (state == null) { // Session was not created by us, we will invalidate an existing // session that was not created by us session.invalidate(); LOG.warning( "An existing session was invalidated. This might be a session fixture attempt, so failing the authentication."); return AuthStatus.SEND_FAILURE; } else if (SessionState.ESTABLISHED.equals(state)) { // The context was already fully established once within this // session. return AuthStatus.SUCCESS; } } debugRequest(request); // should specify encoder final String authorization = request.getHeader(AUTHORIZATION_HEADER); if (authorization != null && authorization.startsWith(NEGOTIATE)) { final String negotiateString = authorization.substring(NEGOTIATE.length() + 1); final byte[] requestToken = Base64.decodeBase64(negotiateString); if (serviceSubject == null) { // If no service subject was provided by the container then set // a service subject // from the global context. serviceSubject = this.serviceSubject; } try { // Create a validation action byte[] gssToken = null; final KerberosValidateAction kva = new KerberosValidateAction(this.servicePrincipal, requestToken, serviceSubject); try { // Validate using the service (server) Subject gssToken = Subject.doAs(this.serviceSubject, kva); } catch (final PrivilegedActionException e) { final GSSException gex = new GSSException(GSSException.DEFECTIVE_TOKEN); gex.initCause(e); gex.setMinor(GSSException.UNAVAILABLE, "Unable to perform Kerberos validation"); throw gex; } if (kva.getContext() != null) { final String responseToken = Base64.encodeBase64String(gssToken); response.setHeader(AUTHENTICATION_HEADER, "Negotiate " + responseToken); debugToken("GSS Response token set to {0}", gssToken); } if (!kva.isEstablished()) { debug("GSS Dialog must continue to succeed"); session.setAttribute(MAGIC_SESSION_STATE_KEY, SessionState.IN_PROGRESS); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return AuthStatus.SEND_CONTINUE; } else { final Oid mechId = kva.getMech(); final GSSName name = kva.getSrcName(); if (!authorizeCaller(request, requestToken, name, clientSubject)) { return sendFailureMessage(response, "Failed to authorize the caller/client"); } // As no valid session should exist anymore, simply create a // new one session = request.getSession(true); final Principal clientPrincipal = new KerberosPrincipal( name.canonicalize(GSS_KRB5_MECH_OID).toString()); updateSessionAndHeader(request, session, clientPrincipal); session.setAttribute(MAGIC_SESSION_STATE_KEY, SessionState.ESTABLISHED); /* * Store the mechId in the MessageInfo to indicate which * authentication mechanism was used successfully (JASPIC * Requirement) */ messageInfo.getMap().put(AUTH_TYPE_INFO_KEY, mechId != null ? mechId.toString() : "Undefined GSS Mechanism"); debug("GSS Dialog is complete"); } } catch (final GSSException gsse) { debug("GSS Dialog has failed : {0}", gsse); if (requestToken != null) { debug("Bad token detected {0}", gsse); debugToken("Bad token was {0}", requestToken); if (isNTLMToken(requestToken)) { // There is a high probability it was a NTLM SPNEGO // token return sendFailureMessage(response, "No support for NTLM"); } } // for other errors throw an AuthException final AuthException ae = new AuthException(); ae.initCause(gsse); throw ae; } } else if (this.mandatory) { response.setHeader(AUTHENTICATION_HEADER, NEGOTIATE); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); debug("Negotiate was added to the HTTP header : {0}", NEGOTIATE); return AuthStatus.SEND_CONTINUE; } else if (authorization != null) { LOG.warning("An authorization header was ignored."); } return AuthStatus.SUCCESS; } private void updateSessionAndHeader(HttpServletRequest request, HttpSession session, Principal principal) { if (principal != null) { final String[] principalParts = principal.getName().split("@"); session.setAttribute(USERNAME_SESSION_KEY, principalParts[0]); session.setAttribute(REALM_SESSION_KEY, principalParts[1]); debug("Setting extra session users info name={0} realm={1}", principalParts[0], principalParts[1]); // TODO Implement userHeader on a generic way /* * if(this.userHeader!=null && !"".equals(userHeader.trim())){ * request.adHeader(...) ? } */ } if (this.sessionAttributes != null) { for (final Map.Entry<String, String> entry : this.sessionAttributes.entrySet()) { session.setAttribute(entry.getKey(), entry.getValue()); } debug("Setting extra session pairs : {0}", this.sessionAttributes); } debug("Session was update sessionId {0} for user {1}", session.getId(), principal); } /** * Secure a service response before sending it to the client. * <p/> * This method is called to transform the response message acquired by * calling getResponseMessage (on messageInfo) into the mechanism-specific * form to be sent by the runtime. * <p> * This method conveys the outcome of its message processing either by * returning an AuthStatus value or by throwing an AuthException. * * @param messageInfo * A contextual object that encapsulates the client request and * server response objects, and that may be used to save state * across a sequence of calls made to the methods of this * interface for the purpose of completing a secure message * exchange. * @param serviceSubject * A Subject that represents the source of the service response, * or null. It may be used by the method implementation to * retrieve Principals and credentials necessary to secure the * response. If the Subject is not null, the method * implementation may add additional Principals or credentials * (pertaining to the source of the service response) to the * Subject. * @return An AuthStatus object representing the completion status of the * processing performed by the method. The AuthStatus values that * may be returned by this method are defined as follows: * <p/> * <ul> * <li>AuthStatus.SEND_SUCCESS when the application response message * was successfully secured. The secured response message may be * obtained by calling getResponseMessage on messageInfo. * <p/> * <li>AuthStatus.SEND_CONTINUE to indicate that the application * response message (within messageInfo) was replaced with a * security message that should elicit a security-specific response * (in the form of a request) from the peer. * <p/> * This status value serves to inform the calling runtime that (to * successfully complete the message exchange) it will need to be * capable of continuing the message dialog by processing at least * one additional request/response exchange (after having sent the * response message returned in messageInfo). * <p/> * When this status value is returned, the application response must * be saved by the authentication module such that it can be * recovered when the module's validateRequest message is called to * process the elicited response. * <p/> * <li>AuthStatus.SEND_FAILURE to indicate that a failure occurred * while securing the response message and that an appropriate * failure response message is available by calling * getResponseMeessage on messageInfo. * </ul> * @throws AuthException When the message processing failed without * establishing a failure response message (in messageInfo). */ public AuthStatus secureResponse(MessageInfo messageInfo, Subject serviceSubject) throws AuthException { final HttpServletRequest request = (HttpServletRequest) messageInfo.getRequestMessage(); final Principal clientPrincipal = request.getUserPrincipal(); // There should be a session as validate request created one at the end final HttpSession session = request.getSession(false); // As the session might have changed in the middle (for security // reason), // we make sure the settings are saved. Plus if coming from // SessionState.ESTABLISHED // on an existing session, we might have still something to set for the // next Filters updateSessionAndHeader(request, session, clientPrincipal); debug("secureResponse was called and session was updated"); return AuthStatus.SEND_SUCCESS; } /** * Remove method specific principals and credentials from the subject. * * @param messageInfo * a contextual object that encapsulates the client request and * server response objects, and that may be used to save state * across a sequence of calls made to the methods of this * interface for the purpose of completing a secure message * exchange. * @param subject * the Subject instance from which the Principals and credentials * are to be removed. throws AuthException If an error occurs * during the Subject processing. */ public void cleanSubject(MessageInfo messageInfo, Subject subject) throws AuthException { LOG.fine("cleanSubject called"); } /** * Sends a failure message in the response * * @param response * @param message * @return */ protected AuthStatus sendFailureMessage(HttpServletResponse response, String message) { try { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.sendError(HttpServletResponse.SC_FORBIDDEN, message); } catch (final Throwable t) { // status code has been set, and proper AuthStatus will be returned LOG.log(Level.WARNING, "Fail to set FORBIDDEN status {0}", t); } return AuthStatus.SEND_FAILURE; } /** * Create a Kerberos Subject for the Principal whose name is passed * * @param name * the name to use * @return a valid Subject */ private Subject createSubject(GSSName name) { // return com.sun.security.jgss.GSSUtil.createSubject(name, null); // // this was Sun JVM only ;-) final Set<KerberosPrincipal> krb5Principals = new HashSet<KerberosPrincipal>(); try { // First create a canonical string representation of KRB5 final String krb5name = name.canonicalize(GSS_KRB5_MECH_OID).toString(); // Then, create a Kerberos Principal from the canonical name final KerberosPrincipal krbPrinc = new KerberosPrincipal(krb5name); krb5Principals.add(krbPrinc); } catch (final GSSException e) { LOG.log(Level.SEVERE, "Unable to create the Kerberos context", e); } return new Subject(false, krb5Principals, new HashSet<Object>(), new HashSet<Object>()); } private void debug(String message, Object... parameters) { if (this.debug || LOG.isLoggable(Level.FINE)) { LOG.log(this.debugLevel, message, parameters); } } private boolean authorizeCaller(HttpServletRequest request, byte[] serviceToken, GSSName name, final Subject clientSubject) { // create Subject with principals from name final Subject kerberosServiceSubject = createSubject(name); final Set<Principal> kerberosServicePrincipals = kerberosServiceSubject.getPrincipals(); if (kerberosServicePrincipals.size() > 0) { final Set<Principal> clientPrincipals = clientSubject.getPrincipals(); clientPrincipals.addAll(kerberosServicePrincipals); // Pickup the first Principal as the caller final Principal caller = kerberosServicePrincipals.iterator().next(); if (caller != null) { // Fetch the list of extra groups final Set<String> extraGroups = fetchExtraGroups(request, this.serviceSubject, this.options); // Let's add all the groups as valid Principal as part of the // clientSubject final String[] groups = buildGroupsFromPAC(serviceToken, this.serviceSubject, extraGroups); final List<String> groupList = Arrays.asList(groups); if (this.mandatoryGroups != null && this.mandatoryGroups.size() > 0) { // There was some mandatory group to check if (!groupList.containsAll(this.mandatoryGroups)) { // None of the global constraint was found, so exiting debug("Not all the mandatory groups required ({1}) where found in the user groups {0} so failing the authentication.", groupList, this.mandatoryGroups); return false; } } // Check global constraints if (this.smartcardSecuredUsersOnly || this.delegatedSecuredUsersOnly) { final List<String> contraintGroupList = new ArrayList<String>(); if (this.smartcardSecuredUsersOnly) { contraintGroupList.add(GROUP_SMARTCARD_AUTHENTICATED); } if (this.delegatedSecuredUsersOnly) { contraintGroupList.add(GROUP_DELEGATED_AUTHENTICATED); } // Test if at least one of the constraints are matched if (Collections.disjoint(groupList, contraintGroupList)) { // None of the global constraint was found, so exiting debug("The global contrainted group {1} where not found in the user groups {0} so failing the authentication.", groupList, contraintGroupList); return false; } } final GroupPrincipalCallback groupPrincipalCallback = new GroupPrincipalCallback(clientSubject, groups); try { // notify caller for the groups this.handler.handle(new Callback[] { groupPrincipalCallback }); debug("Groups found {0}", groupList); } catch (final IOException e) { LOG.log(Level.WARNING, "Unable to set the groups " + groupList, e); } catch (final UnsupportedCallbackException e) { LOG.log(Level.WARNING, "Unable to set the groups " + groupList, e); } } // Create the caller principal to pass to caller final CallerPrincipalCallback callerPrincipalCallback = new CallerPrincipalCallback(clientSubject, caller); try { // notify caller for the Principal this.handler.handle(new Callback[] { callerPrincipalCallback }); debug("Caller principal is {0}", (Object) caller); return true; } catch (final IOException e) { LOG.log(Level.WARNING, "Unable to set caller principal {0}", e); } catch (final UnsupportedCallbackException e) { LOG.log(Level.WARNING, "Unable to set caller principal {0}", e); } } return false; } /** * This method can easily be overridden to provide extra groups * * @param request * the request * @param serverSubject * the subject of the server containing the KerberosKey * @param options * the options passed from the JASPIC context * @return the mutable set of extra roles to add or null if an error happens */ public Set<String> fetchExtraGroups(HttpServletRequest request, Subject serverSubject, Map<String, ?> options) { final Set<String> groups = new HashSet<String>(); // Check for request context groups // Test channel security if (request.isSecure()) { groups.add(GROUP_SECURED_CHANNEL); debug("The request context is secured so the {0} group was added to the user", GROUP_SECURED_CHANNEL); } else { groups.add(GROUP_UNSECURED_CHANNEL); debug("The request context is not secured so the {0} group was added to the user", GROUP_UNSECURED_CHANNEL); } return groups; } /* * interface GroupFetcher { String[] fetchExtraGroups() } */ /** * Test if the token is a NTLM Initial * * @param bytes * @return true if the token is an NTLM token */ private boolean isNTLMToken(byte[] bytes) { return new String(bytes).startsWith(NTLM_INITIAL_TOKEN); } /** * Log some debug data about a token * * @param message * the message to display * @param token * the token to log */ private void debugToken(String message, byte[] token) { if (this.debug || LOG.isLoggable(Level.FINE)) { final StringBuffer sb = new StringBuffer(); sb.append("\n"); sb.append("Token "); sb.append(Base64.isBase64(token) ? "is" : "is Not"); sb.append(" Base64 encoded\n"); sb.append("bytes: "); boolean first = true; for (final byte b : token) { final int i = b; if (first) { sb.append(i); first = false; } else { sb.append(", ").append(i); } } LOG.log(this.debugLevel, message, sb); } } /** * Log the request for debug purpose * * @param request * the HTTP Servlet Request */ private void debugRequest(HttpServletRequest request) { if (this.debug || LOG.isLoggable(Level.FINE)) { final StringBuffer sb = new StringBuffer(); sb.append("\n"); try { sb.append("Request: ").append(request.getRequestURL()).append("\n"); sb.append("UserPrincipal: ").append(request.getUserPrincipal()).append("\n"); sb.append("AuthType: ").append(request.getAuthType()).append("\n"); sb.append("Headers:\n"); @SuppressWarnings("rawtypes") final Enumeration names = request.getHeaderNames(); while (names.hasMoreElements()) { final String name = (String) names.nextElement(); sb.append("\t").append(name).append("\t").append(request.getHeader(name)).append("\n"); } LOG.log(this.debugLevel, "HTTP Request is : {0}", sb); } catch (final Throwable t) { LOG.log(Level.WARNING, "An unexpected problem has occured during log : {0}", t); } } } private KerberosKey[] getSubjectKeys(Subject subject) { final List<KerberosKey> serverKeys = new ArrayList<KerberosKey>(); final Set<Object> serverPrivateCredentials = subject.getPrivateCredentials(); for (final Object credential : serverPrivateCredentials) { if (credential instanceof KerberosKey) { serverKeys.add((KerberosKey) credential); } } return serverKeys.toArray(new KerberosKey[0]); } /** * Fetch the list of SID group from the PAC for a given Kerberos service * token * * @param serviceToken * the service token * @param serverSubject * the subject of the server containing the KerberosKey * @param groups * a set of extra groups, if null a new empty set will be created * as a basis * @return the array of matching roles or null if an error happens */ private String[] buildGroupsFromPAC(byte[] serviceToken, Subject serverSubject, Set<String> groups) { final KerberosKey[] keys = getSubjectKeys(serverSubject); try { final SpnegoToken spnegoToken = SpnegoToken.parse(serviceToken); final String mechanism = spnegoToken.getMechanism(); debug("Mechanism found {0}", mechanism); // Fetch all the SIDs and put it in a set final Set<String> sids = groups == null ? new HashSet<String>() : groups; if (SpnegoConstants.KERBEROS_MECHANISM.equals(mechanism) || SpnegoConstants.LEGACY_KERBEROS_MECHANISM.equals(mechanism)) { final byte[] mechanismToken = spnegoToken.getMechanismToken(); // Decoding Kerberos token final KerberosToken kerberosToken = new KerberosToken(mechanismToken, keys); final List<KerberosAuthData> userAuthorizations = kerberosToken.getTicket().getEncData() .getUserAuthorizations(); for (final KerberosAuthData kerberosAuthData : userAuthorizations) { if (kerberosAuthData instanceof KerberosPacAuthData) { final Pac pac = ((KerberosPacAuthData) kerberosAuthData).getPac(); final PacLogonInfo logonInfo = pac.getLogonInfo(); if (logonInfo.getGroupSid() != null) { final PacSid sid = logonInfo.getGroupSid(); sids.add(ADUtil.convertSID(sid.getBytes())); // sids.add( "SID_"+sid.toString()); } for (final PacSid pacSid : logonInfo.getGroupSids()) { sids.add(ADUtil.convertSID(pacSid.getBytes())); // sids.add("SID_" + pacSid.toString()); } for (final PacSid pacSid : logonInfo.getExtraSids()) { sids.add(ADUtil.convertSID(pacSid.getBytes())); // sids.add("SID_" + pacSid.toString()); } for (final PacSid pacSid : logonInfo.getResourceGroupSids()) { sids.add(ADUtil.convertSID(pacSid.getBytes())); // sids.add("SID_" + pacSid.toString()); } // Remove fake Smarcard Logon Role if existing if (sids.contains(GROUP_SMARTCARD_AUTHENTICATED)) { // Right, there is a role matching the sids.remove(GROUP_SMARTCARD_AUTHENTICATED); LOG.warning( "Smartcard role was already assigned to a user, this could be a security issue. Meanwhile, the fake role was removed."); } // Let's check for smartcard logon debug("Testing MS-PAC for Smartcard Logon"); if (pac.getCredentialType() != null) { // Add the synthetic group to indicate a smartcard // logon was secured sids.add(GROUP_SMARTCARD_AUTHENTICATED); debug("Smartcard logon was detected from the MS-PAC"); } debug("Checking secure SIDs indicating MS-KILE Authentication Mechanism Assurance"); if (this.secureGroups != null && !Collections.disjoint(sids, this.secureGroups)) { // Add the synthetic group to indicate a smartcard // logon was secured sids.add(GROUP_SMARTCARD_AUTHENTICATED); debug("Smartcard logon was detected because of MS-KILE Authentication Mechanism Assurance"); } debug("Checking KCD call scenario"); if (pac.getDelegationInfos() != null && pac.getDelegationInfos().size() > 0) { // There is some delegation info found, so let's add // the synthetic group to indicate KCD was detected sids.add(GROUP_DELEGATED_AUTHENTICATED); debug("Kerberos Constrainted Delegation call scenario was detected"); } if (this.groupMapping != null && this.groupMapping.size() > 0) { for (final String groupKey : this.groupMapping.stringPropertyNames()) { // provide some mapping // TODO provide a better mapping with N-N // support if (sids.contains(groupKey)) { final String groupValue = this.groupMapping.getProperty(groupKey); sids.add(groupValue); } } } } } } debug("Groups assigned from PAC : {0}", sids); // Return the group sids as an array final String[] idsArray = new String[sids.size()]; return sids.toArray(idsArray); } catch (final Exception e) { // In any case it will just fail to prevent any groups from beeing // fetched debugToken("Failed to fetch credential from PAC with service token {0}", serviceToken); LOG.log(Level.WARNING, "Unable to get the groups from the given PAC, will return null to the caller", e); return null; } } /** * Krb5LoginConfig centralize the Kerberos configuration file. */ private static class Krb5LoginConfig extends Configuration { private static final Logger LOG = Logger.getLogger(Krb5LoginConfig.class.getName()); private final String keyTabLocation; private final String servicePrincipalName; private final boolean debug; public Krb5LoginConfig(String keyTabLocation, String servicePrincipalName, boolean debug) { this.keyTabLocation = keyTabLocation; this.servicePrincipalName = servicePrincipalName; this.debug = debug; } static final String IBM_KRB_MODULE = "com.ibm.security.auth.module.Krb5LoginModule"; static final String SUN_KRB_MODULE = "com.sun.security.auth.module.Krb5LoginModule"; /** * Return a valid Kerberos context matching the underlying JVM * capability * * @param name * the name of the configuration * @return a valid JAAS configuration */ @Override public AppConfigurationEntry[] getAppConfigurationEntry(String name) { if (isSunKerberosAvailable()) { LOG.info("Sun Kerberos provider selected"); return getSunAppConfigurationEntry(name); } else if (isIbmKerberosAvailable()) { LOG.info("IBM Kerberos provider selected"); return getIbmAppConfigurationEntry(name); } else { // should throw a better exception but done for compatibility // issues throw new UnsupportedOperationException( "Unable to find a matching KerberosLoginModule in the Java runtime"); } } private AppConfigurationEntry[] getSunAppConfigurationEntry(String name) { final HashMap<String, String> options = new HashMap<String, String>(); options.put("useKeyTab", "true"); options.put("keyTab", this.keyTabLocation); options.put("principal", this.servicePrincipalName); options.put("storeKey", "true"); options.put("doNotPrompt", "true"); if (this.debug) { options.put("debug", "true"); } options.put("isInitiator", "false"); return new AppConfigurationEntry[] { new AppConfigurationEntry(SUN_KRB_MODULE, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options), }; } private boolean isSunKerberosAvailable() { try { Class.forName(SUN_KRB_MODULE); return true; } catch (final Exception e) { return false; } } private AppConfigurationEntry[] getIbmAppConfigurationEntry(String name) { final HashMap<String, String> options = new HashMap<String, String>(); options.put("useKeytab", this.keyTabLocation); options.put("principal", this.servicePrincipalName); if (this.debug) { options.put("debug", "true"); } options.put("credsType", "acceptor"); return new AppConfigurationEntry[] { new AppConfigurationEntry(IBM_KRB_MODULE, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options), }; } private boolean isIbmKerberosAvailable() { try { Class.forName(IBM_KRB_MODULE); return true; } catch (final Exception e) { return false; } } } /** * This class is used to validate the kerberos token * */ private static class KerberosValidateAction implements PrivilegedExceptionAction<byte[]> { private final byte[] kerberosTicket; private final Subject serviceSubject; private GSSContext context; private boolean established = false; private Oid mech = null; private GSSName srcName = null; private String servicePrincipal = null; public KerberosValidateAction(String servicePrincipal, byte[] kerberosTicket, Subject serviceSubject) { this.servicePrincipal = servicePrincipal; this.kerberosTicket = kerberosTicket; this.serviceSubject = serviceSubject; } // @Override public byte[] run() throws Exception { // Create the manager // Simple default GSSContext creation does not work with an IBM SDK // GSSManager.getInstance().createContext((GSSCredential) null) // hence, we need to shape the GSS context a bit more by hitting the // exact usage scenario : SPNEGO with Kerberos final GSSManager manager = GSSManager.getInstance(); final Oid spnegoMechOid = new Oid("1.3.6.1.5.5.2"); final Oid krb5MechOid = new Oid("1.2.840.113554.1.2.2"); // Create the service name // BUGFIX : // createName("HTTP/host@DOMAIN",GSSName.NT_HOSTBASED_SERVICE, // krb5MechOid) might fail on IBM SDK so defaulting to null final GSSName serviceName = manager.createName(this.servicePrincipal, null, krb5MechOid); // Create a credential accepting SPNEGO + Kerberos // BUGFIX : DEFAULT_LIFETIME can not be used on IBM SDK see bug // http://www-304.ibm.com/support/docview.wss?uid=swg1IZ54545 final GSSCredential serviceCredential = manager.createCredential(serviceName, GSSCredential.INDEFINITE_LIFETIME, new Oid[] { spnegoMechOid, krb5MechOid }, GSSCredential.ACCEPT_ONLY); // Now create the GSS context this.context = manager.createContext(serviceCredential); try { // Check if the ticket is acceptable final byte[] result = this.context.acceptSecContext(this.kerberosTicket, 0, this.kerberosTicket.length); this.established = this.context.isEstablished(); try { this.mech = this.context.getMech(); } catch (final GSSException gsse) { this.mech = null; // LOG.log(Level.FINE, "Unable to get the mech from the GSSContext, defaulting to null", gsse); } this.srcName = this.context.getSrcName(); /** * @todo Try to use generic GSS instead, an attempt was done to extract here the encoded MS-PAC ticket, this * currently fails, the code hint is : * <code> * MessageProp prop = new MessageProp(0,false); byte[] * unCipheredPac = context.unwrap(kerberosTicket, 0, * kerberosTicket.length, prop); * </code> */ return result; } finally { this.context.dispose(); } } public GSSContext getContext() { return this.context; } public byte[] getKerberosTicket() { return this.kerberosTicket; } public boolean isEstablished() { return this.established; } public Oid getMech() { return this.mech; } public GSSName getSrcName() { return this.srcName; } } }