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 2013-2015 ForgeRock AS
026 */
027package org.opends.server.extensions;
028
029import java.security.cert.Certificate;
030import java.util.List;
031
032import org.forgerock.i18n.LocalizableMessage;
033import org.opends.server.admin.server.ConfigurationChangeListener;
034import org.opends.server.admin.std.server.ExternalSASLMechanismHandlerCfg;
035import org.opends.server.admin.std.server.SASLMechanismHandlerCfg;
036import org.opends.server.api.CertificateMapper;
037import org.opends.server.api.ClientConnection;
038import org.opends.server.api.SASLMechanismHandler;
039import org.forgerock.opendj.config.server.ConfigChangeResult;
040import org.forgerock.opendj.config.server.ConfigException;
041import org.opends.server.core.BindOperation;
042import org.opends.server.core.DirectoryServer;
043import org.forgerock.i18n.slf4j.LocalizedLogger;
044import org.opends.server.protocols.ldap.LDAPClientConnection;
045import org.opends.server.types.*;
046import org.forgerock.opendj.ldap.ResultCode;
047import org.forgerock.opendj.ldap.ByteString;
048import static org.opends.messages.ExtensionMessages.*;
049import static org.opends.server.config.ConfigConstants.*;
050import static org.opends.server.util.ServerConstants.*;
051import static org.opends.server.util.StaticUtils.*;
052
053/**
054 * This class provides an implementation of a SASL mechanism that relies on some
055 * form of authentication that has already been done outside the LDAP layer.  At
056 * the present time, this implementation only provides support for SSL-based
057 * clients that presented their own certificate to the Directory Server during
058 * the negotiation process.  Future implementations may be updated to look in
059 * other places to find and evaluate this external authentication information.
060 */
061public class ExternalSASLMechanismHandler
062       extends SASLMechanismHandler<ExternalSASLMechanismHandlerCfg>
063       implements ConfigurationChangeListener<
064                       ExternalSASLMechanismHandlerCfg>
065{
066  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
067
068  /**
069   * The attribute type that should hold the certificates to use for the
070   * validation.
071   */
072  private AttributeType certificateAttributeType;
073
074  /**
075   * Indicates whether to attempt to validate the certificate presented by the
076   * client with a certificate in the user's entry.
077   */
078  private CertificateValidationPolicy validationPolicy;
079
080  /** The current configuration for this SASL mechanism handler. */
081  private ExternalSASLMechanismHandlerCfg currentConfig;
082
083
084
085  /**
086   * Creates a new instance of this SASL mechanism handler.  No initialization
087   * should be done in this method, as it should all be performed in the
088   * <CODE>initializeSASLMechanismHandler</CODE> method.
089   */
090  public ExternalSASLMechanismHandler()
091  {
092    super();
093  }
094
095
096
097  /** {@inheritDoc} */
098  @Override
099  public void initializeSASLMechanismHandler(
100                   ExternalSASLMechanismHandlerCfg configuration)
101         throws ConfigException, InitializationException
102  {
103    configuration.addExternalChangeListener(this);
104    currentConfig = configuration;
105
106    // See if we should attempt to validate client certificates against those in
107    // the corresponding user's entry.
108    switch (configuration.getCertificateValidationPolicy())
109    {
110      case NEVER:
111        validationPolicy = CertificateValidationPolicy.NEVER;
112        break;
113      case IFPRESENT:
114        validationPolicy = CertificateValidationPolicy.IFPRESENT;
115        break;
116      case ALWAYS:
117        validationPolicy = CertificateValidationPolicy.ALWAYS;
118        break;
119    }
120
121
122    // Get the attribute type to use for validating the certificates.  If none
123    // is provided, then default to the userCertificate type.
124    certificateAttributeType = configuration.getCertificateAttribute();
125    if (certificateAttributeType == null)
126    {
127      certificateAttributeType =
128           DirectoryServer.getAttributeTypeOrDefault(DEFAULT_VALIDATION_CERT_ATTRIBUTE);
129    }
130
131
132    DirectoryServer.registerSASLMechanismHandler(SASL_MECHANISM_EXTERNAL, this);
133  }
134
135
136
137  /** {@inheritDoc} */
138  @Override
139  public void finalizeSASLMechanismHandler()
140  {
141    currentConfig.removeExternalChangeListener(this);
142    DirectoryServer.deregisterSASLMechanismHandler(SASL_MECHANISM_EXTERNAL);
143  }
144
145
146
147
148  /** {@inheritDoc} */
149  @Override
150  public void processSASLBind(BindOperation bindOperation)
151  {
152    ExternalSASLMechanismHandlerCfg config = currentConfig;
153    AttributeType certificateAttributeType = this.certificateAttributeType;
154    CertificateValidationPolicy validationPolicy = this.validationPolicy;
155
156
157    // Get the client connection used for the bind request, and get the
158    // security manager for that connection.  If either are null, then fail.
159    ClientConnection clientConnection = bindOperation.getClientConnection();
160    if (clientConnection == null) {
161      bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
162      LocalizableMessage message = ERR_SASLEXTERNAL_NO_CLIENT_CONNECTION.get();
163      bindOperation.setAuthFailureReason(message);
164      return;
165    }
166
167    if(!(clientConnection instanceof LDAPClientConnection)) {
168        bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
169        LocalizableMessage message = ERR_SASLEXTERNAL_NOT_LDAP_CLIENT_INSTANCE.get();
170        bindOperation.setAuthFailureReason(message);
171        return;
172    }
173    LDAPClientConnection lc = (LDAPClientConnection) clientConnection;
174    Certificate[] clientCertChain = lc.getClientCertificateChain();
175    if (clientCertChain == null || clientCertChain.length == 0) {
176      bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
177      LocalizableMessage message = ERR_SASLEXTERNAL_NO_CLIENT_CERT.get();
178      bindOperation.setAuthFailureReason(message);
179      return;
180    }
181
182
183    // Get the certificate mapper to use to map the certificate to a user entry.
184    DN certificateMapperDN = config.getCertificateMapperDN();
185    CertificateMapper<?> certificateMapper =
186         DirectoryServer.getCertificateMapper(certificateMapperDN);
187
188
189    // Use the Directory Server certificate mapper to map the client certificate
190    // chain to a single user DN.
191    Entry userEntry;
192    try
193    {
194      userEntry = certificateMapper.mapCertificateToUser(clientCertChain);
195    }
196    catch (DirectoryException de)
197    {
198      logger.traceException(de);
199
200      bindOperation.setResponseData(de);
201      return;
202    }
203
204
205    // If the user DN is null, then we couldn't establish a mapping and
206    // therefore the authentication failed.
207    if (userEntry == null)
208    {
209      bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
210
211      LocalizableMessage message = ERR_SASLEXTERNAL_NO_MAPPING.get();
212      bindOperation.setAuthFailureReason(message);
213      return;
214    }
215    else
216    {
217      bindOperation.setSASLAuthUserEntry(userEntry);
218    }
219
220
221    // Get the userCertificate attribute from the user's entry for use in the
222    // validation process.
223    List<Attribute> certAttrList =
224         userEntry.getAttribute(certificateAttributeType);
225    switch (validationPolicy)
226    {
227      case ALWAYS:
228        if (certAttrList == null)
229        {
230          if (validationPolicy == CertificateValidationPolicy.ALWAYS)
231          {
232            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
233
234            LocalizableMessage message = ERR_SASLEXTERNAL_NO_CERT_IN_ENTRY.get(userEntry.getName());
235            bindOperation.setAuthFailureReason(message);
236            return;
237          }
238        }
239        else
240        {
241          try
242          {
243            ByteString certBytes = ByteString.wrap(clientCertChain[0].getEncoded());
244            if (!find(certAttrList, certBytes))
245            {
246              bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
247
248              LocalizableMessage message = ERR_SASLEXTERNAL_PEER_CERT_NOT_FOUND.get(userEntry.getName());
249              bindOperation.setAuthFailureReason(message);
250              return;
251            }
252          }
253          catch (Exception e)
254          {
255            logger.traceException(e);
256
257            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
258
259            LocalizableMessage message = ERR_SASLEXTERNAL_CANNOT_VALIDATE_CERT.get(
260                userEntry.getName(), getExceptionMessage(e));
261            bindOperation.setAuthFailureReason(message);
262            return;
263          }
264        }
265        break;
266
267      case IFPRESENT:
268        if (certAttrList != null)
269        {
270          try
271          {
272            ByteString certBytes = ByteString.wrap(clientCertChain[0].getEncoded());
273            if (!find(certAttrList, certBytes))
274            {
275              bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
276
277              LocalizableMessage message = ERR_SASLEXTERNAL_PEER_CERT_NOT_FOUND.get(userEntry.getName());
278              bindOperation.setAuthFailureReason(message);
279              return;
280            }
281          }
282          catch (Exception e)
283          {
284            logger.traceException(e);
285
286            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
287
288            LocalizableMessage message = ERR_SASLEXTERNAL_CANNOT_VALIDATE_CERT.get(
289                    userEntry.getName(), getExceptionMessage(e));
290            bindOperation.setAuthFailureReason(message);
291            return;
292          }
293        }
294    }
295
296
297    AuthenticationInfo authInfo = new AuthenticationInfo(userEntry,
298        SASL_MECHANISM_EXTERNAL, DirectoryServer.isRootDN(userEntry.getName()));
299    bindOperation.setAuthenticationInfo(authInfo);
300    bindOperation.setResultCode(ResultCode.SUCCESS);
301  }
302
303
304
305  private boolean find(List<Attribute> certAttrList, ByteString certBytes)
306  {
307    for (Attribute a : certAttrList)
308    {
309      if (a.contains(certBytes))
310      {
311        return true;
312      }
313    }
314    return false;
315  }
316
317
318
319  /** {@inheritDoc} */
320  @Override
321  public boolean isPasswordBased(String mechanism)
322  {
323    // This is not a password-based mechanism.
324    return false;
325  }
326
327
328
329  /** {@inheritDoc} */
330  @Override
331  public boolean isSecure(String mechanism)
332  {
333    // This may be considered a secure mechanism.
334    return true;
335  }
336
337
338
339  /** {@inheritDoc} */
340  @Override
341  public boolean isConfigurationAcceptable(
342                      SASLMechanismHandlerCfg configuration,
343                      List<LocalizableMessage> unacceptableReasons)
344  {
345    ExternalSASLMechanismHandlerCfg config =
346         (ExternalSASLMechanismHandlerCfg) configuration;
347    return isConfigurationChangeAcceptable(config, unacceptableReasons);
348  }
349
350
351
352  /** {@inheritDoc} */
353  public boolean isConfigurationChangeAcceptable(
354                      ExternalSASLMechanismHandlerCfg configuration,
355                      List<LocalizableMessage> unacceptableReasons)
356  {
357    return true;
358  }
359
360
361
362  /** {@inheritDoc} */
363  public ConfigChangeResult applyConfigurationChange(
364              ExternalSASLMechanismHandlerCfg configuration)
365  {
366    final ConfigChangeResult ccr = new ConfigChangeResult();
367
368
369    // See if we should attempt to validate client certificates against those in
370    // the corresponding user's entry.
371    CertificateValidationPolicy newValidationPolicy =
372         CertificateValidationPolicy.ALWAYS;
373    switch (configuration.getCertificateValidationPolicy())
374    {
375      case NEVER:
376        newValidationPolicy = CertificateValidationPolicy.NEVER;
377        break;
378      case IFPRESENT:
379        newValidationPolicy = CertificateValidationPolicy.IFPRESENT;
380        break;
381      case ALWAYS:
382        newValidationPolicy = CertificateValidationPolicy.ALWAYS;
383        break;
384    }
385
386
387    // Get the attribute type to use for validating the certificates.  If none
388    // is provided, then default to the userCertificate type.
389    AttributeType newCertificateType = configuration.getCertificateAttribute();
390    if (newCertificateType == null)
391    {
392      newCertificateType =
393           DirectoryServer.getAttributeTypeOrDefault(DEFAULT_VALIDATION_CERT_ATTRIBUTE);
394    }
395
396
397    if (ccr.getResultCode() == ResultCode.SUCCESS)
398    {
399      validationPolicy         = newValidationPolicy;
400      certificateAttributeType = newCertificateType;
401      currentConfig            = configuration;
402    }
403
404    return ccr;
405  }
406}
407