CAS process analysis server login (with Service)

Processing flow configuration of login request

/WEB-INF/web. The configuration of the welcome page in XML is as follows:

web.xml

<welcome-file-list>
    <welcome-file>index.jsp</welcome-file>
</welcome-file-list>

index. The contents of the JSP are as follows:

<%@ page language="java"  session="false" %>

<%
final String queryString = request.getQueryString();
final String url = request.getContextPath() + "/login" + (queryString != null ? '?' + queryString : "");
response.sendRedirect(response.encodeURL(url));%>

Redirect access requests to[ http://ip:port/cas/login?queryString ];

At / WEB-INF / Web The servlet for intercepting and processing login requests is defined in the XML. The configuration is as follows:

web.xml

<servlet>
    <servlet-name>cas</servlet-name>
    <servlet-class>
        org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <!-- Load the child application context. Start with the default, then modules, then overlays. -->
        <param-value>/WEB-INF/cas-servlet.xml,classpath*:/META-INF/cas-servlet-*.xml,/WEB-INF/cas-servlet-*.xml</param-value>
    </init-param>
    <init-param>
        <param-name>publishContext</param-name>
        <param-value>false</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
    <async-supported>true</async-supported>
</servlet>


<servlet-mapping>
    <servlet-name>cas</servlet-name>
    <url-pattern>/login</url-pattern>
</servlet-mapping>
...

/WEB-INF/web.xml will be introduced into / WEB-INF / CAS servlet XML, which defines the mapping logic of the login request. The related configurations are as follows:

cas-servlet.xml

<!-- login webflow configuration -->
<bean id="loginFlowHandlerMapping" class="org.springframework.webflow.mvc.servlet.FlowHandlerMapping"
      p:flowRegistry-ref="loginFlowRegistry" p:order="2">
    <property name="interceptors">
        <array value-type="org.springframework.web.servlet.HandlerInterceptor">
            <ref bean="localeChangeInterceptor"/>
            <ref bean="authenticationThrottle"/>
        </array>
    </property>
</bean>

<bean name="loginFlowExecutor" class="org.springframework.webflow.executor.FlowExecutorImpl"
      c:definitionLocator-ref="loginFlowRegistry"
      c:executionFactory-ref="loginFlowExecutionFactory"
      c:executionRepository-ref="loginFlowExecutionRepository"/>

<bean name="loginFlowExecutionFactory" class="org.springframework.webflow.engine.impl.FlowExecutionImplFactory"
      p:executionKeyFactory-ref="loginFlowExecutionRepository"/>

<bean id="loginFlowExecutionRepository" class=" org.jasig.spring.webflow.plugin.ClientFlowExecutionRepository"
      c:flowExecutionFactory-ref="loginFlowExecutionFactory"
      c:flowDefinitionLocator-ref="loginFlowRegistry"
      c:transcoder-ref="loginFlowStateTranscoder"/>

<bean id="loginFlowStateTranscoder" class="org.jasig.spring.webflow.plugin.EncryptedTranscoder"
      c:cipherBean-ref="loginFlowCipherBean" />

[loginFlowRegistry] is configured in / WEB-INF / spring configuration / webflowcontext.xml. The configuration file is imported from / WEB-INF/web.xml, as follows:

web.xml

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
        /WEB-INF/spring-configuration/*.xml
        /WEB-INF/deployerConfigContext.xml
        <!-- this enables extensions and addons to contribute to overall CAS' application context
             by loading spring context files from classpath i.e. found in classpath jars, etc. -->
        classpath*:/META-INF/spring/*.xml
    </param-value>
</context-param>

Configuration file / WEB-INF / spring configuration / webflowcontext The configuration of [loginFlowRegistry] in XML is as follows:

webflowContext.xml

<webflow:flow-registry id="loginFlowRegistry" flow-builder-services="builder" base-path="/WEB-INF/webflow">
    <webflow:flow-location-pattern value="/login/*-webflow.xml"/>
</webflow:flow-registry>

The process configuration file path is defined: / WEB-INF / webflow / login / * - webflow XML, i.e. / WEB-INF / webflow / login / login webflow XML, which defines the processing flow of login request;

The processing flow of login request is started

According to / WEB-INF / CAS servlet XML configuration:

cas-servlet.xml

<bean id="loginHandlerAdapter" class="org.jasig.cas.web.flow.SelectiveFlowHandlerAdapter"
      p:supportedFlowId="login" p:flowExecutor-ref="loginFlowExecutor" p:flowUrlHandler-ref="loginFlowUrlHandler"/>

...
<bean name="loginFlowExecutor" class="org.springframework.webflow.executor.FlowExecutorImpl"
      c:definitionLocator-ref="loginFlowRegistry"
      c:executionFactory-ref="loginFlowExecutionFactory"
      c:executionRepository-ref="loginFlowExecutionRepository"/>

It can be seen from [p:supportedFlowId="login"] that the request[ http://ip:port/cas/login?queryString ]It will be processed by [loginHandlerAdapter], and the defined [loginFlowExecutor] attribute will be referenced to [loginFlowRegistry], that is, the processing flow configured in / WEB-INF/webflow/login/login-webflow.xml file;

First analyze the processing of SelectiveFlowHandlerAdapter. Its core logic is inherited from the parent class FlowHandlerAdapter. The code is as follows:

public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    FlowHandler flowHandler = (FlowHandler)handler;
    this.checkAndPrepare(request, response, false);
    // According to CAS servlet XML configuration < bean id = "loginflowurlhandler" class = "org. Jasig. CAS. Web. Flow. Casdefaultflowurlhandler" / >
    // flowUrlHandler is CasDefaultFlowUrlHandler
    // The implementation of getFlowExecutionKey(request) is: request getParameter("execution")
    String flowExecutionKey = this.flowUrlHandler.getFlowExecutionKey(request);
    // For the first request, the "execution" property of the request is null
    if (flowExecutionKey != null) {
        try {
            ServletExternalContext context = this.createServletExternalContext(request, response);
            FlowExecutionResult result = this.flowExecutor.resumeExecution(flowExecutionKey, context);
            this.handleFlowExecutionResult(result, context, request, response, flowHandler);
        } catch (FlowException var11) {
            this.handleFlowException(var11, request, response, flowHandler);
        }
    } else {
        try {
            // flowId is "login"
            String flowId = this.getFlowId(flowHandler, request);
            MutableAttributeMap<Object> input = this.getInputMap(flowHandler, request);
            ServletExternalContext context = this.createServletExternalContext(request, response);
            // Start login process processing
            FlowExecutionResult result = this.flowExecutor.launchExecution(flowId, input, context);
            this.handleFlowExecutionResult(result, context, request, response, flowHandler);
        } catch (FlowException var10) {
            this.handleFlowException(var10, request, response, flowHandler);
        }
    }

    return null;
}

Where this flowExecutor. Launchexecution is flowexecutorimpl The code of launchexecution is as follows:

public FlowExecutionResult launchExecution(String flowId, MutableAttributeMap<?> input, ExternalContext context) throws FlowException {
    FlowExecutionResult var6;
    try {
        if (logger.isDebugEnabled()) {
            logger.debug("Launching new execution of flow '" + flowId + "' with input " + input);
        }

        ExternalContextHolder.setExternalContext(context);
        // Load the process definition according to [loginFlowRegistry]
        FlowDefinition flowDefinition = this.definitionLocator.getFlowDefinition(flowId);
        // Create a process according to [loginFlowExecutionFactory]
        FlowExecution flowExecution = this.executionFactory.createFlowExecution(flowDefinition);
        // Open process
        flowExecution.start(input, context);
        if (!flowExecution.hasEnded()) {
            FlowExecutionLock lock = this.executionRepository.getLock(flowExecution.getKey());
            lock.lock();

            try {
                this.executionRepository.putFlowExecution(flowExecution);
            } finally {
                lock.unlock();
            }

            FlowExecutionResult var7 = this.createPausedResult(flowExecution);
            return var7;
        }

        var6 = this.createEndResult(flowExecution);
    } finally {
        ExternalContextHolder.setExternalContext((ExternalContext)null);
    }

    return var6;
}

Detailed description of the processing flow of login request

The processing flow of login request is defined in / WEB-INF / webflow / login / login webflow XML file;

First, execute the < on Start > action. The configuration is as follows:

login-webflow.xml

<on-start>
    <evaluate expression="initialFlowSetupAction"/>
</on-start>

[initialflowsetupaction] corresponds to initialflowsetupaction. Its parent class AbstractAction defines the algorithm template to be executed. The code is as follows:

public final Event execute(RequestContext context) throws Exception {
    Event result = this.doPreExecute(context);
    if (result == null) {
        // Start processing flow
        result = this.doExecute(context);
        this.doPostExecute(context);
    } else if (this.logger.isInfoEnabled()) {
        this.logger.info("Action execution disallowed; pre-execution result is '" + result.getId() + "'");
    }

    return result;
}

InitialFlowSetupAction implements the algorithm details doExecute. The code is as follows:

protected Event doExecute(final RequestContext context) throws Exception {
    final HttpServletRequest request = WebUtils.getHttpServletRequest(context);

    final String contextPath = context.getExternalContext().getContextPath();
    final String cookiePath = StringUtils.isNotBlank(contextPath) ? contextPath + '/' : "/";

    // Set cookie path
    if (StringUtils.isBlank(warnCookieGenerator.getCookiePath())) {
        logger.info("Setting path for cookies for warn cookie generator to: {} ", cookiePath);
        this.warnCookieGenerator.setCookiePath(cookiePath);
    } else {
        logger.debug("Warning cookie path is set to {} and path {}", warnCookieGenerator.getCookieDomain(),
                warnCookieGenerator.getCookiePath());
    }
    if (StringUtils.isBlank(ticketGrantingTicketCookieGenerator.getCookiePath())) {
        logger.info("Setting path for cookies for TGC cookie generator to: {} ", cookiePath);
        this.ticketGrantingTicketCookieGenerator.setCookiePath(cookiePath);
    } else {
        logger.debug("TGC cookie path is set to {} and path {}", ticketGrantingTicketCookieGenerator.getCookieDomain(),
                ticketGrantingTicketCookieGenerator.getCookiePath());
    }

    // Save TGT information in cookie
    WebUtils.putTicketGrantingTicketInScopes(context,
            this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request));

    // Save the WARNING information in the cookie
    WebUtils.putWarningCookie(context,
            Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request)));

    // Get the Service information carried in the request
    final Service service = WebUtils.getService(this.argumentExtractors, context);

    if (service != null) {
        logger.debug("Placing service in context scope: [{}]", service.getId());

        // Find the registered Service information according to the Service information carried in the request
        final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
        // The default is DefaultRegisteredServiceAccessStrategy, which supports access
        if (registeredService != null && registeredService.getAccessStrategy().isServiceAccessAllowed()) {
            logger.debug("Placing registered service [{}] with id [{}] in context scope",
                    registeredService.getServiceId(),
                    registeredService.getId());
            // Save registered Service information
            WebUtils.putRegisteredService(context, registeredService);

            final RegisteredServiceAccessStrategy accessStrategy = registeredService.getAccessStrategy();
            if (accessStrategy.getUnauthorizedRedirectUrl() != null) {
                logger.debug("Placing registered service's unauthorized redirect url [{}] with id [{}] in context scope",
                        accessStrategy.getUnauthorizedRedirectUrl(),
                        registeredService.getServiceId());
                // Save redirect URL information
                WebUtils.putUnauthorizedRedirectUrl(context, accessStrategy.getUnauthorizedRedirectUrl());
            }
        }
    } else if (!this.enableFlowOnAbsentServiceRequest) {
        // Empty Service requests are not supported
        logger.warn("No service authentication request is available at [{}]. CAS is configured to disable the flow.",
                WebUtils.getHttpServletRequest(context).getRequestURL());
        throw new NoSuchFlowExecutionException(context.getFlowExecutionContext().getKey(),
                new UnauthorizedServiceException("screen.service.required.message", "Service is required"));
    }
    // Save Service information
    WebUtils.putService(context, service);
    return result("success");
}

The processing of the Service information carried in the login request is as follows:

  1. Obtain the Service object from the request context;
  2. If the Service object exists, then
    1. Search the Service information registered in the system according to the Service object;
    2. If the registered Service information exists and access is allowed, then
      1. Save the registered Service information into the request context;
      2. If there is an unauthorized URL in the registered Service information, save the unauthorized URL to the request context;
  3. If the Service object does not exist, judge whether it supports processing requests that do not carry Service,
    1. If it is not supported, throw an exception;
  4. Save the Service object to the request context;
  5. Return success event;

        1. Get Service object from request context

The code is as follows:

final Service service = WebUtils.getService(this.argumentExtractors, context);

@Resource(name="argumentExtractors")
public void setArgumentExtractors(final List<ArgumentExtractor> argumentExtractors) {
    this.argumentExtractors = argumentExtractors;
}


WebUtils.java

public static WebApplicationService getService(
    final List<ArgumentExtractor> argumentExtractors,
    final RequestContext context) {
    final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
    return getService(argumentExtractors, request);
}

public static WebApplicationService getService(
    final List<ArgumentExtractor> argumentExtractors,
    final HttpServletRequest request) {
    // Traverse the ArgumentExtractor collection
    for (final ArgumentExtractor argumentExtractor : argumentExtractors) {
        final WebApplicationService service = argumentExtractor
            .extractService(request);

        if (service != null) {
            return service;
        }
    }

    return null;
}
services-context.xml

<util:list id="argumentExtractors">
    <ref bean="defaultArgumentExtractor"/>
</util:list>

[defaultargumentextractor] corresponds to defaultargumentextractor. Its extractService inherits from the parent class AbstractArgumentExtractor. AbstractArgumentExtractor implements the algorithm template of extractService, provides algorithm details, extractServiceInternal is implemented by subclasses, and also provides serviceFactoryList for subclass implementation. Its code is as follows:

public final WebApplicationService extractService(final HttpServletRequest request) {
    // Extract Service information from request
    final WebApplicationService service = extractServiceInternal(request);

    // log
    if (service == null) {
        logger.debug("Extractor did not generate service.");
    } else {
        logger.debug("Extractor generated service for: {}", service.getId());
    }

    return service;
}

protected abstract WebApplicationService extractServiceInternal(HttpServletRequest request);

@Resource(name="serviceFactoryList")
protected List<ServiceFactory<? extends WebApplicationService>> serviceFactoryList;

protected final List<ServiceFactory<? extends WebApplicationService>> getServiceFactories() {
    return serviceFactoryList;
}
services-context.xml

<util:list id="serviceFactoryList" value-type="org.jasig.cas.authentication.principal.ServiceFactory">
    <ref bean="webApplicationServiceFactory" />
</util:list>

The DefaultArgumentExtractor implements the algorithm details extractServiceInternal. The code is as follows:

public WebApplicationService extractServiceInternal(final HttpServletRequest request) {
    for (final ServiceFactory<? extends WebApplicationService> factory : getServiceFactories()) {
        final WebApplicationService service = factory.createService(request);
        if (service != null) {
            // If the creation is successful, it will be returned directly
            logger.debug("Created {} based on {}", service, factory);
            return service;
        }
    }
    logger.debug("No service could be extracted based on the given request");
    return null;
}

[webapplicationservicefactory] corresponds to webapplicationservicefactory, and its createService method code is as follows:

public WebApplicationService createService(final HttpServletRequest request) {
    final String targetService = request.getParameter(CasProtocolConstants.PARAMETER_TARGET_SERVICE);
    final String service = request.getParameter(CasProtocolConstants.PARAMETER_SERVICE);
    final String serviceAttribute = (String) request.getAttribute(CasProtocolConstants.PARAMETER_SERVICE);
    final String method = request.getParameter(CasProtocolConstants.PARAMETER_METHOD);
    final String format = request.getParameter(CasProtocolConstants.PARAMETER_FORMAT);

    final String serviceToUse;
    if (StringUtils.isNotBlank(targetService)) {
        // targetService is preferred
        serviceToUse = targetService;
    } else if (StringUtils.isNotBlank(service)) {
        // Second, use the service in the request parameter 
        serviceToUse = service;
    } else {
        // Finally, use the service in the request attribute
        serviceToUse = serviceAttribute;
    }

    // Verify service information
    if (StringUtils.isBlank(serviceToUse)) {
        return null;
    }

    // Remove jsession information
    final String id = AbstractServiceFactory.cleanupUrl(serviceToUse);
    // Get the ticket information in the request parameter and take it as the artifact ID of the Service
    final String artifactId = request.getParameter(CasProtocolConstants.PARAMETER_TICKET);

    final Response.ResponseType type = HttpMethod.POST.name().equalsIgnoreCase(method) ? Response.ResponseType.POST
            : Response.ResponseType.REDIRECT;

    // Create SimpleWebApplicationServiceImpl
    final SimpleWebApplicationServiceImpl webApplicationService =
            new SimpleWebApplicationServiceImpl(id, serviceToUse,
                    artifactId, new WebApplicationServiceResponseBuilder(type));

    try {
        if (StringUtils.isNotBlank(format)) {
            // If format information exists in the request parameters, set this property
            final ValidationResponseType formatType = ValidationResponseType.valueOf(format.toUpperCase());
            webApplicationService.setFormat(formatType);
        }
    } catch (final Exception e) {
        logger.error("Format specified in the request [{}] is not recognized", format);
        return null;
    }
    return webApplicationService;
}

The Service created by WebApplicationServiceFactory is SimpleWebApplicationServiceImpl, which supports single sign out;

public final class SimpleWebApplicationServiceImpl extends AbstractWebApplicationService
public abstract class AbstractWebApplicationService implements SingleLogoutService
public interface SingleLogoutService extends WebApplicationService
public interface WebApplicationService extends Service

        2. Find the Service information registered in the system according to the Service object

The code is as follows:

final RegisteredService registeredService = this.servicesManager.findServiceBy(service);

private ServicesManager servicesManager;

@Autowired
public void setServicesManager(@Qualifier("servicesManager") final ServicesManager servicesManager) {
    this.servicesManager = servicesManager;
}

[servicesManager] corresponds to DefaultServicesManagerImpl. The code of findServiceBy method is as follows:

public RegisteredService findServiceBy(final Service service) {
    final Collection<RegisteredService> c = convertToTreeSet();

    // Traverse registered services
    for (final RegisteredService r : c) {
        if (r.matches(service)) {
            // If it matches, the registered Service will be returned
            return r;
        }
    }

    return null;
}

public TreeSet<RegisteredService> convertToTreeSet() {
    return new TreeSet<>(this.services.values());
}

private ConcurrentHashMap<Long, RegisteredService> services = new ConcurrentHashMap<>();

public void load() {
    final ConcurrentHashMap<Long, RegisteredService> localServices =
            new ConcurrentHashMap<>();

    // With this Service registrydao loads the registration information
    for (final RegisteredService r : this.serviceRegistryDao.load()) {
        LOGGER.debug("Adding registered service {}", r.getServiceId());
        localServices.put(r.getId(), r);
    }

    this.services = localServices;
    LOGGER.info("Loaded {} services from {}.", this.services.size(),
            this.serviceRegistryDao);
}

public DefaultServicesManagerImpl(@Qualifier("serviceRegistryDao") final ServiceRegistryDao serviceRegistryDao) {
    this.serviceRegistryDao = serviceRegistryDao;
    load();
}

There are many implementations of ServiceRegistryDao interface. You can configure the implementation of this interface in the configuration file according to the actual needs; Take InMemoryServiceRegistryDaoImpl as an example to analyze the implementation of the load method. The code is as follows:

public List<RegisteredService> load() {
    return this.registeredServices;
}

@PostConstruct
public void afterPropertiesSet() {
    final String[] aliases =
        this.applicationContext.getAutowireCapableBeanFactory().getAliases("inMemoryServiceRegistryDao");
    // If "inMemoryServiceRegistryDao" is configured
    if (aliases.length > 0) {
        LOGGER.debug("{} is used as the active service registry dao", this.getClass().getSimpleName());

        try {
            // Locate the configuration of "inMemoryRegisteredServices" from the IOC container
            final List<RegisteredService> list = (List<RegisteredService>)
                this.applicationContext.getBean("inMemoryRegisteredServices", List.class);
            if (list != null) {
                LOGGER.debug("Loaded {} services from the application context for {}",
                    list.size(),
                    this.getClass().getSimpleName());
                this.registeredServices = list;
            }
        } catch (final Exception e) {
            LOGGER.debug("No registered services are defined for {}", this.getClass().getSimpleName());
        }
    }
}

The configuration information of [inMemoryRegisteredServices] is as follows:

<util:list id="inMemoryRegisteredServices">
    <bean class="org.jasig.cas.services.RegexRegisteredService"
          p:id="0" p:name="HTTP and IMAP" p:description="Allows HTTP(S) and IMAP(S) protocols"
          p:serviceId="^(https?|imaps?)://.*" p:evaluationOrder="10000001" >
        <property name="attributeReleasePolicy">
            <bean class="org.jasig.cas.services.ReturnAllAttributeReleasePolicy" />
        </property>
    </bean>
</util:list>

The implementation class of the commonly used RegisteredService interface is RegexRegisteredService, which supports regular expressions. The implementation code of its match method is as follows:

public boolean matches(final Service service) {
    if (this.servicePattern == null) {
        this.servicePattern = RegexUtils.createPattern(this.serviceId);
    }
    // Match according to the regular rules of serviceId
    return service != null && this.servicePattern != null
            && this.servicePattern.matcher(service.getId()).matches();
}

Because there is no start state, the first action state is executed according to the process configuration. The configuration is as follows:

login-webflow.xml

<action-state id="ticketGrantingTicketCheck">
    <evaluate expression="ticketGrantingTicketCheckAction"/>
    <transition on="notExists" to="gatewayRequestCheck"/>
    <transition on="invalid" to="terminateSession"/>
    <transition on="valid" to="hasServiceCheck"/>
</action-state>

[ticketGrantingTicketCheck] corresponds to TicketGrantingTicketCheckAction, which inherits from AbstractAction and implements the algorithm details doExecute. The code is as follows:

protected Event doExecute(final RequestContext requestContext) throws Exception {
    // Get TGT information from context
    final String tgtId = WebUtils.getTicketGrantingTicketId(requestContext);
    if (!StringUtils.hasText(tgtId)) {
        // Not if it does not exist_ Exists event
        return new Event(this, NOT_EXISTS);
    }

    // Invalid default TGT
    String eventId = INVALID;
    try {
        // Get ticket information
        final Ticket ticket = this.centralAuthenticationService.getTicket(tgtId, Ticket.class);
        if (ticket != null && !ticket.isExpired()) {
            // TGT effective
            eventId = VALID;
        }
    } catch (final AbstractTicketException e) {
        logger.trace("Could not retrieve ticket id {} from registry.", e);
    }
    return new Event(this,  eventId);
}

During the login request, there is no TGT information in the cookie, so go to [notExists], i.e. [gatewayRequestCheck], and its configuration is as follows:

login-webflow.xml

<decision-state id="gatewayRequestCheck">
    <if test="requestParameters.gateway != '' and requestParameters.gateway != null and flowScope.service != null"
        then="gatewayServicesManagementCheck" else="serviceAuthorizationCheck"/>
</decision-state>

According to the expression, the login request without gateway information will go through [serviceAuthorizationCheck], and its configuration is as follows:

login-webflow.xml

<action-state id="serviceAuthorizationCheck">
    <evaluate expression="serviceAuthorizationCheck"/>
    <transition to="initializeLogin"/>
</action-state>

[serviceauthorizationcheck] corresponds to serviceauthorizationcheck, which inherits from AbstractAction and implements the algorithm details doExecute. The code is as follows:

protected Event doExecute(final RequestContext context) throws Exception {
    // Get Service information from context
    final Service service = WebUtils.getService(context);
    //No service == plain /login request. Return success indicating transition to the login form
    if (service == null) {
        // There is no Service returned directly
        return success();
    }
    
    if (this.servicesManager.getAllServices().isEmpty()) {
        // No Service is configured
        final String msg = String.format("No service definitions are found in the service manager. "
                + "Service [%s] will not be automatically authorized to request authentication.", service.getId());
        logger.warn(msg);
        throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_EMPTY_SVC_MGMR);
    }
    // Get matching registered services
    final RegisteredService registeredService = this.servicesManager.findServiceBy(service);

    if (registeredService == null) {
        // If there is no matching registered Service, the authorization verification fails
        final String msg = String.format("Service Management: Unauthorized Service Access. "
                + "Service [%s] is not found in service registry.", service.getId());
        logger.warn(msg);
        throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_UNAUTHZ_SERVICE, msg);
    }
    if (!registeredService.getAccessStrategy().isServiceAccessAllowed()) {
        // Without permission, authorization verification fails
        final String msg = String.format("Service Management: Unauthorized Service Access. "
                + "Service [%s] is not allowed access via the service registry.", service.getId());
        
        logger.warn(msg);
        // Set redirect URL
        WebUtils.putUnauthorizedRedirectUrlIntoFlowScope(context,
                registeredService.getAccessStrategy().getUnauthorizedRedirectUrl());
        throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_UNAUTHZ_SERVICE, msg);
    }

    return success();
}

The Service information carried in the login request can match the registered Service information and support access, so go to [initializeLogin], and its configuration is as follows:

login-webflow.xml

<action-state id="initializeLogin">
    <evaluate expression="'success'"/>
    <transition on="success" to="viewLoginForm"/>
</action-state>

According to the value of the expression, go directly to [viewLoginForm], and its configuration is as follows:

login-webflow.xml

<var name="credential" class="org.jasig.cas.authentication.UsernamePasswordCredential"/>
<view-state id="viewLoginForm" view="casLoginView" model="credential">
    <binder>
        <binding property="username" required="true"/>
        <binding property="password" required="true"/>

        <!--
        <binding property="rememberMe" />
        -->
    </binder>
    <on-entry>
        <set name="viewScope.commandName" value="'credential'"/>

        <!--
        <evaluate expression="samlMetadataUIParserAction" />
        -->
    </on-entry>
    <transition on="submit" bind="true" validate="true" to="realSubmit"/>
</view-state>

cas-servlet.xml

<bean id="urlBasedViewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver"
      p:viewClass="org.springframework.web.servlet.view.InternalResourceView"
      p:prefix="${cas.themeResolver.pathprefix:/WEB-INF/view/jsp}/"
      p:suffix=".jsp"
      p:order="2000"/>

According to the configuration of [urlBasedViewResolver] and registeredservicethemebasedviewresolver.this_location_pattern (% s/%s/ui /)

// viewName is casLoginView
final String defaultThemePrefix = String.format(THEME_LOCATION_PATTERN, getPrefix(), "default");
final String defaultViewUrl = defaultThemePrefix + viewName + getSuffix();

It can be seen that the corresponding page of [casLoginView] is / WEB-INF/view/jsp/default/ui/casLoginView.jsp. The main contents of this page are as follows:

<form:input cssClass="required" cssErrorClass="error" id="username" size="25" tabindex="1" accesskey="${userNameAccessKey}" path="username" autocomplete="off" htmlEscape="true" />
<form:password cssClass="required" cssErrorClass="error" id="password" size="25" tabindex="2" path="password"  accesskey="${passwordAccessKey}" htmlEscape="true" autocomplete="off" />
<input type="hidden" name="execution" value="${flowExecutionKey}" />
<input type="hidden" name="_eventId" value="submit" />
<input class="btn-submit" name="submit" accesskey="l" value="<spring:message code="screen.welcome.button.login" />" tabindex="6" type="submit" />
<input class="btn-reset" name="reset" accesskey="c" value="<spring:message code="screen.welcome.button.clear" />" tabindex="7" type="reset" />

casLoginView. The parameters submitted by the JSP page are bound to the UsernamePasswordCredential object. After the page is submitted, go to realSubmit. Its configuration is as follows:

login-webflow.xml

<action-state id="realSubmit">
    <evaluate
            expression="authenticationViaFormAction.submit(flowRequestContext, flowScope.credential, messageContext)"/>
    <transition on="warn" to="warn"/>
    <!--
    To enable AUP workflows, replace the 'success' transition with the following:
    <transition on="success" to="acceptableUsagePolicyCheck" />
    -->
    <transition on="success" to="sendTicketGrantingTicket"/>
    <transition on="successWithWarnings" to="showMessages"/>
    <transition on="authenticationFailure" to="handleAuthenticationFailure"/>
    <transition on="error" to="initializeLogin"/>
</action-state>

[authenticationviaformaction] corresponds to authenticationviaformaction. The code of its submit method is as follows:

public final Event submit(final RequestContext context, final Credential credential,
                          final MessageContext messageContext)  {
    if (isRequestAskingForServiceTicket(context)) {
        // If ST is requested, create ST
        return grantServiceTicket(context, credential);
    }

    // Create TGT
    return createTicketGrantingTicket(context, credential, messageContext);
}

protected boolean isRequestAskingForServiceTicket(final RequestContext context) {
    // Get TGT information from context
    final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);
    // Get Service information from context
    final Service service = WebUtils.getService(context);
    // If the "renew" attribute of the request exists and both TGT information and Service information exist, the request is considered to be used to obtain ST
    // The login request does not meet the determination condition
    return (StringUtils.isNotBlank(context.getRequestParameters().get(CasProtocolConstants.PARAMETER_RENEW))
            && ticketGrantingTicketId != null
            && service != null);
}

protected Event createTicketGrantingTicket(final RequestContext context, final Credential credential,
                                           final MessageContext messageContext) {
    try {
        final Service service = WebUtils.getService(context);
        final AuthenticationContextBuilder builder = new DefaultAuthenticationContextBuilder(
                this.authenticationSystemSupport.getPrincipalElectionStrategy());
        // Login parameters submitted by packaging page
        final AuthenticationTransaction transaction =
                AuthenticationTransaction.wrap(credential);
        // Authentication login parameters
        this.authenticationSystemSupport.getAuthenticationTransactionManager().handle(transaction,  builder);
        final AuthenticationContext authenticationContext = builder.build(service);

        // After the verification is passed, a TGT is created
        final TicketGrantingTicket tgt = this.centralAuthenticationService.createTicketGrantingTicket(authenticationContext);
        // Put TGT into context
        WebUtils.putTicketGrantingTicketInScopes(context, tgt);
        WebUtils.putWarnCookieIfRequestParameterPresent(this.warnCookieGenerator, context);
        putPublicWorkstationToFlowIfRequestParameterPresent(context);
        if (addWarningMessagesToMessageContextIfNeeded(tgt, messageContext)) {
            return newEvent(SUCCESS_WITH_WARNINGS);
        }
        return newEvent(AbstractCasWebflowConfigurer.TRANSITION_ID_SUCCESS);
    } catch (final AuthenticationException e) {
        logger.debug(e.getMessage(), e);
        // Verification failed
        return newEvent(AUTHENTICATION_FAILURE, e);
    } catch (final Exception e) {
        logger.debug(e.getMessage(), e);
        // Process handling exception
        return newEvent(AbstractCasWebflowConfigurer.TRANSITION_ID_ERROR, e);
    }
}

The core logic is as follows:

  1. Packaging login parameters;
  2. Authentication login parameters;
  3. Create TGT;

        1. Packaging login parameters

In fact, the valid Credential is packaged as an AuthenticationTransaction. The code is as follows:

public static AuthenticationTransaction wrap(final Credential... credentials) {
    return new AuthenticationTransaction(sanitizeCredentials(credentials));
}

private static Set<Credential> sanitizeCredentials(final Credential[] credentials) {
    if (credentials != null && credentials.length > 0) {
        final Set<Credential> set = new HashSet<>(Arrays.asList(credentials));
        final Iterator<Credential> it = set.iterator();
        while (it.hasNext()) {
            if (it.next() == null) {
                // Filter invalid values
                it.remove();
            }
        }
        return set;
    }
    return Collections.emptySet();
}

        2. Authentication login parameters

Implement authentication with PolicyBasedAuthenticationManager and store the authentication results in AuthenticationContextBuilder. The code is as follows:

public AuthenticationTransactionManager handle(final AuthenticationTransaction authenticationTransaction,
                                               final AuthenticationContextBuilder authenticationContext)
        throws AuthenticationException {
    if (!authenticationTransaction.getCredentials().isEmpty()) {
        // Complete authentication with PolicyBasedAuthenticationManager
        final Authentication authentication = this.authenticationManager.authenticate(authenticationTransaction);
        LOGGER.debug("Successful authentication; Collecting authentication result [{}]", authentication);
        // Collect authentication results
        authenticationContext.collect(authentication);
    }
    LOGGER.debug("Transaction ignored since there are no credentials to authenticate");
    return this;
}

PolicyBasedAuthenticationManager implements the AuthenticationManager interface. The core code is as follows:

public Authentication authenticate(final AuthenticationTransaction transaction) throws AuthenticationException {

    // authentication 
    final AuthenticationBuilder builder = authenticateInternal(transaction.getCredentials());
    final Authentication authentication = builder.build();
    final Principal principal = authentication.getPrincipal();
    if (principal instanceof NullPrincipal) {
        // Authentication failed
        throw new UnresolvedPrincipalException(authentication);
    }

    // Add the authentication method attribute, that is, AuthenticationHandler
    addAuthenticationMethodAttribute(builder, authentication);

    logger.info("Authenticated {} with credentials {}.", principal, transaction.getCredentials());
    logger.debug("Attribute map for {}: {}", principal.getId(), principal.getAttributes());

    // Fill in authentication metadata attribute
    populateAuthenticationMetadataAttributes(builder, transaction.getCredentials());

    // Building Authentication
    return builder.build();
}

protected AuthenticationBuilder authenticateInternal(final Collection<Credential> credentials)
        throws AuthenticationException {

    // Initial constructor
    final AuthenticationBuilder builder = new DefaultAuthenticationBuilder(NullPrincipal.getInstance());
    // Populate Credential data
    for (final Credential c : credentials) {
        builder.addCredential(new BasicCredentialMetaData(c));
    }
    boolean found;

    // Traverse Credential
    for (final Credential credential : credentials) {
        found = false;
        // Traverse the configured AuthenticationHandler and PrincipalResolver MAP
        for (final Map.Entry<AuthenticationHandler, PrincipalResolver> entry : this.handlerResolverMap.entrySet()) {
            final AuthenticationHandler handler = entry.getKey();
            // Does the current AuthenticationHandler support processing the current Credential
            if (handler.supports(credential)) {
                // There is an AuthenticationHandler that handles the current Credential
                found = true;
                try {
                    // Authenticate and parse Principal
                    authenticateAndResolvePrincipal(builder, credential, entry.getValue(), handler);
                    // Determine whether the exit conditions are met
                    if (this.authenticationPolicy.isSatisfiedBy(builder.build())) {
                        return builder;
                    }
                } catch (final GeneralSecurityException e) {
                    logger.info("{} failed authenticating {}", handler.getName(), credential);
                    logger.debug("{} exception details: {}", handler.getName(), e.getMessage());
                    builder.addFailure(handler.getName(), e.getClass());
                } catch (final PreventedException e) {
                    logger.error("{}: {}  (Details: {})", handler.getName(), e.getMessage(), e.getCause().getMessage());
                    builder.addFailure(handler.getName(), e.getClass());
                }
            }
        }
        if (!found) {
            logger.warn(
                    "Cannot find authentication handler that supports [{}] of type [{}], which suggests a configuration problem.",
                    credential, credential.getClass().getSimpleName());
        }
    }
    // Verify the generated authentication context
    // If there is no successfully authenticated HandlerResult or the conditions of AuthenticationPolicy are not met, the corresponding exception will be thrown
    evaluateProducedAuthenticationContext(builder);

    return builder;
}

The core logic is as follows:

  1. Traverse the Credential set and authenticate each Credential;
  2. For the current Credential, traverse the configured handlerResolverMap to find the matching AuthenticationHandler;
  3. Authenticate the current Credential according to the matched AuthenticationHandler and PrincipalResolver;
    1. If authentication is successful, determine whether to end authentication according to the conditions of AuthenticationPolicy;
      1. After that, the authentication result is returned directly;
      2. If not, continue to the next round of AuthenticationHandler and PrincipalResolver, and then continue to the next round of Credential;
    2. If authentication fails, AuthenticationBuilder records the failure information;
  4. After traversing the Credential set, verify the AuthenticationBuilder;

2.1} traversing the Credential set

In the login request processing process, there is only one Credential in the Credential set;

2.2} traverse the configured handlerResolverMap

The configuration information of handlerResolverMap is as follows:

deployerConfigContext.xml

<util:map id="authenticationHandlersResolvers">
    <entry key-ref="proxyAuthenticationHandler" value-ref="proxyPrincipalResolver" />
    <entry key-ref="primaryAuthenticationHandler" value-ref="primaryPrincipalResolver" />
</util:map>

<alias name="acceptUsersAuthenticationHandler" alias="primaryAuthenticationHandler" />
<alias name="personDirectoryPrincipalResolver" alias="primaryPrincipalResolver" />

[acceptusersauthenticationhandler] corresponds to acceptusersauthenticationhandler. Its matching logic is inherited from the parent class AbstractUsernamePasswordAuthenticationHandler. The code is as follows:

public boolean supports(final Credential credential) {
    // Whether it is of UsernamePasswordCredential type,
    return credential instanceof UsernamePasswordCredential;
}

[proxyAuthenticationHandler] corresponds to HttpBasedServiceCredentialsAuthenticationHandler. The matching logic is as follows:

public boolean supports(final Credential credential) {
    return credential instanceof HttpBasedServiceCredential;
}
login-webflow.xml

<var name="credential" class="org.jasig.cas.authentication.UsernamePasswordCredential"/>

According to the configuration defined by [credential], accepturussauthenticationhandler will process the credential submitted by this login request, and accepturussauthenticationhandler will not process the credential submitted by this login request;

2.3. AuthenticationHandler and PrincipalResolver authenticate the current Credential

The authentication code is as follows:

private void authenticateAndResolvePrincipal(final AuthenticationBuilder builder, final Credential credential,
                                             final PrincipalResolver resolver, final AuthenticationHandler handler)
        throws GeneralSecurityException, PreventedException {

    Principal principal;
    // Authentication is completed by AuthenticationHandler
    final HandlerResult result = handler.authenticate(credential);
    // Authentication succeeded, and the AuthenticationHandler information is recorded
    builder.addSuccess(handler.getName(), result);
    logger.info("{} successfully authenticated {}", handler.getName(), credential);

    if (resolver == null) {
        // If the parser is empty, the Principal is taken from HandlerResult
        principal = result.getPrincipal();
        logger.debug(
                "No resolver configured for {}. Falling back to handler principal {}",
                handler.getName(),
                principal);
    } else {
        // Parsing Principal
        principal = resolvePrincipal(handler.getName(), resolver, credential);
        
        if (principal == null) {
            logger.warn("Principal resolution handled by {} produced a null principal. "
                       + "This is likely due to misconfiguration or missing attributes; CAS will attempt to use the principal "
                       + "produced by the authentication handler, if any.", resolver.getClass().getSimpleName());
            // If the resolution fails, the Principal is obtained from HandlerResult
            principal = result.getPrincipal();
        }
    }
    // Must avoid null principal since AuthenticationBuilder/ImmutableAuthentication
    // require principal to be non-null
    if (principal != null) {
        // Set Principal
        builder.setPrincipal(principal);
    }
    
    logger.debug("Final principal resolved for this authentication event is {}", principal);
}

Firstly, the AuthenticationHandler, AcceptUsersAuthenticationHandler, authenticates the current Credential. Its parent class AbstractPreAndPostProcessingAuthenticationHandler implements the algorithm template of authenticate and provides the default implementation of preAuthenticate and postAuthenticate algorithm details. It can be rewritten by subclasses. It provides doAuthentication algorithm details, which are implemented by subclasses, The code is as follows:

public final HandlerResult authenticate(final Credential credential)
        throws GeneralSecurityException, PreventedException {
    // Authentication preprocessing
    if (!preAuthenticate(credential)) {
        throw new FailedLoginException();
    }

    // Post authentication processing
    return postAuthenticate(credential, doAuthentication(credential));
}

protected boolean preAuthenticate(final Credential credential) {
    return true;
}

protected HandlerResult postAuthenticate(final Credential credential, final HandlerResult result) {
    return result;
}

protected abstract HandlerResult doAuthentication(Credential credential)
        throws GeneralSecurityException, PreventedException;

// Provide an interface to create HandlerResult for subclass implementation
protected final HandlerResult createHandlerResult(final Credential credential, final Principal principal,
                                                  final List<MessageDescriptor> warnings) {
    return new DefaultHandlerResult(this, new BasicCredentialMetaData(credential), principal, warnings);
}

The parent class AbstractUsernamePasswordAuthenticationHandler of AcceptUsersAuthenticationHandlerde implements the algorithm template of abstract doAuthentication algorithm details, and provides authenticateUsernamePasswordInternal algorithm details. It is implemented by subclasses, and its code is as follows:

protected final HandlerResult doAuthentication(final Credential credential)
        throws GeneralSecurityException, PreventedException {
    final UsernamePasswordCredential userPass = (UsernamePasswordCredential) credential;
    if (userPass.getUsername() == null) {
        // Throw an exception if there is no user name
        throw new AccountNotFoundException("Username is null.");
    }
    
    // Policy mode for user name conversion is supported
    // Nopprincipalnametransformer is used by default without conversion
    final String transformedUsername= this.principalNameTransformer.transform(userPass.getUsername());
    if (transformedUsername == null) {
        throw new AccountNotFoundException("Transformed username is null.");
    }
    // Update to converted user name
    userPass.setUsername(transformedUsername);
    // Authentication converted data
    return authenticateUsernamePasswordInternal(userPass);
}

protected abstract HandlerResult authenticateUsernamePasswordInternal(UsernamePasswordCredential transformedCredential)
        throws GeneralSecurityException, PreventedException;

AcceptUsersAuthenticationHandlerde implements the details of the authenticateUsernamePasswordInternal algorithm. The code is as follows:

private static final String DEFAULT_SEPARATOR = "::";
private static final Pattern USERS_PASSWORDS_SPLITTER_PATTERN = Pattern.compile(DEFAULT_SEPARATOR);

private Map<String, String> users;

@Value("${accept.authn.users:}")
private String acceptedUsers;

@PostConstruct
public void init() {
    if (StringUtils.isNotBlank(this.acceptedUsers) && this.users == null) {
        // If the configured user information (configured by cas.properties) exists, the currently cached user information does not exist, and the configured user information is used
        // Configuration information format: username::password
        final Set<String> usersPasswords = org.springframework.util.StringUtils.commaDelimitedListToSet(this.acceptedUsers);
        final Map<String, String> parsedUsers = new HashMap<>();
        for (final String usersPassword : usersPasswords) {
            final String[] splitArray = USERS_PASSWORDS_SPLITTER_PATTERN.split(usersPassword);
            parsedUsers.put(splitArray[0], splitArray[1]);
        }
        setUsers(parsedUsers);
    }
}

protected final HandlerResult authenticateUsernamePasswordInternal(final UsernamePasswordCredential credential)
        throws GeneralSecurityException, PreventedException {

    if (users == null || users.isEmpty()) {
        // If the current user information does not exist, login is not supported
        throw new FailedLoginException("No user can be accepted because none is defined");
    }
    final String username = credential.getUsername();
    final String cachedPassword = this.users.get(username);


    if (cachedPassword == null) {
        // If the password corresponding to the user does not exist, the account does not exist
       logger.debug("{} was not found in the map.", username);
       throw new AccountNotFoundException(username + " not found in backing map.");
    }

    // Encrypt submitted password information
    // PlainTextPasswordEncoder is used by default, i.e. no encryption
    final String encodedPassword = this.getPasswordEncoder().encode(credential.getPassword());
    // Compare password information
    if (!cachedPassword.equals(encodedPassword)) {
        throw new FailedLoginException();
    }
    // If the password verification passes, a HandlerResult is created
    // By default, DefaultPrincipalFactory is used to create SimplePrincipal, and its attribute is empty MAP
    return createHandlerResult(credential, this.principalFactory.createPrincipal(username), null);
}

public final void setUsers(@NotNull final Map<String, String> users) {
    this.users = Collections.unmodifiableMap(users);
}

User information is transferred in two ways:

        1. Inject through setUsers, with high priority;

deployerConfigContext.xml

<bean id="acceptUsersAuthenticationHandler"
      class="org.jasig.cas.authentication.AcceptUsersAuthenticationHandler">
    <property name="users">
        <map>
            <entry key="casuser" value="Mellon"/>
        </map>
    </property>
</bean>

        2. Through CAS Properties configuration, low priority;

cas.properties

accept.authn.users=casuser::Mellon

If the AuthenticationHandler fails to authenticate, an exception is thrown to end; If the AuthenticationHandler is authenticated successfully, the PrincipalResolver is used to parse the Principal information of the current Credential. The code is as follows:

protected Principal resolvePrincipal(
        final String handlerName, final PrincipalResolver resolver, final Credential credential) {
    // Does the current PrincipalResolver support resolving credentials
    if (resolver.supports(credential)) {
        try {
            // Resolve the Principal information of the current Credential
            final Principal p = resolver.resolve(credential);
            logger.debug("{} resolved {} from {}", resolver, p, credential);
            return p;
        } catch (final Exception e) {
            logger.error("{} failed to resolve principal from {}", resolver, credential, e);
        }
    } else {
        logger.warn(
                "{} is configured to use {} but it does not support {}, which suggests a configuration problem.",
                handlerName,
                resolver,
                credential);
    }
    return null;
}
deployerConfigContext.xml

<alias name="personDirectoryPrincipalResolver" alias="primaryPrincipalResolver" />

According to the configuration of [primaryPrincipalResolver], [persondirectoryprincipalresolver] corresponds to persondirectoryprincipalresolver, which will process the Credential submitted by this login request. The code is as follows:

/**
 * Does this PrincipalResolver support handling Credential
 */
public boolean supports(final Credential credential) {
    // It is supported as long as the ID of the Credential exists
    return credential != null && credential.getId() != null;
}

/**
 * Resolve Credential
 */
public Principal resolve(final Credential credential) {
    logger.debug("Attempting to resolve a principal...");

    final String principalId = extractPrincipalId(credential);

    // Verify principalId
    if (principalId == null) {
        logger.debug("Got null for extracted principal ID; returning null.");
        return null;
    }

    logger.debug("Creating SimplePrincipal for [{}]", principalId);

    // Gets the property collection corresponding to the principalId
    final Map<String, List<Object>> attributes = retrievePersonAttributes(principalId, credential);

    // Verification attribute set
    if (attributes == null || attributes.isEmpty()) {
        logger.debug("Principal id [{}] did not specify any attributes", principalId);

        // Return null directly
        if (!this.returnNullIfNoAttributes) {
            logger.debug("Returning the principal with id [{}] without any attributes", principalId);
            // Return null property Principal
            return this.principalFactory.createPrincipal(principalId);
        }
        logger.debug("[{}] is configured to return null if no attributes are found for [{}]",
                this.getClass().getName(), principalId);
        return null;
    }
    logger.debug("Retrieved [{}] attribute(s) from the repository", attributes.size());

    // When converting the attribute collection, the principalId parameter may be updated if this. Is configured Principalattributename attribute
    final Pair<String, Map<String, Object>> pair = convertPersonAttributesToPrincipal(principalId, attributes);
    // Create a Principal based on the principalId and attribute set
    return this.principalFactory.createPrincipal(pair.getFirst(), pair.getSecond());
}

protected String extractPrincipalId(final Credential credential) {
    return credential.getId();
}

protected Map<String, List<Object>> retrievePersonAttributes(final String principalId, final Credential credential) {
    // Query user attributes from IPersonAttributeDao
    final IPersonAttributes personAttributes = this.attributeRepository.getPerson(principalId);
    final Map<String, List<Object>> attributes;

    // Verify the queried user attributes
    if (personAttributes == null) {
        attributes = null;
    } else {
        attributes = personAttributes.getAttributes();
    }
    return attributes;
}

protected Pair<String, Map<String, Object>> convertPersonAttributesToPrincipal(final String extractedPrincipalId,
                                                                               final Map<String, List<Object>> attributes) {
    final Map<String, Object> convertedAttributes = new HashMap<>();
    // Default principalId
    String principalId = extractedPrincipalId;
    for (final Map.Entry<String, List<Object>> entry : attributes.entrySet()) {
        final String key = entry.getKey();
        final List<Object> values = entry.getValue();
        if (StringUtils.isNotBlank(this.principalAttributeName)
                && key.equalsIgnoreCase(this.principalAttributeName)) {
            // If this is configured Principalattributename attribute, the first attribute corresponding to the attribute value is used as the principalId
            if (values.isEmpty()) {
                logger.debug("{} is empty, using {} for principal", this.principalAttributeName, extractedPrincipalId);
            } else {
                // Update principalId
                principalId = values.get(0).toString();
                logger.debug(
                        "Found principal attribute value {}; removing {} from attribute map.",
                        extractedPrincipalId,
                        this.principalAttributeName);
            }
        } else {
            // Save attribute values
            convertedAttributes.put(key, values.size() == 1 ? values.get(0) : values);
        }
    }

    return new Pair<>(principalId, convertedAttributes);
}

Firstly, we analyze the method to obtain the user attribute set: retrievePersonAttributes, which uses this The attributerepository attribute is completed. The core code of the assignment logic of the attribute is as follows:

// Stubbersonattributedao is used by default
protected IPersonAttributeDao attributeRepository = new StubPersonAttributeDao(new HashMap<String, List<Object>>());

// It can be customized through Setter injection
public final void setAttributeRepository(@Qualifier("attributeRepository")
                                         final IPersonAttributeDao attributeRepository) {
    this.attributeRepository = attributeRepository;
}

[attributeRepository] is defined in the deployerConfigContext.xml configuration file. The configuration is as follows:

deployerConfigContext.xml

<bean id="attributeRepository" class="org.jasig.services.persondir.support.NamedStubPersonAttributeDao"
      p:backingMap-ref="attrRepoBackingMap" />

<util:map id="attrRepoBackingMap">
    <entry key="uid" value="uid" />
    <entry key="eduPersonAffiliation" value="eduPersonAffiliation" />
    <entry key="groupMembership" value="groupMembership" />
    <entry>
        <key><value>memberOf</value></key>
        <list>
            <value>faculty</value>
            <value>staff</value>
            <value>org</value>
        </list>
    </entry>
</util:map>

The getPerson implementation of NamedStubPersonAttributeDao inherits from the parent class StubPersonAttributeDao. The code is as follows:

public IPersonAttributes getPerson(String uid) {
  if (uid == null) {
    throw new IllegalArgumentException("Illegal to invoke getPerson(String) with a null argument.");
  } else {
    // Directly return this Backingperson, i.e. "attrRepoBackingMap" in the configuration file
    return this.backingPerson;
  }
}

Then analyze the logic of convertPersonAttributesToPrincipal, which will be based on the configured this Principalattributename attribute (if configured), take the first value from the attribute set corresponding to the attribute value to update the principalId information. The core code of the assignment logic of this attribute is as follows:

// Optional attribute values
protected String principalAttributeName;

// It can be customized through Setter injection
public void setPrincipalAttributeName(@Value("${cas.principal.resolver.persondir.principal.attribute:}")
                                      final String attribute) {
    this.principalAttributeName = attribute;
}

This parameter can be configured in CAS In the properties file, the default value is:

cas.properties

#cas.principal.resolver.persondir.principal.attribute=cn

        2.3. 1. Authentication is successful. Determine whether to end authentication according to the conditions of AuthenticationPolicy

After successful authentication, it will go through this Authenticationpolicy determines whether the current authentication process can be ended. The core code of the assignment logic of this attribute is as follows:

// The default is AnyAuthenticationPolicy, which is satisfied as long as there is a successfully authenticated Credential
private AuthenticationPolicy authenticationPolicy = new AnyAuthenticationPolicy();

// It can be customized through Setter injection
@Resource(name="authenticationPolicy")
public void setAuthenticationPolicy(final AuthenticationPolicy policy) {
    this.authenticationPolicy = policy;
}

Analyze the implementation of isSatisfiedBy of AuthenticationPolicy. The code is as follows:

// Default false
private boolean tryAll;

// It can be customized through Setter injection
public void setTryAll(@Value("${cas.authn.policy.any.tryall:false}") final boolean tryAll) {
    this.tryAll = tryAll;
}

public boolean isSatisfiedBy(final Authentication authn) {
    // Whether to traverse all. The default is false. Users can use CAS Properties configuration
    if (this.tryAll) {
        return authn.getCredentials().size() == authn.getSuccesses().size() + authn.getFailures().size();
    }
    // As long as there is a Credential with successful authentication
    return !authn.getSuccesses().isEmpty();
}

        2.3. 2. Authentication failed. AuthenticationBuilder records the failure information

After authentication fails, AuthenticationBuilder records the failure information, then continues the next round of AuthenticationHandler and PrincipalResolver, and then continues the next round of Credential;

2.4 verifying AuthenticationBuilder

After traversing the Credential set, the validity of AuthenticationBuilder will be verified before exiting normally. If there is no Credential with successful authentication or this If the end condition of authenticationpolicy, an exception is thrown. The code is as follows:

private void evaluateProducedAuthenticationContext(final AuthenticationBuilder builder) throws AuthenticationException {
    // We apply an implicit security policy of at least one successful authentication
    if (builder.getSuccesses().isEmpty()) {
        throw new AuthenticationException(builder.getFailures(), builder.getSuccesses());
    }
    // Apply the configured security policy
    if (!this.authenticationPolicy.isSatisfiedBy(builder.build())) {
        throw new AuthenticationException(builder.getFailures(), builder.getSuccesses());
    }
}

After authenticateInternal authentication, the validity of the Principal of the authentication result will be verified again (NullPrincipal may be returned in the authentication process), then the information of AuthenticationHandler will be added, and then the metadata attribute will be filled through the configured this.authenticationMetaDataPopulators, and finally the authentication result will be returned;

At this point, the authentication process ends;

        3. Create TGT

After the authentication is successful, this The centralauthenticationservice attribute creates TGT information. The core code of the assignment logic of this attribute is as follows:

@Autowired
@Qualifier("centralAuthenticationService")
private CentralAuthenticationService centralAuthenticationService;

[centralAuthenticationService] corresponds to CentralAuthenticationServiceImpl, and its TGT creation code is as follows:

public TicketGrantingTicket createTicketGrantingTicket(final AuthenticationContext context)
        throws AuthenticationException, AbstractTicketException {

    // Obtain authentication results
    final Authentication authentication = context.getAuthentication();
    // Get TGT create factory
    final TicketGrantingTicketFactory factory = this.ticketFactory.get(TicketGrantingTicket.class);
    // Create TGT based on authentication results
    final TicketGrantingTicket ticketGrantingTicket = factory.create(authentication);

    // TGT created by cache
    this.ticketRegistry.addTicket(ticketGrantingTicket);

    // Publish TGT creation event
    doPublishEvent(new CasTicketGrantingTicketCreatedEvent(this, ticketGrantingTicket));

    return ticketGrantingTicket;
}

this. The TicketFactory property stores the TicketFactory information to be configured. The core code of the assignment logic of this property is as follows:

@Resource(name="defaultTicketFactory")
protected TicketFactory ticketFactory;

[defaultticketfactory] corresponds to defaultticketfactory. The core code is:

// Save instances of various ticketfactories
private Map<String, Object> factoryMap;

@Autowired
@Qualifier("defaultProxyTicketFactory")
private ProxyTicketFactory proxyTicketFactory;


@Autowired
@Qualifier("defaultServiceTicketFactory")
private ServiceTicketFactory serviceTicketFactory;


@Autowired
@Qualifier("defaultTicketGrantingTicketFactory")
private TicketGrantingTicketFactory ticketGrantingTicketFactory;


@Autowired
@Qualifier("defaultProxyGrantingTicketFactory")
private ProxyGrantingTicketFactory proxyGrantingTicketFactory;

@PostConstruct
public void initialize() {
    this.factoryMap = new HashMap<>();

    validateFactoryInstances();

    this.factoryMap.put(ProxyGrantingTicket.class.getCanonicalName(), this.proxyGrantingTicketFactory);
    this.factoryMap.put(TicketGrantingTicket.class.getCanonicalName(), this.ticketGrantingTicketFactory);
    this.factoryMap.put(ServiceTicket.class.getCanonicalName(), this.serviceTicketFactory);
    this.factoryMap.put(ProxyTicket.class.getCanonicalName(), this.proxyTicketFactory);
}

public <T extends TicketFactory> T get(final Class<? extends Ticket> clazz) {
    validateFactoryInstances();

    // Find the corresponding instance from the cached factoryMap according to class
    return (T) this.factoryMap.get(clazz.getCanonicalName());
}

Through this ticketFactory. Get (TicketGrantingTicket.class) can find [defaultTicketGrantingTicketFactory], namely DefaultTicketGrantingTicketFactory, and then call its create method to create TGT. The code is as follows:

public <T extends TicketGrantingTicket> T create(final Authentication authentication) {
    final TicketGrantingTicket ticketGrantingTicket = new TicketGrantingTicketImpl(
            this.ticketGrantingTicketUniqueTicketIdGenerator.getNewTicketId(TicketGrantingTicket.PREFIX),
            authentication, ticketGrantingTicketExpirationPolicy);
    return (T) ticketGrantingTicket;
}

The final created TGT is TicketGrantingTicketImpl, and its id value is determined by this The ticketgrantingticketuniqueticketidgenerator attribute is generated. The core code of the assignment logic of this attribute is as follows:

@NotNull
@Resource(name="ticketGrantingTicketUniqueIdGenerator")
protected UniqueTicketIdGenerator ticketGrantingTicketUniqueTicketIdGenerator;

[ticketgrantingticketiqueidgenerator] corresponds to TicketGrantingTicketIdGenerator. Its getNewTicketId method inherits from the parent defaultuniquetickeidgenerator. The code is as follows:

public final String getNewTicketId(final String prefix) {
    final String number = this.numericGenerator.getNextNumberAsString();
    final StringBuilder buffer = new StringBuilder(prefix.length() + 2
            + (StringUtils.isNotBlank(this.suffix) ? this.suffix.length() : 0) + this.randomStringGenerator.getMaxLength()
            + number.length());

    buffer.append(prefix);
    buffer.append('-');
    buffer.append(number);
    buffer.append('-');
    buffer.append(this.randomStringGenerator.getNewString());

    if (this.suffix != null) {
        buffer.append(this.suffix);
    }

    return buffer.toString();
}

At this point, the TGT creation process ends;

After the TGT is successfully created, it will go through the [success] process, that is, [gatewayRequestCheck], and its configuration is as follows:

login-webflow.xml

<action-state id="sendTicketGrantingTicket">
    <evaluate expression="sendTicketGrantingTicketAction"/>
    <transition to="serviceCheck"/>
</action-state>

[sendticketgrantingticketaction] corresponds to sendticketgrantingticketaction, which inherits from AbstractAction and implements the algorithm details doExecute. The code is as follows:

protected Event doExecute(final RequestContext context) {
    // Get TGT from context
    final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);
    // Get TGT from cookie
    final String ticketGrantingTicketValueFromCookie = (String) context.getFlowScope().get("ticketGrantingTicketId");

    if (ticketGrantingTicketId == null) {
        // TGT direct return does not exist in the context
        return success();
    }


    if (isAuthenticatingAtPublicWorkstation(context))  {
        // If you authenticate through the public workbench, no cookie s will be generated
        LOGGER.info("Authentication is at a public workstation. "
                + "SSO cookie will not be generated. Subsequent requests will be challenged for authentication.");
    } else if (!this.createSsoSessionCookieOnRenewAuthentications && isAuthenticationRenewed(context)) {
        LOGGER.info("Authentication session is renewed but CAS is not configured to create the SSO session. "
                + "SSO cookie will not be generated. Subsequent requests will be challenged for authentication.");
    } else {
        LOGGER.debug("Setting TGC for current session.");
        // Set TGT cookie
        this.ticketGrantingTicketCookieGenerator.addCookie(WebUtils.getHttpServletRequest(context), WebUtils
            .getHttpServletResponse(context), ticketGrantingTicketId);
    }

    if (ticketGrantingTicketValueFromCookie != null && !ticketGrantingTicketId.equals(ticketGrantingTicketValueFromCookie)) {
        // If the TGT in the original cookie is different from the TGT in the context, the TGT in the original cookie will be destroyed
        this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketValueFromCookie);
    }

    return success();
}

Destroying TGT is implemented by CentralAuthenticationServiceImpl. The code is as follows:

public List<LogoutRequest> destroyTicketGrantingTicket(@NotNull final String ticketGrantingTicketId) {
    try {
        logger.debug("Removing ticket [{}] from registry...", ticketGrantingTicketId);
        // Query TGT according to TGT ID
        final TicketGrantingTicket ticket = getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);
        logger.debug("Ticket found. Processing logout requests and then deleting the ticket...");
        // Log out the single sign out service using the TGT
        final List<LogoutRequest> logoutRequests = logoutManager.performLogout(ticket);
        // Remove TGT from cache
        this.ticketRegistry.deleteTicket(ticketGrantingTicketId);

        // Release TGT destruction event
        doPublishEvent(new CasTicketGrantingTicketDestroyedEvent(this, ticket));

        return logoutRequests;
    } catch (final InvalidTicketException e) {
        logger.debug("TicketGrantingTicket [{}] cannot be found in the ticket registry.", ticketGrantingTicketId);
    }
    return Collections.emptyList();
}

logoutManager.performLogout is implemented by logotmanagerimpl. The code is as follows:

public List<LogoutRequest> performLogout(final TicketGrantingTicket ticket) {
    // Gets the Service collection of the current TGT binding
    final Map<String, Service> services = ticket.getServices();
    final List<LogoutRequest> logoutRequests = new ArrayList<>();
    // if SLO is not disabled
    if (!this.singleLogoutCallbacksDisabled) {
        // Enable SLO function
        // through all services
        for (final Map.Entry<String, Service> entry : services.entrySet()) {
            // it's a SingleLogoutService, else ignore
            final Service service = entry.getValue();
            if (service instanceof SingleLogoutService) {
                // If it is a single sign out service, you need to log out
                final LogoutRequest logoutRequest = handleLogoutForSloService((SingleLogoutService) service, entry.getKey());
                if (logoutRequest != null) {
                    LOGGER.debug("Captured logout request [{}]", logoutRequest);
                    logoutRequests.add(logoutRequest);
                }
            }
        }
    }

    return logoutRequests;
}

private LogoutRequest handleLogoutForSloService(final SingleLogoutService singleLogoutService, final String ticketId) {

    // Is the service logged out
    if (!singleLogoutService.isLoggedOutAlready()) {
        // Found registered service
        final RegisteredService registeredService = servicesManager.findServiceBy(singleLogoutService);
        
        // Determine whether the service supports single sign out
        if (serviceSupportsSingleLogout(registeredService)) {

            // Gets the logout URL of the service
            final URL logoutUrl = determineLogoutUrl(registeredService, singleLogoutService);
            // Encapsulate logout request
            final DefaultLogoutRequest logoutRequest = new DefaultLogoutRequest(ticketId, singleLogoutService, logoutUrl);
            // Gets the logout type of the service
            final LogoutType type = registeredService.getLogoutType() == null
                    ? LogoutType.BACK_CHANNEL : registeredService.getLogoutType();

            // Perform corresponding operations according to the login type of the service
            switch (type) {
                case BACK_CHANNEL:
                    // Attempt to send a logout request
                    if (performBackChannelLogout(logoutRequest)) {
                        logoutRequest.setStatus(LogoutRequestStatus.SUCCESS);
                    } else {
                        logoutRequest.setStatus(LogoutRequestStatus.FAILURE);
                        LOGGER.warn("Logout message not sent to [{}]; Continuing processing...", singleLogoutService.getId());
                    }
                    break;
                default:
                    // Other types, the reverse channel logout operation is not supported
                    logoutRequest.setStatus(LogoutRequestStatus.NOT_ATTEMPTED);
                    break;
            }
            return logoutRequest;
        }
    }
    return null;
}

private boolean serviceSupportsSingleLogout(final RegisteredService registeredService) {
    return registeredService != null
            && registeredService.getAccessStrategy().isServiceAccessAllowed()
            && registeredService.getLogoutType() != LogoutType.NONE;
}

private URL determineLogoutUrl(final RegisteredService registeredService, final SingleLogoutService singleLogoutService) {
    try {
        URL logoutUrl = new URL(singleLogoutService.getOriginalUrl());
        final URL serviceLogoutUrl = registeredService.getLogoutUrl();

        if (serviceLogoutUrl != null) {
            LOGGER.debug("Logout request will be sent to [{}] for service [{}]",
                    serviceLogoutUrl, singleLogoutService);
            // Priority is given to the login URL of the registered service
            logoutUrl = serviceLogoutUrl;
        }
        return logoutUrl;
    } catch (final Exception e) {
        throw new IllegalArgumentException(e);
    }
}

private boolean performBackChannelLogout(final LogoutRequest request) {
    try {
        final String logoutRequest = this.logoutMessageBuilder.create(request);
        final SingleLogoutService logoutService = request.getService();
        // Set the logout ID of the service
        logoutService.setLoggedOutAlready(true);

        LOGGER.debug("Sending logout request for: [{}]", logoutService.getId());
        final LogoutHttpMessage msg = new LogoutHttpMessage(request.getLogoutUrl(), logoutRequest);
        LOGGER.debug("Prepared logout message to send is [{}]", msg);
        // Send logout request
        return this.httpClient.sendMessageToEndPoint(msg);
    } catch (final Exception e) {
        LOGGER.error(e.getMessage(), e);
    }
    return false;
}

The cookie in the login request will not carry TGT information. It will directly return success() and go to [serviceCheck] for processing. Its configuration is as follows:

login-webflow.xml

<decision-state id="serviceCheck">
    <if test="flowScope.service != null" then="generateServiceTicket" else="viewGenericLoginSuccess"/>
</decision-state>

The cookie in the login request carries the Service information and goes to [generateServiceTicket] for processing. Its configuration is as follows:

login-webflow.xml

<action-state id="generateServiceTicket">
    <evaluate expression="generateServiceTicketAction"/>
    <transition on="success" to="warn"/>
    <transition on="authenticationFailure" to="handleAuthenticationFailure"/>
    <transition on="error" to="initializeLogin"/>
    <transition on="gateway" to="gatewayServicesManagementCheck"/>
</action-state>

[generateserviceticketaction] corresponds to generateserviceticketaction, which inherits from AbstractAction and implements the algorithm details doExecute. The code is as follows:

protected Event doExecute(final RequestContext context) {
    // Get Service information from context
    final Service service = WebUtils.getService(context);
    // Get TGT information from context
    final String ticketGrantingTicket = WebUtils.getTicketGrantingTicketId(context);

    try {
        /**
         * In the initial primary authentication flow, credentials are cached and available.
         * Since they are authenticated as part of submission first, there is no need to doubly
         * authenticate and verify credentials.
         *
         * In subsequent authentication flows where a TGT is available and only an ST needs to be
         * created, there are no cached copies of the credential, since we do have a TGT available.
         * So we will simply grab the available authentication and produce the final result based on that.
         */
        // Obtain the corresponding authentication result according to TGT
        final Authentication authentication = ticketRegistrySupport.getAuthenticationFrom(ticketGrantingTicket);
        if (authentication == null) {
            // If TGT has no corresponding authentication result, it is invalid
            throw new InvalidTicketException(new AuthenticationException(), ticketGrantingTicket);
        }

        final AuthenticationContextBuilder builder = new DefaultAuthenticationContextBuilder(
                this.authenticationSystemSupport.getPrincipalElectionStrategy());
        final AuthenticationContext authenticationContext = builder.collect(authentication).build(service);

        // Create ST according to TGT, Service, etc
        final ServiceTicket serviceTicketId = this.centralAuthenticationService
                .grantServiceTicket(ticketGrantingTicket, service, authenticationContext);
        WebUtils.putServiceTicketInRequestScope(context, serviceTicketId);
        return success();
    } catch (final AuthenticationException e) {
        logger.error("Could not verify credentials to grant service ticket", e);
    } catch (final AbstractTicketException e) {
        if (e instanceof InvalidTicketException) {
            // Destroy TGT
            this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicket);
        }
        if (isGatewayPresent(context)) {
            return result("gateway");
        }

        return newEvent(AbstractCasWebflowConfigurer.TRANSITION_ID_ERROR, e);
    }

    return error();
}

this.centralAuthenticationService is CentralAuthenticationServiceImpl, which was introduced when creating TGT. The grantServiceTicket method is introduced here. The code is as follows:

public ServiceTicket grantServiceTicket(
        final String ticketGrantingTicketId,
        final Service service, final AuthenticationContext context)
        throws AuthenticationException, AbstractTicketException {

    // Get cached TGT information according to TGT ID
    final TicketGrantingTicket ticketGrantingTicket = getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);
    // Obtain registered Service information according to Service information matching
    final RegisteredService registeredService = this.servicesManager.findServiceBy(service);

    // Verify registered Service information
    verifyRegisteredServiceProperties(registeredService, service);
    // Verify whether the current authentication result is consistent with the authentication result cached in TGT
    final Authentication currentAuthentication = evaluatePossibilityOfMixedPrincipals(context, ticketGrantingTicket);
    
    // Verify the usage record of TGT and the access policy of registered Service
    if (ticketGrantingTicket.getCountOfUses() > 0 && !registeredService.getAccessStrategy().isServiceAccessAllowedForSso()) {
        // If TGT has bound ST and the currently registered Service does not support SSO, throw an exception
        logger.warn("Service [{}] is not allowed to use SSO.", service.getId());
        throw new UnauthorizedSsoServiceException();
    }

    // Verify agent Service if necessary
    // This example does not involve
    evaluateProxiedServiceIfNeeded(service, ticketGrantingTicket, registeredService);


    // Perform security policy check by getting the authentication that satisfies the configured policy
    // This throws if no suitable policy is found
    // Obtain the authentication results that meet the security policy
    // AcceptAnyAuthenticationPolicyFactory is used by default, which is satisfied by default
    getAuthenticationSatisfiedByPolicy(ticketGrantingTicket.getRoot(), new ServiceContext(service, registeredService));

    // Obtain the authentication result of the TGT cache and the authentication result of the cache of the ST associated with the TGT
    final List<Authentication> authentications = ticketGrantingTicket.getChainedAuthentications();
    // Obtain the Principal of the last authentication result
    final Principal principal = authentications.get(authentications.size() - 1).getPrincipal();

    // The default is empty
    final RegisteredServiceAttributeReleasePolicy releasePolicy = registeredService.getAttributeReleasePolicy();
    final Map<String, Object> principalAttrs;
    if (releasePolicy != null) {
        principalAttrs = releasePolicy.getAttributes(principal);
    } else {
        principalAttrs = new HashMap<>();
    }

    // Verify whether the principal meets the requirements of registeredserviceattributrereleasepolicy. The default principal attributes is empty, so it meets the requirements
    if (!registeredService.getAccessStrategy().doPrincipalAttributesAllowServiceAccess(principal.getId(), principalAttrs)) {
        logger.warn("Cannot grant service ticket because Service [{}] is not authorized for use by [{}].",
                service.getId(), principal);
        throw new UnauthorizedServiceForPrincipalException();
    }

    // Get the ServiceTicketFactory instance, DefaultServiceTicketFactory
    final ServiceTicketFactory factory = this.ticketFactory.get(ServiceTicket.class);
    // Create ST according to TGT Service
    final ServiceTicket serviceTicket = factory.create(ticketGrantingTicket, service, currentAuthentication != null);
    // Cache ST
    this.ticketRegistry.addTicket(serviceTicket);

    logger.info("Granted ticket [{}] for service [{}] and principal [{}]",
            serviceTicket.getId(), service.getId(), principal.getId());

    // Publish ST creation event
    doPublishEvent(new CasServiceTicketGrantedEvent(this, ticketGrantingTicket, serviceTicket));

    // Return to ST
    return serviceTicket;
}

protected final void verifyRegisteredServiceProperties(final RegisteredService registeredService,
                                                       final Service service) throws UnauthorizedServiceException {
    if (registeredService == null) {
        final String msg = String.format("ServiceManagement: Unauthorized Service Access. "
                + "Service [%s] is not found in service registry.", service.getId());
        logger.warn(msg);
        throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_UNAUTHZ_SERVICE, msg);
    }
    if (!registeredService.getAccessStrategy().isServiceAccessAllowed()) {
        final String msg = String.format("ServiceManagement: Unauthorized Service Access. "
                + "Service [%s] is not enabled in service registry.", service.getId());


        logger.warn(msg);
        throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_UNAUTHZ_SERVICE, msg);
    }
}

private static Authentication evaluatePossibilityOfMixedPrincipals(final AuthenticationContext context,
                                                            final TicketGrantingTicket ticketGrantingTicket)
        throws MixedPrincipalException {
    Authentication currentAuthentication = null;
    if (context != null) {
        currentAuthentication = context.getAuthentication();
        if (currentAuthentication != null) {
            final Authentication original = ticketGrantingTicket.getAuthentication();
            // Verify whether the current authentication result is consistent with the authentication result cached in TGT
            if (!currentAuthentication.getPrincipal().equals(original.getPrincipal())) {
                throw new MixedPrincipalException(
                        currentAuthentication, currentAuthentication.getPrincipal(), original.getPrincipal());
            }
            ticketGrantingTicket.getSupplementalAuthentications().clear();
            ticketGrantingTicket.getSupplementalAuthentications().add(currentAuthentication);
        }
    }
    return currentAuthentication;
}

protected final void evaluateProxiedServiceIfNeeded(final Service service, final TicketGrantingTicket ticketGrantingTicket,
                                            final RegisteredService registeredService) {
    final Service proxiedBy = ticketGrantingTicket.getProxiedBy();
    if (proxiedBy != null) {
        logger.debug("TGT is proxied by [{}]. Locating proxy service in registry...", proxiedBy.getId());
        final RegisteredService proxyingService = servicesManager.findServiceBy(proxiedBy);

        if (proxyingService != null) {
            logger.debug("Located proxying service [{}] in the service registry", proxyingService);
            if (!proxyingService.getProxyPolicy().isAllowedToProxy()) {
                logger.warn("Found proxying service {}, but it is not authorized to fulfill the proxy attempt made by {}",
                        proxyingService.getId(), service.getId());
                throw new UnauthorizedProxyingException("Proxying is not allowed for registered service "
                        + registeredService.getId());
            }
        } else {
            logger.warn("No proxying service found. Proxy attempt by service [{}] (registered service [{}]) is not allowed.",
                    service.getId(), registeredService.getId());
            throw new UnauthorizedProxyingException("Proxying is not allowed for registered service "
                    + registeredService.getId());
        }
    } else {
        logger.trace("TGT is not proxied by another service");
    }
}

protected final Authentication getAuthenticationSatisfiedByPolicy(
        final TicketGrantingTicket ticket, final ServiceContext context) throws AbstractTicketException {

    final ContextualAuthenticationPolicy<ServiceContext> policy =
            serviceContextAuthenticationPolicyFactory.createPolicy(context);
    if (policy.isSatisfiedBy(ticket.getAuthentication())) {
        return ticket.getAuthentication();
    }
    for (final Authentication auth : ticket.getSupplementalAuthentications()) {
        if (policy.isSatisfiedBy(auth)) {
            return auth;
        }
    }
    throw new UnsatisfiedAuthenticationPolicyException(policy);
}


AcceptAnyAuthenticationPolicyFactory.java

public ContextualAuthenticationPolicy<ServiceContext> createPolicy(final ServiceContext context) {
    return new ContextualAuthenticationPolicy<ServiceContext>() {

        @Override
        public ServiceContext getContext() {
            return context;
        }

        @Override
        public boolean isSatisfiedBy(final Authentication authentication) {
            return true;
        }
    };
}

The create method of DefaultServiceTicketFactory is as follows:

public <T extends Ticket> T create(final TicketGrantingTicket ticketGrantingTicket,
                                   final Service service,
                                   final boolean credentialsProvided) {

    final String uniqueTicketIdGenKey = service.getClass().getName();
    UniqueTicketIdGenerator serviceTicketUniqueTicketIdGenerator = null;
    // Attempt to get Service specific UniqueTicketIdGenerator
    if (this.uniqueTicketIdGeneratorsForService != null && !uniqueTicketIdGeneratorsForService.isEmpty()) {
        logger.debug("Looking up service ticket id generator for [{}]", uniqueTicketIdGenKey);
        serviceTicketUniqueTicketIdGenerator = this.uniqueTicketIdGeneratorsForService.get(uniqueTicketIdGenKey);
    }
    if (serviceTicketUniqueTicketIdGenerator == null) {
        // If the Service does not have a specific UniqueTicketIdGenerator, the default value is used
        serviceTicketUniqueTicketIdGenerator = this.defaultServiceTicketIdGenerator;
        logger.debug("Service ticket id generator not found for [{}]. Using the default generator...",
                uniqueTicketIdGenKey);
    }

    // Generate ST ID
    final String ticketId = serviceTicketUniqueTicketIdGenerator.getNewTicketId(ServiceTicket.PREFIX);
    // Create ST with TGT
    final ServiceTicket serviceTicket = ticketGrantingTicket.grantServiceTicket(
            ticketId,
            service,
            this.serviceTicketExpirationPolicy,
            credentialsProvided,
            this.onlyTrackMostRecentSession);
    return (T) serviceTicket;
}

TGT is of TicketGrantingTicketImpl type. The code of its grantServiceTicket method is as follows:

public final synchronized ServiceTicket grantServiceTicket(final String id,
    final Service service, final ExpirationPolicy expirationPolicy,
    final boolean credentialsProvided, final boolean onlyTrackMostRecentSession) {

    // Create ST
    final ServiceTicket serviceTicket = new ServiceTicketImpl(id, this,
            service, this.getCountOfUses() == 0 || credentialsProvided,
            expirationPolicy);

    // 
    updateServiceAndTrackSession(serviceTicket.getId(), service, onlyTrackMostRecentSession);
    return serviceTicket;
}

protected void updateServiceAndTrackSession(final String id, final Service service, final boolean onlyTrackMostRecentSession) {
    updateState();

    // Update service Principal information
    final List<Authentication> authentications = getChainedAuthentications();
    service.setPrincipal(authentications.get(authentications.size()-1).getPrincipal());

    // If you enable tracking of recent access sessions, you need to clear the cached service information, if any
    if (onlyTrackMostRecentSession) {
        final String path = normalizePath(service);
        final Collection<Service> existingServices = services.values();
        // loop on existing services
        for (final Service existingService : existingServices) {
            final String existingPath = normalizePath(existingService);
            // if an existing service has the same normalized path, remove it
            // and its service ticket to keep the latest one
            if (StringUtils.equals(path, existingPath)) {
                // If the cache information of the service exists, it needs to be cleared
                existingServices.remove(existingService);
                LOGGER.trace("Removed previous tickets for service: {}", existingService);
                break;
            }
        }
    }
    // Cache the information of the service
    this.services.put(id, service);
}

protected final void updateState() {
    // Update record
    this.previousLastTimeUsed = this.lastTimeUsed;
    this.lastTimeUsed = System.currentTimeMillis();
    this.countOfUses++;
}

At this point, the process of creating ST ends;

After the ST is successfully created, it will go through the [success] process, that is, [warn], and its configuration is as follows:

login-webflow.xml

<decision-state id="warn">
    <if test="flowScope.warnCookieValue" then="showWarningView" else="redirect"/>
</decision-state>

According to the expression, if there is no warn information, go to [redirect], and its configuration is as follows:

login-webflow.xml

<action-state id="redirect">
    <evaluate expression="flowScope.service.getResponse(requestScope.serviceTicketId)"
              result-type="org.jasig.cas.authentication.principal.Response" result="requestScope.response"/>
    <transition to="postRedirectDecision"/>
</action-state>

According to the expression, get the Response with ST ID information according to the service. The code is as follows:

AbstractWebApplicationService.java

public Response getResponse(final String ticketId) {
    return this.responseBuilder.build(this, ticketId);
}


WebApplicationServiceResponseBuilder.java

public Response build(final WebApplicationService service, final String ticketId) {
    final Map<String, String> parameters = new HashMap<>();

    if (StringUtils.hasText(ticketId)) {
        // Save ST ID information
        parameters.put(CasProtocolConstants.PARAMETER_TICKET, ticketId);
    }

    if (responseType.equals(Response.ResponseType.POST)) {
        return buildPost(service, parameters);
    }
    if (responseType.equals(Response.ResponseType.REDIRECT)) {
        return buildRedirect(service, parameters);
    }

    throw new IllegalArgumentException("Response type is valid. Only POST/REDIRECT are supported");
}

After creating the Response, go to [postRedirectDecision], and its configuration is as follows:

login-webflow.xml

<decision-state id="postRedirectDecision">
    <if test="requestScope.response.responseType.name() == 'POST'" then="postView" else="redirectView"/>
</decision-state>

According to the expression, if the response is of POST type, go to postView; otherwise, go to redirectView; in this case, go to redirectView. The configuration is as follows:

login-webflow.xml

<end-state id="redirectView" view="externalRedirect:#{requestScope.response.url}"/>

According to the expression, redirection is realized through ExternalRedirectAction, which inherits from AbstractAction and implements the algorithm details doExecute. The code is as follows:

protected Event doExecute(RequestContext context) throws Exception {
  String resourceUri = (String)this.resourceUri.getValue(context);
  context.getExternalContext().requestExternalRedirect(resourceUri);
  return this.success();
}

Redirect to the Service carried in the login request and carry the newly created ST information;

At this point, the processing flow of the login request without Service is completed.

Reference articles

CAS single sign on

Keywords: cas sso

Added by FraggleRock on Wed, 22 Dec 2021 03:43:09 +0200