| /* |
| * 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; |
| } |
| } |