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
- Verify the Cognito user pool configuration (region, pool id, client id) in cas.authn.amazon-cognito[0] matches the pool actually issuing the tokens.
- 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'.
- Confirm the JWKS endpoint / signing key configuration so jwtProcessor processes the token with the correct keys rather than mis-decoding it.
- 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
- Decode a sample ID token (jwt.io) to confirm 'sub' is present in your pool's tokens.
- Review any pre-token-generation Lambda so it never removes required standard claims.
- Keep user pool, client, region, and JWKS settings in sync with the pool actually issuing tokens.
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
- Unable to verify JWT assertion with any of the configured…
- Unable to find account
- Account password on record for
- Authentication handler is disabled
- No user can be accepted because none is defined
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)