apereo/cas · warning
Unable to verify credentials
Error message
Unable to verify credentials
What it means
After passing the enablement checks, OidcJwtAuthenticator calls verifyCredentials to validate the client's client_assertion JWT (signature, issuer, expiry, audience). If verification fails and returns null, the authenticator cannot produce Credentials and returns empty, logging this warning.
Solutions
- Inspect verifyCredentials failure details (enable debug logging) — typically signature verification against the registered JWKS failed.
- Verify the client's registered jwks URI/content matches the private key actually used to sign the assertion (correct kid).
- Ensure assertion claims are correct: iss and sub = client_id, aud = token endpoint/issuer URL, exp/iat within allowed clock skew.
- Regenerate the client assertion with a supported algorithm and current timestamp; sync client_secret if using client_secret_jwt.
Example fix
// before
const assertion = await new SignJWT({ }).setIssuer('wrong-client').setAudience('https://cas/oidc')...
// after
const assertion = await new SignJWT({ }).setIssuer('myclient').setSubject('myclient')
.setAudience('https://cas/cas/oidc/accessToken').setIssuedAt().setExpirationTime('now + 5m')
.setProtectedHeader({ alg: 'RS256', kid: 'key1' }).sign(privateKey); Defensive patterns
Strategy: validation
Validate before calling
// validate assertion claims locally before sending
const now = Math.floor(Date.now()/1000);
if (assertion.payload.exp < now) throw new Error('client assertion expired');
if (assertion.payload.iss !== clientId || assertion.payload.sub !== clientId) throw new Error('assertion iss/sub must equal client_id'); Try / catch
try {
const token = await exchangeWithClientAssertion(assertion);
} catch (e) {
if (e.message.includes('invalid_client')) regenerateAssertionAndRetry();
else throw e;
} Prevention
- Regenerate assertions per request with short exp (e.g. 5 minutes) and correct aud (token endpoint URL).
- Keep client JWKS in sync with signing keys; set kid in the JWT header.
- Sync NTP on the client host to avoid clock-skew expiry issues.
When it happens
Trigger: A private_key_jwt / client_secret_jwt assertion posted to the OIDC token endpoint fails verification in verifyCredentials — e.g. JWT signed with a key not present in the client's registered JWKS, wrong issuer/subject/client_id, expired assertion, or alg not accepted.
Common situations: Client's JWKS rotated or key IDs mismatched; clock skew making the assertion expired/not-yet-valid; assertion `iss`/`sub` not matching the authenticated client_id; client_secret_jwt HMAC secret mismatch after a secret change; malformed assertion produced by the client SDK.
Related errors
- Unable to verify JWT assertion with any of the configured…
- Private key JWT authentication method is not enabled for…
- Unable to verify JWT assertion with any of the configured…
- Invalid signature
- Proof nonce is invalid or missing
AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08).
Data as JSON: /api/errors/0715868dbbf4dc6b.
Report an issue: GitHub.
Appendix: source
Thrown at support/cas-server-support-oidc-core-api/src/main/java/org/apereo/cas/oidc/authn/OidcJwtAuthenticator.java:126
RegisteredServiceAccessStrategyUtils.ensureServiceAccessIsAllowed(registeredService);
Objects.requireNonNull(registeredService, "regisetered service is null");
if (OAuth20Utils.isAccessTokenRequest(callContext.webContext())) {
val authMethodDisabled = oidcServerDiscoverySettings.getTokenEndpointAuthMethodsSupported()
.stream()
.map(OAuth20ClientAuthenticationMethods::parse)
.noneMatch(method -> method == OAuth20ClientAuthenticationMethods.CLIENT_SECRET_JWT || method == OAuth20ClientAuthenticationMethods.PRIVATE_KEY_JWT);
if (authMethodDisabled || !OAuth20Utils.isTokenAuthenticationMethodSupportedFor(callContext, registeredService,
OAuth20ClientAuthenticationMethods.CLIENT_SECRET_JWT, OAuth20ClientAuthenticationMethods.PRIVATE_KEY_JWT)) {
LOGGER.warn("Private key JWT authentication method is not enabled for CAS, or is not supported for service [{}]", registeredService.getName());
return Optional.<Credentials>empty();
}
}
val credentials = (UsernamePasswordCredentials) creds;
val jwt = verifyCredentials(credentials, callContext.webContext());
if (jwt == null) {
LOGGER.warn("Unable to verify credentials");
return Optional.<Credentials>empty();
}
val keys = new JsonWebKeySet();
clientJwksRegistrationStore.ifAvailable(Unchecked.consumer(store -> {
if (jwt instanceof final SignedJWT signedJWT) {
val jwk = signedJWT.getHeader().getJWK();
val kid = signedJWT.getHeader().getKeyID();
val jkt = jwk != null ? jwk.computeThumbprint().toString() : StringUtils.EMPTY;
store.findBy(registeredService.getClientId(), jkt)
.or(() -> store.findBy(registeredService.getClientId(), kid))
.map(ClientJwksRegistrationEntry::jwk)
.ifPresent(registereredKey -> {
val webKey = EncodingUtils.newJsonWebKey(registereredKey);
keys.addJsonWebKey(webKey);
});
}
}));View on GitHub (pinned to e7288fc434)