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