001    /*
002     * CDDL HEADER START
003     *
004     * The contents of this file are subject to the terms of the
005     * Common Development and Distribution License, Version 1.0 only
006     * (the "License").  You may not use this file except in compliance
007     * with the License.
008     *
009     * You can obtain a copy of the license at
010     * trunk/opends/resource/legal-notices/OpenDS.LICENSE
011     * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
012     * See the License for the specific language governing permissions
013     * and limitations under the License.
014     *
015     * When distributing Covered Code, include this CDDL HEADER in each
016     * file and include the License file at
017     * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
018     * add the following below this CDDL HEADER, with the fields enclosed
019     * by brackets "[]" replaced with your own identifying information:
020     *      Portions Copyright [yyyy] [name of copyright owner]
021     *
022     * CDDL HEADER END
023     *
024     *
025     *      Copyright 2006-2008 Sun Microsystems, Inc.
026     */
027    package org.opends.server.extensions;
028    import org.opends.messages.Message;
029    
030    
031    
032    import java.security.PrivilegedExceptionAction;
033    import java.util.HashMap;
034    import javax.security.auth.Subject;
035    import javax.security.auth.callback.Callback;
036    import javax.security.auth.callback.CallbackHandler;
037    import javax.security.auth.callback.NameCallback;
038    import javax.security.auth.callback.UnsupportedCallbackException;
039    import javax.security.auth.login.LoginContext;
040    import javax.security.sasl.AuthorizeCallback;
041    import javax.security.sasl.Sasl;
042    import javax.security.sasl.SaslServer;
043    
044    import org.opends.server.api.ClientConnection;
045    import org.opends.server.core.BindOperation;
046    import org.opends.server.core.DirectoryServer;
047    import org.opends.server.protocols.asn1.ASN1OctetString;
048    import org.opends.server.types.AuthenticationInfo;
049    import org.opends.server.types.ByteString;
050    import org.opends.server.types.DirectoryException;
051    import org.opends.server.types.Entry;
052    import org.opends.server.types.InitializationException;
053    import org.opends.server.types.ResultCode;
054    
055    import static org.opends.server.loggers.debug.DebugLogger.*;
056    import org.opends.server.loggers.debug.DebugTracer;
057    import org.opends.server.types.DebugLogLevel;
058    import static org.opends.messages.ExtensionMessages.*;
059    import static org.opends.server.util.ServerConstants.*;
060    import static org.opends.server.util.StaticUtils.*;
061    
062    
063    
064    /**
065     * This class defines a data structure that holds state information needed for
066     * processing a SASL GSSAPI bind from a client.
067     */
068    public class GSSAPIStateInfo
069           implements PrivilegedExceptionAction<Boolean>, CallbackHandler
070    {
071      /**
072       * The tracer object for the debug logger.
073       */
074      private static final DebugTracer TRACER = getTracer();
075    
076    
077    
078    
079      // The bind operation with which this state is associated.
080      private BindOperation bindOperation;
081    
082      // The client connection with which this state is associated.
083      private ClientConnection clientConnection;
084    
085      // The entry of the user that authenticated in this session.
086      private Entry userEntry;
087    
088      // The GSSAPI authentication handler that created this state information.
089      private GSSAPISASLMechanismHandler gssapiHandler;
090    
091      // The login context used to perform server-side authentication.
092      private LoginContext loginContext;
093    
094      // The SASL server that will be used to actually perform the authentication.
095      private SaslServer saslServer;
096    
097      // The protocol that the client is using to communicate with the server.
098      private String protocol;
099    
100      // The FQDN of this system to use in the authentication process.
101      private String serverFQDN;
102    
103    
104    
105    
106      /**
107       * Creates a new GSSAPI state info structure with the provided information.
108       *
109       * @param  gssapiHandler  The GSSAPI authentication handler that created this
110       *                        state information.
111       * @param  bindOperation  The bind operation with which this state is
112       *                        associated.
113       * @param  serverFQDN     The fully-qualified domain name for the server to
114       *                        use in the authentication process.
115       *
116       * @throws  InitializationException  If it is not possible to authenticate to
117       *                                   the KDC to verify the client credentials.
118       */
119      public GSSAPIStateInfo(GSSAPISASLMechanismHandler gssapiHandler,
120                             BindOperation bindOperation, String serverFQDN)
121             throws InitializationException
122      {
123        this.gssapiHandler = gssapiHandler;
124        this.bindOperation = bindOperation;
125        this.serverFQDN    = serverFQDN;
126    
127        clientConnection = bindOperation.getClientConnection();
128        protocol         = toLowerCase(clientConnection.getProtocol());
129        userEntry        = null;
130    
131    
132        // Create the LoginContext and do the server-side authentication.
133        // FIXME -- Can this be moved to a one-time call in the GSSAPI handler
134        //          rather than once per GSSAPI bind attempt?
135        try
136        {
137          loginContext =
138               new LoginContext(GSSAPISASLMechanismHandler.class.getName(), this);
139        }
140        catch (Exception e)
141        {
142          if (debugEnabled())
143          {
144            TRACER.debugCaught(DebugLogLevel.ERROR, e);
145          }
146    
147          Message message = ERR_SASLGSSAPI_CANNOT_CREATE_LOGIN_CONTEXT.get(
148              getExceptionMessage(e));
149          throw new InitializationException(message, e);
150        }
151    
152        try
153        {
154          loginContext.login();
155        }
156        catch (Exception e)
157        {
158          if (debugEnabled())
159          {
160            TRACER.debugCaught(DebugLogLevel.ERROR, e);
161          }
162    
163          Message message =
164              ERR_SASLGSSAPI_CANNOT_AUTHENTICATE_SERVER.get(getExceptionMessage(e));
165          throw new InitializationException(message, e);
166        }
167    
168    
169        saslServer = null;
170      }
171    
172    
173    
174      /**
175       * Sets the bind operation for the next stage of processing in the GSSAPI
176       * authentication.  This must be called before the processing is performed so
177       * that the appropriate response may be sent to the client.
178       *
179       * @param  bindOperation  The bind operation for the next stage of processing
180       *                        in the GSSAPI authentication.
181       */
182      public void setBindOperation(BindOperation bindOperation)
183      {
184        this.bindOperation = bindOperation;
185      }
186    
187    
188    
189      /**
190       * Retrieves the entry of the user that has authenticated on this GSSAPI
191       * session.  This should only be available after a successful GSSAPI
192       * authentication.  The return value of this method should be considered
193       * unreliable if GSSAPI authentication has not yet completed successfully.
194       *
195       * @return  x
196       */
197      public Entry getUserEntry()
198      {
199        return userEntry;
200      }
201    
202    
203    
204      /**
205       * Destroys any sensitive information that might be associated with the SASL
206       * server instance.
207       */
208      public void dispose()
209      {
210        try
211        {
212          saslServer.dispose();
213        }
214        catch (Exception e)
215        {
216          if (debugEnabled())
217          {
218            TRACER.debugCaught(DebugLogLevel.ERROR, e);
219          }
220        }
221      }
222    
223    
224    
225      /**
226       * Processes the next stage of the GSSAPI bind process.  This may be used for
227       * the first stage or any stage thereafter until the authentication is
228       * complete.  It will automatically take care of the JAAS processing behind
229       * the scenes as necessary.
230       */
231      public void processAuthenticationStage()
232      {
233        try
234        {
235          Subject.doAs(loginContext.getSubject(), this);
236        }
237        catch (Exception e)
238        {
239          if (debugEnabled())
240          {
241            TRACER.debugCaught(DebugLogLevel.ERROR, e);
242          }
243        }
244      }
245    
246    
247    
248      /**
249       * Processes a stage of the SASL GSSAPI bind request.  The
250       * <CODE>setBindOperation</CODE> method must have been called to update the
251       * reference to the latest bind request before invoking this method through
252       * <CODE>doAs</CODE> or <CODE>doAsPrivileged</CODE>.
253       *
254       * @return  <CODE>true</CODE> if there was no error during this stage of the
255       *          bind and processing can continue, or <CODE>false</CODE> if an
256       *          error occurred and and processing should not continue.
257       */
258      public Boolean run()
259      {
260        if (saslServer == null)
261        {
262          // Create the SASL server instance for use with this authentication
263          // attempt.
264          try
265          {
266            HashMap<String,String> saslProperties = new HashMap<String,String>();
267    
268            // FIXME -- We need to add support for auth-int and auth-conf.
269            // propertyMap.put(Sasl.QOP, "auth,auth-int,auth-conf");
270            saslProperties.put(Sasl.QOP, "auth");
271    
272            saslProperties.put(Sasl.REUSE, "false");
273    
274            saslServer = Sasl.createSaslServer(SASL_MECHANISM_GSSAPI, protocol,
275                                               serverFQDN, saslProperties, this);
276          }
277          catch (Exception e)
278          {
279            if (debugEnabled())
280            {
281              TRACER.debugCaught(DebugLogLevel.ERROR, e);
282            }
283    
284            Message message = ERR_SASLGSSAPI_CANNOT_CREATE_SASL_SERVER.get(
285                    getExceptionMessage(e));
286    
287            clientConnection.setSASLAuthStateInfo(null);
288            bindOperation.setAuthFailureReason(message);
289            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
290            return false;
291          }
292        }
293    
294    
295        // Get the SASL credentials from the bind request.
296        byte[] clientCredBytes;
297        ByteString clientCredentials = bindOperation.getSASLCredentials();
298        if (clientCredentials == null)
299        {
300          clientCredBytes = new byte[0];
301        }
302        else
303        {
304          clientCredBytes = clientCredentials.value();
305        }
306    
307    
308        // Process the client SASL credentials and get the data to include in the
309        // server SASL credentials of the response.
310        ASN1OctetString serverSASLCredentials;
311        try
312        {
313          byte[] serverCredBytes = saslServer.evaluateResponse(clientCredBytes);
314    
315          if (serverCredBytes == null)
316          {
317            serverSASLCredentials = null;
318          }
319          else
320          {
321            serverSASLCredentials = new ASN1OctetString(serverCredBytes);
322          }
323        }
324        catch (Exception e)
325        {
326          if (debugEnabled())
327          {
328            TRACER.debugCaught(DebugLogLevel.ERROR, e);
329          }
330    
331          try
332          {
333            saslServer.dispose();
334          }
335          catch (Exception e2)
336          {
337            if (debugEnabled())
338            {
339              TRACER.debugCaught(DebugLogLevel.ERROR, e2);
340            }
341          }
342    
343          Message message = ERR_SASLGSSAPI_CANNOT_EVALUATE_RESPONSE.get(
344                  getExceptionMessage(e));
345    
346          clientConnection.setSASLAuthStateInfo(null);
347          bindOperation.setAuthFailureReason(message);
348          bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
349          return false;
350        }
351    
352    
353        // If the authentication is not yet complete, then send a "SASL bind in
354        // progress" response to the client.
355        if (! saslServer.isComplete())
356        {
357          clientConnection.setSASLAuthStateInfo(saslServer);
358          bindOperation.setResultCode(ResultCode.SASL_BIND_IN_PROGRESS);
359          bindOperation.setServerSASLCredentials(serverSASLCredentials);
360          return true;
361        }
362    
363    
364        // If the authentication is complete, then get the authorization ID from the
365        // SASL server and map that to a user in the directory.
366        String authzID = saslServer.getAuthorizationID();
367        if ((authzID == null) || (authzID.length() == 0))
368        {
369          try
370          {
371            saslServer.dispose();
372          }
373          catch (Exception e)
374          {
375            if (debugEnabled())
376            {
377              TRACER.debugCaught(DebugLogLevel.ERROR, e);
378            }
379          }
380    
381          Message message = ERR_SASLGSSAPI_NO_AUTHZ_ID.get();
382    
383          clientConnection.setSASLAuthStateInfo(null);
384          bindOperation.setAuthFailureReason(message);
385          bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
386          return false;
387        }
388    
389    
390        try
391        {
392          userEntry = gssapiHandler.getUserForAuthzID(bindOperation, authzID);
393        }
394        catch (DirectoryException de)
395        {
396          if (debugEnabled())
397          {
398            TRACER.debugCaught(DebugLogLevel.ERROR, de);
399          }
400    
401          try
402          {
403            saslServer.dispose();
404          }
405          catch (Exception e)
406          {
407            if (debugEnabled())
408            {
409              TRACER.debugCaught(DebugLogLevel.ERROR, e);
410            }
411          }
412    
413          bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
414          bindOperation.setAuthFailureReason(de.getMessageObject());
415          clientConnection.setSASLAuthStateInfo(null);
416          return false;
417        }
418    
419    
420        // If the user entry is null, then we couldn't map the authorization ID to
421        // a user.
422        if (userEntry == null)
423        {
424          try
425          {
426            saslServer.dispose();
427          }
428          catch (Exception e)
429          {
430            if (debugEnabled())
431            {
432              TRACER.debugCaught(DebugLogLevel.ERROR, e);
433            }
434          }
435    
436          Message message = ERR_SASLGSSAPI_CANNOT_MAP_AUTHZID.get(authzID);
437    
438          clientConnection.setSASLAuthStateInfo(null);
439          bindOperation.setAuthFailureReason(message);
440          bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
441          return false;
442        }
443        else
444        {
445          bindOperation.setSASLAuthUserEntry(userEntry);
446        }
447    
448    
449        // The authentication was successful, so set the proper state information
450        // in the client connection and return success.
451        AuthenticationInfo authInfo =
452             new AuthenticationInfo(userEntry, SASL_MECHANISM_GSSAPI,
453                                    DirectoryServer.isRootDN(userEntry.getDN()));
454        bindOperation.setAuthenticationInfo(authInfo);
455        bindOperation.setResultCode(ResultCode.SUCCESS);
456    
457        // FIXME -- If we're using integrity or confidentiality, then we can't do
458        // this.
459        clientConnection.setSASLAuthStateInfo(null);
460        try
461        {
462          saslServer.dispose();
463        }
464        catch (Exception e)
465        {
466          if (debugEnabled())
467          {
468            TRACER.debugCaught(DebugLogLevel.ERROR, e);
469          }
470        }
471    
472        return true;
473      }
474    
475    
476    
477      /**
478       * Handles any callbacks that might be required in order to process a SASL
479       * GSSAPI bind on the server.  In this case, if an authorization ID was
480       * provided, then a callback may be used to determine whether it is
481       * acceptable.
482       *
483       * @param  callbacks  The callbacks needed to provide information for the
484       *                    GSSAPI authentication process.
485       *
486       * @throws  UnsupportedCallbackException  If an unexpected callback is
487       *                                        included in the provided set.
488       */
489      public void handle(Callback[] callbacks)
490             throws UnsupportedCallbackException
491      {
492        for (Callback callback : callbacks)
493        {
494          if (callback instanceof NameCallback)
495          {
496            String authID = toLowerCase(clientConnection.getProtocol()) + "/" +
497                            serverFQDN;
498            ((NameCallback) callback).setName(authID);
499          }
500          else if (callback instanceof AuthorizeCallback)
501          {
502            // FIXME -- Should we allow an authzID different from the authID?
503            // FIXME -- Do we need to do anything else here?
504            AuthorizeCallback authzCallback = (AuthorizeCallback) callback;
505            String authID  = authzCallback.getAuthenticationID();
506            String authzID = authzCallback.getAuthorizationID();
507    
508            if (authID.equals(authzID))
509            {
510              authzCallback.setAuthorizedID(authzID);
511              authzCallback.setAuthorized(true);
512            }
513            else
514            {
515              Message message = ERR_SASLGSSAPI_DIFFERENT_AUTHID_AND_AUTHZID.get(
516                      authID, authzID);
517              bindOperation.setAuthFailureReason(message);
518              authzCallback.setAuthorized(false);
519            }
520          }
521          else
522          {
523            // We weren't prepared for this type of callback.
524            Message message =
525                INFO_SASLGSSAPI_UNEXPECTED_CALLBACK.get(String.valueOf(callback));
526            throw new UnsupportedCallbackException(callback, message.toString());
527          }
528        }
529      }
530    }
531