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