/*
|
* CDDL HEADER START
|
*
|
* The contents of this file are subject to the terms of the
|
* Common Development and Distribution License, Version 1.0 only
|
* (the "License"). You may not use this file except in compliance
|
* with the License.
|
*
|
* You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
|
* or http://forgerock.org/license/CDDLv1.0.html.
|
* See the License for the specific language governing permissions
|
* and limitations under the License.
|
*
|
* When distributing Covered Code, include this CDDL HEADER in each
|
* file and include the License file at legal-notices/CDDLv1_0.txt.
|
* If applicable, add the following below this CDDL HEADER, with the
|
* fields enclosed by brackets "[]" replaced with your own identifying
|
* information:
|
* Portions Copyright [yyyy] [name of copyright owner]
|
*
|
* CDDL HEADER END
|
*
|
*
|
* Copyright 2006-2009 Sun Microsystems, Inc.
|
* Portions Copyright 2013-2015 ForgeRock AS
|
*/
|
package org.opends.server.extensions;
|
|
import java.security.cert.Certificate;
|
import java.util.List;
|
|
import org.forgerock.i18n.LocalizableMessage;
|
import org.opends.server.admin.server.ConfigurationChangeListener;
|
import org.opends.server.admin.std.server.ExternalSASLMechanismHandlerCfg;
|
import org.opends.server.admin.std.server.SASLMechanismHandlerCfg;
|
import org.opends.server.api.CertificateMapper;
|
import org.opends.server.api.ClientConnection;
|
import org.opends.server.api.SASLMechanismHandler;
|
import org.forgerock.opendj.config.server.ConfigChangeResult;
|
import org.forgerock.opendj.config.server.ConfigException;
|
import org.opends.server.core.BindOperation;
|
import org.opends.server.core.DirectoryServer;
|
import org.forgerock.i18n.slf4j.LocalizedLogger;
|
import org.opends.server.protocols.ldap.LDAPClientConnection;
|
import org.opends.server.types.*;
|
import org.forgerock.opendj.ldap.ResultCode;
|
import org.forgerock.opendj.ldap.ByteString;
|
import static org.opends.messages.ExtensionMessages.*;
|
import static org.opends.server.config.ConfigConstants.*;
|
import static org.opends.server.util.ServerConstants.*;
|
import static org.opends.server.util.StaticUtils.*;
|
|
/**
|
* This class provides an implementation of a SASL mechanism that relies on some
|
* form of authentication that has already been done outside the LDAP layer. At
|
* the present time, this implementation only provides support for SSL-based
|
* clients that presented their own certificate to the Directory Server during
|
* the negotiation process. Future implementations may be updated to look in
|
* other places to find and evaluate this external authentication information.
|
*/
|
public class ExternalSASLMechanismHandler
|
extends SASLMechanismHandler<ExternalSASLMechanismHandlerCfg>
|
implements ConfigurationChangeListener<
|
ExternalSASLMechanismHandlerCfg>
|
{
|
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
|
|
/**
|
* The attribute type that should hold the certificates to use for the
|
* validation.
|
*/
|
private AttributeType certificateAttributeType;
|
|
/**
|
* Indicates whether to attempt to validate the certificate presented by the
|
* client with a certificate in the user's entry.
|
*/
|
private CertificateValidationPolicy validationPolicy;
|
|
/** The current configuration for this SASL mechanism handler. */
|
private ExternalSASLMechanismHandlerCfg currentConfig;
|
|
|
|
/**
|
* Creates a new instance of this SASL mechanism handler. No initialization
|
* should be done in this method, as it should all be performed in the
|
* <CODE>initializeSASLMechanismHandler</CODE> method.
|
*/
|
public ExternalSASLMechanismHandler()
|
{
|
super();
|
}
|
|
|
|
/** {@inheritDoc} */
|
@Override
|
public void initializeSASLMechanismHandler(
|
ExternalSASLMechanismHandlerCfg configuration)
|
throws ConfigException, InitializationException
|
{
|
configuration.addExternalChangeListener(this);
|
currentConfig = configuration;
|
|
// See if we should attempt to validate client certificates against those in
|
// the corresponding user's entry.
|
switch (configuration.getCertificateValidationPolicy())
|
{
|
case NEVER:
|
validationPolicy = CertificateValidationPolicy.NEVER;
|
break;
|
case IFPRESENT:
|
validationPolicy = CertificateValidationPolicy.IFPRESENT;
|
break;
|
case ALWAYS:
|
validationPolicy = CertificateValidationPolicy.ALWAYS;
|
break;
|
}
|
|
|
// Get the attribute type to use for validating the certificates. If none
|
// is provided, then default to the userCertificate type.
|
certificateAttributeType = configuration.getCertificateAttribute();
|
if (certificateAttributeType == null)
|
{
|
certificateAttributeType =
|
DirectoryServer.getAttributeTypeOrDefault(DEFAULT_VALIDATION_CERT_ATTRIBUTE);
|
}
|
|
|
DirectoryServer.registerSASLMechanismHandler(SASL_MECHANISM_EXTERNAL, this);
|
}
|
|
|
|
/** {@inheritDoc} */
|
@Override
|
public void finalizeSASLMechanismHandler()
|
{
|
currentConfig.removeExternalChangeListener(this);
|
DirectoryServer.deregisterSASLMechanismHandler(SASL_MECHANISM_EXTERNAL);
|
}
|
|
|
|
|
/** {@inheritDoc} */
|
@Override
|
public void processSASLBind(BindOperation bindOperation)
|
{
|
ExternalSASLMechanismHandlerCfg config = currentConfig;
|
AttributeType certificateAttributeType = this.certificateAttributeType;
|
CertificateValidationPolicy validationPolicy = this.validationPolicy;
|
|
|
// Get the client connection used for the bind request, and get the
|
// security manager for that connection. If either are null, then fail.
|
ClientConnection clientConnection = bindOperation.getClientConnection();
|
if (clientConnection == null) {
|
bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
|
LocalizableMessage message = ERR_SASLEXTERNAL_NO_CLIENT_CONNECTION.get();
|
bindOperation.setAuthFailureReason(message);
|
return;
|
}
|
|
if(!(clientConnection instanceof LDAPClientConnection)) {
|
bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
|
LocalizableMessage message = ERR_SASLEXTERNAL_NOT_LDAP_CLIENT_INSTANCE.get();
|
bindOperation.setAuthFailureReason(message);
|
return;
|
}
|
LDAPClientConnection lc = (LDAPClientConnection) clientConnection;
|
Certificate[] clientCertChain = lc.getClientCertificateChain();
|
if (clientCertChain == null || clientCertChain.length == 0) {
|
bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
|
LocalizableMessage message = ERR_SASLEXTERNAL_NO_CLIENT_CERT.get();
|
bindOperation.setAuthFailureReason(message);
|
return;
|
}
|
|
|
// Get the certificate mapper to use to map the certificate to a user entry.
|
DN certificateMapperDN = config.getCertificateMapperDN();
|
CertificateMapper<?> certificateMapper =
|
DirectoryServer.getCertificateMapper(certificateMapperDN);
|
|
|
// Use the Directory Server certificate mapper to map the client certificate
|
// chain to a single user DN.
|
Entry userEntry;
|
try
|
{
|
userEntry = certificateMapper.mapCertificateToUser(clientCertChain);
|
}
|
catch (DirectoryException de)
|
{
|
logger.traceException(de);
|
|
bindOperation.setResponseData(de);
|
return;
|
}
|
|
|
// If the user DN is null, then we couldn't establish a mapping and
|
// therefore the authentication failed.
|
if (userEntry == null)
|
{
|
bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
|
|
LocalizableMessage message = ERR_SASLEXTERNAL_NO_MAPPING.get();
|
bindOperation.setAuthFailureReason(message);
|
return;
|
}
|
else
|
{
|
bindOperation.setSASLAuthUserEntry(userEntry);
|
}
|
|
|
// Get the userCertificate attribute from the user's entry for use in the
|
// validation process.
|
List<Attribute> certAttrList =
|
userEntry.getAttribute(certificateAttributeType);
|
switch (validationPolicy)
|
{
|
case ALWAYS:
|
if (certAttrList == null)
|
{
|
if (validationPolicy == CertificateValidationPolicy.ALWAYS)
|
{
|
bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
|
|
LocalizableMessage message = ERR_SASLEXTERNAL_NO_CERT_IN_ENTRY.get(userEntry.getName());
|
bindOperation.setAuthFailureReason(message);
|
return;
|
}
|
}
|
else
|
{
|
try
|
{
|
ByteString certBytes = ByteString.wrap(clientCertChain[0].getEncoded());
|
if (!find(certAttrList, certBytes))
|
{
|
bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
|
|
LocalizableMessage message = ERR_SASLEXTERNAL_PEER_CERT_NOT_FOUND.get(userEntry.getName());
|
bindOperation.setAuthFailureReason(message);
|
return;
|
}
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
|
bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
|
|
LocalizableMessage message = ERR_SASLEXTERNAL_CANNOT_VALIDATE_CERT.get(
|
userEntry.getName(), getExceptionMessage(e));
|
bindOperation.setAuthFailureReason(message);
|
return;
|
}
|
}
|
break;
|
|
case IFPRESENT:
|
if (certAttrList != null)
|
{
|
try
|
{
|
ByteString certBytes = ByteString.wrap(clientCertChain[0].getEncoded());
|
if (!find(certAttrList, certBytes))
|
{
|
bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
|
|
LocalizableMessage message = ERR_SASLEXTERNAL_PEER_CERT_NOT_FOUND.get(userEntry.getName());
|
bindOperation.setAuthFailureReason(message);
|
return;
|
}
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
|
bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
|
|
LocalizableMessage message = ERR_SASLEXTERNAL_CANNOT_VALIDATE_CERT.get(
|
userEntry.getName(), getExceptionMessage(e));
|
bindOperation.setAuthFailureReason(message);
|
return;
|
}
|
}
|
}
|
|
|
AuthenticationInfo authInfo = new AuthenticationInfo(userEntry,
|
SASL_MECHANISM_EXTERNAL, DirectoryServer.isRootDN(userEntry.getName()));
|
bindOperation.setAuthenticationInfo(authInfo);
|
bindOperation.setResultCode(ResultCode.SUCCESS);
|
}
|
|
|
|
private boolean find(List<Attribute> certAttrList, ByteString certBytes)
|
{
|
for (Attribute a : certAttrList)
|
{
|
if (a.contains(certBytes))
|
{
|
return true;
|
}
|
}
|
return false;
|
}
|
|
|
|
/** {@inheritDoc} */
|
@Override
|
public boolean isPasswordBased(String mechanism)
|
{
|
// This is not a password-based mechanism.
|
return false;
|
}
|
|
|
|
/** {@inheritDoc} */
|
@Override
|
public boolean isSecure(String mechanism)
|
{
|
// This may be considered a secure mechanism.
|
return true;
|
}
|
|
|
|
/** {@inheritDoc} */
|
@Override
|
public boolean isConfigurationAcceptable(
|
SASLMechanismHandlerCfg configuration,
|
List<LocalizableMessage> unacceptableReasons)
|
{
|
ExternalSASLMechanismHandlerCfg config =
|
(ExternalSASLMechanismHandlerCfg) configuration;
|
return isConfigurationChangeAcceptable(config, unacceptableReasons);
|
}
|
|
|
|
/** {@inheritDoc} */
|
public boolean isConfigurationChangeAcceptable(
|
ExternalSASLMechanismHandlerCfg configuration,
|
List<LocalizableMessage> unacceptableReasons)
|
{
|
return true;
|
}
|
|
|
|
/** {@inheritDoc} */
|
public ConfigChangeResult applyConfigurationChange(
|
ExternalSASLMechanismHandlerCfg configuration)
|
{
|
final ConfigChangeResult ccr = new ConfigChangeResult();
|
|
|
// See if we should attempt to validate client certificates against those in
|
// the corresponding user's entry.
|
CertificateValidationPolicy newValidationPolicy =
|
CertificateValidationPolicy.ALWAYS;
|
switch (configuration.getCertificateValidationPolicy())
|
{
|
case NEVER:
|
newValidationPolicy = CertificateValidationPolicy.NEVER;
|
break;
|
case IFPRESENT:
|
newValidationPolicy = CertificateValidationPolicy.IFPRESENT;
|
break;
|
case ALWAYS:
|
newValidationPolicy = CertificateValidationPolicy.ALWAYS;
|
break;
|
}
|
|
|
// Get the attribute type to use for validating the certificates. If none
|
// is provided, then default to the userCertificate type.
|
AttributeType newCertificateType = configuration.getCertificateAttribute();
|
if (newCertificateType == null)
|
{
|
newCertificateType =
|
DirectoryServer.getAttributeTypeOrDefault(DEFAULT_VALIDATION_CERT_ATTRIBUTE);
|
}
|
|
|
if (ccr.getResultCode() == ResultCode.SUCCESS)
|
{
|
validationPolicy = newValidationPolicy;
|
certificateAttributeType = newCertificateType;
|
currentConfig = configuration;
|
}
|
|
return ccr;
|
}
|
}
|