/* * Copyright (c) 2013, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. * * WSO2 Inc. 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.wso2.carbon.identity.application.authentication.framework.handler.step.impl; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.wso2.carbon.identity.application.authentication.framework.ApplicationAuthenticator; import org.wso2.carbon.identity.application.authentication.framework.AuthenticatorFlowStatus; import org.wso2.carbon.identity.application.authentication.framework.FederatedApplicationAuthenticator; import org.wso2.carbon.identity.application.authentication.framework.config.ConfigurationFacade; import org.wso2.carbon.identity.application.authentication.framework.config.builder.FileBasedConfigurationBuilder; import org.wso2.carbon.identity.application.authentication.framework.config.model.AuthenticatorConfig; import org.wso2.carbon.identity.application.authentication.framework.config.model.ExternalIdPConfig; import org.wso2.carbon.identity.application.authentication.framework.config.model.SequenceConfig; import org.wso2.carbon.identity.application.authentication.framework.config.model.StepConfig; import org.wso2.carbon.identity.application.authentication.framework.context.AuthenticationContext; import org.wso2.carbon.identity.application.authentication.framework.exception.AuthenticationFailedException; import org.wso2.carbon.identity.application.authentication.framework.exception.FrameworkException; import org.wso2.carbon.identity.application.authentication.framework.exception.InvalidCredentialsException; import org.wso2.carbon.identity.application.authentication.framework.exception.LogoutFailedException; import org.wso2.carbon.identity.application.authentication.framework.handler.step.StepHandler; import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedIdPData; import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkConstants; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.core.model.IdentityErrorMsgContext; import org.wso2.carbon.identity.core.util.IdentityUtil; import org.wso2.carbon.idp.mgt.IdentityProviderManagementException; import org.wso2.carbon.user.core.UserCoreConstants; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.HashMap; import java.util.List; import java.util.Map; public class DefaultStepHandler implements StepHandler { private static final Log log = LogFactory.getLog(DefaultStepHandler.class); private static volatile DefaultStepHandler instance; public static DefaultStepHandler getInstance() { if (instance == null) { synchronized (DefaultStepHandler.class) { if (instance == null) { instance = new DefaultStepHandler(); } } } return instance; } @Override public void handle(HttpServletRequest request, HttpServletResponse response, AuthenticationContext context) throws FrameworkException { StepConfig stepConfig = context.getSequenceConfig().getStepMap() .get(context.getCurrentStep()); List<AuthenticatorConfig> authConfigList = stepConfig.getAuthenticatorList(); String authenticatorNames = FrameworkUtils.getAuthenticatorIdPMappingString(authConfigList); String loginPage = ConfigurationFacade.getInstance().getAuthenticationEndpointURL(); String fidp = request.getParameter(FrameworkConstants.RequestParams.FEDERATED_IDP); Map<String, AuthenticatedIdPData> authenticatedIdPs = context.getPreviousAuthenticatedIdPs(); Map<String, AuthenticatorConfig> authenticatedStepIdps = FrameworkUtils .getAuthenticatedStepIdPs(stepConfig, authenticatedIdPs); // check passive authentication if (context.isPassiveAuthenticate()) { if (authenticatedStepIdps.isEmpty()) { context.setRequestAuthenticated(false); } else { String authenticatedIdP = authenticatedStepIdps.entrySet().iterator().next().getKey(); AuthenticatedIdPData authenticatedIdPData = authenticatedIdPs.get(authenticatedIdP); populateStepConfigWithAuthenticationDetails(stepConfig, authenticatedIdPData); } stepConfig.setCompleted(true); return; } // if Request has fidp param and if this is the first step if (fidp != null && stepConfig.getOrder() == 1) { handleHomeRealmDiscovery(request, response, context); return; } else if (context.isReturning()) { // if this is a request from the multi-option page if (request.getParameter(FrameworkConstants.RequestParams.AUTHENTICATOR) != null && !request.getParameter(FrameworkConstants.RequestParams.AUTHENTICATOR) .isEmpty()) { handleRequestFromLoginPage(request, response, context); return; } else { // if this is a response from external parties (e.g. federated IdPs) handleResponse(request, response, context); return; } } // if dumbMode else if (ConfigurationFacade.getInstance().isDumbMode()) { if (log.isDebugEnabled()) { log.debug("Executing in Dumb mode"); } try { request.setAttribute(FrameworkConstants.RequestParams.FLOW_STATUS, AuthenticatorFlowStatus.INCOMPLETE); response.sendRedirect(loginPage + ("?" + context.getContextIdIncludedQueryParams()) + "&authenticators=" + URLEncoder.encode(authenticatorNames, "UTF-8") + "&hrd=true"); } catch (IOException e) { throw new FrameworkException(e.getMessage(), e); } } else { if (!context.isForceAuthenticate() && !authenticatedStepIdps.isEmpty()) { Map.Entry<String, AuthenticatorConfig> entry = authenticatedStepIdps.entrySet() .iterator().next(); String idp = entry.getKey(); AuthenticatorConfig authenticatorConfig = entry.getValue(); if (context.isReAuthenticate()) { if (log.isDebugEnabled()) { log.debug("Re-authenticating with " + idp + " IdP"); } try { context.setExternalIdP(ConfigurationFacade.getInstance().getIdPConfigByName( idp, context.getTenantDomain())); } catch (IdentityProviderManagementException e) { log.error("Exception while getting IdP by name", e); } doAuthentication(request, response, context, authenticatorConfig); return; } else { if (log.isDebugEnabled()) { log.debug("Already authenticated. Skipping the step"); } // skip the step if this is a normal request AuthenticatedIdPData authenticatedIdPData = authenticatedIdPs.get(idp); populateStepConfigWithAuthenticationDetails(stepConfig, authenticatedIdPData); stepConfig.setCompleted(true); return; } } else { // Find if step contains only a single authenticator with a single // IdP. If yes, don't send to the multi-option page. Call directly. boolean sendToPage = false; AuthenticatorConfig authenticatorConfig = null; // Are there multiple authenticators? if (authConfigList.size() > 1) { sendToPage = true; } else { // Are there multiple IdPs in the single authenticator? authenticatorConfig = authConfigList.get(0); if (authenticatorConfig.getIdpNames().size() > 1) { sendToPage = true; } } if (!sendToPage) { // call directly if (!authenticatorConfig.getIdpNames().isEmpty()) { if (log.isDebugEnabled()) { log.debug("Step contains only a single IdP. Going to call it directly"); } // set the IdP to be called in the context try { context.setExternalIdP(ConfigurationFacade.getInstance() .getIdPConfigByName(authenticatorConfig.getIdpNames().get(0), context.getTenantDomain())); } catch (IdentityProviderManagementException e) { log.error("Exception while getting IdP by name", e); } } doAuthentication(request, response, context, authenticatorConfig); return; } else { // else send to the multi option page. if (log.isDebugEnabled()) { log.debug("Sending to the Multi Option page"); } Map<String, String> parameterMap = getAuthenticatorConfig().getParameterMap(); String showAuthFailureReason = null; if (parameterMap != null) { showAuthFailureReason = parameterMap.get(FrameworkConstants.SHOW_AUTHFAILURE_RESON_CONFIG); if (log.isDebugEnabled()) { log.debug("showAuthFailureReason has been set as : " + showAuthFailureReason); } } String retryParam = ""; if (stepConfig.isRetrying()) { context.setCurrentAuthenticator(null); retryParam = "&authFailure=true&authFailureMsg=login.fail.message"; } try { request.setAttribute(FrameworkConstants.RequestParams.FLOW_STATUS, AuthenticatorFlowStatus .INCOMPLETE); response.sendRedirect(getRedirectUrl(request, response, context, authenticatorNames, showAuthFailureReason, retryParam, loginPage)); } catch (IOException e) { throw new FrameworkException(e.getMessage(), e); } return; } } } } protected void handleHomeRealmDiscovery(HttpServletRequest request, HttpServletResponse response, AuthenticationContext context) throws FrameworkException { if (log.isDebugEnabled()) { log.debug("Request contains fidp parameter. Initiating Home Realm Discovery"); } String domain = request.getParameter(FrameworkConstants.RequestParams.FEDERATED_IDP); if (log.isDebugEnabled()) { log.debug("Received domain: " + domain); } SequenceConfig sequenceConfig = context.getSequenceConfig(); StepConfig stepConfig = sequenceConfig.getStepMap().get(context.getCurrentStep()); List<AuthenticatorConfig> authConfigList = stepConfig.getAuthenticatorList(); String authenticatorNames = FrameworkUtils.getAuthenticatorIdPMappingString(authConfigList); String redirectURL = ConfigurationFacade.getInstance().getAuthenticationEndpointURL(); if (domain.trim().length() == 0) { //SP hasn't specified a domain. We assume it wants to get the domain from the user try { request.setAttribute(FrameworkConstants.RequestParams.FLOW_STATUS, AuthenticatorFlowStatus.INCOMPLETE); response.sendRedirect(redirectURL + ("?" + context.getContextIdIncludedQueryParams()) + "&authenticators=" + URLEncoder.encode(authenticatorNames, "UTF-8") + "&hrd=true"); } catch (IOException e) { throw new FrameworkException(e.getMessage(), e); } return; } // call home realm discovery handler to retrieve the realm String homeRealm = FrameworkUtils.getHomeRealmDiscoverer().discover(domain); if (log.isDebugEnabled()) { log.debug("Home realm discovered: " + homeRealm); } // try to find an IdP with the retrieved realm ExternalIdPConfig externalIdPConfig = null; try { externalIdPConfig = ConfigurationFacade.getInstance() .getIdPConfigByRealm(homeRealm, context.getTenantDomain()); } catch (IdentityProviderManagementException e) { log.error("Exception while getting IdP by realm", e); } // if an IdP exists if (externalIdPConfig != null) { String idpName = externalIdPConfig.getIdPName(); if (log.isDebugEnabled()) { log.debug("Found IdP of the realm: " + idpName); } Map<String, AuthenticatedIdPData> authenticatedIdPs = context.getPreviousAuthenticatedIdPs(); Map<String, AuthenticatorConfig> authenticatedStepIdps = FrameworkUtils .getAuthenticatedStepIdPs(stepConfig, authenticatedIdPs); if (authenticatedStepIdps.containsKey(idpName) && !context.isForceAuthenticate() && !context .isReAuthenticate()) { // skip the step if this is a normal request AuthenticatedIdPData authenticatedIdPData = authenticatedIdPs.get(idpName); populateStepConfigWithAuthenticationDetails(stepConfig, authenticatedIdPData); stepConfig.setCompleted(true); return; } // try to find an authenticator of the current step, that is mapped to the IdP for (AuthenticatorConfig authConfig : authConfigList) { // if found if (authConfig.getIdpNames().contains(idpName)) { context.setExternalIdP(externalIdPConfig); doAuthentication(request, response, context, authConfig); return; } } } if (log.isDebugEnabled()) { log.debug("An IdP was not found for the sent domain. Sending to the domain page"); } String errorMsg = "domain.unknown"; try { response.sendRedirect(redirectURL + ("?" + context.getContextIdIncludedQueryParams()) + "&authenticators=" + URLEncoder.encode(authenticatorNames, "UTF-8") + "&authFailure=true" + "&authFailureMsg=" + errorMsg + "&hrd=true"); } catch (IOException e) { throw new FrameworkException(e.getMessage(), e); } } protected void handleRequestFromLoginPage(HttpServletRequest request, HttpServletResponse response, AuthenticationContext context) throws FrameworkException { if (log.isDebugEnabled()) { log.debug("Relieved a request from the multi option page"); } SequenceConfig sequenceConfig = context.getSequenceConfig(); int currentStep = context.getCurrentStep(); StepConfig stepConfig = sequenceConfig.getStepMap().get(currentStep); // if request from the login page with a selected IdP String selectedIdp = request.getParameter(FrameworkConstants.RequestParams.IDP); if (selectedIdp != null) { if (log.isDebugEnabled()) { log.debug("User has selected IdP: " + selectedIdp); } try { ExternalIdPConfig externalIdPConfig = ConfigurationFacade.getInstance() .getIdPConfigByName(selectedIdp, context.getTenantDomain()); // TODO [IMPORTANT] validate the idp is inside the step. context.setExternalIdP(externalIdPConfig); } catch (IdentityProviderManagementException e) { log.error("Exception while getting IdP by name", e); } } for (AuthenticatorConfig authenticatorConfig : stepConfig.getAuthenticatorList()) { ApplicationAuthenticator authenticator = authenticatorConfig .getApplicationAuthenticator(); // TODO [IMPORTANT] validate the authenticator is inside the step. if (authenticator != null && authenticator.getName().equalsIgnoreCase( request.getParameter(FrameworkConstants.RequestParams.AUTHENTICATOR))) { doAuthentication(request, response, context, authenticatorConfig); return; } } // TODO handle idp null // TODO handle authenticator name unmatching } protected void handleResponse(HttpServletRequest request, HttpServletResponse response, AuthenticationContext context) throws FrameworkException { if (log.isDebugEnabled()) { log.debug("Receive a response from the external party"); } SequenceConfig sequenceConfig = context.getSequenceConfig(); int currentStep = context.getCurrentStep(); boolean isNoneCanHandle = true; StepConfig stepConfig = sequenceConfig.getStepMap().get(currentStep); for (AuthenticatorConfig authenticatorConfig : stepConfig.getAuthenticatorList()) { ApplicationAuthenticator authenticator = authenticatorConfig .getApplicationAuthenticator(); // Call authenticate if canHandle if (authenticator != null && authenticator.canHandle(request) && (context.getCurrentAuthenticator() == null || authenticator.getName() .equals(context.getCurrentAuthenticator()))) { isNoneCanHandle = false; if (log.isDebugEnabled()) { log.debug(authenticator.getName() + " can handle the request."); } doAuthentication(request, response, context, authenticatorConfig); break; } } if (isNoneCanHandle) { throw new FrameworkException("No authenticator can handle the request in step : " + currentStep); } } protected void doAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationContext context, AuthenticatorConfig authenticatorConfig) throws FrameworkException { SequenceConfig sequenceConfig = context.getSequenceConfig(); int currentStep = context.getCurrentStep(); StepConfig stepConfig = sequenceConfig.getStepMap().get(currentStep); ApplicationAuthenticator authenticator = authenticatorConfig.getApplicationAuthenticator(); if (authenticator == null) { log.error("Authenticator is null"); return; } try { context.setAuthenticatorProperties(FrameworkUtils.getAuthenticatorPropertyMapFromIdP( context.getExternalIdP(), authenticator.getName())); AuthenticatorFlowStatus status = authenticator.process(request, response, context); request.setAttribute(FrameworkConstants.RequestParams.FLOW_STATUS, status); if (log.isDebugEnabled()) { log.debug(authenticator.getName() + " returned: " + status.toString()); } if (status == AuthenticatorFlowStatus.INCOMPLETE) { if (log.isDebugEnabled()) { log.debug(authenticator.getName() + " is redirecting"); } return; } if (authenticator instanceof FederatedApplicationAuthenticator) { if (context.getSubject().getUserName() == null) { // Set subject identifier as the default username for federated users String authenticatedSubjectIdentifier = context.getSubject().getAuthenticatedSubjectIdentifier(); context.getSubject().setUserName(authenticatedSubjectIdentifier); } if (context.getSubject().getFederatedIdPName() == null && context.getExternalIdP() != null) { // Setting identity provider's name String idpName = context.getExternalIdP().getIdPName(); context.getSubject().setFederatedIdPName(idpName); } if (context.getSubject().getTenantDomain() == null) { // Setting service provider's tenant domain as the default tenant for federated users String tenantDomain = context.getTenantDomain(); context.getSubject().setTenantDomain(tenantDomain); } } AuthenticatedIdPData authenticatedIdPData = new AuthenticatedIdPData(); // store authenticated user AuthenticatedUser authenticatedUser = context.getSubject(); stepConfig.setAuthenticatedUser(authenticatedUser); authenticatedIdPData.setUser(authenticatedUser); authenticatorConfig.setAuthenticatorStateInfo(context.getStateInfo()); stepConfig.setAuthenticatedAutenticator(authenticatorConfig); String idpName = FrameworkConstants.LOCAL_IDP_NAME; if (context.getExternalIdP() != null) { idpName = context.getExternalIdP().getIdPName(); } // store authenticated idp stepConfig.setAuthenticatedIdP(idpName); authenticatedIdPData.setIdpName(idpName); authenticatedIdPData.setAuthenticator(authenticatorConfig); //add authenticated idp data to the session wise map context.getCurrentAuthenticatedIdPs().put(idpName, authenticatedIdPData); } catch (InvalidCredentialsException e) { if (log.isDebugEnabled()) { log.debug("InvalidCredentialsException", e); } log.warn("A login attempt was failed due to invalid credentials"); context.setRequestAuthenticated(false); } catch (AuthenticationFailedException e) { log.error(e.getMessage(), e); context.setRequestAuthenticated(false); } catch (LogoutFailedException e) { throw new FrameworkException(e.getMessage(), e); } stepConfig.setCompleted(true); } protected void populateStepConfigWithAuthenticationDetails(StepConfig stepConfig, AuthenticatedIdPData authenticatedIdPData) { stepConfig.setAuthenticatedUser(authenticatedIdPData.getUser()); stepConfig.setAuthenticatedIdP(authenticatedIdPData.getIdpName()); stepConfig.setAuthenticatedAutenticator(authenticatedIdPData.getAuthenticator()); } private String getRedirectUrl(HttpServletRequest request, HttpServletResponse response, AuthenticationContext context, String authenticatorNames, String showAuthFailureReason, String retryParam, String loginPage) throws IOException { IdentityErrorMsgContext errorContext = IdentityUtil.getIdentityErrorMsg(); IdentityUtil.clearIdentityErrorMsg(); if (showAuthFailureReason != null && "true".equals(showAuthFailureReason)) { if (errorContext != null) { String errorCode = errorContext.getErrorCode(); int remainingAttempts = errorContext.getMaximumLoginAttempts() - errorContext.getFailedLoginAttempts(); if (log.isDebugEnabled()) { StringBuilder debugString = new StringBuilder(); debugString.append("Identity error message context is not null. Error details are as follows."); debugString.append("errorCode : " + errorCode + "\n"); debugString.append("username : " + request.getParameter("username") + "\n"); debugString.append("remainingAttempts : " + remainingAttempts); log.debug(debugString.toString()); } if (errorCode.equals(UserCoreConstants.ErrorCode.INVALID_CREDENTIAL)) { retryParam = retryParam + "&errorCode=" + errorCode + "&failedUsername=" + URLEncoder.encode (request.getParameter("username"), "UTF-8") + "&remainingAttempts=" + remainingAttempts; return response.encodeRedirectURL(loginPage + ("?" + context.getContextIdIncludedQueryParams())) + "&authenticators=" + authenticatorNames + ":" + FrameworkConstants.LOCAL + retryParam; } else if (errorCode.equals(UserCoreConstants.ErrorCode.USER_IS_LOCKED)) { String redirectURL; if (remainingAttempts == 0) { redirectURL = response.encodeRedirectURL(loginPage + ("?" + context .getContextIdIncludedQueryParams())) + "&errorCode=" + errorCode + "&failedUsername=" + URLEncoder.encode(request.getParameter("username"), "UTF-8") + "&remainingAttempts=0" + "&authenticators=" + URLEncoder.encode(authenticatorNames, "UTF-8") + retryParam; } else { redirectURL = response.encodeRedirectURL(loginPage + ("?" + context .getContextIdIncludedQueryParams())) + "&errorCode=" + errorCode + "&failedUsername=" + URLEncoder.encode(request.getParameter("username"), "UTF-8") + "&authenticators=" + URLEncoder.encode(authenticatorNames, "UTF-8") + retryParam; } return redirectURL; } else if (errorCode.equals(UserCoreConstants.ErrorCode.USER_DOES_NOT_EXIST)) { retryParam = retryParam + "&errorCode=" + errorCode + "&failedUsername=" + URLEncoder.encode (request.getParameter("username"), "UTF-8"); return response.encodeRedirectURL(loginPage + ("?" + context.getContextIdIncludedQueryParams())) + "&authenticators=" + authenticatorNames + ":" + FrameworkConstants.LOCAL + retryParam; } } else { return response.encodeRedirectURL(loginPage + ("?" + context.getContextIdIncludedQueryParams())) + "&authenticators=" + authenticatorNames + ":" + FrameworkConstants.LOCAL + retryParam; } } else { String errorCode = errorContext != null ? errorContext.getErrorCode() : null; if (errorCode != null && errorCode.equals(UserCoreConstants.ErrorCode.USER_IS_LOCKED)) { String redirectURL; redirectURL = response.encodeRedirectURL(loginPage + ("?" + context.getContextIdIncludedQueryParams() )) + "&failedUsername=" + URLEncoder.encode(request.getParameter("username"), "UTF-8") + "&authenticators=" + authenticatorNames + ":" + FrameworkConstants.LOCAL + retryParam; return redirectURL; } else { return response.encodeRedirectURL(loginPage + ("?" + context.getContextIdIncludedQueryParams())) + "&authenticators=" + authenticatorNames + ":" + FrameworkConstants.LOCAL + retryParam; } } return loginPage + ("?" + context.getContextIdIncludedQueryParams() + "&authenticators=" + URLEncoder.encode (authenticatorNames, "UTF-8") + retryParam); } private AuthenticatorConfig getAuthenticatorConfig() { AuthenticatorConfig authConfig = FileBasedConfigurationBuilder.getInstance().getAuthenticatorBean (FrameworkConstants.BASIC_AUTHENTICATOR_CLASS); if (authConfig == null) { authConfig = new AuthenticatorConfig(); authConfig.setParameterMap(new HashMap()); } return authConfig; } }