apereo/cas · warning

User Agent header [ ] is not supported in the list of…

Error message

User Agent header [{}] is not supported in the list of supported browsers [{}]

What it means

SpnegoNegotiateCredentialsAction only initiates SPNEGO negotiation for user agents on the configured supported-browsers list. If the request's User-Agent header is empty or does not match any configured supported browser pattern, it logs this warning and returns the webflow error so a normal (form) login proceeds. This is the deliberate gate that keeps SPNEGO for known-capable browsers.

Solutions

  1. Add the client's user-agent pattern to cas.authn.spnego.supported-browsers (regex list).
  2. If the client is a script/app, either bypass SPNEGO or use an alternative auth mechanism (Basic/OAuth) instead of forcing Negotiate.
  3. Check that proxies do not strip or rewrite the User-Agent header.
  4. Keep the default wildcard-inclusive patterns if you want broad browser coverage.

Example fix

// before
cas.authn.spnego.supported-browsers=MSIE,Trident
// after
cas.authn.spnego.supported-browsers=MSIE,Trident,Firefox,AppleWebKit,Chrome,Edg
Defensive patterns

Strategy: validation

Validate before calling

String ua = request.getHeader("User-Agent");
boolean supported = supportedBrowsers.stream().anyMatch(p -> ua != null && ua.matches(p));
if (!supported) { /* route to form login instead of negotiate */ }

Type guard

static boolean hasUserAgent(HttpServletRequest r) {
    String ua = r.getHeader("User-Agent");
    return ua != null && !ua.isBlank();
}

Prevention

When it happens

Trigger: doExecuteInternal receives a request whose User-Agent is blank, or cas.authn.spnego[...].supportedBrowsers patterns (default targeting MSIE/Trident, Firefox, AppleWebKit) do not regex-match the UA string, causing isSupportedBrowser to return false.

Common situations: Non-browser clients (curl, Java HttpClient, mobile apps) hitting CAS; very new or exotic browser UA strings not matching the default patterns; proxy/gateway scrubbing the User-Agent header; supportedBrowsers misconfigured to a too-narrow regex.

Related errors


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

Appendix: source

Thrown at support/cas-server-support-spnego-webflow/src/main/java/org/apereo/cas/web/flow/SpnegoNegotiateCredentialsAction.java:75

     */
    private final boolean mixedModeAuthentication;

    @Override
    protected @Nullable Event doExecuteInternal(final RequestContext context) {
        val request = WebUtils.getHttpServletRequestFromExternalWebflowContext(context);
        val response = WebUtils.getHttpServletResponseFromExternalWebflowContext(context);

        val authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
        val userAgent = HttpRequestUtils.getHttpServletRequestUserAgent(request);

        LOGGER.debug("Authorization header [{}], User Agent header [{}]", authorizationHeader, userAgent);
        if (!StringUtils.hasText(userAgent) || this.supportedBrowser.isEmpty()) {
            LOGGER.warn("User Agent header [{}] is empty, or no browsers are supported", userAgent);
            return error();
        }

        if (!isSupportedBrowser(userAgent)) {
            LOGGER.warn("User Agent header [{}] is not supported in the list of supported browsers [{}]",
                userAgent, this.supportedBrowser);
            return error();
        }

        if (!StringUtils.hasText(authorizationHeader)
            || !authorizationHeader.startsWith(SpnegoConstants.NEGOTIATE)
            || authorizationHeader.length() <= SpnegoConstants.NEGOTIATE.length()) {

            LOGGER.debug("Authorization header not found or does not match the message prefix [{}]. Sending [{}] header [{}]",
                SpnegoConstants.NEGOTIATE, SpnegoConstants.HEADER_AUTHENTICATE, SpnegoConstants.NEGOTIATE);
            response.setHeader(SpnegoConstants.HEADER_AUTHENTICATE, SpnegoConstants.NEGOTIATE);

            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            /*
             The responseComplete flag tells the pausing view-state not to render the response
             because another object has taken care of it. If mixed mode authentication is allowed
             then responseComplete should not be called so that webflow will display the login page.
              */

View on GitHub (pinned to e7288fc434)