CAS process analysis server handles login request without Service

Posted by dsainteclaire on Wed, 02 Feb 2022 05:55:43 +0100

Processing flow configuration of login request

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

<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:

<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 login request. The relevant 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 composed of / WEB-INF / Web XML file, 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 path of process configuration file 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:

<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"/>

According to p:supportedFlowId="login", the request http://ip:port/cas/login?queryString It will be processed by loginHandlerAdapter, and its defined loginFlowExecutor attribute will be referenced to loginFlowRegistry, i.e. / WEB-INF / webflow / login / login webflow Processing flow configured in XML file;
The core code of the selectflowerhandler inherits from the selectflowerhadapter's parent class and is analyzed 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 from loginFlowExecutionFactory
        FlowExecution flowExecution = this.executionFactory.createFlowExecution(flowDefinition);
        // Start 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;
}

Brief analysis of processing flow of login request

1 on-start

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:

<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 by the request
        final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
        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");
}

2 action-state

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 processing, that is, gatewayRequestCheck. 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 Service information will go through serviceAuthorizationCheck. 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) {
        // Service does not exist and returns 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 login request does not carry Service information, so go to initializeLogin. 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 for processing, 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>

According to the configuration of urlBasedViewResolver:

<!-- 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"/>

And registeredservicethemebasedviewresolver THEME_ LOCATION_ Parameter value of pattern (i.e.% s/%s/ui /), via code

// 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 judgment 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 verification, create TGT
        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 the help of 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 HandlerResult with successful authentication 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 the authentication is successful, judge whether to end the authentication according to the conditions of AuthenticationPolicy;
      1. After that, the authentication result is returned directly;
      2. If not, continue the next round of AuthenticationHandler and PrincipalResolver, and then continue 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, and 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, and its 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 definition and configuration of 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 authentication Credential
AuthenticationHandler and PrincipalResolver authenticate the current Credential. The code is as follows:

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

    Principal principal;
    // AuthenticationHandler completes authentication
    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, i.e. AcceptUsersAuthenticationHandler, authenticates the current Credential. Its parent class AbstractPreAndPostProcessingAuthenticationHandler implements the algorithm template of authenticate and provides the default implementation of the details of preAuthenticate and postAuthenticate algorithms. It can be rewritten by subclasses. It provides the details of doAuthentication algorithm, which is 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 that supports user name conversion
    // 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 transmitted 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>
  1. Through CAS Properties configuration, low priority;
accept.authn.users=casuser::Mellon

If the AuthenticationHandler fails to authenticate, an exception will be thrown to end; If the AuthenticationHandler is authenticated successfully, the PrincipalResolver will be 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;
}

Configuration by primaryPrincipalResolver:

<!-- deployerConfigContext.xml -->
<alias name="personDirectoryPrincipalResolver" alias="primaryPrincipalResolver" />

It can be seen that the persondirectoryprincipalresolver corresponds to the 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);

    // Get the property set corresponding to 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);

        // Whether to 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 set, 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);
}

First, analyze the method of obtaining user attribute set: retrievePersonAttributes, which uses this The attributerepository attribute is completed. The core code of the assignment logic of this 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 in deployerconfigcontext It is defined in the XML configuration file, and 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 and update the principalId information. The core code of the assignment logic of this attribute is as follows:

// Optional attribute value
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.principal.resolver.persondir.principal.attribute=cn

2.3.1 successful authentication
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 successful authentication 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 failure
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 this will be configured Authenticationmetadatapopulators, fill in the metadata attribute, and finally return the authentication result;
At this point, the authentication process ends;

3 Create TGT
After successful authentication, this will be used The assignment logic of the attribute of tgcentralservice 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 holds the information of the TicketFactory 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, that is, 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 ticketgrantingticketiqueidgenerator. Its getNewTicketId method inherits from the parent class 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 creation process of TGT is completed;

After the TGT is successfully created, it will go through the success process, that is, gatewayRequestCheck. 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 this 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 LogoutManagerImpl. 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 sign 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);
        
        // Judge whether the service supports single sign out
        if (serviceSupportsSingleLogout(registeredService)) {

            // Get 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 do not support reverse channel logout
                    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 through serviceCheck. 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 will not carry Service information. It is processed by viewGenericLoginSuccess. Its configuration is as follows:

<!-- login-webflow.xml -->
<end-state id="viewGenericLoginSuccess" view="casGenericSuccessView">
    <on-entry>
        <evaluate expression="genericSuccessViewAction.getAuthenticationPrincipal(flowScope.ticketGrantingTicketId)"
                  result="requestScope.principal"
                  result-type="org.jasig.cas.authentication.principal.Principal"/>
    </on-entry>
</end-state>

The corresponding page of casGenericSuccessView is / WEB-INF / view / JSP / default / UI / casGenericSuccessView jsp;
Genericsuccessviewaction corresponds to genericsuccessviewaction. The code of getAuthenticationPrincipal method is as follows:

public Principal getAuthenticationPrincipal(final String ticketGrantingTicketId) {
    try {
        // Obtain TGT information according to TGT ID
        final TicketGrantingTicket ticketGrantingTicket =
                this.centralAuthenticationService.getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);
        // Return the Principal information of the authentication result in TGT
        return ticketGrantingTicket.getAuthentication().getPrincipal();
    } catch (final InvalidTicketException e){
        logger.warn(e.getMessage());
    }
    logger.debug("In the absence of valid TGT, the authentication principal cannot be determined. Returning {}",
            NullPrincipal.class.getSimpleName());
    // Return null Principal in case of exception
    return NullPrincipal.getInstance();
}

The return result of getAuthenticationPrincipal method of GenericSuccessViewAction will be reflected in the page / WEB-INF / view / JSP / default / UI / casgenericsuccessview JSP, the main contents of this page are as follows:

<div id="msg" class="success">
  <h2><spring:message code="screen.success.header" /></h2>
  <p><spring:message code="screen.success.success" arguments="${principal.id}"/></p>
  <p><spring:message code="screen.success.security" /></p>
</div>

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

Topics: cas