apereo/cas · error · FailedLoginException

Failed: status with message

Error message

Failed: status %s with message: %s

What it means

AzureActiveDirectoryAuthenticationHandler.getUserInfoFromGraph performs a raw HTTP GET against the Microsoft Graph user-info endpoint over HttpURLConnection. When the response status is not 2xx it formats "Failed: status <code> with message: <reason>" and throws FailedLoginException. This is the handler reporting that Microsoft Graph rejected or failed the user-info request during username/password authentication.

Solutions

  1. Read the logged status code: 401/403 → fix the Azure AD app registration (grant admin consent for the required Graph permissions); 404 → correct the tenant/graph endpoint configuration.
  2. Verify cas.authn.azuread[0] settings (tenant, client-id, client-secret, base URL) are correct and the credentials are still valid — rotate expired client secrets.
  3. Test the same Graph call manually (curl with a token from the OAuth2 token endpoint) to confirm the app can reach the user-info endpoint.
  4. Check conditional-access / MFA policies: the ROPC-style resource-owner credential flow fails for users where Azure requires interactive auth, producing non-2xx here.
  5. Retry on 429/5xx and monitor Azure AD service health if the status indicates throttling or transient server errors.

Example fix

// before (log: Failed: status 401 with message: Unauthorized)
cas.authn.azuread[0].client-id=old-app-id
cas.authn.azuread[0].client-secret=expired-secret

// after: fix app registration / secret
cas.authn.azuread[0].client-id=<correct-app-id>
cas.authn.azuread[0].client-secret=<new-secret>  # + admin-consented Graph permissions
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-flight: verify Graph reachability and app credentials
HttpURLConnection c = (HttpURLConnection) new URL(graphUserInfoUrl).openConnection();
if (c.getResponseCode() >= 400) LOGGER.warn("Graph endpoint returned {}", c.getResponseCode());

Try / catch

try {
    return azureAdHandler.authenticate(credential);
} catch (FailedLoginException e) {
    if (e.getMessage().contains("status 4")) {
        LOGGER.error("Graph auth/client error — check Azure AD app registration: {}", e.getMessage());
    }
    throw e;
}

Prevention

When it happens

Trigger: The conn.getInputStream() path is skipped because HttpStatus.valueOf(httpResponseCode) is not 2xx — e.g. 401/403 (bad/expired access token from getAccessTokenFromUserCredentials), 404 (wrong tenant or graph URL), or 429/5xx from Graph.

Common situations: Wrong tenant name / graph base URL in cas.authn.azuread[0] configuration; client credentials lacking permission to call Graph (admin consent not granted); ROPC flow denied by conditional access policies; network proxy or Azure AD outage producing 5xx.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


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

Appendix: source

Thrown at support/cas-server-support-azuread-authentication/src/main/java/org/apereo/cas/azure/ad/authentication/AzureActiveDirectoryAuthenticationHandler.java:66

    }

    private String getUserInfoFromGraph(final IAuthenticationResult authenticationResult, final String username) throws Exception {
        val url = new URI(Strings.CI.appendIfMissing(properties.getResource(), "/") + "v1.0/users/" + username).toURL();
        val conn = (HttpURLConnection) url.openConnection();
        try {
            conn.setRequestMethod("GET");
            conn.setRequestProperty(HttpHeaders.AUTHORIZATION, "Bearer " + authenticationResult.accessToken());
            conn.setRequestProperty(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);

            LOGGER.debug("Fetching user info from [{}] using access token [{}]", url.toExternalForm(), authenticationResult.accessToken());
            val httpResponseCode = conn.getResponseCode();
            if (HttpStatus.valueOf(httpResponseCode).is2xxSuccessful()) {
                try (val in = conn.getInputStream()) {
                    return IOUtils.toString(in, StandardCharsets.UTF_8);
                }
            }
            val msg = String.format("Failed: status %s with message: %s", httpResponseCode, conn.getResponseMessage());
            throw new FailedLoginException(msg);
        } finally {
            conn.disconnect();
        }
    }

    protected IAuthenticationResult getAccessTokenFromUserCredentials(final String username, final String password) throws Exception {
        val clientId = SpringExpressionLanguageValueResolver.getInstance().resolve(properties.getClientId());
        val scopes = org.springframework.util.StringUtils.commaDelimitedListToSet(properties.getScope());
        if (StringUtils.isNotBlank(properties.getClientSecret())) {
            val clientSecret = SpringExpressionLanguageValueResolver.getInstance().resolve(properties.getClientSecret());
            val clientCredential = ClientCredentialFactory.createFromSecret(clientSecret);
            val context = ConfidentialClientApplication.builder(clientId, clientCredential)
                .authority(properties.getLoginUrl())
                .validateAuthority(true)
                .build();
            val resource = Strings.CI.appendIfMissing(properties.getResource(), "/").concat(".default");
            val parameters = ClientCredentialParameters.builder(Set.of(resource))
                .tenant(properties.getTenant())

View on GitHub (pinned to e7288fc434)