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

  1. Inspect verifyCredentials failure details (enable debug logging) — typically signature verification against the registered JWKS failed.
  2. Verify the client's registered jwks URI/content matches the private key actually used to sign the assertion (correct kid).
  3. Ensure assertion claims are correct: iss and sub = client_id, aud = token endpoint/issuer URL, exp/iat within allowed clock skew.
  4. 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

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


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)