package org.apereo.cas.support.pac4j.web.flow; import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.apereo.cas.CasProtocolConstants; import org.apereo.cas.CentralAuthenticationService; import org.apereo.cas.authentication.AuthenticationResult; import org.apereo.cas.authentication.AuthenticationSystemSupport; import org.apereo.cas.authentication.principal.ClientCredential; import org.apereo.cas.authentication.principal.Service; import org.apereo.cas.authentication.principal.WebApplicationService; import org.apereo.cas.ticket.TicketGrantingTicket; import org.apereo.cas.web.support.WebUtils; import org.pac4j.core.client.BaseClient; import org.pac4j.core.client.Clients; import org.pac4j.core.client.IndirectClient; import org.pac4j.core.context.WebContext; import org.pac4j.core.credentials.Credentials; import org.pac4j.core.exception.HttpAction; import org.pac4j.core.profile.CommonProfile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.web.servlet.ModelAndView; import org.springframework.webflow.action.AbstractAction; import org.springframework.webflow.context.ExternalContext; import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.RequestContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.Serializable; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Optional; import java.util.Set; /** * This class represents an action to put at the beginning of the webflow. * <p> * Before any authentication, redirection urls are computed for the different clients defined as well as the theme, * locale, method and service are saved into the web session.</p> * After authentication, appropriate information are expected on this callback url to finish the authentication * process with the provider. * * @author Jerome Leleu * @since 3.5.0 */ public class DelegatedClientAuthenticationAction extends AbstractAction { /** * Stop the webflow for pac4j and route to view. */ public static final String STOP_WEBFLOW = "stopWebflow"; /** * Stop the webflow. */ public static final String STOP = "stop"; /** * Client action state id in the webflow. */ public static final String CLIENT_ACTION = "clientAction"; /** * All the urls and names of the pac4j clients. */ public static final String PAC4J_URLS = "pac4jUrls"; /** * View id that stops the webflow. */ public static final String VIEW_ID_STOP_WEBFLOW = "casPac4jStopWebflow"; private static final Logger LOGGER = LoggerFactory.getLogger(DelegatedClientAuthenticationAction.class); private final Clients clients; private final AuthenticationSystemSupport authenticationSystemSupport; private final CentralAuthenticationService centralAuthenticationService; private final String themeParamName; private final String localParamName; private final boolean autoRedirect; public DelegatedClientAuthenticationAction(final Clients clients, final AuthenticationSystemSupport authenticationSystemSupport, final CentralAuthenticationService centralAuthenticationService, final String themeParamName, final String localParamName, final boolean autoRedirect) { this.clients = clients; this.authenticationSystemSupport = authenticationSystemSupport; this.centralAuthenticationService = centralAuthenticationService; this.themeParamName = themeParamName; this.localParamName = localParamName; this.autoRedirect = autoRedirect; } @Override protected Event doExecute(final RequestContext context) throws Exception { final HttpServletRequest request = WebUtils.getHttpServletRequest(context); final HttpServletResponse response = WebUtils.getHttpServletResponse(context); final HttpSession session = request.getSession(); // web context final WebContext webContext = WebUtils.getPac4jJ2EContext(request, response); // get client final String clientName = request.getParameter(this.clients.getClientNameParameter()); LOGGER.debug("clientName: [{}]", clientName); if (hasDelegationRequestFailed(request, response.getStatus()).isPresent()) { return stopWebflow(); } // it's an authentication if (StringUtils.isNotBlank(clientName)) { // get client final BaseClient<Credentials, CommonProfile> client = (BaseClient<Credentials, CommonProfile>) this.clients.findClient(clientName); LOGGER.debug("Client: [{}]", client); // get credentials final Credentials credentials; try { credentials = client.getCredentials(webContext); LOGGER.debug("Retrieved credentials: [{}]", credentials); } catch (final Exception e) { LOGGER.debug("The request requires http action", e); return stopWebflow(); } // retrieve parameters from web session final Service service = (Service) session.getAttribute(CasProtocolConstants.PARAMETER_SERVICE); context.getFlowScope().put(CasProtocolConstants.PARAMETER_SERVICE, service); LOGGER.debug("Retrieve service: [{}]", service); if (service != null) { request.setAttribute(CasProtocolConstants.PARAMETER_SERVICE, service.getId()); } restoreRequestAttribute(request, session, this.themeParamName); restoreRequestAttribute(request, session, this.localParamName); restoreRequestAttribute(request, session, CasProtocolConstants.PARAMETER_METHOD); // credentials not null -> try to authenticate if (credentials != null) { final AuthenticationResult authenticationResult = this.authenticationSystemSupport.handleAndFinalizeSingleAuthenticationTransaction(service, new ClientCredential(credentials)); final TicketGrantingTicket tgt = this.centralAuthenticationService.createTicketGrantingTicket(authenticationResult); WebUtils.putTicketGrantingTicketInScopes(context, tgt); return success(); } } // no or aborted authentication : go to login page prepareForLoginPage(context); if (response.getStatus() == HttpStatus.UNAUTHORIZED.value()) { return stopWebflow(); } if (this.autoRedirect) { final Set<ProviderLoginPageConfiguration> urls = context.getFlowScope().get(PAC4J_URLS, Set.class); if (urls != null && urls.size() == 1) { final ProviderLoginPageConfiguration cfg = urls.stream().findFirst().get(); LOGGER.debug("Auto-redirecting to client url [{}]", cfg.getRedirectUrl()); response.sendRedirect(cfg.getRedirectUrl()); final ExternalContext externalContext = context.getExternalContext(); externalContext.recordResponseComplete(); return stopWebflow(); } } return error(); } /** * Prepare the data for the login page. * * @param context The current webflow context * @throws HttpAction the http action */ protected void prepareForLoginPage(final RequestContext context) throws HttpAction { final HttpServletRequest request = WebUtils.getHttpServletRequest(context); final HttpServletResponse response = WebUtils.getHttpServletResponse(context); final HttpSession session = request.getSession(); // web context final WebContext webContext = WebUtils.getPac4jJ2EContext(request, response); // save parameters in web session final WebApplicationService service = WebUtils.getService(context); LOGGER.debug("save service: [{}]", service); session.setAttribute(CasProtocolConstants.PARAMETER_SERVICE, service); saveRequestParameter(request, session, this.themeParamName); saveRequestParameter(request, session, this.localParamName); saveRequestParameter(request, session, CasProtocolConstants.PARAMETER_METHOD); final Set<ProviderLoginPageConfiguration> urls = new LinkedHashSet<>(); this.clients.findAllClients().forEach(client -> { try { final IndirectClient indirectClient = (IndirectClient) client; final String name = client.getName().replaceAll("Client\\d*", StringUtils.EMPTY); final String redirectionUrl = indirectClient.getRedirectAction(webContext).getLocation(); LOGGER.debug("[{}] -> [{}]", name, redirectionUrl); urls.add(new ProviderLoginPageConfiguration(name, redirectionUrl, name.toLowerCase())); } catch (final HttpAction e) { if (e.getCode() == HttpStatus.UNAUTHORIZED.value()) { LOGGER.debug("Authentication request was denied from the provider [{}]", client.getName()); } else { LOGGER.warn(e.getMessage(), e); } } catch (final Exception e) { LOGGER.error("Cannot process client [{}]", client, e); } }); if (!urls.isEmpty()) { context.getFlowScope().put(PAC4J_URLS, urls); } else if (response.getStatus() != HttpStatus.UNAUTHORIZED.value()) { LOGGER.warn("No clients could be determined based on the provided configuration"); } } /** * Restore an attribute in web session as an attribute in request. * * @param request The HTTP request * @param session The HTTP session * @param name The name of the parameter */ private static void restoreRequestAttribute(final HttpServletRequest request, final HttpSession session, final String name) { final String value = (String) session.getAttribute(name); request.setAttribute(name, value); } /** * Save a request parameter in the web session. * * @param request The HTTP request * @param session The HTTP session * @param name The name of the parameter */ private static void saveRequestParameter(final HttpServletRequest request, final HttpSession session, final String name) { final String value = request.getParameter(name); if (value != null) { session.setAttribute(name, value); } } private Event stopWebflow() { return new Event(this, STOP); } /** * Determine if request has errors. * * @param request the request * @param status the status * @return the optional model and view, if request is an error. */ public static Optional<ModelAndView> hasDelegationRequestFailed(final HttpServletRequest request, final int status) { final Map<String, String[]> params = request.getParameterMap(); if (params.containsKey("error") || params.containsKey("error_code") || params.containsKey("error_description") || params.containsKey("error_message")) { final Map<String, Object> model = new HashMap<>(); if (params.containsKey("error_code")) { model.put("code", StringEscapeUtils.escapeHtml4(request.getParameter("error_code"))); } else { model.put("code", status); } model.put("error", StringEscapeUtils.escapeHtml4(request.getParameter("error"))); model.put("reason", StringEscapeUtils.escapeHtml4(request.getParameter("error_reason"))); if (params.containsKey("error_description")) { model.put("description", StringEscapeUtils.escapeHtml4(request.getParameter("error_description"))); } else if (params.containsKey("error_message")) { model.put("description", StringEscapeUtils.escapeHtml4(request.getParameter("error_message"))); } model.put(CasProtocolConstants.PARAMETER_SERVICE, request.getAttribute(CasProtocolConstants.PARAMETER_SERVICE)); model.put("client", StringEscapeUtils.escapeHtml4(request.getParameter("client_name"))); LOGGER.debug("Delegation request has failed. Details are [{}]", model); return Optional.of(new ModelAndView("casPac4jStopWebflow", model)); } return Optional.empty(); } /** * The Provider login page configuration. */ public static class ProviderLoginPageConfiguration implements Serializable { private static final long serialVersionUID = 6216882278086699364L; private final String name; private final String redirectUrl; private final String type; /** * Instantiates a new Provider ui configuration. * * @param name the name * @param redirectUrl the redirect url * @param type the type */ ProviderLoginPageConfiguration(final String name, final String redirectUrl, final String type) { this.name = name; this.redirectUrl = redirectUrl; this.type = type; } public String getName() { return name; } public String getRedirectUrl() { return redirectUrl; } public String getType() { return type; } } }