/*
* JBoss, Home of Professional Open Source.
* Copyright 2011, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.as.domain.management.security;
import static org.jboss.as.domain.management.logging.DomainManagementLogger.SECURITY_LOGGER;
import static org.jboss.as.domain.management.RealmConfigurationConstants.VERIFY_PASSWORD_CALLBACK_SUPPORTED;
import static org.wildfly.common.Assert.checkNotNullParam;
import java.io.IOException;
import java.net.URI;
import java.security.Principal;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.naming.NamingException;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.sasl.AuthorizeCallback;
import javax.security.sasl.RealmCallback;
import org.jboss.as.domain.management.AuthMechanism;
import org.jboss.as.domain.management.logging.DomainManagementLogger;
import org.jboss.as.domain.management.SecurityRealm;
import org.jboss.as.domain.management.connections.ldap.LdapConnectionManager;
import org.jboss.as.domain.management.security.LdapSearcherCache.AttachmentKey;
import org.jboss.as.domain.management.security.LdapSearcherCache.SearchResult;
import org.jboss.msc.inject.Injector;
import org.jboss.msc.service.Service;
import org.jboss.msc.service.ServiceName;
import org.jboss.msc.service.StartContext;
import org.jboss.msc.service.StartException;
import org.jboss.msc.service.StopContext;
import org.jboss.msc.value.InjectedValue;
import org.wildfly.security.auth.SupportLevel;
import org.wildfly.security.auth.callback.EvidenceVerifyCallback;
import org.wildfly.security.auth.server.RealmIdentity;
import org.wildfly.security.auth.server.RealmUnavailableException;
import org.wildfly.security.credential.Credential;
import org.wildfly.security.evidence.Evidence;
import org.wildfly.security.evidence.PasswordGuessEvidence;
/**
* A CallbackHandler for users within an LDAP directory.
*
* @author <a href="mailto:darran.lofthouse@jboss.com">Darran Lofthouse</a>
*/
public class UserLdapCallbackHandler implements Service<CallbackHandlerService>, CallbackHandlerService {
private static final AttachmentKey<PasswordCredential> PASSWORD_KEY = AttachmentKey.create(PasswordCredential.class);
private static final String SERVICE_SUFFIX = "ldap";
public static final String DEFAULT_USER_DN = "dn";
private final InjectedValue<LdapConnectionManager> connectionManager = new InjectedValue<LdapConnectionManager>();
private final InjectedValue<LdapSearcherCache<LdapEntry, String>> userSearcherInjector = new InjectedValue<LdapSearcherCache<LdapEntry, String>>();
private final boolean allowEmptyPassword;
private final boolean shareConnection;
protected final int searchTimeLimit = 10000; // TODO - Maybe make configurable.
public UserLdapCallbackHandler(boolean allowEmptyPassword, boolean shareConnection) {
this.allowEmptyPassword = allowEmptyPassword;
this.shareConnection = shareConnection;
}
/*
* CallbackHandlerService Methods
*/
public AuthMechanism getPreferredMechanism() {
return AuthMechanism.PLAIN;
}
public Set<AuthMechanism> getSupplementaryMechanisms() {
return Collections.emptySet();
}
public Map<String, String> getConfigurationOptions() {
return Collections.singletonMap(VERIFY_PASSWORD_CALLBACK_SUPPORTED, Boolean.TRUE.toString());
}
@Override
public boolean isReadyForHttpChallenge() {
// Configured for LDAP so assume we have some users.
return true;
}
public CallbackHandler getCallbackHandler(Map<String, Object> sharedState) {
return new LdapCallbackHandler(sharedState);
}
@Override
public org.wildfly.security.auth.server.SecurityRealm getElytronSecurityRealm() {
return new SecurityRealmImpl();
}
/*
* Service Methods
*/
public void start(StartContext context) throws StartException {
}
public void stop(StopContext context) {
}
public CallbackHandlerService getValue() throws IllegalStateException, IllegalArgumentException {
return this;
}
/*
* Access to Injectors
*/
public InjectedValue<LdapConnectionManager> getConnectionManagerInjector() {
return connectionManager;
}
public Injector<LdapSearcherCache<LdapEntry, String>> getLdapUserSearcherInjector() {
return userSearcherInjector;
}
private LdapConnectionHandler createLdapConnectionHandler() {
LdapConnectionManager connectionManager = this.connectionManager.getValue();
return LdapConnectionHandler.newInstance(connectionManager);
}
/*
* CallbackHandler Method
*/
private class LdapCallbackHandler implements CallbackHandler {
private final Map<String, Object> sharedState;
private LdapCallbackHandler(final Map<String, Object> sharedState) {
this.sharedState = sharedState;
}
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
if (callbacks.length == 1 && callbacks[0] instanceof AuthorizeCallback) {
AuthorizeCallback acb = (AuthorizeCallback) callbacks[0];
String authenticationId = acb.getAuthenticationID();
String authorizationId = acb.getAuthorizationID();
boolean authorized = authenticationId.equals(authorizationId);
if (authorized == false) {
SECURITY_LOGGER.tracef(
"Checking 'AuthorizeCallback', authorized=false, authenticationID=%s, authorizationID=%s.",
authenticationId, authorizationId);
}
acb.setAuthorized(authorized);
return;
}
EvidenceVerifyCallback evidenceVerifyCallback = null;
String username = null;
for (Callback current : callbacks) {
if (current instanceof NameCallback) {
username = ((NameCallback) current).getDefaultName();
} else if (current instanceof RealmCallback) {
// TODO - Nothing at the moment
} else if (current instanceof EvidenceVerifyCallback) {
evidenceVerifyCallback = (EvidenceVerifyCallback) current;
} else {
throw new UnsupportedCallbackException(current);
}
}
if (username == null || username.length() == 0) {
SECURITY_LOGGER.trace("No username or 0 length username supplied.");
throw DomainManagementLogger.ROOT_LOGGER.noUsername();
}
if (evidenceVerifyCallback == null || evidenceVerifyCallback.getEvidence() == null) {
SECURITY_LOGGER.trace("No password to verify.");
throw DomainManagementLogger.ROOT_LOGGER.noPassword();
}
final String password;
if (evidenceVerifyCallback.getEvidence() instanceof PasswordGuessEvidence) {
char[] guess = ((PasswordGuessEvidence) evidenceVerifyCallback.getEvidence()).getGuess();
password = guess != null ? new String(guess) : null;
} else {
password = null;
}
if (password == null || (allowEmptyPassword == false && password.length() == 0)) {
SECURITY_LOGGER.trace("No password or 0 length password supplied.");
throw DomainManagementLogger.ROOT_LOGGER.noPassword();
}
LdapConnectionHandler lch = createLdapConnectionHandler();
try {
// 2 - Search to identify the DN of the user connecting
SearchResult<LdapEntry> searchResult = userSearcherInjector.getValue().search(lch, username);
evidenceVerifyCallback.setVerified(verifyPassword(lch, searchResult, username, password, sharedState));
} catch (Exception e) {
SECURITY_LOGGER.trace("Unable to verify identity.", e);
throw DomainManagementLogger.ROOT_LOGGER.cannotPerformVerification(e);
} finally {
if (shareConnection && lch != null && evidenceVerifyCallback != null && evidenceVerifyCallback.isVerified()) {
sharedState.put(LdapConnectionHandler.class.getName(), lch);
} else {
lch.close();
}
}
}
}
private static boolean verifyPassword(LdapConnectionHandler ldapConnectionHandler, SearchResult<LdapEntry> searchResult, String username, String password, Map<String, Object> sharedState) {
LdapEntry ldapEntry = searchResult.getResult();
// 3 - Connect as user once their DN is identified
final PasswordCredential cachedCredential = searchResult.getAttachment(PASSWORD_KEY);
if (cachedCredential != null) {
if (cachedCredential.verify(password)) {
SECURITY_LOGGER.tracef("Password verified for user '%s' (using cached password)", username);
sharedState.put(LdapEntry.class.getName(), ldapEntry);
if (username.equals(ldapEntry.getSimpleName()) == false) {
sharedState.put(SecurityRealmService.LOADED_USERNAME_KEY, ldapEntry.getSimpleName());
}
return true;
} else {
SECURITY_LOGGER.tracef("Password verification failed for user (using cached password) '%s'", username);
return false;
}
} else {
try {
LdapConnectionHandler verificationHandler = ldapConnectionHandler;
URI referralUri = ldapEntry.getReferralUri();
if (referralUri != null) {
verificationHandler = verificationHandler.findForReferral(referralUri);
}
if (verificationHandler != null) {
verificationHandler.verifyIdentity(ldapEntry.getDistinguishedName(), password);
SECURITY_LOGGER.tracef("Password verified for user '%s' (using connection attempt)", username);
searchResult.attach(PASSWORD_KEY, new PasswordCredential(password));
sharedState.put(LdapEntry.class.getName(), ldapEntry);
if (username.equals(ldapEntry.getSimpleName()) == false) {
sharedState.put(SecurityRealmService.LOADED_USERNAME_KEY, ldapEntry.getSimpleName());
}
return true;
} else {
SECURITY_LOGGER.tracef(
"Password verification failed for user '%s', no connection for referral '%s'", username,
referralUri.toString());
return false;
}
} catch (Exception e) {
SECURITY_LOGGER.tracef("Password verification failed for user (using connection attempt) '%s'",
username);
return false;
}
}
}
private void safeClose(LdapConnectionHandler ldapConnectionHandler) {
try {
if (ldapConnectionHandler != null) {
ldapConnectionHandler.close();
}
} catch (IOException e) {
SECURITY_LOGGER.trace("Unable to close ldapConnectionHandler", e);
}
}
private class SecurityRealmImpl implements org.wildfly.security.auth.server.SecurityRealm {
@Override
public RealmIdentity getRealmIdentity(Principal principal) throws RealmUnavailableException {
final String name = principal.getName();
if (name.length() == 0) {
return RealmIdentity.NON_EXISTENT;
}
LdapConnectionHandler ldapConnectionHandler = createLdapConnectionHandler();
try {
SearchResult<LdapEntry> searchResult = userSearcherInjector.getValue().search(ldapConnectionHandler, name);
return new RealmIdentityImpl(name, ldapConnectionHandler, searchResult, SecurityRealmService.SharedStateSecurityRealm.getSharedState());
} catch (IllegalStateException e) {
safeClose(ldapConnectionHandler);
return RealmIdentity.NON_EXISTENT;
} catch (IOException | NamingException e) {
safeClose(ldapConnectionHandler);
throw new RealmUnavailableException(e);
}
}
@Override
public SupportLevel getCredentialAcquireSupport(Class<? extends Credential> credentialType, String algorithmName) throws RealmUnavailableException {
return SupportLevel.UNSUPPORTED;
}
@Override
public SupportLevel getEvidenceVerifySupport(Class<? extends Evidence> evidenceType, String algorithmName) throws RealmUnavailableException {
checkNotNullParam("evidenceType", evidenceType);
return PasswordGuessEvidence.class.isAssignableFrom(evidenceType) ? SupportLevel.SUPPORTED : SupportLevel.UNSUPPORTED;
}
private class RealmIdentityImpl implements RealmIdentity {
private final String username;
private final LdapConnectionHandler ldapConnectionHandler;
private final SearchResult<LdapEntry> searchResult;
private final Map<String, Object> sharedState;
private RealmIdentityImpl(final String username, final LdapConnectionHandler ldapConnectionHandler, final SearchResult<LdapEntry> searchResult, final Map<String, Object> sharedState) {
this.username = username;
this.ldapConnectionHandler = ldapConnectionHandler;
this.searchResult = searchResult;
this.sharedState = sharedState != null ? sharedState : new HashMap<>();
}
@Override
public SupportLevel getCredentialAcquireSupport(Class<? extends Credential> credentialType, String algorithmName)throws RealmUnavailableException {
return SecurityRealmImpl.this.getCredentialAcquireSupport(credentialType, algorithmName);
}
@Override
public <C extends Credential> C getCredential(Class<C> credentialType) throws RealmUnavailableException {
return null;
}
@Override
public SupportLevel getEvidenceVerifySupport(Class<? extends Evidence> evidenceType, String algorithmName)throws RealmUnavailableException {
return SecurityRealmImpl.this.getEvidenceVerifySupport(evidenceType, algorithmName);
}
@Override
public boolean verifyEvidence(Evidence evidence) throws RealmUnavailableException {
if (evidence instanceof PasswordGuessEvidence) {
PasswordGuessEvidence passwordGuessEvidence = (PasswordGuessEvidence) evidence;
char[] guess =passwordGuessEvidence.getGuess();
if (guess == null || (allowEmptyPassword == false && guess.length == 0)) {
SECURITY_LOGGER.trace("No password or 0 length password supplied.");
return false;
}
boolean result = verifyPassword(ldapConnectionHandler, searchResult, username, new String(guess), sharedState);
if (shareConnection && result) {
sharedState.put(LdapConnectionHandler.class.getName(), ldapConnectionHandler);
}
return result;
}
return false;
}
@Override
public boolean exists() throws RealmUnavailableException {
return true;
}
@Override
public void dispose() {
safeClose(ldapConnectionHandler);
}
}
}
public static final class ServiceUtil {
private ServiceUtil() {
}
public static ServiceName createServiceName(final String realmName) {
return SecurityRealm.ServiceUtil.createServiceName(realmName).append(SERVICE_SUFFIX);
}
}
private static final class PasswordCredential {
private final String password;
private PasswordCredential(final String password) {
this.password = password;
}
private boolean verify(final String password) {
return this.password.equals(password);
}
}
}