apereo/cas · error · ResponseStatusException

Invalid signature

Error message

Invalid signature

What it means

After selecting the appropriate verifier for the embedded JWK, the controller verifies the JWS signature of the client JWKS registration request. If verification fails, the request is rejected with HTTP 401 'Invalid signature', since the request cannot be attributed to the holder of the registered key.

Solutions

  1. Re-sign the registration JWT with the private key matching the embedded public JWK and sign again after finalizing the payload
  2. Confirm the JWS algorithm in the header matches the key type and the signer used
  3. Regenerate the keypair and rebuild the signed request from scratch to rule out payload tampering/encoding issues

Example fix

// before: signing with mismatched key
JWSSigner signer = new RSASSASigner(otherRsaKey);
// after: sign with private key of the embedded JWK
JWSSigner signer = new RSASSASigner(embeddedRsaJwk.toRSAPrivateKey());
Defensive patterns

Strategy: validation

Validate before calling

JWSSigner signer = ...; // key matching the embedded JWK
SignedJWT jws = new SignedJWT(header, claims);
jws.sign(signer);
if (!jws.verify(verifierForEmbeddedJwk)) {
    throw new IllegalStateException("JWS does not verify against embedded JWK");
}

Try / catch

try { controller.handleRegistration(request, response); }
catch (ResponseStatusException e) {
    if (e.getStatusCode() == HttpStatus.UNAUTHORIZED && "Invalid signature".equals(e.getReason())) { /* re-sign with the correct private key */ }
    else throw e;
}

Prevention

When it happens

Trigger: handleRegistration(): jws.verify(verifier) returns false — the JWT was signed by a different key than the one embedded/claimed, the payload was modified after signing, or the wrong algorithm/key material was used by the client.

Common situations: Signing with the private key of a different keypair than the embedded public JWK; serializing the JWT in a way that alters the payload after signing; mismatched algorithm between header and actual signature; key rotation on the client mid-request.

Related errors


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

Appendix: source

Thrown at support/cas-server-support-oidc-core-api/src/main/java/org/apereo/cas/oidc/web/controllers/jwks/OidcJwksRegistrationEndpointController.java:114

            .build();
        val accessResult = configurationContext.getRegisteredServiceAccessStrategyEnforcer().execute(audit);
        accessResult.throwExceptionIfNeeded();
        
        val jws = JWSObject.parse(registrationRequest.proof());

        val alg = jws.getHeader().getAlgorithm();
        FunctionUtils.throwIf(!JWSAlgorithm.Family.EC.contains(alg) && !JWSAlgorithm.Family.RSA.contains(alg) && !JWSAlgorithm.EdDSA.equals(alg),
            () -> new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid algorithm: " + alg));

        val jwk = jws.getHeader().getJWK();
        val verifier = switch (jwk) {
            case ECKey ecKey -> new ECDSAVerifier(ecKey);
            case RSAKey rsaKey -> new RSASSAVerifier(rsaKey);
            case OctetKeyPair okp -> new Ed25519Verifier(okp.toPublicJWK());
            default -> throw new IllegalArgumentException("Unsupported key type: " + jwk.getKeyType());
        };
        if (!jws.verify((JWSVerifier) verifier)) {
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid signature");
        }
        val jkt = jwk.computeThumbprint().toString();
        clientJwksRegistrationStore.save(accessTokenTicket.getClientId(), jkt, jwk.toPublicJWK().toJSONString());
        return ResponseEntity.ok(new ClientJwksRegistrationResponse(jkt));
    }

    /**
     * Handle errors.
     *
     * @param ex the ex
     * @return the response entity
     */
    @ExceptionHandler(Exception.class)
    @SuppressWarnings("UnusedMethod")
    private static ResponseEntity<String> handle(final Exception ex) {
        LoggingUtils.error(LOGGER, ex);
        if (ex instanceof final ResponseStatusException rse) {
            return ResponseEntity.status(rse.getStatusCode()).body(rse.getReason());

View on GitHub (pinned to e7288fc434)