All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apereo.cas.web.flow.DelegatedClientAuthenticationAction Maven / Gradle / Ivy

There is a newer version: 7.2.0-RC3
Show newest version
package org.apereo.cas.web.flow;

import org.apereo.cas.CasProtocolConstants;
import org.apereo.cas.authentication.Authentication;
import org.apereo.cas.authentication.adaptive.UnauthorizedAuthenticationException;
import org.apereo.cas.authentication.principal.ClientCredential;
import org.apereo.cas.authentication.principal.Service;
import org.apereo.cas.configuration.model.support.delegation.DelegationAutoRedirectTypes;
import org.apereo.cas.services.UnauthorizedServiceException;
import org.apereo.cas.ticket.AbstractTicketException;
import org.apereo.cas.ticket.TicketGrantingTicket;
import org.apereo.cas.util.LoggingUtils;
import org.apereo.cas.util.function.FunctionUtils;
import org.apereo.cas.web.flow.actions.AbstractAuthenticationAction;
import org.apereo.cas.web.support.WebUtils;

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.jooq.lambda.Unchecked;
import org.pac4j.core.client.BaseClient;
import org.pac4j.core.client.Client;
import org.pac4j.core.context.JEEContext;
import org.pac4j.core.context.WebContext;
import org.pac4j.core.credentials.Credentials;
import org.pac4j.core.exception.http.HttpAction;
import org.pac4j.core.http.adapter.JEEHttpActionAdapter;
import org.pac4j.core.util.Pac4jConstants;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.webflow.core.collection.LocalAttributeMap;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.RequestContext;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Optional;
import java.util.stream.Stream;

/**
 * This class represents an action to put at the beginning of the webflow.
 * 

* 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.

* 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 */ @Slf4j @Getter public class DelegatedClientAuthenticationAction extends AbstractAuthenticationAction { /** * The configuration context. */ protected final DelegatedClientAuthenticationConfigurationContext configContext; private final DelegatedClientAuthenticationWebflowManager delegatedClientAuthenticationWebflowManager; public DelegatedClientAuthenticationAction(final DelegatedClientAuthenticationConfigurationContext context, final DelegatedClientAuthenticationWebflowManager delegatedClientAuthenticationWebflowManager) { super(context.getInitialAuthenticationAttemptWebflowEventResolver(), context.getServiceTicketRequestWebflowEventResolver(), context.getAdaptiveAuthenticationPolicy()); this.configContext = context; this.delegatedClientAuthenticationWebflowManager = delegatedClientAuthenticationWebflowManager; } /** * 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 hasDelegationRequestFailed(final HttpServletRequest request, final int status) { val params = request.getParameterMap(); if (Stream.of("error", "error_code", "error_description", "error_message").anyMatch(params::containsKey)) { val 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(Pac4jConstants.DEFAULT_CLIENT_NAME_PARAMETER))); LOGGER.debug("Delegation request has failed. Details are [{}]", model); return Optional.of(new ModelAndView(CasWebflowConstants.VIEW_ID_PAC4J_STOP_WEBFLOW, model)); } return Optional.empty(); } /** * Is this a SAML logout request? * * @param request the HTTP request * @return whether it is a SAML logout request */ protected static boolean isLogoutRequest(final HttpServletRequest request) { return request.getParameter(Pac4jConstants.LOGOUT_ENDPOINT_PARAMETER) != null; } @Override public Event doExecute(final RequestContext context) { val request = WebUtils.getHttpServletRequestFromExternalWebflowContext(context); val response = WebUtils.getHttpServletResponseFromExternalWebflowContext(context); val webContext = new JEEContext(request, response); try { val clientName = retrieveClientName(webContext); LOGGER.trace("Delegated authentication is handled by client name [{}]", clientName); var service = (Service) null; if (!isLogoutRequest(request) && singleSignOnSessionExists(context) && StringUtils.isNotBlank(clientName)) { LOGGER.trace("Found existing single sign-on session"); service = populateContextWithService(context, webContext, clientName); if (singleSignOnSessionAuthorizedForService(context)) { val providers = configContext.getDelegatedClientIdentityProvidersProducer().produce(context); LOGGER.debug("Skipping delegation and routing back to CAS authentication flow with providers [{}]", providers); return super.doExecute(context); } val resolvedService = resolveServiceFromRequestContext(context); LOGGER.debug("Single sign-on session in unauthorized for service [{}]", resolvedService); val tgt = WebUtils.getTicketGrantingTicketId(context); configContext.getCentralAuthenticationService().deleteTicket(tgt); } if (hasDelegationRequestFailed(request, response.getStatus()).isPresent()) { throw new IllegalArgumentException("Delegated authentication has failed with client " + clientName); } if (StringUtils.isNotBlank(clientName)) { if (service == null) { service = populateContextWithService(context, webContext, clientName); } val client = findDelegatedClientByName(request, clientName, service); WebUtils.putDelegatedAuthenticationClientName(context, client.getName()); populateContextWithClientCredential(client, webContext, context); return super.doExecute(context); } produceDelegatedAuthenticationClientsForContext(context); } catch (final HttpAction e) { FunctionUtils.doIf(LOGGER.isDebugEnabled(), o -> LOGGER.debug(e.getMessage(), e), o -> LOGGER.info(e.getMessage())).accept(e); JEEHttpActionAdapter.INSTANCE.adapt(e, webContext); return isLogoutRequest(request) ? error() : success(); } catch (final UnauthorizedServiceException e) { LOGGER.warn(e.getMessage(), e); throw e; } catch (final Exception e) { LoggingUtils.error(LOGGER, e); return stopWebflow(e, context); } return error(); } /** * Retrieve the client name when calling back the CAS server after the delegated authentication. * * @param webContext the web context * @return the client name */ protected String retrieveClientName(final WebContext webContext) { return webContext.getRequestParameter(Pac4jConstants.DEFAULT_CLIENT_NAME_PARAMETER).orElse(null); } /** * Produce delegated authentication clients for context. * * @param context the context */ protected void produceDelegatedAuthenticationClientsForContext(final RequestContext context) { val response = WebUtils.getHttpServletResponseFromExternalWebflowContext(context); val providers = configContext.getDelegatedClientIdentityProvidersProducer().produce(context); LOGGER.trace("Delegated authentication providers are finalized as [{}]", providers); WebUtils.createCredential(context); if (response.getStatus() == HttpStatus.UNAUTHORIZED.value()) { throw new UnauthorizedAuthenticationException("Authentication is not authorized: " + response.getStatus()); } configContext.getDelegatedClientIdentityProviderConfigurationPostProcessor().process(context, providers); if (!singleSignOnSessionExists(context)) { providers .stream() .filter(provider -> provider.getAutoRedirectType() == DelegationAutoRedirectTypes.SERVER) .findFirst() .ifPresent(Unchecked.consumer(provider -> { LOGGER.debug("Redirecting to [{}]", provider.getRedirectUrl()); response.sendRedirect(provider.getRedirectUrl()); })); } } @Override protected Event doPreExecute(final RequestContext context) throws Exception { val casProperties = configContext.getCasProperties(); if (casProperties.getAuthn().getPac4j().getCore().isReplicateSessions() && casProperties.getSessionReplication().getCookie().isAutoConfigureCookiePath()) { val contextPath = context.getExternalContext().getContextPath(); val cookiePath = StringUtils.isNotBlank(contextPath) ? contextPath + '/' : "/"; val path = configContext.getDelegatedClientDistributedSessionCookieGenerator().getCookiePath(); if (StringUtils.isBlank(path)) { LOGGER.debug("Setting path for cookies for distributed session cookie generator to: [{}]", cookiePath); configContext.getDelegatedClientDistributedSessionCookieGenerator().setCookiePath(cookiePath); } else { LOGGER.trace("Delegated authentication cookie domain is [{}] with path [{}]", configContext.getDelegatedClientDistributedSessionCookieGenerator().getCookieDomain(), path); } } return super.doPreExecute(context); } /** * Populate context with service service. * * @param context the context * @param webContext the web context * @param clientName the client name * @return the service */ protected Service populateContextWithService(final RequestContext context, final JEEContext webContext, final String clientName) { val service = restoreAuthenticationRequestInContext(context, webContext, clientName); val resolvedService = configContext.getAuthenticationRequestServiceSelectionStrategies().resolveService(service); LOGGER.trace("Authentication is resolved by service request from [{}]", service); val registeredService = configContext.getServicesManager().findServiceBy(resolvedService); LOGGER.trace("Located registered service [{}] mapped to resolved service [{}]", registeredService, resolvedService); WebUtils.putRegisteredService(context, registeredService); WebUtils.putServiceIntoFlowScope(context, service); return service; } /** * Populate context with client credential. * * @param client the client * @param webContext the web context * @param requestContext the request context */ protected void populateContextWithClientCredential(final BaseClient client, final JEEContext webContext, final RequestContext requestContext) { LOGGER.debug("Fetching credentials from delegated client [{}]", client); val credentials = getCredentialsFromDelegatedClient(webContext, client); val clientCredential = new ClientCredential(credentials, client.getName()); LOGGER.info("Credentials are successfully authenticated using the delegated client [{}]", client.getName()); WebUtils.putCredential(requestContext, clientCredential); } /** * Gets credentials from delegated client. * * @param webContext the web context * @param client the client * @return the credentials from delegated client */ protected Credentials getCredentialsFromDelegatedClient(final JEEContext webContext, final BaseClient client) { val credentials = client.getCredentials(webContext, configContext.getSessionStore()); LOGGER.debug("Retrieved credentials from client as [{}]", credentials); if (credentials.isEmpty()) { throw new IllegalArgumentException("Unable to determine credentials from the context with client " + client.getName()); } return credentials.get(); } /** * Find delegated client by name base client. * * @param request the request * @param clientName the client name * @param service the service * @return the base client */ protected BaseClient findDelegatedClientByName(final HttpServletRequest request, final String clientName, final Service service) { val clientResult = configContext.getClients().findClient(clientName); if (clientResult.isEmpty()) { LOGGER.warn("Delegated client [{}] can not be located", clientName); throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_UNAUTHZ_SERVICE, StringUtils.EMPTY); } val client = BaseClient.class.cast(clientResult.get()); LOGGER.debug("Delegated authentication client is [{}] with service [{}]", client, service); if (service != null) { request.setAttribute(CasProtocolConstants.PARAMETER_SERVICE, service.getId()); if (!isDelegatedClientAuthorizedForService(client, service, request)) { LOGGER.warn("Delegated client [{}] is not authorized by service [{}]", client, service); throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_UNAUTHZ_SERVICE, StringUtils.EMPTY); } } client.init(); return client; } /** * Stop webflow event. * * @param e the exception * @param requestContext the request context * @return the event */ protected Event stopWebflow(final Exception e, final RequestContext requestContext) { requestContext.getFlashScope().put(CasWebflowConstants.ATTRIBUTE_ERROR_ROOT_CAUSE_EXCEPTION, e); return new Event(this, CasWebflowConstants.TRANSITION_ID_STOP, new LocalAttributeMap<>("error", e)); } /** * Restore authentication request in context service (return null for a logout call). * * @param requestContext the request context * @param webContext the web context * @param clientName the client name * @return the service */ protected Service restoreAuthenticationRequestInContext(final RequestContext requestContext, final JEEContext webContext, final String clientName) { val logoutEndpoint = isLogoutRequest(webContext.getNativeRequest()); if (logoutEndpoint) { return null; } try { val clientResult = configContext.getClients().findClient(clientName); if (clientResult.isPresent()) { return delegatedClientAuthenticationWebflowManager.retrieve(requestContext, webContext, BaseClient.class.cast(clientResult.get())); } LOGGER.warn("Unable to locate client [{}] in registered clients", clientName); } catch (final Exception e) { LoggingUtils.error(LOGGER, e); } throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_UNAUTHZ_SERVICE, StringUtils.EMPTY); } /** * Single sign on session authorized for service boolean. * * @param requestContext the context * @return the boolean */ protected boolean singleSignOnSessionAuthorizedForService(final RequestContext requestContext) { val resolvedService = resolveServiceFromRequestContext(requestContext); val authentication = getSingleSignOnAuthenticationFrom(requestContext); val request = WebUtils.getHttpServletRequestFromExternalWebflowContext(requestContext); val authorized = authentication .map(authn -> configContext.getDelegatedAuthenticationAccessStrategyHelper() .isDelegatedClientAuthorizedForAuthentication(authn, resolvedService, request)) .orElse(Boolean.FALSE); val strategy = configContext.getSingleSignOnParticipationStrategy(); val ssoRequest = SingleSignOnParticipationRequest.builder() .requestContext(requestContext) .build(); return authorized && strategy.supports(ssoRequest) && strategy.isParticipating(ssoRequest); } /** * Is there a current SSO session? * * @param requestContext the request context * @return whether there is a current SSO session */ protected boolean singleSignOnSessionExists(final RequestContext requestContext) { try { val authn = getSingleSignOnAuthenticationFrom(requestContext); if (authn.isPresent()) { LOGGER.trace("Located a valid ticket-granting ticket. Examining existing single sign-on session strategies..."); val authentication = authn.get(); val builder = configContext.getAuthenticationSystemSupport() .establishAuthenticationContextFromInitial(authentication); LOGGER.trace("Recording and tracking initial authentication results in the request context"); WebUtils.putAuthenticationResultBuilder(builder, requestContext); WebUtils.putAuthentication(authentication, requestContext); val strategy = configContext.getSingleSignOnParticipationStrategy(); val ssoRequest = SingleSignOnParticipationRequest.builder() .requestContext(requestContext) .build(); return strategy.supports(ssoRequest) && strategy.isParticipating(ssoRequest); } } catch (final AbstractTicketException e) { LOGGER.trace("Could not retrieve ticket id [{}] from registry.", e.getMessage()); } LOGGER.trace("Ticket-granting ticket found in the webflow context is invalid or has expired"); return false; } private Service resolveServiceFromRequestContext(final RequestContext context) { val service = WebUtils.getService(context); return configContext.getAuthenticationRequestServiceSelectionStrategies().resolveService(service); } private Optional getSingleSignOnAuthenticationFrom(final RequestContext requestContext) { val tgtId = WebUtils.getTicketGrantingTicketId(requestContext); if (StringUtils.isBlank(tgtId)) { LOGGER.trace("No ticket-granting ticket could be located in the webflow context"); return Optional.empty(); } val ticket = configContext.getCentralAuthenticationService().getTicket(tgtId, TicketGrantingTicket.class); LOGGER.trace("Located a valid ticket-granting ticket"); return Optional.of(ticket.getAuthentication()); } private boolean isDelegatedClientAuthorizedForService(final Client client, final Service service, final HttpServletRequest request) { return configContext.getDelegatedAuthenticationAccessStrategyHelper() .isDelegatedClientAuthorizedForService(client, service, request); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy