apereo/cas · error · UnauthorizedServiceException

screen.service.error.message

screen.service.error.message

Error message

Unauthorized: %s

What it means

AuthenticationPolicyAwareServiceTicketValidationAuthorizer.authorize throws UnauthorizedServiceException (message 'Unauthorized: <serviceId>') during service ticket validation when the primary authentication record has no SUCCESSFUL_AUTHENTICATION_HANDLERS attribute, so the authorizer cannot verify that the required handlers satisfied the assertion.

Solutions

  1. Ensure the authentication flow records AuthenticationHandler.SUCCESSFUL_AUTHENTICATION_HANDLERS in primary authentication attributes (default CAS behavior — check for customization).
  2. Review the registered service's required-handler policy; relax requiredAuthenticationHandlers if handler tracking is not needed.
  3. Force re-authentication (renew=true) so a fresh authentication record with handler attributes is created.
  4. Verify all nodes in the cluster run a CAS version that propagates the handler attribute into assertions.
  5. Check that resolved handlers' names actually match the recorded names (custom handler getName overrides).

Example fix

// before: service policy demands handlers but session lacks them
// after: force fresh authentication at login
https://cas.example.org/cas/login?service=...&renew=true
// or in service registry JSON:
"requiredHandlers": []
Defensive patterns

Strategy: try-catch

Validate before calling

// Before relying on handler-based policy, confirm the attribute exists:
var handlers = assertion.getPrimaryAuthentication().getAttributes()
    .get(AuthenticationHandler.SUCCESSFUL_AUTHENTICATION_HANDLERS);
if (handlers == null || handlers.isEmpty()) {
    LOGGER.warn("Assertion lacks successful-handler attributes; validation will be denied");
}

Type guard

boolean hasHandlerRecord(PrimaryAuthentication auth) {
    return auth.getAttributes().containsKey(AuthenticationHandler.SUCCESSFUL_AUTHENTICATION_HANDLERS);
}

Try / catch

try {
    authorizer.authorize(request, response, assertion, registeredService);
} catch (UnauthorizedServiceException e) {
    LOGGER.error("Ticket validation denied for {}: {}", service.getId(), e.getMessage());
    // redirect user to re-authenticate with renew=true
}

Prevention

When it happens

Trigger: Validating an ST for a service whose registered service policy requires specific authentication handlers, when the assertion's primary authentication attributes lack the successful-handlers attribute — e.g. the ticket was issued by a flow/path that does not record handler names, or assertion data was reconstructed/serialized without it.

Common situations: Ticket issued by an older CAS node or a proxying chain that strips authentication attributes; custom authentication handlers not registering in the execution plan; policy on the service requiring handlers (requiredHandlers) while the SSO session predates that configuration.

Understand the failure class

Background: "You do not have permission" / 403 Forbidden errors: authenticated but not allowed — causes and fixes across open-source libraries — this error's family across 31 libraries.

Related errors


AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08). Data as JSON: /api/errors/b74618b46bf05838. Report an issue: GitHub.

Appendix: source

Thrown at core/cas-server-core-validation-api/src/main/java/org/apereo/cas/validation/AuthenticationPolicyAwareServiceTicketValidationAuthorizer.java:45

@RequiredArgsConstructor
public class AuthenticationPolicyAwareServiceTicketValidationAuthorizer implements ServiceTicketValidationAuthorizer {
    private final ServicesManager servicesManager;

    private final AuthenticationEventExecutionPlan authenticationEventExecutionPlan;

    private final ConfigurableApplicationContext applicationContext;

    @Override
    public void authorize(final HttpServletRequest request, final Service service, final Assertion assertion) {
        val registeredService = servicesManager.findServiceBy(service);
        RegisteredServiceAccessStrategyUtils.ensureServiceAccessIsAllowed(service, registeredService);

        LOGGER.debug("Evaluating service [{}] to ensure required authentication handlers can satisfy assertion", service);
        val primaryAuthentication = assertion.getPrimaryAuthentication();
        val attributes = primaryAuthentication.getAttributes();
        if (!attributes.containsKey(AuthenticationHandler.SUCCESSFUL_AUTHENTICATION_HANDLERS)) {
            LOGGER.warn("No successful authentication handlers are recorded for the authentication attempt");
            throw UnauthorizedServiceException.denied("Unauthorized: %s".formatted(service.getId()));
        }
        val successfulHandlerNames = CollectionUtils.toCollection(attributes.get(AuthenticationHandler.SUCCESSFUL_AUTHENTICATION_HANDLERS));
        val assertedHandlers = authenticationEventExecutionPlan.resolveAuthenticationHandlers()
            .stream()
            .filter(BeanSupplier::isNotProxy)
            .filter(handler -> successfulHandlerNames.contains(handler.getName()))
            .collect(Collectors.toSet());

        val policies = authenticationEventExecutionPlan.getAuthenticationPolicies(primaryAuthentication);
        policies.forEach(policy -> {
            try {
                val simpleName = policy.getClass().getSimpleName();
                LOGGER.trace("Executing authentication policy [{}]", simpleName);
                val result = policy.isSatisfiedBy(primaryAuthentication, assertedHandlers, applicationContext,
                    Map.of(Assertion.class.getName(), assertion, RegisteredService.class.getName(), registeredService));
                if (!result.isSuccess()) {
                    throw UnauthorizedServiceException.denied("Unauthorized: %s".formatted(service.getId()));
                }

View on GitHub (pinned to e7288fc434)