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 legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2006-2009 Sun Microsystems, Inc.
025 *      Portions Copyright 2011-2015 ForgeRock AS.
026 */
027package org.opends.server.extensions;
028
029import static org.opends.messages.CoreMessages.*;
030import static org.opends.messages.ExtensionMessages.*;
031import static org.opends.server.util.ServerConstants.*;
032import static org.opends.server.util.StaticUtils.*;
033
034import java.util.List;
035import org.forgerock.i18n.LocalizableMessage;
036import org.opends.server.admin.server.ConfigurationChangeListener;
037import org.opends.server.admin.std.server.PlainSASLMechanismHandlerCfg;
038import org.opends.server.admin.std.server.SASLMechanismHandlerCfg;
039import org.opends.server.api.AuthenticationPolicyState;
040import org.opends.server.api.IdentityMapper;
041import org.opends.server.api.SASLMechanismHandler;
042import org.forgerock.opendj.config.server.ConfigChangeResult;
043import org.forgerock.opendj.config.server.ConfigException;
044import org.opends.server.core.BindOperation;
045import org.opends.server.core.DirectoryServer;
046import org.forgerock.i18n.slf4j.LocalizedLogger;
047import org.opends.server.protocols.internal.InternalClientConnection;
048import org.opends.server.types.*;
049import org.forgerock.opendj.ldap.ResultCode;
050import org.forgerock.opendj.ldap.ByteString;
051
052/**
053 * This class provides an implementation of a SASL mechanism that uses
054 * plain-text authentication.  It is based on the proposal defined in
055 * draft-ietf-sasl-plain-08 in which the SASL credentials are in the form:
056 * <BR>
057 * <BLOCKQUOTE>[authzid] UTF8NULL authcid UTF8NULL passwd</BLOCKQUOTE>
058 * <BR>
059 * Note that this is a weak mechanism by itself and does not offer any
060 * protection for the password, so it may need to be used in conjunction with a
061 * connection security provider to prevent exposing the password.
062 */
063public class PlainSASLMechanismHandler
064       extends SASLMechanismHandler<PlainSASLMechanismHandlerCfg>
065       implements ConfigurationChangeListener<
066                       PlainSASLMechanismHandlerCfg>
067{
068  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
069
070  /** The identity mapper that will be used to map ID strings to user entries.*/
071  private IdentityMapper<?> identityMapper;
072
073  /** The current configuration for this SASL mechanism handler. */
074  private PlainSASLMechanismHandlerCfg currentConfig;
075
076
077
078  /**
079   * Creates a new instance of this SASL mechanism handler.  No initialization
080   * should be done in this method, as it should all be performed in the
081   * <CODE>initializeSASLMechanismHandler</CODE> method.
082   */
083  public PlainSASLMechanismHandler()
084  {
085    super();
086  }
087
088
089
090  /** {@inheritDoc} */
091  @Override
092  public void initializeSASLMechanismHandler(
093                   PlainSASLMechanismHandlerCfg configuration)
094         throws ConfigException, InitializationException
095  {
096    configuration.addPlainChangeListener(this);
097    currentConfig = configuration;
098
099
100    // Get the identity mapper that should be used to find users.
101    DN identityMapperDN = configuration.getIdentityMapperDN();
102    identityMapper = DirectoryServer.getIdentityMapper(identityMapperDN);
103
104
105    DirectoryServer.registerSASLMechanismHandler(SASL_MECHANISM_PLAIN, this);
106  }
107
108
109
110  /** {@inheritDoc} */
111  @Override
112  public void finalizeSASLMechanismHandler()
113  {
114    currentConfig.removePlainChangeListener(this);
115    DirectoryServer.deregisterSASLMechanismHandler(SASL_MECHANISM_PLAIN);
116  }
117
118
119
120
121  /** {@inheritDoc} */
122  @Override
123  public void processSASLBind(BindOperation bindOperation)
124  {
125    // Get the SASL credentials provided by the user and decode them.
126    String authzID  = null;
127    String authcID  = null;
128    String password = null;
129
130    ByteString saslCredentials = bindOperation.getSASLCredentials();
131    if (saslCredentials == null)
132    {
133      bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
134
135      LocalizableMessage message = ERR_SASLPLAIN_NO_SASL_CREDENTIALS.get();
136      bindOperation.setAuthFailureReason(message);
137      return;
138    }
139
140    String credString = saslCredentials.toString();
141    int    length     = credString.length();
142    int    nullPos1   = credString.indexOf('\u0000');
143    if (nullPos1 < 0)
144    {
145      bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
146
147      LocalizableMessage message = ERR_SASLPLAIN_NO_NULLS_IN_CREDENTIALS.get();
148      bindOperation.setAuthFailureReason(message);
149      return;
150    }
151
152    if (nullPos1 > 0)
153    {
154      authzID = credString.substring(0, nullPos1);
155    }
156
157
158    int nullPos2 = credString.indexOf('\u0000', nullPos1+1);
159    if (nullPos2 < 0)
160    {
161      bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
162
163      LocalizableMessage message = ERR_SASLPLAIN_NO_SECOND_NULL.get();
164      bindOperation.setAuthFailureReason(message);
165      return;
166    }
167
168    if (nullPos2 == (nullPos1+1))
169    {
170      bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
171
172      LocalizableMessage message = ERR_SASLPLAIN_ZERO_LENGTH_AUTHCID.get();
173      bindOperation.setAuthFailureReason(message);
174      return;
175    }
176
177    if (nullPos2 == (length-1))
178    {
179      bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
180
181      LocalizableMessage message = ERR_SASLPLAIN_ZERO_LENGTH_PASSWORD.get();
182      bindOperation.setAuthFailureReason(message);
183      return;
184    }
185
186    authcID  = credString.substring(nullPos1+1, nullPos2);
187    password = credString.substring(nullPos2+1);
188
189
190    // Get the user entry for the authentication ID.  Allow for an
191    // authentication ID that is just a username (as per the SASL PLAIN spec),
192    // but also allow a value in the authzid form specified in RFC 2829.
193    Entry  userEntry    = null;
194    String lowerAuthcID = toLowerCase(authcID);
195    if (lowerAuthcID.startsWith("dn:"))
196    {
197      // Try to decode the user DN and retrieve the corresponding entry.
198      DN userDN;
199      try
200      {
201        userDN = DN.valueOf(authcID.substring(3));
202      }
203      catch (DirectoryException de)
204      {
205        logger.traceException(de);
206
207        bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
208
209        LocalizableMessage message = ERR_SASLPLAIN_CANNOT_DECODE_AUTHCID_AS_DN.get(
210                authcID, de.getMessageObject());
211        bindOperation.setAuthFailureReason(message);
212        return;
213      }
214
215      if (userDN.isRootDN())
216      {
217        bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
218
219        LocalizableMessage message = ERR_SASLPLAIN_AUTHCID_IS_NULL_DN.get();
220        bindOperation.setAuthFailureReason(message);
221        return;
222      }
223
224      DN rootDN = DirectoryServer.getActualRootBindDN(userDN);
225      if (rootDN != null)
226      {
227        userDN = rootDN;
228      }
229
230      try
231      {
232        userEntry = DirectoryServer.getEntry(userDN);
233      }
234      catch (DirectoryException de)
235      {
236        logger.traceException(de);
237
238        bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
239
240        LocalizableMessage message = ERR_SASLPLAIN_CANNOT_GET_ENTRY_BY_DN.get(userDN, de.getMessageObject());
241        bindOperation.setAuthFailureReason(message);
242        return;
243      }
244    }
245    else
246    {
247      // Use the identity mapper to resolve the username to an entry.
248      if (lowerAuthcID.startsWith("u:"))
249      {
250        authcID = authcID.substring(2);
251      }
252
253      try
254      {
255        userEntry = identityMapper.getEntryForID(authcID);
256      }
257      catch (DirectoryException de)
258      {
259        logger.traceException(de);
260
261        bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
262
263        LocalizableMessage message = ERR_SASLPLAIN_CANNOT_MAP_USERNAME.get(authcID, de.getMessageObject());
264        bindOperation.setAuthFailureReason(message);
265        return;
266      }
267    }
268
269
270    // At this point, we should have a user entry.  If we don't then fail.
271    if (userEntry == null)
272    {
273      bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
274
275      LocalizableMessage message = ERR_SASLPLAIN_NO_MATCHING_ENTRIES.get(authcID);
276      bindOperation.setAuthFailureReason(message);
277      return;
278    }
279    else
280    {
281      bindOperation.setSASLAuthUserEntry(userEntry);
282    }
283
284
285    // If an authorization ID was provided, then make sure that it is
286    // acceptable.
287    Entry authZEntry = userEntry;
288    if (authzID != null)
289    {
290      String lowerAuthzID = toLowerCase(authzID);
291      if (lowerAuthzID.startsWith("dn:"))
292      {
293        DN authzDN;
294        try
295        {
296          authzDN = DN.valueOf(authzID.substring(3));
297        }
298        catch (DirectoryException de)
299        {
300          logger.traceException(de);
301
302          bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
303
304          LocalizableMessage message = ERR_SASLPLAIN_AUTHZID_INVALID_DN.get(
305                  authzID, de.getMessageObject());
306          bindOperation.setAuthFailureReason(message);
307          return;
308        }
309
310        DN actualAuthzDN = DirectoryServer.getActualRootBindDN(authzDN);
311        if (actualAuthzDN != null)
312        {
313          authzDN = actualAuthzDN;
314        }
315
316        if (! authzDN.equals(userEntry.getName()))
317        {
318          AuthenticationInfo tempAuthInfo =
319            new AuthenticationInfo(userEntry,
320                     DirectoryServer.isRootDN(userEntry.getName()));
321          InternalClientConnection tempConn =
322               new InternalClientConnection(tempAuthInfo);
323          if (! tempConn.hasPrivilege(Privilege.PROXIED_AUTH, bindOperation))
324          {
325            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
326
327            LocalizableMessage message = ERR_SASLPLAIN_AUTHZID_INSUFFICIENT_PRIVILEGES.get(userEntry.getName());
328            bindOperation.setAuthFailureReason(message);
329            return;
330          }
331
332          if (authzDN.isRootDN())
333          {
334            authZEntry = null;
335          }
336          else
337          {
338            try
339            {
340              authZEntry = DirectoryServer.getEntry(authzDN);
341              if (authZEntry == null)
342              {
343                bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
344
345                LocalizableMessage message = ERR_SASLPLAIN_AUTHZID_NO_SUCH_ENTRY.get(authzDN);
346                bindOperation.setAuthFailureReason(message);
347                return;
348              }
349            }
350            catch (DirectoryException de)
351            {
352              logger.traceException(de);
353
354              bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
355
356              LocalizableMessage message = ERR_SASLPLAIN_AUTHZID_CANNOT_GET_ENTRY.get(authzDN, de.getMessageObject());
357              bindOperation.setAuthFailureReason(message);
358              return;
359            }
360          }
361        }
362      }
363      else
364      {
365        String idStr;
366        if (lowerAuthzID.startsWith("u:"))
367        {
368          idStr = authzID.substring(2);
369        }
370        else
371        {
372          idStr = authzID;
373        }
374
375        if (idStr.length() == 0)
376        {
377          authZEntry = null;
378        }
379        else
380        {
381          try
382          {
383            authZEntry = identityMapper.getEntryForID(idStr);
384            if (authZEntry == null)
385            {
386              bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
387
388              LocalizableMessage message = ERR_SASLPLAIN_AUTHZID_NO_MAPPED_ENTRY.get(
389                      authzID);
390              bindOperation.setAuthFailureReason(message);
391              return;
392            }
393          }
394          catch (DirectoryException de)
395          {
396            logger.traceException(de);
397
398            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
399
400            LocalizableMessage message = ERR_SASLPLAIN_AUTHZID_CANNOT_MAP_AUTHZID.get(
401                    authzID, de.getMessageObject());
402            bindOperation.setAuthFailureReason(message);
403            return;
404          }
405        }
406
407        if (authZEntry == null || !authZEntry.getName().equals(userEntry.getName()))
408        {
409          AuthenticationInfo tempAuthInfo =
410            new AuthenticationInfo(userEntry,
411                     DirectoryServer.isRootDN(userEntry.getName()));
412          InternalClientConnection tempConn =
413               new InternalClientConnection(tempAuthInfo);
414          if (! tempConn.hasPrivilege(Privilege.PROXIED_AUTH, bindOperation))
415          {
416            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
417
418            LocalizableMessage message = ERR_SASLPLAIN_AUTHZID_INSUFFICIENT_PRIVILEGES.get(userEntry.getName());
419            bindOperation.setAuthFailureReason(message);
420            return;
421          }
422        }
423      }
424    }
425
426
427    // Get the password policy for the user and use it to determine if the
428    // provided password was correct.
429    try
430    {
431      // FIXME: we should store store the auth state in with the bind operation
432      // so that any state updates, such as cached passwords, are persisted to
433      // the user's entry when the bind completes.
434      AuthenticationPolicyState authState = AuthenticationPolicyState.forUser(
435          userEntry, false);
436
437      if (authState.isDisabled())
438      {
439        // Check to see if the user is administratively disabled or locked.
440        bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
441        LocalizableMessage message = ERR_BIND_OPERATION_ACCOUNT_DISABLED.get();
442        bindOperation.setAuthFailureReason(message);
443        return;
444      }
445
446      if (!authState.passwordMatches(ByteString.valueOfUtf8(password)))
447      {
448        bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
449        LocalizableMessage message = ERR_SASLPLAIN_INVALID_PASSWORD.get();
450        bindOperation.setAuthFailureReason(message);
451        return;
452      }
453    }
454    catch (Exception e)
455    {
456      logger.traceException(e);
457
458      bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
459
460      LocalizableMessage message = ERR_SASLPLAIN_CANNOT_CHECK_PASSWORD_VALIDITY.get(userEntry.getName(), e);
461      bindOperation.setAuthFailureReason(message);
462      return;
463    }
464
465
466    // If we've gotten here, then the authentication was successful.
467    bindOperation.setResultCode(ResultCode.SUCCESS);
468
469    AuthenticationInfo authInfo =
470         new AuthenticationInfo(userEntry, authZEntry, SASL_MECHANISM_PLAIN,
471                                bindOperation.getSASLCredentials(),
472                                DirectoryServer.isRootDN(userEntry.getName()));
473    bindOperation.setAuthenticationInfo(authInfo);
474    return;
475  }
476
477
478
479  /** {@inheritDoc} */
480  @Override
481  public boolean isPasswordBased(String mechanism)
482  {
483    // This is a password-based mechanism.
484    return true;
485  }
486
487
488
489  /** {@inheritDoc} */
490  @Override
491  public boolean isSecure(String mechanism)
492  {
493    // This is not a secure mechanism.
494    return false;
495  }
496
497
498
499  /** {@inheritDoc} */
500  @Override
501  public boolean isConfigurationAcceptable(
502                      SASLMechanismHandlerCfg configuration,
503                      List<LocalizableMessage> unacceptableReasons)
504  {
505    PlainSASLMechanismHandlerCfg config =
506         (PlainSASLMechanismHandlerCfg) configuration;
507    return isConfigurationChangeAcceptable(config, unacceptableReasons);
508  }
509
510
511
512  /** {@inheritDoc} */
513  @Override
514  public boolean isConfigurationChangeAcceptable(
515                      PlainSASLMechanismHandlerCfg configuration,
516                      List<LocalizableMessage> unacceptableReasons)
517  {
518    return true;
519  }
520
521
522
523  /** {@inheritDoc} */
524  @Override
525  public ConfigChangeResult applyConfigurationChange(
526              PlainSASLMechanismHandlerCfg configuration)
527  {
528    final ConfigChangeResult ccr = new ConfigChangeResult();
529
530    // Get the identity mapper that should be used to find users.
531    DN identityMapperDN = configuration.getIdentityMapperDN();
532    identityMapper = DirectoryServer.getIdentityMapper(identityMapperDN);
533    currentConfig  = configuration;
534
535    return ccr;
536  }
537}