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.util.ArrayList;
033    import java.util.List;
034    import java.util.concurrent.locks.Lock;
035    
036    import org.opends.server.admin.server.ConfigurationChangeListener;
037    import org.opends.server.admin.std.server.PlainSASLMechanismHandlerCfg;
038    import org.opends.server.admin.std.server.SASLMechanismHandlerCfg;
039    import org.opends.server.api.IdentityMapper;
040    import org.opends.server.api.SASLMechanismHandler;
041    import org.opends.server.config.ConfigException;
042    import org.opends.server.core.BindOperation;
043    import org.opends.server.core.DirectoryServer;
044    import org.opends.server.core.PasswordPolicyState;
045    import org.opends.server.protocols.asn1.ASN1OctetString;
046    import org.opends.server.protocols.internal.InternalClientConnection;
047    import org.opends.server.types.AuthenticationInfo;
048    import org.opends.server.types.ByteString;
049    import org.opends.server.types.ConfigChangeResult;
050    import org.opends.server.types.DirectoryException;
051    import org.opends.server.types.DN;
052    import org.opends.server.types.Entry;
053    import org.opends.server.types.InitializationException;
054    import org.opends.server.types.LockManager;
055    import org.opends.server.types.Privilege;
056    import org.opends.server.types.ResultCode;
057    
058    import static org.opends.server.loggers.debug.DebugLogger.*;
059    import org.opends.server.loggers.debug.DebugTracer;
060    import org.opends.server.types.DebugLogLevel;
061    import static org.opends.messages.ExtensionMessages.*;
062    
063    import static org.opends.server.util.ServerConstants.*;
064    import static org.opends.server.util.StaticUtils.*;
065    
066    
067    
068    /**
069     * This class provides an implementation of a SASL mechanism that uses
070     * plain-text authentication.  It is based on the proposal defined in
071     * draft-ietf-sasl-plain-08 in which the SASL credentials are in the form:
072     * <BR>
073     * <BLOCKQUOTE>[authzid] UTF8NULL authcid UTF8NULL passwd</BLOCKQUOTE>
074     * <BR>
075     * Note that this is a weak mechanism by itself and does not offer any
076     * protection for the password, so it may need to be used in conjunction with a
077     * connection security provider to prevent exposing the password.
078     */
079    public class PlainSASLMechanismHandler
080           extends SASLMechanismHandler<PlainSASLMechanismHandlerCfg>
081           implements ConfigurationChangeListener<
082                           PlainSASLMechanismHandlerCfg>
083    {
084      /**
085       * The tracer object for the debug logger.
086       */
087      private static final DebugTracer TRACER = getTracer();
088    
089      // The identity mapper that will be used to map ID strings to user entries.
090      private IdentityMapper<?> identityMapper;
091    
092      // The current configuration for this SASL mechanism handler.
093      private PlainSASLMechanismHandlerCfg currentConfig;
094    
095    
096    
097      /**
098       * Creates a new instance of this SASL mechanism handler.  No initialization
099       * should be done in this method, as it should all be performed in the
100       * <CODE>initializeSASLMechanismHandler</CODE> method.
101       */
102      public PlainSASLMechanismHandler()
103      {
104        super();
105      }
106    
107    
108    
109      /**
110       * {@inheritDoc}
111       */
112      @Override()
113      public void initializeSASLMechanismHandler(
114                       PlainSASLMechanismHandlerCfg configuration)
115             throws ConfigException, InitializationException
116      {
117        configuration.addPlainChangeListener(this);
118        currentConfig = configuration;
119    
120    
121        // Get the identity mapper that should be used to find users.
122        DN identityMapperDN = configuration.getIdentityMapperDN();
123        identityMapper = DirectoryServer.getIdentityMapper(identityMapperDN);
124    
125    
126        DirectoryServer.registerSASLMechanismHandler(SASL_MECHANISM_PLAIN, this);
127      }
128    
129    
130    
131      /**
132       * {@inheritDoc}
133       */
134      @Override()
135      public void finalizeSASLMechanismHandler()
136      {
137        currentConfig.removePlainChangeListener(this);
138        DirectoryServer.deregisterSASLMechanismHandler(SASL_MECHANISM_PLAIN);
139      }
140    
141    
142    
143    
144      /**
145       * {@inheritDoc}
146       */
147      @Override()
148      public void processSASLBind(BindOperation bindOperation)
149      {
150        IdentityMapper<?> identityMapper = this.identityMapper;
151    
152        // Get the SASL credentials provided by the user and decode them.
153        String authzID  = null;
154        String authcID  = null;
155        String password = null;
156    
157        ByteString saslCredentials = bindOperation.getSASLCredentials();
158        if (saslCredentials == null)
159        {
160          bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
161    
162          Message message = ERR_SASLPLAIN_NO_SASL_CREDENTIALS.get();
163          bindOperation.setAuthFailureReason(message);
164          return;
165        }
166    
167        String credString = saslCredentials.stringValue();
168        int    length     = credString.length();
169        int    nullPos1   = credString.indexOf('\u0000');
170        if (nullPos1 < 0)
171        {
172          bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
173    
174          Message message = ERR_SASLPLAIN_NO_NULLS_IN_CREDENTIALS.get();
175          bindOperation.setAuthFailureReason(message);
176          return;
177        }
178    
179        if (nullPos1 > 0)
180        {
181          authzID = credString.substring(0, nullPos1);
182        }
183    
184    
185        int nullPos2 = credString.indexOf('\u0000', nullPos1+1);
186        if (nullPos2 < 0)
187        {
188          bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
189    
190          Message message = ERR_SASLPLAIN_NO_SECOND_NULL.get();
191          bindOperation.setAuthFailureReason(message);
192          return;
193        }
194    
195        if (nullPos2 == (nullPos1+1))
196        {
197          bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
198    
199          Message message = ERR_SASLPLAIN_ZERO_LENGTH_AUTHCID.get();
200          bindOperation.setAuthFailureReason(message);
201          return;
202        }
203    
204        if (nullPos2 == (length-1))
205        {
206          bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
207    
208          Message message = ERR_SASLPLAIN_ZERO_LENGTH_PASSWORD.get();
209          bindOperation.setAuthFailureReason(message);
210          return;
211        }
212    
213        authcID  = credString.substring(nullPos1+1, nullPos2);
214        password = credString.substring(nullPos2+1);
215    
216    
217        // Get the user entry for the authentication ID.  Allow for an
218        // authentication ID that is just a username (as per the SASL PLAIN spec),
219        // but also allow a value in the authzid form specified in RFC 2829.
220        Entry  userEntry    = null;
221        String lowerAuthcID = toLowerCase(authcID);
222        if (lowerAuthcID.startsWith("dn:"))
223        {
224          // Try to decode the user DN and retrieve the corresponding entry.
225          DN userDN;
226          try
227          {
228            userDN = DN.decode(authcID.substring(3));
229          }
230          catch (DirectoryException de)
231          {
232            if (debugEnabled())
233            {
234              TRACER.debugCaught(DebugLogLevel.ERROR, de);
235            }
236    
237            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
238    
239            Message message = ERR_SASLPLAIN_CANNOT_DECODE_AUTHCID_AS_DN.get(
240                    authcID, de.getMessageObject());
241            bindOperation.setAuthFailureReason(message);
242            return;
243          }
244    
245          if (userDN.isNullDN())
246          {
247            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
248    
249            Message message = ERR_SASLPLAIN_AUTHCID_IS_NULL_DN.get();
250            bindOperation.setAuthFailureReason(message);
251            return;
252          }
253    
254          DN rootDN = DirectoryServer.getActualRootBindDN(userDN);
255          if (rootDN != null)
256          {
257            userDN = rootDN;
258          }
259    
260          // Acquire a read lock on the user entry.  If this fails, then so will the
261          // authentication.
262          Lock readLock = null;
263          for (int i=0; i < 3; i++)
264          {
265            readLock = LockManager.lockRead(userDN);
266            if (readLock != null)
267            {
268              break;
269            }
270          }
271    
272          if (readLock == null)
273          {
274            bindOperation.setResultCode(DirectoryServer.getServerErrorResultCode());
275    
276            Message message = INFO_SASLPLAIN_CANNOT_LOCK_ENTRY.get(String.valueOf(
277                    userDN));
278            bindOperation.setAuthFailureReason(message);
279            return;
280          }
281    
282          try
283          {
284            userEntry = DirectoryServer.getEntry(userDN);
285          }
286          catch (DirectoryException de)
287          {
288            if (debugEnabled())
289            {
290              TRACER.debugCaught(DebugLogLevel.ERROR, de);
291            }
292    
293            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
294    
295            Message message = ERR_SASLPLAIN_CANNOT_GET_ENTRY_BY_DN.get(
296                    String.valueOf(userDN),
297                    de.getMessageObject());
298            bindOperation.setAuthFailureReason(message);
299            return;
300          }
301          finally
302          {
303            LockManager.unlock(userDN, readLock);
304          }
305        }
306        else
307        {
308          // Use the identity mapper to resolve the username to an entry.
309          if (lowerAuthcID.startsWith("u:"))
310          {
311            authcID = authcID.substring(2);
312          }
313    
314          try
315          {
316            userEntry = identityMapper.getEntryForID(authcID);
317          }
318          catch (DirectoryException de)
319          {
320            if (debugEnabled())
321            {
322              TRACER.debugCaught(DebugLogLevel.ERROR, de);
323            }
324    
325            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
326    
327            Message message = ERR_SASLPLAIN_CANNOT_MAP_USERNAME.get(
328                    String.valueOf(authcID),
329                    de.getMessageObject());
330            bindOperation.setAuthFailureReason(message);
331            return;
332          }
333        }
334    
335    
336        // At this point, we should have a user entry.  If we don't then fail.
337        if (userEntry == null)
338        {
339          bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
340    
341          Message message = ERR_SASLPLAIN_NO_MATCHING_ENTRIES.get(authcID);
342          bindOperation.setAuthFailureReason(message);
343          return;
344        }
345        else
346        {
347          bindOperation.setSASLAuthUserEntry(userEntry);
348        }
349    
350    
351        // If an authorization ID was provided, then make sure that it is
352        // acceptable.
353        Entry authZEntry = userEntry;
354        if (authzID != null)
355        {
356          String lowerAuthzID = toLowerCase(authzID);
357          if (lowerAuthzID.startsWith("dn:"))
358          {
359            DN authzDN;
360            try
361            {
362              authzDN = DN.decode(authzID.substring(3));
363            }
364            catch (DirectoryException de)
365            {
366              if (debugEnabled())
367              {
368                TRACER.debugCaught(DebugLogLevel.ERROR, de);
369              }
370    
371              bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
372    
373              Message message = ERR_SASLPLAIN_AUTHZID_INVALID_DN.get(
374                      authzID, de.getMessageObject());
375              bindOperation.setAuthFailureReason(message);
376              return;
377            }
378    
379            DN actualAuthzDN = DirectoryServer.getActualRootBindDN(authzDN);
380            if (actualAuthzDN != null)
381            {
382              authzDN = actualAuthzDN;
383            }
384    
385            if (! authzDN.equals(userEntry.getDN()))
386            {
387              AuthenticationInfo tempAuthInfo =
388                new AuthenticationInfo(userEntry,
389                         DirectoryServer.isRootDN(userEntry.getDN()));
390              InternalClientConnection tempConn =
391                   new InternalClientConnection(tempAuthInfo);
392              if (! tempConn.hasPrivilege(Privilege.PROXIED_AUTH, bindOperation))
393              {
394                bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
395    
396                Message message = ERR_SASLPLAIN_AUTHZID_INSUFFICIENT_PRIVILEGES.get(
397                        String.valueOf(userEntry.getDN()));
398                bindOperation.setAuthFailureReason(message);
399                return;
400              }
401    
402              if (authzDN.isNullDN())
403              {
404                authZEntry = null;
405              }
406              else
407              {
408                try
409                {
410                  authZEntry = DirectoryServer.getEntry(authzDN);
411                  if (authZEntry == null)
412                  {
413                    bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
414    
415                    Message message = ERR_SASLPLAIN_AUTHZID_NO_SUCH_ENTRY.get(
416                            String.valueOf(authzDN));
417                    bindOperation.setAuthFailureReason(message);
418                    return;
419                  }
420                }
421                catch (DirectoryException de)
422                {
423                  if (debugEnabled())
424                  {
425                    TRACER.debugCaught(DebugLogLevel.ERROR, de);
426                  }
427    
428                  bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
429    
430                  Message message = ERR_SASLPLAIN_AUTHZID_CANNOT_GET_ENTRY.get(
431                          String.valueOf(authzDN),
432                          de.getMessageObject());
433                  bindOperation.setAuthFailureReason(message);
434                  return;
435                }
436              }
437            }
438          }
439          else
440          {
441            String idStr;
442            if (lowerAuthzID.startsWith("u:"))
443            {
444              idStr = authzID.substring(2);
445            }
446            else
447            {
448              idStr = authzID;
449            }
450    
451            if (idStr.length() == 0)
452            {
453              authZEntry = null;
454            }
455            else
456            {
457              try
458              {
459                authZEntry = identityMapper.getEntryForID(idStr);
460                if (authZEntry == null)
461                {
462                  bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
463    
464                  Message message = ERR_SASLPLAIN_AUTHZID_NO_MAPPED_ENTRY.get(
465                          authzID);
466                  bindOperation.setAuthFailureReason(message);
467                  return;
468                }
469              }
470              catch (DirectoryException de)
471              {
472                if (debugEnabled())
473                {
474                  TRACER.debugCaught(DebugLogLevel.ERROR, de);
475                }
476    
477                bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
478    
479                Message message = ERR_SASLPLAIN_AUTHZID_CANNOT_MAP_AUTHZID.get(
480                        authzID, de.getMessageObject());
481                bindOperation.setAuthFailureReason(message);
482                return;
483              }
484            }
485    
486            if ((authZEntry == null) ||
487                (! authZEntry.getDN().equals(userEntry.getDN())))
488            {
489              AuthenticationInfo tempAuthInfo =
490                new AuthenticationInfo(userEntry,
491                         DirectoryServer.isRootDN(userEntry.getDN()));
492              InternalClientConnection tempConn =
493                   new InternalClientConnection(tempAuthInfo);
494              if (! tempConn.hasPrivilege(Privilege.PROXIED_AUTH, bindOperation))
495              {
496                bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
497    
498                Message message = ERR_SASLPLAIN_AUTHZID_INSUFFICIENT_PRIVILEGES.get(
499                        String.valueOf(userEntry.getDN()));
500                bindOperation.setAuthFailureReason(message);
501                return;
502              }
503            }
504          }
505        }
506    
507    
508        // Get the password policy for the user and use it to determine if the
509        // provided password was correct.
510        try
511        {
512          PasswordPolicyState pwPolicyState =
513               new PasswordPolicyState(userEntry, false);
514          if (! pwPolicyState.passwordMatches(new ASN1OctetString(password)))
515          {
516            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
517    
518            Message message = ERR_SASLPLAIN_INVALID_PASSWORD.get();
519            bindOperation.setAuthFailureReason(message);
520            return;
521          }
522        }
523        catch (Exception e)
524        {
525          if (debugEnabled())
526          {
527            TRACER.debugCaught(DebugLogLevel.ERROR, e);
528          }
529    
530          bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
531    
532          Message message = ERR_SASLPLAIN_CANNOT_CHECK_PASSWORD_VALIDITY.get(
533                  String.valueOf(userEntry.getDN()),
534                  String.valueOf(e));
535          bindOperation.setAuthFailureReason(message);
536          return;
537        }
538    
539    
540        // If we've gotten here, then the authentication was successful.
541        bindOperation.setResultCode(ResultCode.SUCCESS);
542    
543        AuthenticationInfo authInfo =
544             new AuthenticationInfo(userEntry, authZEntry, SASL_MECHANISM_PLAIN,
545                                    DirectoryServer.isRootDN(userEntry.getDN()));
546        bindOperation.setAuthenticationInfo(authInfo);
547        return;
548      }
549    
550    
551    
552      /**
553       * {@inheritDoc}
554       */
555      @Override()
556      public boolean isPasswordBased(String mechanism)
557      {
558        // This is a password-based mechanism.
559        return true;
560      }
561    
562    
563    
564      /**
565       * {@inheritDoc}
566       */
567      @Override()
568      public boolean isSecure(String mechanism)
569      {
570        // This is not a secure mechanism.
571        return false;
572      }
573    
574    
575    
576      /**
577       * {@inheritDoc}
578       */
579      @Override()
580      public boolean isConfigurationAcceptable(
581                          SASLMechanismHandlerCfg configuration,
582                          List<Message> unacceptableReasons)
583      {
584        PlainSASLMechanismHandlerCfg config =
585             (PlainSASLMechanismHandlerCfg) configuration;
586        return isConfigurationChangeAcceptable(config, unacceptableReasons);
587      }
588    
589    
590    
591      /**
592       * {@inheritDoc}
593       */
594      public boolean isConfigurationChangeAcceptable(
595                          PlainSASLMechanismHandlerCfg configuration,
596                          List<Message> unacceptableReasons)
597      {
598        return true;
599      }
600    
601    
602    
603      /**
604       * {@inheritDoc}
605       */
606      public ConfigChangeResult applyConfigurationChange(
607                  PlainSASLMechanismHandlerCfg configuration)
608      {
609        ResultCode        resultCode          = ResultCode.SUCCESS;
610        boolean           adminActionRequired = false;
611        ArrayList<Message> messages            = new ArrayList<Message>();
612    
613    
614        // Get the identity mapper that should be used to find users.
615        DN identityMapperDN = configuration.getIdentityMapperDN();
616        identityMapper = DirectoryServer.getIdentityMapper(identityMapperDN);
617        currentConfig  = configuration;
618    
619    
620        return new ConfigChangeResult(resultCode, adminActionRequired, messages);
621      }
622    }
623