/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at * trunk/opends/resource/legal-notices/OpenDS.LICENSE * or https://OpenDS.dev.java.net/OpenDS.LICENSE. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable, * add the following below this CDDL HEADER, with the fields enclosed * by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Portions Copyright 2006-2007 Sun Microsystems, Inc. */ package org.opends.server.extensions; import java.security.PrivilegedExceptionAction; import java.util.HashMap; import javax.security.auth.Subject; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.UnsupportedCallbackException; import javax.security.auth.login.LoginContext; import javax.security.sasl.AuthorizeCallback; import javax.security.sasl.Sasl; import javax.security.sasl.SaslServer; import org.opends.server.api.ClientConnection; import org.opends.server.core.BindOperation; import org.opends.server.core.DirectoryServer; import org.opends.server.protocols.asn1.ASN1OctetString; import org.opends.server.types.AuthenticationInfo; import org.opends.server.types.ByteString; import org.opends.server.types.DirectoryException; import org.opends.server.types.Entry; import org.opends.server.types.InitializationException; import org.opends.server.types.ResultCode; import static org.opends.server.loggers.debug.DebugLogger.debugCaught; import static org.opends.server.loggers.debug.DebugLogger.debugEnabled; import org.opends.server.types.DebugLogLevel; import static org.opends.server.messages.ExtensionsMessages.*; import static org.opends.server.messages.MessageHandler.*; import static org.opends.server.util.ServerConstants.*; import static org.opends.server.util.StaticUtils.*; /** * This class defines a data structure that holds state information needed for * processing a SASL GSSAPI bind from a client. */ public class GSSAPIStateInfo implements PrivilegedExceptionAction, CallbackHandler { // The bind operation with which this state is associated. private BindOperation bindOperation; // The client connection with which this state is associated. private ClientConnection clientConnection; // The entry of the user that authenticated in this session. private Entry userEntry; // The GSSAPI authentication handler that created this state information. private GSSAPISASLMechanismHandler gssapiHandler; // The login context used to perform server-side authentication. private LoginContext loginContext; // The SASL server that will be used to actually perform the authentication. private SaslServer saslServer; // The protocol that the client is using to communicate with the server. private String protocol; // The FQDN of this system to use in the authentication process. private String serverFQDN; /** * Creates a new GSSAPI state info structure with the provided information. * * @param gssapiHandler The GSSAPI authentication handler that created this * state information. * @param bindOperation The bind operation with which this state is * associated. * @param serverFQDN The fully-qualified domain name for the server to * use in the authentication process. * * @throws InitializationException If it is not possible to authenticate to * the KDC to verify the client credentials. */ public GSSAPIStateInfo(GSSAPISASLMechanismHandler gssapiHandler, BindOperation bindOperation, String serverFQDN) throws InitializationException { this.gssapiHandler = gssapiHandler; this.bindOperation = bindOperation; this.serverFQDN = serverFQDN; clientConnection = bindOperation.getClientConnection(); protocol = toLowerCase(clientConnection.getProtocol()); userEntry = null; // Create the LoginContext and do the server-side authentication. // FIXME -- Can this be moved to a one-time call in the GSSAPI handler // rather than once per GSSAPI bind attempt? try { loginContext = new LoginContext(GSSAPISASLMechanismHandler.class.getName(), this); } catch (Exception e) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e); } int msgID = MSGID_SASLGSSAPI_CANNOT_CREATE_LOGIN_CONTEXT; String message = getMessage(msgID, stackTraceToSingleLineString(e)); throw new InitializationException(msgID, message, e); } try { loginContext.login(); } catch (Exception e) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e); } int msgID = MSGID_SASLGSSAPI_CANNOT_AUTHENTICATE_SERVER; String message = getMessage(msgID, stackTraceToSingleLineString(e)); throw new InitializationException(msgID, message, e); } saslServer = null; } /** * Sets the bind operation for the next stage of processing in the GSSAPI * authentication. This must be called before the processing is performed so * that the appropriate response may be sent to the client. * * @param bindOperation The bind operation for the next stage of processing * in the GSSAPI authentication. */ public void setBindOperation(BindOperation bindOperation) { this.bindOperation = bindOperation; } /** * Retrieves the entry of the user that has authenticated on this GSSAPI * session. This should only be available after a successful GSSAPI * authentication. The return value of this method should be considered * unreliable if GSSAPI authentication has not yet completed successfully. * * @return x */ public Entry getUserEntry() { return userEntry; } /** * Destroys any sensitive information that might be associated with the SASL * server instance. */ public void dispose() { try { saslServer.dispose(); } catch (Exception e) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e); } } } /** * Processes the next stage of the GSSAPI bind process. This may be used for * the first stage or any stage thereafter until the authentication is * complete. It will automatically take care of the JAAS processing behind * the scenes as necessary. */ public void processAuthenticationStage() { try { Subject.doAs(loginContext.getSubject(), this); } catch (Exception e) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e); } } } /** * Processes a stage of the SASL GSSAPI bind request. The * setBindOperation method must have been called to update the * reference to the latest bind request before invoking this method through * doAs or doAsPrivileged. * * @return true if there was no error during this stage of the * bind and processing can continue, or false if an * error occurred and and processing should not continue. */ public Boolean run() { if (saslServer == null) { // Create the SASL server instance for use with this authentication // attempt. try { HashMap saslProperties = new HashMap(); // FIXME -- We need to add support for auth-int and auth-conf. // propertyMap.put(Sasl.QOP, "auth,auth-int,auth-conf"); saslProperties.put(Sasl.QOP, "auth"); saslProperties.put(Sasl.REUSE, "false"); saslServer = Sasl.createSaslServer(SASL_MECHANISM_GSSAPI, protocol, serverFQDN, saslProperties, this); } catch (Exception e) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e); } int msgID = MSGID_SASLGSSAPI_CANNOT_CREATE_SASL_SERVER; String message = getMessage(msgID, stackTraceToSingleLineString(e)); clientConnection.setSASLAuthStateInfo(null); bindOperation.setAuthFailureReason(msgID, message); bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); return false; } } // Get the SASL credentials from the bind request. byte[] clientCredBytes; ByteString clientCredentials = bindOperation.getSASLCredentials(); if (clientCredentials == null) { clientCredBytes = new byte[0]; } else { clientCredBytes = clientCredentials.value(); } // Process the client SASL credentials and get the data to include in the // server SASL credentials of the response. ASN1OctetString serverSASLCredentials; try { byte[] serverCredBytes = saslServer.evaluateResponse(clientCredBytes); if (serverCredBytes == null) { serverSASLCredentials = null; } else { serverSASLCredentials = new ASN1OctetString(serverCredBytes); } } catch (Exception e) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e); } try { saslServer.dispose(); } catch (Exception e2) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e2); } } int msgID = MSGID_SASLGSSAPI_CANNOT_EVALUATE_RESPONSE; String message = getMessage(msgID, stackTraceToSingleLineString(e)); clientConnection.setSASLAuthStateInfo(null); bindOperation.setAuthFailureReason(msgID, message); bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); return false; } // If the authentication is not yet complete, then send a "SASL bind in // progress" response to the client. if (! saslServer.isComplete()) { clientConnection.setSASLAuthStateInfo(saslServer); bindOperation.setResultCode(ResultCode.SASL_BIND_IN_PROGRESS); bindOperation.setServerSASLCredentials(serverSASLCredentials); return true; } // If the authentication is complete, then get the authorization ID from the // SASL server and map that to a user in the directory. String authzID = saslServer.getAuthorizationID(); if ((authzID == null) || (authzID.length() == 0)) { try { saslServer.dispose(); } catch (Exception e) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e); } } int msgID = MSGID_SASLGSSAPI_NO_AUTHZ_ID; String message = getMessage(msgID); clientConnection.setSASLAuthStateInfo(null); bindOperation.setAuthFailureReason(msgID, message); bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); return false; } try { userEntry = gssapiHandler.getUserForAuthzID(bindOperation, authzID); } catch (DirectoryException de) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, de); } try { saslServer.dispose(); } catch (Exception e) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e); } } bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); bindOperation.setAuthFailureReason(de.getErrorMessageID(), de.getErrorMessage()); clientConnection.setSASLAuthStateInfo(null); return false; } // If the user entry is null, then we couldn't map the authorization ID to // a user. if (userEntry == null) { try { saslServer.dispose(); } catch (Exception e) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e); } } int msgID = MSGID_SASLGSSAPI_CANNOT_MAP_AUTHZID; String message = getMessage(msgID, authzID); clientConnection.setSASLAuthStateInfo(null); bindOperation.setAuthFailureReason(msgID, message); bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); return false; } else { bindOperation.setSASLAuthUserEntry(userEntry); } // The authentication was successful, so set the proper state information // in the client connection and return success. AuthenticationInfo authInfo = new AuthenticationInfo(userEntry, SASL_MECHANISM_GSSAPI, DirectoryServer.isRootDN(userEntry.getDN())); bindOperation.setAuthenticationInfo(authInfo); bindOperation.setResultCode(ResultCode.SUCCESS); // FIXME -- If we're using integrity or confidentiality, then we can't do // this. clientConnection.setSASLAuthStateInfo(null); try { saslServer.dispose(); } catch (Exception e) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e); } } return true; } /** * Handles any callbacks that might be required in order to process a SASL * GSSAPI bind on the server. In this case, if an authorization ID was * provided, then a callback may be used to determine whether it is * acceptable. * * @param callbacks The callbacks needed to provide information for the * GSSAPI authentication process. * * @throws UnsupportedCallbackException If an unexpected callback is * included in the provided set. */ public void handle(Callback[] callbacks) throws UnsupportedCallbackException { for (Callback callback : callbacks) { if (callback instanceof NameCallback) { String authID = toLowerCase(clientConnection.getProtocol()) + "/" + serverFQDN; ((NameCallback) callback).setName(authID); } else if (callback instanceof AuthorizeCallback) { // FIXME -- Should we allow an authzID different from the authID? // FIXME -- Do we need to do anything else here? AuthorizeCallback authzCallback = (AuthorizeCallback) callback; String authID = authzCallback.getAuthenticationID(); String authzID = authzCallback.getAuthorizationID(); if (authID.equals(authzID)) { authzCallback.setAuthorizedID(authzID); authzCallback.setAuthorized(true); } else { int msgID = MSGID_SASLGSSAPI_DIFFERENT_AUTHID_AND_AUTHZID; String message = getMessage(msgID, authID, authzID); bindOperation.setAuthFailureReason(msgID, message); authzCallback.setAuthorized(false); } } else { // We weren't prepared for this type of callback. int msgID = MSGID_SASLGSSAPI_UNEXPECTED_CALLBACK; String message = getMessage(msgID, String.valueOf(callback)); throw new UnsupportedCallbackException(callback, message); } } } }