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.config.server.ConfigurationChangeListener;
027import org.forgerock.opendj.ldap.ByteString;
028import org.forgerock.opendj.ldap.ResultCode;
029import org.forgerock.opendj.ldap.schema.AttributeType;
030import org.forgerock.opendj.server.config.server.ExternalSASLMechanismHandlerCfg;
031import org.forgerock.opendj.server.config.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 = DirectoryServer.getSchema().getAttributeType(DEFAULT_VALIDATION_CERT_ATTRIBUTE);
111    }
112
113
114    DirectoryServer.registerSASLMechanismHandler(SASL_MECHANISM_EXTERNAL, this);
115  }
116
117  private CertificateValidationPolicy toCertificateValidationPolicy(ExternalSASLMechanismHandlerCfg cfg)
118  {
119    switch (cfg.getCertificateValidationPolicy())
120    {
121    case NEVER:
122      return CertificateValidationPolicy.NEVER;
123    case IFPRESENT:
124      return CertificateValidationPolicy.IFPRESENT;
125    default:
126      return CertificateValidationPolicy.ALWAYS;
127    }
128  }
129
130  @Override
131  public void finalizeSASLMechanismHandler()
132  {
133    currentConfig.removeExternalChangeListener(this);
134    DirectoryServer.deregisterSASLMechanismHandler(SASL_MECHANISM_EXTERNAL);
135  }
136
137  @Override
138  public void processSASLBind(BindOperation bindOperation)
139  {
140    ExternalSASLMechanismHandlerCfg config = currentConfig;
141    AttributeType certificateAttributeType = this.certificateAttributeType;
142    CertificateValidationPolicy validationPolicy = this.validationPolicy;
143
144
145    // Get the client connection used for the bind request, and get the
146    // security manager for that connection.  If either are null, then fail.
147    ClientConnection clientConnection = bindOperation.getClientConnection();
148    if (clientConnection == null) {
149      bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
150      LocalizableMessage message = ERR_SASLEXTERNAL_NO_CLIENT_CONNECTION.get();
151      bindOperation.setAuthFailureReason(message);
152      return;
153    }
154
155    if(!(clientConnection instanceof LDAPClientConnection)) {
156        bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
157        LocalizableMessage message = ERR_SASLEXTERNAL_NOT_LDAP_CLIENT_INSTANCE.get();
158        bindOperation.setAuthFailureReason(message);
159        return;
160    }
161    LDAPClientConnection lc = (LDAPClientConnection) clientConnection;
162    Certificate[] clientCertChain = lc.getClientCertificateChain();
163    if (clientCertChain == null || clientCertChain.length == 0) {
164      bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
165      LocalizableMessage message = ERR_SASLEXTERNAL_NO_CLIENT_CERT.get();
166      bindOperation.setAuthFailureReason(message);
167      return;
168    }
169
170
171    // Get the certificate mapper to use to map the certificate to a user entry.
172    DN certificateMapperDN = config.getCertificateMapperDN();
173    CertificateMapper<?> certificateMapper =
174         DirectoryServer.getCertificateMapper(certificateMapperDN);
175
176
177    // Use the Directory Server certificate mapper to map the client certificate
178    // chain to a single user DN.
179    Entry userEntry;
180    try
181    {
182      userEntry = certificateMapper.mapCertificateToUser(clientCertChain);
183    }
184    catch (DirectoryException de)
185    {
186      logger.traceException(de);
187
188      bindOperation.setResponseData(de);
189      return;
190    }
191
192
193    // If the user DN is null, then we couldn't establish a mapping and
194    // therefore the authentication failed.
195    if (userEntry == null)
196    {
197      bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
198
199      LocalizableMessage message = ERR_SASLEXTERNAL_NO_MAPPING.get();
200      bindOperation.setAuthFailureReason(message);
201      return;
202    }
203
204    bindOperation.setSASLAuthUserEntry(userEntry);
205
206
207    // Get the userCertificate attribute from the user's entry for use in the
208    // validation process.
209    List<Attribute> certAttrList = userEntry.getAttribute(certificateAttributeType);
210    switch (validationPolicy)
211    {
212      case ALWAYS:
213        if (certAttrList.isEmpty())
214        {
215          if (validationPolicy == CertificateValidationPolicy.ALWAYS)
216          {
217            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
218
219            LocalizableMessage message = ERR_SASLEXTERNAL_NO_CERT_IN_ENTRY.get(userEntry.getName());
220            bindOperation.setAuthFailureReason(message);
221            return;
222          }
223        }
224        else
225        {
226          try
227          {
228            ByteString certBytes = ByteString.wrap(clientCertChain[0].getEncoded());
229            if (!findAttributeValue(certAttrList, certBytes))
230            {
231              bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
232
233              LocalizableMessage message = ERR_SASLEXTERNAL_PEER_CERT_NOT_FOUND.get(userEntry.getName());
234              bindOperation.setAuthFailureReason(message);
235              return;
236            }
237          }
238          catch (Exception e)
239          {
240            logger.traceException(e);
241
242            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
243
244            LocalizableMessage message = ERR_SASLEXTERNAL_CANNOT_VALIDATE_CERT.get(
245                userEntry.getName(), getExceptionMessage(e));
246            bindOperation.setAuthFailureReason(message);
247            return;
248          }
249        }
250        break;
251
252      case IFPRESENT:
253        if (!certAttrList.isEmpty())
254        {
255          try
256          {
257            ByteString certBytes = ByteString.wrap(clientCertChain[0].getEncoded());
258            if (!findAttributeValue(certAttrList, certBytes))
259            {
260              bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
261
262              LocalizableMessage message = ERR_SASLEXTERNAL_PEER_CERT_NOT_FOUND.get(userEntry.getName());
263              bindOperation.setAuthFailureReason(message);
264              return;
265            }
266          }
267          catch (Exception e)
268          {
269            logger.traceException(e);
270
271            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
272
273            LocalizableMessage message = ERR_SASLEXTERNAL_CANNOT_VALIDATE_CERT.get(
274                    userEntry.getName(), getExceptionMessage(e));
275            bindOperation.setAuthFailureReason(message);
276            return;
277          }
278        }
279    }
280
281
282    AuthenticationInfo authInfo = new AuthenticationInfo(userEntry,
283        SASL_MECHANISM_EXTERNAL, DirectoryServer.isRootDN(userEntry.getName()));
284    bindOperation.setAuthenticationInfo(authInfo);
285    bindOperation.setResultCode(ResultCode.SUCCESS);
286  }
287
288  private boolean findAttributeValue(List<Attribute> certAttrList, ByteString certBytes)
289  {
290    for (Attribute a : certAttrList)
291    {
292      if (a.contains(certBytes))
293      {
294        return true;
295      }
296    }
297    return false;
298  }
299
300  @Override
301  public boolean isPasswordBased(String mechanism)
302  {
303    // This is not a password-based mechanism.
304    return false;
305  }
306
307  @Override
308  public boolean isSecure(String mechanism)
309  {
310    // This may be considered a secure mechanism.
311    return true;
312  }
313
314  @Override
315  public boolean isConfigurationAcceptable(
316                      SASLMechanismHandlerCfg configuration,
317                      List<LocalizableMessage> unacceptableReasons)
318  {
319    ExternalSASLMechanismHandlerCfg config =
320         (ExternalSASLMechanismHandlerCfg) configuration;
321    return isConfigurationChangeAcceptable(config, unacceptableReasons);
322  }
323
324  @Override
325  public boolean isConfigurationChangeAcceptable(
326                      ExternalSASLMechanismHandlerCfg configuration,
327                      List<LocalizableMessage> unacceptableReasons)
328  {
329    return true;
330  }
331
332  @Override
333  public ConfigChangeResult applyConfigurationChange(
334              ExternalSASLMechanismHandlerCfg configuration)
335  {
336    final ConfigChangeResult ccr = new ConfigChangeResult();
337
338
339    // See if we should attempt to validate client certificates against those in
340    // the corresponding user's entry.
341    CertificateValidationPolicy newValidationPolicy = toCertificateValidationPolicy(configuration);
342
343
344    // Get the attribute type to use for validating the certificates.  If none
345    // is provided, then default to the userCertificate type.
346    AttributeType newCertificateType = configuration.getCertificateAttribute();
347    if (newCertificateType == null)
348    {
349      newCertificateType = DirectoryServer.getSchema().getAttributeType(DEFAULT_VALIDATION_CERT_ATTRIBUTE);
350    }
351
352
353    if (ccr.getResultCode() == ResultCode.SUCCESS)
354    {
355      validationPolicy         = newValidationPolicy;
356      certificateAttributeType = newCertificateType;
357      currentConfig            = configuration;
358    }
359
360    return ccr;
361  }
362}