apereo/cas · error · FailedLoginException

Unable to accept the ID token with an invalid [sub] claim

Error message

Unable to accept the ID token with an invalid [sub] claim

What it means

Thrown by AmazonCognitoAuthenticationAuthenticationHandler.authenticateUsernamePasswordInternal after it verifies credentials against AWS Cognito and processes the returned ID token with a JWT processor. If the ID token's 'sub' claim is missing or blank, the handler cannot establish a subject for the principal and raises FailedLoginException. In practice this means Cognito authenticated the user but returned a token payload CAS cannot use.

Solutions

  1. Verify the Cognito user pool configuration (region, pool id, client id) in cas.authn.amazon-cognito[0] matches the pool actually issuing the tokens.
  2. Inspect the ID token (decode its JWT payload) to confirm a 'sub' claim is present; if a pre-token-generation Lambda customizes claims, ensure it does not remove 'sub'.
  3. Confirm the JWKS endpoint / signing key configuration so jwtProcessor processes the token with the correct keys rather than mis-decoding it.
  4. Check that the user completed the full authentication challenge (e.g. NEW_PASSWORD_REQUIRED) so Cognito returns a normal AuthenticationResult with a valid ID token.

Example fix

// before: no pre-token lambda check; ID token missing sub
AdminGetUserRequest.builder().userPoolId(properties.getUserPoolId())...

// after: validate pool settings so ID tokens carry 'sub'
// aws cognito-idp describe-user-pool --user-pool-id <poolId>  (check lambda config)
// ensure pre-token-generation trigger does not strip 'sub'
Defensive patterns

Strategy: validation

Validate before calling

val claims = jwtProcessor.process(authenticationResult.idToken(), new SimpleSecurityContext());
if (claims == null || StringUtils.isBlank(claims.getSubject())) {
    throw new FailedLoginException("ID token has no 'sub' claim; check user pool / pre-token-generation config");
}

Try / catch

try {
    authenticateAgainstCognito(credential);
} catch (FailedLoginException e) {
    LOGGER.warn("Cognito login failed: {}", e.getMessage());
    throw e;
}

Prevention

When it happens

Trigger: result.authenticationResult() yields an idToken whose decoded claims have no 'sub' — e.g. an unexpected/malformed ID token, a token issued by a misconfigured user pool, or jwtProcessor.process() decoding a token payload that lacks the subject claim.

Common situations: User pool misconfiguration (custom authentication flows, token customization via pre-token-generation Lambda stripping claims); pointing CAS at the wrong pool/region so tokens are odd; clocks/JWKS issues causing the processor to read a stale or unexpected token; NEW_PASSWORD_REQUIRED-style flows partially completed.

Understand the failure class

Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at support/cas-server-support-aws-cognito-authentication/src/main/java/org/apereo/cas/authentication/AmazonCognitoAuthenticationAuthenticationHandler.java:72

        try {
            val authParams = new HashMap<String, String>();
            authParams.put("USERNAME", credential.getUsername());
            authParams.put("PASSWORD", credential.toPassword());
            val authRequest = AdminInitiateAuthRequest.builder();

            val request = authRequest.authFlow(AuthFlowType.ADMIN_NO_SRP_AUTH)
                .clientId(properties.getClientId())
                .userPoolId(properties.getUserPoolId())
                .authParameters(authParams).build();
            val result = cognitoIdentityProvider.adminInitiateAuth(request);

            if ("NEW_PASSWORD_REQUIRED".equalsIgnoreCase(result.challengeNameAsString())) {
                throw new CredentialExpiredException();
            }
            val authenticationResult = result.authenticationResult();
            val claims = jwtProcessor.process(authenticationResult.idToken(), new SimpleSecurityContext());
            if (StringUtils.isBlank(claims.getSubject())) {
                throw new FailedLoginException("Unable to accept the ID token with an invalid [sub] claim");
            }

            val userResult = cognitoIdentityProvider.adminGetUser(AdminGetUserRequest.builder()
                .userPoolId(properties.getUserPoolId())
                .username(credential.getUsername()).build());

            val attributes = new LinkedHashMap<String, List<Object>>();
            attributes.put("userStatus", CollectionUtils.wrap(userResult.userStatusAsString()));
            attributes.put("userCreatedDate", CollectionUtils.wrap(userResult.userCreateDate().toEpochMilli()));
            attributes.put("userModifiedDate", CollectionUtils.wrap(userResult.userLastModifiedDate().toEpochMilli()));

            val userAttributes = userResult.userAttributes();
            userAttributes.forEach(attr -> {
                if (!properties.getMappedAttributes().isEmpty() && properties.getMappedAttributes().containsKey(attr.name())) {
                    val newName = properties.getMappedAttributes().get(attr.name());
                    attributes.put(newName, CollectionUtils.wrap(attr.value()));
                } else {
                    attributes.put(attr.name(), CollectionUtils.wrap(attr.value()));

View on GitHub (pinned to e7288fc434)