blob: 348a4c38b6bbe2fa61c389db72fa367d4b1e039e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.jackrabbit.oak.spi.security.authentication.external.impl;
import java.security.Principal;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.jcr.Credentials;
import javax.jcr.RepositoryException;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.LoginException;
import org.apache.jackrabbit.api.security.user.UserManager;
import org.apache.jackrabbit.oak.api.AuthInfo;
import org.apache.jackrabbit.oak.api.CommitFailedException;
import org.apache.jackrabbit.oak.api.Root;
import org.apache.jackrabbit.oak.commons.DebugTimer;
import org.apache.jackrabbit.oak.namepath.NamePathMapper;
import org.apache.jackrabbit.oak.plugins.value.jcr.ValueFactoryImpl;
import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
import org.apache.jackrabbit.oak.spi.security.authentication.AbstractLoginModule;
import org.apache.jackrabbit.oak.spi.security.authentication.AuthInfoImpl;
import org.apache.jackrabbit.oak.spi.security.authentication.ImpersonationCredentials;
import org.apache.jackrabbit.oak.spi.security.authentication.PreAuthenticatedLogin;
import org.apache.jackrabbit.oak.spi.security.authentication.credentials.SimpleCredentialsSupport;
import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityException;
import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityProvider;
import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityProviderManager;
import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityRef;
import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalUser;
import org.apache.jackrabbit.oak.spi.security.authentication.credentials.CredentialsSupport;
import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncContext;
import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncException;
import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncHandler;
import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncManager;
import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncResult;
import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncedIdentity;
import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
import org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@code ExternalLoginModule} implements a {@code LoginModule} that uses an
* {@link ExternalIdentityProvider} for authentication.
*/
public class ExternalLoginModule extends AbstractLoginModule {
private static final Logger log = LoggerFactory.getLogger(ExternalLoginModule.class);
private static final int MAX_SYNC_ATTEMPTS = 50;
/**
* Name of the parameter that configures the name of the external identity provider.
*/
public static final String PARAM_IDP_NAME = SyncHandlerMapping.PARAM_IDP_NAME;
/**
* Name of the parameter that configures the name of the synchronization handler.
*/
public static final String PARAM_SYNC_HANDLER_NAME = SyncHandlerMapping.PARAM_SYNC_HANDLER_NAME;
private ExternalIdentityProviderManager idpManager;
private SyncManager syncManager;
private CredentialsSupport credentialsSupport = SimpleCredentialsSupport.getInstance();
/**
* internal configuration when invoked from a factory rather than jaas
*/
private ConfigurationParameters osgiConfig;
/**
* The external identity provider as specified by the {@link #PARAM_IDP_NAME}
*/
private ExternalIdentityProvider idp;
/**
* The configured sync handler as specified by the {@link #PARAM_SYNC_HANDLER_NAME}
*/
private SyncHandler syncHandler;
/**
* The external user as resolved in the login call.
*/
private ExternalUser externalUser;
/**
* Login credentials
*/
private Credentials credentials;
/**
* Default constructor for the OSGIi LoginModuleFactory case and the default non-OSGi JAAS case.
*/
@SuppressWarnings("UnusedDeclaration")
public ExternalLoginModule() {
}
/**
* Creates a new ExternalLoginModule with the given OSGi config.
* @param osgiConfig the config
*/
public ExternalLoginModule(ConfigurationParameters osgiConfig) {
this.osgiConfig = osgiConfig;
}
//--------------------------------------------------------< LoginModule >---
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> opts) {
super.initialize(subject, callbackHandler, sharedState, opts);
// merge options with osgi options if needed
if (osgiConfig != null) {
options = ConfigurationParameters.of(osgiConfig, options);
}
Whiteboard whiteboard = getWhiteboard();
if (whiteboard == null) {
log.error("External login module needs whiteboard. Will not be used for login.");
return;
}
String idpName = options.getConfigValue(PARAM_IDP_NAME, "");
if (idpName.isEmpty()) {
log.error("External login module needs IPD name. Will not be used for login.");
} else {
if (idpManager == null) {
idpManager = WhiteboardUtils.getService(whiteboard, ExternalIdentityProviderManager.class);
}
if (idpManager == null) {
log.error("External login module needs IDPManager. Will not be used for login.");
} else {
idp = idpManager.getProvider(idpName);
if (idp == null) {
log.error("No IDP found with name {}. Will not be used for login.", idpName);
}
}
}
String syncHandlerName = options.getConfigValue(PARAM_SYNC_HANDLER_NAME, "");
if (syncHandlerName.isEmpty()) {
log.error("External login module needs SyncHandler name. Will not be used for login.");
} else {
if (syncManager == null) {
syncManager = WhiteboardUtils.getService(whiteboard, SyncManager.class);
}
if (syncManager == null) {
log.error("External login module needs SyncManager. Will not be used for login.");
} else {
syncHandler = syncManager.getSyncHandler(syncHandlerName);
if (syncHandler == null) {
log.error("No SyncHandler found with name {}. Will not be used for login.", syncHandlerName);
}
}
}
if (idp instanceof CredentialsSupport) {
credentialsSupport = (CredentialsSupport) idp;
} else {
log.debug("No 'SupportedCredentials' configured. Using default implementation supporting 'SimpleCredentials'.");
}
}
@Override
public boolean login() throws LoginException {
if (idp == null || syncHandler == null) {
return false;
}
credentials = getCredentials();
// check if we have a pre authenticated login from a previous login module
final PreAuthenticatedLogin preAuthLogin = getSharedPreAuthLogin();
final String userId = getUserId(preAuthLogin, credentials);
if (userId == null && credentials == null) {
log.debug("No credentials|userId found for external login module. ignoring.");
return false;
}
// remember identification for log-output
Object logId = (userId != null) ? userId : credentials;
try {
// check if there exists a user with the given ID that has been synchronized
// before into the repository.
SyncedIdentity sId = getSyncedIdentity(userId);
// if there exists an authorizable with the given userid (syncedIdentity != null),
// ignore it if any of the following conditions is met:
// - identity is local (i.e. not an external identity)
// - identity belongs to another IDP
// - identity is valid but we have a preAuthLogin and the user doesn't need an updating sync (OAK-3508)
if (ignore(sId, preAuthLogin)) {
return false;
}
if (preAuthLogin != null) {
externalUser = idp.getUser(preAuthLogin.getUserId());
} else {
externalUser = idp.authenticate(credentials);
}
if (externalUser != null) {
log.debug("IDP {} returned valid user {}", idp.getName(), externalUser);
if (credentials != null) {
//noinspection unchecked
sharedState.put(SHARED_KEY_CREDENTIALS, credentials);
}
//noinspection unchecked
sharedState.put(SHARED_KEY_LOGIN_NAME, externalUser.getId());
syncUser(externalUser);
return true;
} else {
debug("IDP {} returned null for {}", idp.getName(), logId.toString());
if (sId != null) {
// invalidate the user if it exists as synced variant
log.debug("local user exists for '{}'. re-validating.", sId.getId());
validateUser(sId.getId());
}
return false;
}
} catch (ExternalIdentityException e) {
log.error("Error while authenticating '{}' with {}", logId, idp.getName(), e);
return false;
} catch (LoginException e) {
log.debug("IDP {} throws login exception for '{}': {}", idp.getName(), logId, e.getMessage());
throw e;
} catch (Exception e) {
log.debug("SyncHandler {} throws sync exception for '{}'", syncHandler.getName(), logId, e);
LoginException le = new LoginException("Error while syncing user.");
le.initCause(e);
throw le;
}
}
@Override
public boolean commit() throws LoginException {
if (externalUser == null) {
// login attempt in this login module was not successful
clearState();
return false;
}
Set<? extends Principal> principals = getPrincipals(externalUser.getId());
if (!principals.isEmpty()) {
if (!subject.isReadOnly()) {
subject.getPrincipals().addAll(principals);
if (credentials != null) {
subject.getPublicCredentials().add(credentials);
}
setAuthInfo(createAuthInfo(externalUser.getId(), principals), subject);
} else {
log.debug("Could not add information to read only subject {}", subject);
}
return true;
}
clearState();
return false;
}
@Override
public boolean abort() throws LoginException {
clearState();
// do we need to remove the user again, in case we created it during login() ?
return true;
}
//------------------------------------------------------------< private >---
@Nullable
private String getUserId(@Nullable PreAuthenticatedLogin preAuthLogin, @Nullable Credentials credentials) {
if (preAuthLogin != null) {
return preAuthLogin.getUserId();
} else if (credentials != null){
return credentialsSupport.getUserId(credentials);
} else {
return null;
}
}
@Nullable
private SyncedIdentity getSyncedIdentity(@Nullable String userId) throws RepositoryException {
UserManager userMgr = getUserManager();
if (userId != null && userMgr != null) {
return syncHandler.findIdentity(userMgr, userId);
} else {
return null;
}
}
private boolean ignore(@Nullable SyncedIdentity syncedIdentity, @Nullable PreAuthenticatedLogin preAuthLogin) {
if (syncedIdentity != null) {
ExternalIdentityRef externalIdRef = syncedIdentity.getExternalIdRef();
if (externalIdRef == null) {
debug("ignoring local user: {}", syncedIdentity.getId());
return true;
} else if (!idp.getName().equals(externalIdRef.getProviderName())) {
debug("ignoring foreign identity: {} (idp={})", externalIdRef.getString(), idp.getName());
return true;
}
if (preAuthLogin != null && !syncHandler.requiresSync(syncedIdentity)) {
debug("pre-authenticated external user {} does not require syncing.", syncedIdentity.toString());
return true;
}
}
return false;
}
/**
* Initiates synchronization of the external user.
* @param user the external user
* @throws SyncException if an error occurs
*/
private void syncUser(@NotNull ExternalUser user) throws SyncException {
Root root = getRoot();
if (root == null) {
throw new SyncException("Cannot synchronize user. root == null");
}
UserManager userManager = getUserManager();
if (userManager == null) {
throw new SyncException("Cannot synchronize user. userManager == null");
}
int numAttempt = 0;
while (numAttempt++ < MAX_SYNC_ATTEMPTS) {
SyncContext context = null;
try {
DebugTimer timer = new DebugTimer();
context = syncHandler.createContext(idp, userManager, new ValueFactoryImpl(root, NamePathMapper.DEFAULT));
SyncResult syncResult = context.sync(user);
timer.mark("sync");
if (root.hasPendingChanges()) {
root.commit();
timer.mark("commit");
}
debug("syncUser({}) {}, status: {}", user.getId(), timer.getString(), syncResult.getStatus().toString());
return;
} catch (CommitFailedException e) {
log.warn("User synchronization failed during commit: {}. (attempt {}/{})", e.toString(), numAttempt, MAX_SYNC_ATTEMPTS);
root.refresh();
} finally {
if (context != null) {
context.close();
}
}
}
throw new SyncException("User synchronization failed during commit after " + MAX_SYNC_ATTEMPTS + " attempts");
}
/**
* Initiates synchronization of a possible remove user
* @param id the user id
*/
private void validateUser(@NotNull String id) throws SyncException {
SyncContext context = null;
try {
Root root = getRoot();
if (root == null) {
throw new SyncException("Cannot synchronize user. root == null");
}
UserManager userManager = getUserManager();
if (userManager == null) {
throw new SyncException("Cannot synchronize user. userManager == null");
}
DebugTimer timer = new DebugTimer();
context = syncHandler.createContext(idp, userManager, new ValueFactoryImpl(root, NamePathMapper.DEFAULT));
context.sync(id);
timer.mark("sync");
root.commit();
timer.mark("commit");
debug("validateUser({}) {}", id, timer.getString());
} catch (CommitFailedException e) {
throw new SyncException("User synchronization failed during commit.", e);
} finally {
if (context != null) {
context.close();
}
}
}
@NotNull
private AuthInfo createAuthInfo(@NotNull String userId, @NotNull Set<? extends Principal> principals) {
Credentials creds;
if (credentials instanceof ImpersonationCredentials) {
creds = ((ImpersonationCredentials) credentials).getBaseCredentials();
} else {
creds = credentials;
}
Map<String, Object> attributes = new HashMap<String, Object>();
Object shared = sharedState.get(SHARED_KEY_ATTRIBUTES);
if (shared instanceof Map) {
for (Map.Entry entry : ((Map<?,?>) shared).entrySet()) {
attributes.put(entry.getKey().toString(), entry.getValue());
}
} else if (creds != null) {
attributes.putAll(credentialsSupport.getAttributes(creds));
}
return new AuthInfoImpl(userId, attributes, principals);
}
private static void debug(@NotNull String msg, String... args) {
if (log.isDebugEnabled()) {
log.debug(msg, args);
}
}
//------------------------------------------------< AbstractLoginModule >---
@Override
protected void clearState() {
super.clearState();
externalUser = null;
credentials = null;
}
/**
* @return the set of credentials classes as exposed by the configured
* {@link CredentialsSupport} implementation.
*/
@NotNull
@Override
protected Set<Class> getSupportedCredentials() {
return credentialsSupport.getCredentialClasses();
}
//----------------------------------------------< public setters (JAAS) >---
public void setSyncManager(@NotNull SyncManager syncManager) {
this.syncManager = syncManager;
}
public void setIdpManager(@NotNull ExternalIdentityProviderManager idpManager) {
this.idpManager = idpManager;
}
}