# 8.1.0 Release Notes

Source: https://apereo.github.io/cas/development/release_notes/RC.html

Release notes · All release candidates

# 8.1.0

Every change from RC1, RC2, RC3, RC4 in one place. Each change links to the release candidate it shipped in.

- [Requires **JDK 25**](https://apereo.github.io/cas/development/planning/Installation-Requirements.html)
- Built on **Spring Boot 4.2**
- [**618** browser test scenarios](https://apereo.github.io/cas/developer/Test-Process.html)
- **94%** test coverage

- **4** Release candidates
- **53** New & noteworthy topics
- **27** Changes to review before upgrading
- **146** Other changes

## Before You Upgrade

These changes alter existing behavior. Each one links to its full description in the release candidate that introduced it.

- Action

  [RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#theme-builder) [**Theme builder**](https://apereo.github.io/cas/development/release_notes/RC4.html#theme-builder) is turned on by default; turn it off for production with `cas.theme-builder.enabled=false` in theme properties.Interface & Accounts
- Changed

  [RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#theme-properties) [**External identity providers**](https://apereo.github.io/cas/development/release_notes/RC4.html#theme-properties) keep their configured order in the dropdown as well; set `cas.login-providers.sort=true` in theme properties to sort them by name.Interface & Accounts
- Changed

  [RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#theme-properties) [**Error pages**](https://apereo.github.io/cas/development/release_notes/RC4.html#theme-properties) no longer offer the details button unless `cas.error.show-details=true` is set in theme properties.Interface & Accounts
- Action

  [RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#openid-federation) [**OpenID Federation relying parties**](https://apereo.github.io/cas/development/release_notes/RC4.html#openid-federation) registered automatically must send a signed request object or a pushed authorization request with every authorization request.OAuth & OpenID Connect
- Removed

  [RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#tickets--sessions) [**Azure CosmosDb**](https://apereo.github.io/cas/development/release_notes/RC4.html#tickets--sessions) service and ticket registries are removed; move service definitions and tickets to another registry before upgrading.Tickets & Sessions
- Action

  [RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#multifactor-authentication) [**Trusted devices**](https://apereo.github.io/cas/development/release_notes/RC4.html#multifactor-authentication) include the client IP address in the device fingerprint by default, as documented; devices trusted before the upgrade are asked for multifactor authentication once more. Set `cas.authn.mfa.trusted.device-fingerprint.client-ip.enabled=false` to keep the previous fingerprint.Multifactor & Passkeys
- Action

  [RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#authentication-interrupts) [**Interrupt inquirers**](https://apereo.github.io/cas/development/release_notes/RC4.html#authentication-interrupts) no longer receive the Spring Webflow request context; custom inquirers drop that parameter, and Groovy interrupt scripts receive the logger as their fifth argument.Interface & Accounts
- Action

  [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#certificate-bound-access-tokens) [**Certificate-bound access tokens**](https://apereo.github.io/cas/development/release_notes/RC3.html#certificate-bound-access-tokens) issued before the upgrade are rejected; thumbprints now follow RFC 8705.OAuth & OpenID Connect
- Action

  [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#heimdall-authzen) [**Heimdall AuthZEN resources**](https://apereo.github.io/cas/development/release_notes/RC3.html#heimdall-authzen) must define `resourceType`, `actions` and optionally `resourceIdPattern`, or AuthZEN requests are denied.Authorization
- Changed

  [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#heimdall-authzen) [**Heimdall policies**](https://apereo.github.io/cas/development/release_notes/RC3.html#heimdall-authzen) deny access when a resource has none, and one granting policy is enough unless `enforceAllPolicies` is set.Authorization
- Removed

  [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#stateless-ticket-registry) [**`cas.ticket.registry.stateless.storage-type`**](https://apereo.github.io/cas/development/release_notes/RC3.html#stateless-ticket-registry) no longer applies; the ticket-granting ticket is carried by the ticket-granting cookie.Tickets & Sessions
- Changed

  [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#stateless-ticket-registry) [**Stateless ticket-granting tickets**](https://apereo.github.io/cas/development/release_notes/RC3.html#stateless-ticket-registry) no longer carry principal attributes; attribute repositories are consulted each time instead.Tickets & Sessions
- Changed

  [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#authentication-throttling) [**Authentication throttling**](https://apereo.github.io/cas/development/release_notes/RC3.html#authentication-throttling) counts a failed attempt only on a `401` response, once per request.Authentication
- Changed

  [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#webauthn-level-3) [**WebAuthn**](https://apereo.github.io/cas/development/release_notes/RC3.html#webauthn-level-3) requires browsers that support the WebAuthn Level 3 JSON serialization.Multifactor & Passkeys
- Changed

  [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#passwordless-authentication) [**Passwordless tokens**](https://apereo.github.io/cas/development/release_notes/RC3.html#passwordless-authentication) arrive as `PasswordlessTokenCredential`, the only credential the handler accepts; the recorded credential type changes.Passwordless
- Action

  [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#multifactor--passkeys) [**Google Authenticator on DynamoDB**](https://apereo.github.io/cas/development/release_notes/RC3.html#multifactor--passkeys) needs a new `useridIndex` global secondary index; create it yourself if tables are not created at startup.Multifactor & Passkeys
- Action

  [RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#openid-connect-verifiable-credentials) [**Verifiable credential wallets**](https://apereo.github.io/cas/development/release_notes/RC2.html#openid-connect-verifiable-credentials) must speak the final OpenID4VCI 1.0 wire format; draft 13 requests are no longer understood.OAuth & OpenID Connect
- Changed

  [RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#oauth-and-openid-connect) [**Client secrets**](https://apereo.github.io/cas/development/release_notes/RC2.html#oauth-and-openid-connect) are now compared case-sensitively.OAuth & OpenID Connect
- Action

  [RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#attribute-consent) [**Groovy consent activation scripts**](https://apereo.github.io/cas/development/release_notes/RC2.html#attribute-consent) must return a `ConsentQueryResult` instead of a boolean.Attributes & Consent
- Changed

  [RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#cas-protocol) [**CAS validation responses**](https://apereo.github.io/cas/development/release_notes/RC2.html#cas-protocol) report `INVALID_TICKET_SPEC` and `INTERNAL_ERROR` where they used other codes, and replace characters that are not legal in XML attribute names with `_`.CAS & Other Protocols
- Changed

  [RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#saml2-identity-provider) [**SAML2 metadata**](https://apereo.github.io/cas/development/release_notes/RC2.html#saml2-identity-provider) fails to load when its signature certificate cannot be read, and HTTP downloads are capped at 256 MiB by default.SAML2
- Changed

  [RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#views-and-themes) [**Theme names**](https://apereo.github.io/cas/development/release_notes/RC2.html#views-and-themes) without a matching theme definition are ignored, and the next resolver or the default theme applies.Interface & Accounts
- Action

  [RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#palantir-user-vs-admin-roles) [**Palantir admin functions**](https://apereo.github.io/cas/development/release_notes/RC1.html#palantir-user-vs-admin-roles) now require a released `role` attribute of `ADMIN` or `ROLE_ADMIN`; other users only manage registered applications.Platform & Operations
- Changed

  [RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#tickets--sessions) [**Transient session tickets**](https://apereo.github.io/cas/development/release_notes/RC1.html#tickets--sessions) now live for `5` minutes by default instead of `15`.Tickets & Sessions
- Changed

  [RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#general) [**Date formatting**](https://apereo.github.io/cas/development/release_notes/RC1.html#general) in a number of operations now uses `UTC` instead of the system default time zone.
- Changed

  [RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#cas--other-protocols) [**CAS REST APIs**](https://apereo.github.io/cas/development/release_notes/RC1.html#cas--other-protocols) answer unauthorized application requests with `403` instead of a `500` error.CAS & Other Protocols
- Removed

  [RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#tickets--sessions) [**LettuceMod**](https://apereo.github.io/cas/development/release_notes/RC1.html#tickets--sessions) is removed; Lettuce itself now provides its functionality.Tickets & Sessions

## Highlights

- Interface & Accounts New

  ### [Theme Builder](https://apereo.github.io/cas/development/release_notes/RC4.html#theme-builder)

  Edit theme properties and custom.css from the CAS header, preview live and download both files for an overlay.

  RC4
- Interface & Accounts New

  ### [Theme Properties](https://apereo.github.io/cas/development/release_notes/RC4.html#theme-properties)

  Over twenty new theme properties for the login form, identity providers, errors, multifactor, QR codes and consent.

  RC4
- Interface & Accounts New

  ### [Account Profile](https://apereo.github.io/cas/development/release_notes/RC4.html#account-profile)

  Sign-in activity, contact methods, refresh tokens, usage policy, impersonation, QR devices and password expiry in the profile.

  RC4
- Authorization Changed

  ### [Heimdall AuthZEN](https://apereo.github.io/cas/development/release_notes/RC3.html#heimdall-authzen)

  Follows the AuthZEN Authorization API 1.0, with resource instances, the access evaluations API, decision point metadata and decision context.

  RC3
- Multifactor & Passkeys New

  ### [WebAuthn Level 3](https://apereo.github.io/cas/development/release_notes/RC3.html#webauthn-level-3)

  Related origins, the Signal API, passkey provider names and icons, and a passkey upgrade after password login.

  RC3
- Passwordless New

  ### [Passwordless Authentication](https://apereo.github.io/cas/development/release_notes/RC3.html#passwordless-authentication)

  Single-use tokens under load, one-time-code autofill, origin-bound SMS codes and passkeys from the autofill menu.

  RC3
- Tickets & Sessions Action

  ### [Stateless Ticket Registry](https://apereo.github.io/cas/development/release_notes/RC3.html#stateless-ticket-registry)

  The ticket-granting ticket now rides in the ticket-granting cookie, with a versioned format and type-bound tickets.

  RC3
- OAuth & OpenID Connect New

  ### [OpenID Connect Verifiable Credentials](https://apereo.github.io/cas/development/release_notes/RC3.html#openid-connect-verifiable-credentials)

  Proofs follow each credential configuration, and holder keys may be `x5c`, `did:jwk` or Ed25519.

  RC3 RC2 RC1
- Documentation New

  ### [Documentation](https://apereo.github.io/cas/development/release_notes/RC3.html#documentation)

  A refreshed theme, searchable settings, grouped actuator endpoints and feature toggles, and a Quick Start recipe.

  RC3
- OAuth & OpenID Connect Changed

  ### [OAuth & OpenID Connect Hardening](https://apereo.github.io/cas/development/release_notes/RC2.html#oauth-and-openid-connect)

  Replay protection across nodes, client-bound token exchange, introspection and DPoP, and spendable DPoP-bound tokens.

  RC2
- SAML2 Changed

  ### [SAML2 Identity Provider](https://apereo.github.io/cas/development/release_notes/RC2.html#saml2-identity-provider)

  Independently validated request signatures, atomic metadata resolution, MDQ fixes and pooled HTTP clients.

  RC2
- CAS & Other Protocols Fixed

  ### [CAS Protocol](https://apereo.github.io/cas/development/release_notes/RC2.html#cas-protocol)

  Responses can no longer be forged through line breaks or markup, and `renew` is evaluated per request.

  RC2
- Attributes & Consent Fixed

  ### [Attribute Consent](https://apereo.github.io/cas/development/release_notes/RC2.html#attribute-consent)

  Every consent decision gets its own identifier, and lookups and revocations work across all stores.

  RC2
- Platform & Operations New

  ### [JMX Management](https://apereo.github.io/cas/development/release_notes/RC2.html#jmx-management)

  Service reloads, ticket and session counts, cleanup and diagnostics are now available over JMX.

  RC2
- OAuth & OpenID Connect New

  ### [Multiple Client Secrets](https://apereo.github.io/cas/development/release_notes/RC1.html#oauth--openid-connect-client-secrets)

  Applications can hold several expiring client secrets for smooth rotation, managed through the `oauthClientSecrets` endpoint.

  RC1
- Platform & Operations New

  ### [Cluster Topology](https://apereo.github.io/cas/development/release_notes/RC1.html#cluster-topology)

  A `clusterTopology` actuator endpoint reports every node and its status across eight ticket registries.

  RC1
- Attributes & Consent New

  ### [Attribute Definition Dependencies](https://apereo.github.io/cas/development/release_notes/RC1.html#attribute-definition-dependencies)

  Attribute definitions can depend on other definitions, which are resolved first and available during resolution.

  RC1
- OAuth & OpenID Connect Changed

  ### [OpenID Connect Federation](https://apereo.github.io/cas/development/release_notes/RC1.html#openid-connect-federation)

  Federation support is roughly finalized, with remaining edge cases tracked for the next releases.

  RC1
- Interface & Accounts New

  ### [Acceptable Usage Policy & Multitenancy](https://apereo.github.io/cas/development/release_notes/RC1.html#acceptable-usage-policy--multitenancy)

  Acceptable usage policies work per tenant, starting with MongoDb storage.

  RC1

## New & Noteworthy

Topics from every release candidate, grouped by area. When a topic appears in more than one release candidate, the notes of each are shown, newest first.

### Authentication

#### Authentication and Session Security

[RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#authentication-and-session-security)

- The [SAML2 identity provider](https://apereo.github.io/cas/development/authentication/Configuring-SAML2-Authentication.html), [OAuth/OpenID Connect](https://apereo.github.io/cas/development/authentication/OAuth-Authentication.html), [UMA](https://apereo.github.io/cas/development/protocol/OAuth-UMA-Protocol.html), [Heimdall](https://apereo.github.io/cas/development/authorization/Heimdall-Authorization-Overview.html), [JWT authentication](https://apereo.github.io/cas/development/authentication/JWT-Authentication.html) and [Duo Security](https://apereo.github.io/cas/development/mfa/DuoSecurity-Authentication.html) no longer depend on pac4j, which remains only in [delegated authentication](https://apereo.github.io/cas/development/integration/Delegate-Authentication.html) and the features that hand off to it, such as [passwordless authentication](https://apereo.github.io/cas/development/authentication/Passwordless-Authentication.html) and [ADFS integration](https://apereo.github.io/cas/development/integration/ADFS-Integration.html). They are built on Spring Security and CAS components and keep their session state in a CAS session store, in the http session, in browser storage or in the ticket registry as configured. Customizations, Groovy scripts and session store beans that use pac4j types must be updated, and authentication handler names recorded in audit logs no longer carry pac4j class names. OAuth/OpenID Connect sign-ins and pushed authorization requests that are under way during the upgrade must start again. SAML2 requests passed on to [SAML2 identity providers](https://apereo.github.io/cas/development/integration/Delegate-Authentication-SAML2.html) keep their forced and passive authentication, authentication contexts and scoping.
- [JWT authentication](https://apereo.github.io/cas/development/authentication/JWT-Authentication.html) takes the subject of a token as the principal as it is, and every other claim as an attribute. Tokens built with pac4j, whose subject carries a profile type such as `org.pac4j.core.profile.CommonProfile#casuser`, now sign in as that whole subject and must be issued again with a plain one. A service without a signing secret no longer accepts any token. The `tokenAuth` actuator endpoint no longer issues tokens: it validates one and answers the principal and the identifier, name and service id of the service, never its properties or secrets.
- When the [SAML2 identity provider](https://apereo.github.io/cas/development/authentication/Configuring-SAML2-Authentication.html) or [Duo Security](https://apereo.github.io/cas/development/mfa/DuoSecurity-Authentication.html#session-storage) keep their session state in browser storage, the browser receives only the state of that flow rather than every attribute of the http session, and the state expires with the lifetime of transient session tickets (`cas.ticket.tst.time-to-kill-in-seconds`). A state that has expired or belongs to another flow restores nothing; sign-ins under way during the upgrade must start again.
- Signing and encryption of values such as tokens and cookies now report a failure to sign, verify or encrypt a value instead of continuing without that step. A JSON web key that carries only a public key can now be used as a signing key to verify signatures produced elsewhere.
- CSRF protection is configured once for all modules, so OpenID Connect and [WebAuthn](https://apereo.github.io/cas/development/mfa/FIDO2-WebAuthn-Authentication.html) no longer override each other’s settings, and the [backchannel authentication](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-CIBA.html) verification page is now protected. A custom CSRF token repository is defined once, as the `casCsrfTokenRepository` bean.
- [Google Authenticator](https://apereo.github.io/cas/development/mfa/GoogleAuthenticator-Authentication.html) scratch codes are compared by their exact value, and a stored scratch code that cannot be decrypted is ignored instead of being used as it was stored. With scratch codes encrypted (`cas.authn.mfa.gauth.core.scratch-codes.encryption.key`), the [REST registration store](https://apereo.github.io/cas/development/mfa/GoogleAuthenticator-Authentication-Registration-Rest.html) no longer loses precision when reading them back. Scratch codes that can no longer be decrypted, such as codes stored under another key, stop working; users can generate new ones from the account profile.
- A login request that carries both `renew` and `gateway` now shows the login form, as the [CAS protocol](https://apereo.github.io/cas/development/protocol/CAS-Protocol-Specification.html) recommends, instead of returning to the application without a ticket. When renewed authentication is turned off with `cas.sso.renew-authn-enabled`, the `gateway` parameter is honored as before.
- The `throttles` actuator endpoint of [authentication throttling](https://apereo.github.io/cas/development/authentication/Configuring-Authentication-Throttling-Failure.html) can return only the throttled records of one username, matched regardless of case, instead of every record.

#### Authentication Throttling

[RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#authentication-throttling)

[Authentication throttling](https://apereo.github.io/cas/development/authentication/Configuring-Authentication-Throttling-Failure.html) now records a failed attempt only when the response status is `401`, once per request. Previously, any response other than `200`, `201` or `302`, such as a malformed request (`400`), an authorization denial (`403`), a missing resource (`404`) or a server error (`500`), was counted as a failed login, and failures were recorded twice. Failed [SAML2 ECP authentication](https://apereo.github.io/cas/development/authentication/Configuring-SAML2-Authentication.html) attempts, which answer with a SOAP fault, are now counted as well.

[Redis authentication throttling](https://apereo.github.io/cas/development/authentication/Configuring-Authentication-Throttling-Redis.html) no longer reads the audit log. It used to scan and load every audit record stored in Redis on each login attempt, which slowed every login as the audit log grew. Failed attempts are now tracked in a small key per username and IP address that expires once outside the failure range. [Redis audit records](https://apereo.github.io/cas/development/audits/Audits-Redis.html) now expire after `cas.audit.redis.max-age`, 180 days by default, and looking up audit records reads only the newest matching entries instead of loading every record. Audit events for the same principal within one second are now stored separately; previously, later events could overwrite earlier ones, including authentication successes immediately followed by ticket creation. Existing records remain readable.

#### LDAP Integrations

[RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#ldap-integrations)

- [Surrogate authentication](https://apereo.github.io/cas/development/authentication/Surrogate-Authentication-Storage-LDAP.html), [delegated authentication profile selection](https://apereo.github.io/cas/development/integration/Delegate-Authentication-ProfileSelection.html), [acceptable usage policy](https://apereo.github.io/cas/development/webflow/Webflow-Customization-AUP-LDAP.html) and [password management](https://apereo.github.io/cas/development/password_management/Password-Management.html) now build their LDAP connection pools once and reuse them across requests.
- LDAP connection pools are now addressed by the directory and base DN they serve, so multiple configuration blocks that point at the same server no longer collapse onto a single set of connection settings.
- Surrogate search filters that do not reference the impersonated account are now rejected, as such a filter is unable to restrict the accounts a user may impersonate.

### Passwordless

#### Passwordless Authentication

[RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#passwordless-authentication)

- [Passwordless authentication](https://apereo.github.io/cas/development/authentication/Passwordless-Authentication.html) tokens are now single-use under concurrent submissions, and every submitted token, including a wrong one, goes through the authentication manager and is recorded in the [audit log](https://apereo.github.io/cas/development/audits/Audits.html).
- Tokens kept in [JPA](https://apereo.github.io/cas/development/authentication/Passwordless-Authentication-Tokens-JPA.html) or [MongoDb](https://apereo.github.io/cas/development/authentication/Passwordless-Authentication-Tokens-MongoDb.html) are now removed once used, and their cleaner removes expired tokens; it used to remove the valid ones.
- MongoDb and [REST](https://apereo.github.io/cas/development/authentication/Passwordless-Authentication-Tokens-Rest.html) stores no longer return expired tokens, and a REST endpoint must answer a single-token `DELETE` with a `2xx` status only when it removed the token.
- A token that could not be delivered by [email or SMS](https://apereo.github.io/cas/development/authentication/Passwordless-Authentication-Notifications.html) is no longer stored and the user is told so, while a failure in one channel no longer discards a token the other one delivered.
- Submitted tokens now arrive as a dedicated `PasswordlessTokenCredential`, which is the only credential the passwordless authentication handler accepts; other one-time password credentials, such as Duo Security passcodes, are no longer checked against the passwordless token store, and the recorded credential type changes accordingly.
- The token field is now a plain text field marked as `one-time-code`, so browsers and phones can fill in the code. A wrong token no longer causes a new token to be issued and sent.
- SMS messages now end with an [origin-bound one-time code](https://apereo.github.io/cas/development/authentication/Passwordless-Authentication-Notifications.html) line (`@host #token`), which the token page reads through the WebOTP API where available.
- When [WebAuthn primary authentication](https://apereo.github.io/cas/development/authentication/Passwordless-Authentication-Passkeys.html) is allowed, the passwordless username field offers discoverable passkeys from the browser’s autofill menu (WebAuthn conditional mediation) where the browser supports it, and the [passwordless selection menu](https://apereo.github.io/cas/development/authentication/Passwordless-Authentication-UserSelectionMenu.html) offers a passkey option. Both hand the passkey assertion to the existing WebAuthn primary authentication flow.

[RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#passwordless-authentication)

A dedicated actuator endpoint, `passwordless`, is available to allows one to query a username and retrieve the associated [passwordless account](https://apereo.github.io/cas/development/authentication/Passwordless-Authentication-Account-Storage.html) information. This capability is also supported and available for the [Palantir Admin Dashboard](https://apereo.github.io/cas/development/installation/Admin-Dashboard.html).

### Multifactor & Passkeys

#### Multifactor Authentication

[RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#multifactor-authentication)

- The [device fingerprint](https://apereo.github.io/cas/development/mfa/Multifactor-TrustedDevice-Authentication-DeviceFingerprint.html) of trusted devices includes the client IP address by default, as documented; it was only used when `cas.authn.mfa.trusted.device-fingerprint.client-ip.enabled` was set explicitly. Devices trusted before the upgrade are asked for multifactor authentication once more; set that setting to `false` to keep the previous fingerprint.
- The [browser fingerprint](https://apereo.github.io/cas/development/mfa/Multifactor-TrustedDevice-Authentication-DeviceFingerprint.html) that the login form collects is kept for the rest of the multifactor flow, so a device trusted while that fingerprint is part of the device fingerprint is recognized at the next login.
- The Google Authenticator registration QR code includes `digits` and `period` when `cas.authn.mfa.gauth.core.code-digits` or `cas.authn.mfa.gauth.core.time-step-size` differ from the defaults, so authenticator apps generate codes CAS accepts.
- [Principal attribute triggers per application](https://apereo.github.io/cas/development/mfa/Configuring-Multifactor-Authentication-Triggers-PrincipalAttribute-PerApplication.html#provider-selection-mappings) can map each multifactor provider to a pattern for the attribute values, so the service definition alone decides which provider a user is sent to; [Palantir](https://apereo.github.io/cas/development/installation/Admin-Dashboard.html) can edit these mappings.
- [Custom multifactor authentication triggers](https://apereo.github.io/cas/development/mfa/Configuring-Multifactor-Authentication-Triggers-Custom.html#register-triggers) register their event resolvers by tagging the resolver bean with an annotation; CAS collects tagged resolvers on its own, and resolvers no longer add themselves to the initial authentication event resolver. Custom resolvers, and beans that override a built-in trigger resolver, must carry the annotation.
- [YubiKey accounts kept in a JSON file](https://apereo.github.io/cas/development/mfa/YubiKey-Authentication-Registration-JSON.html) are written to a temporary file that then replaces the JSON file in one step, so a failed or concurrent write no longer leaves a truncated file behind that stops CAS from starting; removing a single device no longer fails a change made at the same time.

#### WebAuthn Level 3

[RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#webauthn-level-3)

- [FIDO2 WebAuthn](https://apereo.github.io/cas/development/mfa/FIDO2-WebAuthn-Authentication.html) publishes its origins at `/.well-known/webauthn` for related origin requests, so passkeys can be used from origins whose domain differs from the relying party identifier.
- After a successful authentication, CAS reports the user’s accepted passkeys and current account details to the browser through the [Signal API](https://apereo.github.io/cas/development/mfa/FIDO2-WebAuthn-Authentication.html), and passkey autofill checks `getClientCapabilities()` where the browser offers it.
- Registration and authentication requests carry the user-agent hints set in `cas.authn.mfa.web-authn.core.hints`, and each registration records whether the authenticator reported a discoverable credential (`credProps`).
- When an assertion fails because the owning account no longer holds the passkey, the response says so and the browser is told to stop offering it (`signalUnknownCredential`); the account profile does the same as soon as a passkey is deleted, which also covers the account’s last passkey.
- WebAuthn pages now use the browser’s JSON serialization (`parseCreationOptionsFromJSON`, `parseRequestOptionsFromJSON`, `toJSON()`) and no longer override the configured attestation conveyance preference with `direct`; browsers without this WebAuthn Level 3 support can no longer use WebAuthn in CAS.
- CAS also publishes `/.well-known/passkey-endpoints` so password managers can link users to the pages where passkeys are created and managed; by default both point to the [account profile](https://apereo.github.io/cas/development/registration/Account-Management-Overview.html) when account management is enabled.
- When WebAuthn primary authentication is allowed, the default registration button now asks for a discoverable credential (`residentKey=preferred`), so passkeys registered with it can also log in on their own.
- Each registration now keeps the authenticator’s backup eligibility and backup state, which are updated on every login; a passkey whose backup eligibility changes is rejected, as WebAuthn Level 3 requires. The transports recorded at registration are sent back with the credentials CAS lists to the browser, so it can reach each authenticator directly.
- Registrations also keep the authenticator’s AAGUID, and the account profile names passkeys by provider (for example *Google Password Manager* or *1Password*) with the provider’s icon when the attestation does not name the device; the registration and login pages show that name and icon for the passkey just registered or used.
- A new [passkey upgrade](https://apereo.github.io/cas/development/mfa/FIDO2-WebAuthn-Authentication.html), turned on with `cas.authn.mfa.web-authn.core.passkey-upgrade-enabled` alongside primary authentication and untrusted attestation, shows a short page after a password login that lets the browser’s password manager create a passkey for the account on its own (WebAuthn conditional create), then continues as usual. Such passkeys keep the typed username as their name when CAS later reports account details through the Signal API.

#### WebAuthn Multifactor Authentication

[RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#webauthn-multifactor-authentication)

[FIDO2 WebAuthn](https://apereo.github.io/cas/development/mfa/FIDO2-WebAuthn-Authentication.html) multifactor authentication receives the following improvements:

- Authentication and QR session proofs are now principal-bound and single-use, and configured user verification is enforced for assertions.
- Credential and user-handle resolution now avoids repeated repository-wide scans.

#### Duo Multifactor Authentication

[RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#duo-multifactor-authentication)

[Duo Security](https://apereo.github.io/cas/development/mfa/DuoSecurity-Authentication.html) multifactor authentication receives the following improvements:

- REST passcodes now require completed primary authentication, and Universal Prompt callbacks validate browser-session state before restoring the flow.
- Duo Auth and Admin API clients now reuse outbound connection pools across requests.

### OAuth & OpenID Connect

#### OAuth & OpenID Connect

[RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#oauth--openid-connect)

- [Front-channel logout](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Logout.html) requests now carry the same `sid` as the ID token and the back-channel logout token; the session identifier used to be computed differently, so applications could not match it to the session they had recorded.
- Access tokens issued as JWTs can be revoked through the revocation endpoint, and refresh tokens issued as JWTs can be redeemed with the `refresh_token` grant; both used to be looked up in the ticket registry without being decoded first, so revocation silently did nothing and the refresh request was refused.
- JWT access tokens are no longer signed with keys from the application’s own `jwks` that are marked for encryption (`"use": "enc"`); when the application publishes only encryption keys, CAS signs with its own keys. `jwtAccessTokenSigningAlg` is honored as documented: CAS picks a signing key whose type fits the algorithm, and the key’s own algorithm only applies when none is configured or the configured one does not fit the key. Signatures made with an EC (`ES*`) or RSA-PSS (`PS*`) key are randomized, so such a token is a different string each time it is encoded; the ID token’s `at_hash` is computed over the access token the response carries.
- [OAuth relying parties](https://apereo.github.io/cas/development/authentication/OAuth-Authentication-Clients.html#resource-indicators) may request access tokens for specific resources with the RFC 8707 `resource` parameter, limited to the resources listed in the service definition’s `allowedResources`, with authorization and pushed authorization requests, device authorization and CIBA requests, and the password, client credentials, JWT bearer and pre-authorized code grants. Other resources are refused with `invalid_target`. Requested resources are added to the access token audience. Redeeming a code or refresh token may narrow the access token to some of the granted resources, while the refresh token keeps them all. A [token exchange](https://apereo.github.io/cas/development/authentication/OAuth-ProtocolFlow-TokenExchange.html) issues tokens for its own audience or resource only, without the resources of the exchanged token. In request objects, `resource` may be a single value or a list. The consent screen lists the requested resources, and a remembered consent decision covers only the resources it was given for. Resources can be defined in the Palantir service wizard.
- The `allowedResources` of a service definition and of its [token exchange policy](https://apereo.github.io/cas/development/authentication/OAuth-ProtocolFlow-TokenExchange.html) are regular expressions with the same rule: every requested resource must match one of them in full, case-sensitively. The token exchange policy used to accept a request when any one of its resources was found by any pattern. A token exchange is also checked against the service’s own `allowedResources` when the service defines them.
- [Resources every client may request](https://apereo.github.io/cas/development/authentication/OAuth-Authentication-Clients.html#resources-for-every-client) can be listed with `cas.authn.oauth.core.allowed-resources`, in addition to the `allowedResources` of each service. CAS allows its own [MCP server](https://apereo.github.io/cas/development/installation/Admin-MCP-Server.html) endpoint by default, so that clients registered dynamically, which cannot list resources themselves, may request access tokens for it. `checkOidcClientResource` reports whether the client or this setting allowed a resource.
- OpenID Connect clients may identify themselves with [client ID metadata documents](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Client-ID-Metadata-Documents.html), as MCP clients do: the client id is an HTTPS URL whose JSON document CAS fetches, checks and registers, refreshes when its cache lifetime ends and removes once unused. Documents are only fetched from public addresses, without redirects and up to 5 KB, and these clients always see a consent screen that names the host of the client id. Turned off by default; enable it with `cas.authn.oidc.registration.client-id-metadata-document.enabled=true`.
- The keys of an OpenID Connect application defined inline as JSON, as registered through [OpenID Federation](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Federation.html#automatic-registration) or dynamic client registration, are taken as they are; only keystore locations support [Spring expressions](https://apereo.github.io/cas/development/configuration/Configuration-Spring-Expressions.html). OpenID Federation relying parties that publish their keys at a URL other than `https` are not registered.
- OpenID Connect applications whose keys change no longer keep being checked against the keys cached for their earlier definition until that cache expires.
- Unsigned JWTs, whose algorithm is `none`, are no longer supported: CAS neither issues nor accepts them. Request objects, including those encrypted without being signed, are refused with `invalid_request_object`; subject and actor tokens of a token exchange, risk verification tokens and QR authentication tokens must be signed. JWT [access and refresh tokens](https://apereo.github.io/cas/development/authentication/OAuth-Authentication-JWT-AccessTokens.html), [JWT service and ticket-granting tickets](https://apereo.github.io/cas/development/installation/Configure-ServiceTicket-JWT.html) and [JWT authorization responses](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-JARM.html) that would be neither signed nor encrypted are refused rather than issued, so deployments that turn off both must turn on signing. [ID tokens](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Clients.html) are always signed: the `signIdToken` field of OpenID Connect applications is removed, from the Palantir service editor and MCP client summaries too, and ignored in existing definitions, so applications that turned it off now get signed ID tokens; applications that ask for `none` are refused, and dynamic registration refuses `none` with `invalid_client_metadata`. `none` as the user info signing algorithm means a plain JSON response, and user info that is only encrypted is signed first; [JWT introspection responses](https://apereo.github.io/cas/development/authentication/OAuth-Authentication-TokenIntrospection.html) are always signed, with the default algorithm when none or `none` is set; `none` as an encryption algorithm means no encryption; [UMA requesting party tokens](https://apereo.github.io/cas/development/protocol/OAuth-UMA-Protocol.html#requesting-party-tokens) are signed with their keystore. Discovery no longer advertises `none` for ID tokens, user info, introspection, request objects or backchannel authentication requests, even when configured, for example with `cas.authn.oidc.discovery.id-token-signing-alg-values-supported`. The [CAS command-line shell](https://apereo.github.io/cas/development/installation/Configuring-Commandline-Shell.html) only generates signed JWTs: its JWT commands refuse to run without a signing algorithm and secret size, or without a keystore.
- A request object whose `client_id` differs from the `client_id` parameter of the request is refused with `invalid_request_object`, as [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html#RequestObject) requires, instead of being used for the client it names.
- [DPoP](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-DPoP.html) proofs are remembered as used for as long as their `iat` keeps them acceptable, not only for the lifetime of transient session tickets.
- [Encrypted request objects](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-JWKS.html#encrypted-request-objects) now work: CAS decrypts them with the encryption keys it publishes in its JWKS (any rotation state, chosen by `kid`) and verifies the nested request object with the client’s keys; before, it tried the client’s own keys and every encrypted request object failed, OpenID Federation relying parties included. Discovery no longer advertises algorithms CAS cannot decrypt with: `cas.authn.oidc.discovery.request-object-encryption-alg-values-supported` now defaults to `RSA-OAEP`, `RSA-OAEP-256` and the `ECDH-ES` family, without `RSA1_5` and symmetric key wrapping. `RSA1_5` is never accepted nor advertised, even when listed, and the encryption keys are read from the keystore once per JWKS cache period instead of for every request object, and again as soon as the node sees the keystore change or rotates or revokes its keys; signing keys are never used to decrypt.
- Elliptic curve keys that CAS generates for its [OpenID Connect keystore](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-JWKS.html#key-types) carry the signing algorithm of their curve. P-256 keys used to be tagged `ES512`, which the JWKS published and which made signing with them fail, and a key size of `521` produced P-256 keys instead of P-521. Existing keystores are corrected as they are loaded, without new keys, encryption keys no longer carry a signing algorithm, and a configured signing algorithm only selects an elliptic curve key of the matching curve.
- An authorization request whose request object cannot be decrypted or its signature verified is refused with `invalid_request_object`, as OpenID Connect requires. CAS used to ignore such a request object and carry on with the query parameters alone. Request objects signed with `HS256`, `HS384` or `HS512` are [verified with the client secret](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-JWKS-Clients.html#request-objects) of the application, which is checked for expiry and decoded as when the application authenticates with it; one that names no registered client or verifies with none of its secrets is refused the same way instead of failing with a server error. [Backchannel authentication](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-CIBA.html) requests whose request object cannot be verified, or is for another client, are refused with `invalid_request` instead of being answered from their other parameters. [Signed backchannel authentication requests](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-CIBA.html#signed-authentication-requests) must be signed with an asymmetric algorithm, name the client as their issuer, be addressed to the CAS issuer and carry `exp`, `iat`, `nbf` and `jti`, as CIBA requires; they may not be issued in the future, may be valid for 60 minutes at most and are used once. They are verified with the keys of the client that authenticated the request, also when the request names no `client_id`, and refused when CAS does not support the request parameter.
- [Dynamically registered](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Dynamic-Registration.html) OpenID Connect clients, and [OpenID Federation](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Federation.html#automatic-registration) relying parties registered automatically, have their redirect URIs matched exactly, case included. Dynamic registration used to join them into a pattern where `.` matched any character, so look-alike hosts matched, and `?` was not literal, so a redirect URI with a query never matched; automatic registration joined them so that relying parties with two or more redirect URIs never matched. Clients registered dynamically before keep their pattern until they update their registration.
- [Pairwise subject identifiers](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Claims-Pairwise.html) of clients that CAS registered, dynamically or through OpenID Federation, without a sector identifier URI use the host of their first redirect URI, as before. When their redirect URIs name no host, as with native applications, each client is now its own sector; such clients used to share a single sector, so their users receive new identifiers. OpenID Federation relying parties that ask for pairwise identifiers now receive them; they used to receive the plain user id. Clients whose definition carries no salt of their own, which includes clients that CAS registers, calculate them with `cas.authn.oidc.core.pairwise-subject-salt`, read when they are used and never written into client definitions; without it, federation relying parties that ask for them are not registered, and clients registered dynamically are each given a random salt kept with their registration, where they used to get one that changed with every restart. Clients registered dynamically before keep such a changing salt until the setting is defined or they update their registration. Clients that ask for pairwise identifiers with redirect URIs on several hosts must register a sector identifier URI, which must be an `https` URL that can be fetched and lists all of their redirect URIs; the sector identifier URI of a federation relying party is fetched and checked the same way. For clients that CAS registers, only `http` and `https` redirect URIs on hosts other than loopback ones give a sector host; any other redirect URI, such as the private-use scheme of a native application or a loopback redirect URI, puts the client in a sector of its own, its client id, whatever it names, so clients registered by CAS with such redirect URIs receive new identifiers, and mixing both kinds needs a sector identifier URI. Clients registered dynamically before keep the sector they had until they next update their registration, which then gives them the sector described here, and so new identifiers, even when their redirect URIs stay the same. Clients defined by hand keep the host read from their service id; those whose service id names no host, such as `^https?://app.example.org/.*`, in which the `?` ends what is read as the host, or whose sector identifier URI names no host, used to share one sector, and are now each a sector of their own and receive new identifiers. A service id whose host is a pattern, such as `^https://.*`, gives that pattern as its sector; without a salt of their own, such clients used to get a salt that changed with every restart, and with the setting above they now share identifiers. Sectors are read the same way when clients register and when identifiers are calculated, and an `http` or `https` redirect URI whose host cannot be read is refused. Sector identifier documents of dynamically registered clients are fetched from public addresses only, without redirects or retries, within a limited time and size, and must be valid JSON, whatever type they are served as. A client configuration update that is refused now leaves the client as it was and is answered with `invalid_client_metadata`, and an update keeps the generator of pairwise identifiers the client has.
- [Pushed authorization requests](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-PAR.html) are accepted again when [verifiable credentials](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Verifiable-Credentials.html) are turned on; CAS answered them with `401` as if they needed a single sign-on session. A wallet that pushes its authorization request now receives the credentials named by the `authorization_details` it pushed; CAS issued the authorization code without them. Authorization details that cannot be granted are refused with `invalid_authorization_details` when they are pushed, instead of after the redirect to the authorization endpoint.
- [Pushed authorization requests](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-PAR.html) that fail validation are answered with `400` and the actual error, such as `invalid_request_object` or `unsupported_response_type`, as RFC 9126 requires, instead of `403` with a generic `invalid_request`. When pushed authorization requests are required, an authorization request without one is refused at every path of the authorization endpoint, including encoded ones and the OAuth path under the OpenID Connect base path. Endpoints that require client or token authentication now recognize their paths with path parameters, such as `;x=y`, too.
- The `aud` of a token introspection response is the audience of the JWT access token: the service `audience` (or the client ID) and the token’s resources, or the audience requested by a token exchange. It used to be the service URL the token was issued for. Custom introspection response generators now receive the registered service of the client.
- OAuth/OpenID Connect clients are held to what they registered. They authenticate with their registered method, and only as the client the request names, at the token endpoint and also for [pushed authorization](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-PAR.html), [backchannel authentication](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-CIBA.html), revocation and [verifiable credentials](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Verifiable-Credentials.html); an access token no longer authenticates a client. Clients that use [mutual TLS](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-AccessToken-AuthMethods.html#mutual-tls-client-authentication) must register the subject DN or a subject alternative name of their certificate, unless their client id is exactly the certificate’s [SPIFFE](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-AccessToken-AuthMethods.html#spiffe) ID; a subject DN written as a regular expression must now match the whole subject, as `CN=([^,]+),O=Example,C=US` does. User profiles keep every value of a multi-valued attribute, and the `cas.authn.oauth.csrf-cookie` settings are removed since CAS no longer issues that cookie.
- The page where users approve a [device user code](https://apereo.github.io/cas/development/authentication/OAuth-ProtocolFlow-DeviceAuthorization.html) now carries an approval token tied to the single sign-on session, and an approval without it is refused, so another site can no longer make a signed-in browser approve a code. Customized `protocol/oauth/deviceCodeApproval.html` templates must add the hidden `approvaltoken` input of the default template to their form. The error messages of the page are now defined, under `cas.oauth.device.error.*`.
- OAuth/OpenID Connect follow the specifications more closely. [Sender-constrained](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-DPoP.html) access tokens always require their proof, `prompt=login` and `max_age=0` request parameters always lead to a fresh login and `prompt=none` to `login_required`, authorization requests keep all their parameters through login and [consent](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Clients-ScopeApproval.html), and refused requests receive the error and challenge the specifications call for instead of a server error. OpenID Connect JWT access tokens no longer fail to sign now and then under load.
- [Access strategies](https://apereo.github.io/cas/development/services/Configuring-Service-Access-Strategy.html) of OAuth/OpenID Connect applications see the attributes of the authentication, such as `clientIpAddress`, at the authorization endpoint and when a code issued from a single sign-on session is exchanged for tokens, as they do for CAS applications. They used to see only the attributes of the principal there, so a strategy that relied on an authentication attribute refused access.
- OAuth/OpenID Connect endpoints no longer send the `X-XSS-Protection` header, which browsers ignore. Only the `X-Requested-With: XMLHttpRequest` header now marks a script request that is answered with `401` instead of a redirect to the login; the `is_ajax_request` header and parameter are no longer recognized.
- [OpenID Connect logout](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Logout.html) no longer follows any `post_logout_redirect_uri` sent along with a `client_id`. The URI must match the client of the ID token hint, one of its logout URLs, or a registered service; otherwise the session ends without a redirect. An ID token hint that is not a JWT, names no registered client or whose signature cannot be verified is answered with `400` instead of a server error.

#### OpenID Federation

[RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#openid-federation)

- [OpenID Federation](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Federation.html#automatic-registration) relying parties registered automatically must prove they control their keys. Every authorization request, on the OpenID Connect or the OAuth authorization endpoint, wherever they answer and however their path is encoded, carries a signed request object addressed to the CAS issuer, valid for 10 minutes at most and used once, or refers to a pushed authorization request for which the relying party authenticated itself; the proof is for the client the request is actually for, taken from the request object when the request has no `client_id`; at the pushed authorization endpoint, a client assertion must be addressed to the CAS issuer only. Requests that fail are refused without a redirect. Entity identifiers must be `https` URLs, relying parties authenticate with `private_key_jwt` or `tls_client_auth`, client assertions may be addressed to the issuer, and relying parties are registered before client authentication, so a pushed authorization request can be the first contact. The end-to-end federation scenario runs against the go-oidfed OFFA relying party.
- An [OpenID Federation](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Federation.html#trust-anchors) OpenID provider trusts the trust anchors defined with `cas.authn.oidc.federation.trust-anchors`, with their keys obtained out of band, instead of treating its authority hints as trust anchors and taking their keys from whoever answers; without trust anchors, the authority hints are still used, with a warning. Trust chains are resolved within bounds, `cas.authn.oidc.federation.max-authority-hints` authority hints per entity and `cas.authn.oidc.federation.max-path-length` intermediates, and an entity that could not be resolved is not resolved again for `cas.authn.oidc.federation.unresolved-entity-retention`, during which a registration that failed to refresh is kept. Each resolution sends at most `cas.authn.oidc.federation.max-requests-per-resolution` requests (50), reusing statements it already fetched, within `cas.authn.oidc.federation.resolution-timeout` (10 seconds) all requests together, with responses limited in size, the TLS settings of CAS and no redirects; at most `cas.authn.oidc.federation.max-concurrent-resolutions` resolutions (10) run at once. The time limit covers each whole request, host name lookup and TLS handshake included, and requests are only sent over `https` and never to link-local, unspecified or multicast addresses, IPv6 forms included, unless a proxy of the JVM resolves them: an HTTP proxy, or a SOCKS proxy for host names CAS cannot resolve. A trust anchor that cannot be reached, answer in time or answers with a server error, or whose historical keys cannot be read, a sector identifier document that cannot be fetched for now, and a resolution that runs out of time or requests or finds no room among the running requests make the resolution unavailable rather than failed, so the relying party is not remembered as unresolved; a relying party that is not registered yet is then answered with `temporarily_unavailable` instead of as an unknown client. Using a trust chain the relying party sent and resolving its trust chains share one time limit and request budget. The constraints of subordinate statements (maximum path length, naming constraints, allowed entity types) now apply to the trust chains of relying parties and of their trust mark issuers; they used to be ignored. A maximum path length must be a whole number that is not negative.
- [Application keys](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Clients.html) published at a URL are no longer requested at link-local, unspecified or multicast addresses, and redirects are not followed, so keys published behind a redirect must be published at their final location. The [OpenID Federation](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Federation.html#key-rollover) keystore is replaced at once when it changes, so it is never read half written, keeping its permissions and any symbolic link to it. A keystore that holds no keys is given new ones, and one that cannot be read stops CAS from starting, where CAS used to sign with keys of its own on each node and restart, kept nowhere.
- A CAS [OpenID Federation](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Federation.html#subordinates) trust anchor or intermediate issues subordinate statements that carry only the subordinate’s keys, its metadata without the fetch, list and trust mark status endpoints, and the `constraints` of its definition rather than a fixed `max_path_length`; statements are valid to the second, so lifetimes under a day work. The fetch endpoint answers unknown subordinates with `404 not_found` and other failures, such as malformed metadata policies in a subordinate definition, with JSON errors, the listing filters by `entity_type`, matching the entity types a subordinate definition lists or else those of its metadata, and by `intermediate`, and an issuer defined with a trailing slash is used without it as the entity identifier.
- The [OpenID Federation](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Federation.html#entity-configuration) entity configuration of a CAS OpenID provider that issues verifiable credentials also describes it as `openid_credential_issuer` and `oauth_authorization_server`, so federation-based wallets can trust the credential issuer through its trust chain. Trust marks issued to CAS, defined with `cas.authn.oidc.federation.trust-marks`, are published as `trust_marks`. Discovery advertises `client_registration_types_supported` only when CAS is a federation OpenID provider, and the delegated relying party entity statement endpoint answers `404` for an unknown client instead of an empty page.
- [OpenID Federation metadata policies](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Federation.html#metadata-policies): a CAS trust anchor or intermediate publishes the metadata policies of its subordinates, along with the operators they mark critical, and no longer requires metadata in subordinate definitions. A CAS OpenID provider resolves the metadata of relying parties as the specification describes: the metadata their superior states about them comes first, operators beyond the standard ones are ignored unless marked critical, `scope` is handled as a list, and superiors can only shape entity types the relying party publishes itself. Relying parties whose metadata names no `application_type` are registered as `web` applications; their registration used to fail. Relying parties without redirect URIs are not registered, and post-logout redirect URIs that contain a comma are left out.
- An [OpenID Federation](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Federation.html#automatic-registration) relying party can send its trust chain in its request object or client assertion, which CAS verifies and uses instead of resolving the relying party, provided it is not older than the current registration and the request is signed with the keys it carries. A relying party that rolls its keys over is no longer refused until its registration expires: a request object or client assertion signed with a key its registration does not hold has CAS resolve it again, at most once per `cas.authn.oidc.federation.unresolved-entity-retention`.
- An [OpenID Federation](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Federation.html#required-trust-marks) OpenID provider can require trust marks of the relying parties it registers automatically, listed with `cas.authn.oidc.federation.required-trust-marks`. Each trust mark is checked against the issuers that the trust anchor’s own entity configuration accepts in `trust_mark_issuers`, verified with its issuer’s keys through the issuer’s own trust chain, its delegation checked when the trust anchor names an owner of the type, and its status asked of the issuer when it publishes a status endpoint, which must be an `https` URL. A trust mark that cannot be checked for now leaves the relying party unregistered for now, unless another trust mark of its type is valid and no required type is missing. A CAS trust anchor publishes the issuers it accepts, defined with `cas.authn.oidc.federation.trust-mark-issuers`.
- CAS can act as an [OpenID Federation trust mark issuer](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Federation.html#trust-mark-issuer) in any role: the trust mark types it issues and the entities each is issued to are defined with `cas.authn.oidc.federation.issued-trust-marks`, and CAS publishes the trust mark, trust mark status and trust marked entities listing endpoints. Removing an entity from a type revokes its trust marks of that type. A trust anchor also publishes the owners of delegated trust mark types, defined with `cas.authn.oidc.federation.trust-mark-owners`. Trust mark requests can be authenticated with `private_key_jwt` by subordinates, which then only obtain their own trust marks, optionally or always, using `cas.authn.oidc.federation.trust-mark-endpoint-authentication`; only signing algorithms CAS can verify are advertised for them.
- A CAS [OpenID Federation](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Federation.html#resolve-endpoint) trust anchor or intermediate publishes a resolve endpoint, which returns the resolved metadata, trust chain and valid trust marks of an entity for a trust anchor it trusts, as a signed resolve response. Resolutions and entities that could not be resolved are remembered for at most `cas.authn.oidc.federation.unresolved-entity-retention`; a trust mark that cannot be checked for now is left out of the response, which is then not remembered. Resolve requests can be authenticated with `private_key_jwt` by subordinates, optionally or always, using `cas.authn.oidc.federation.resolve-endpoint-authentication`; the response is then addressed to the requester.
- [OpenID Federation keys roll over](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Federation.html#key-rollover): the federation keystore holds current, future and previous keys, all published in the entity configuration, rotated and retired on a schedule (`cas.authn.oidc.federation.rotation.schedule.enabled`, `cas.authn.oidc.federation.revocation.schedule.enabled`) or through the new `oidcFederationJwks` actuator endpoint, which can also revoke a key, for instance when it is compromised. Retired and revoked keys are published by the new federation historical keys endpoint. A scheduled rotation or revocation only changes keys that are old enough, at half the time between two runs of its schedule, so it does not run again after a restart, and nodes that share the keystore file read it again when it changes; in a cluster, the schedules are best enabled on one node. Keys are changed on copies and used only once saved. New keystores start with a future key; existing single-key keystores keep working, and published keys carry the algorithm they sign with, matching the key’s curve. A CAS OpenID provider does not use trust chains whose statements do not name their signing key, or that the trust anchor signed with a key it has revoked or had retired before, as the historical keys it publishes over `https` show; they are read through the entity configuration fetched from the trust anchor, never one a chain carries, and a trust anchor whose historical keys cannot be checked is not trusted. Trust marks signed with a key that is retired but not revoked remain valid until they expire.
- CAS as a [verifiable presentation verifier](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Verifiable-Credentials.html#openid-federation-verifier) can identify itself to wallets with the OpenID4VP `openid_federation` client identifier prefix, chosen with `cas.authn.oidc.vc.presentation.client-identifier-prefix=OPENID_FEDERATION`: the client identifier is the entity identifier of CAS, request objects are signed and named by `kid` and carry no client metadata, and wallets read the `openid_credential_verifier` metadata (keys, response URI, formats) from the CAS entity configuration through its trust chain. Encrypted responses then use the encryption keys of the OpenID Connect keystore (`ECDH-ES` or `RSA-OAEP-256`), in any rotation state, instead of a key per request. Request objects under any prefix are now signed with the algorithm of the signing key’s curve (`ES384`, `ES512`), no longer always `ES256`.

#### Certificate-Bound Access Tokens

[RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#certificate-bound-access-tokens)

The certificate thumbprint that CAS records for [mutual TLS client authentication](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-AccessToken-AuthMethods.html) and emits as the `cnf` `x5t#S256` claim of access tokens and introspection responses is now computed as specified by [RFC 8705](https://www.rfc-editor.org/rfc/rfc8705): the base64url-encoded SHA-256 hash of the DER-encoded certificate. Previously, it was computed from the certificate’s public key and was not encoded as specified. Certificate-bound tokens issued before the upgrade carry the old value and are no longer accepted by resource servers that verify the binding.

#### OpenID Connect Verifiable Credentials

[RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#openid-connect-verifiable-credentials)

- Credential proofs now follow what each [credential configuration](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Verifiable-Credentials.html) advertises: a proof signed with an algorithm outside `proof-signing-alg-values-supported`, or naming its key by a binding method outside `cryptographic-binding-methods-supported`, is refused with `invalid_proof`. Holder keys may now also be given as an `x5c` certificate or a `did:jwk` key identifier, and Ed25519 (`EdDSA`) keys are accepted.
- Authorization server metadata advertises `pre-authorized_grant_anonymous_access_supported` whenever the pre-authorized code grant is supported, so wallets without a client registration know they may redeem a pre-authorized code.
- A wallet may request a credential in the [authorization code flow](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Verifiable-Credentials.html) by the `scope` its credential configuration publishes, as OpenID4VCI 1.0 allows, instead of authorization details. Such scopes were previously dropped unless listed among the discovery scopes, and the resulting token was refused at the credential endpoint.
- A wallet that declines a [verifiable presentation](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Verifiable-Credentials.html) request can now say so: its error response is accepted, answered as OpenID4VP requires, and reported to the relying party as an `error` outcome once the request expires. Since nothing authenticates an error response, it does not end the request, and a valid presentation that arrives before the request expires takes precedence. Previously error responses were rejected.
- Issuer metadata describes each credential format as OpenID4VCI 1.0 requires: `jwt_vc_json` and `jwt_vc_json-ld` configurations publish `credential_definition` instead of `vct`. JSON-LD credentials no longer reference a context document CAS never served, and a configuration without a scope no longer issues a `null` credential type.
- CAS publishes JWT VC Issuer Metadata at `/.well-known/jwt-vc-issuer`, so verifiers other than CAS can find the keys that sign the credentials it issues. Deployments under a context path should add `jwt-vc-issuer` to the [well-known rewrite rule](https://apereo.github.io/cas/development/installation/Servlet-Container-Embedded-Tomcat-RewriteValve.html).
- The verifier accepts RSA and Ed25519 holder keys in addition to EC keys, advertises those key binding algorithms, and advertises the signing algorithms of its `dc+sd-jwt` credential configurations instead of a fixed list.
- A presentation request may carry a registered `redirect_uri` for a [same-device flow](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Verifiable-Credentials.html): the wallet is sent back to it with a `response_code`, which the relying party must present to collect the outcome. Outcomes are now released only to the client that created the request.
- Claims marked `"required": false` in a presentation request are now optional: they are requested through DCQL `claim_sets` and may be withheld. Previously the flag was ignored and every claim was required.
- Issuer metadata can describe the issuer for wallets via `cas.authn.oidc.vc.issuer.display` (name, language, logo).
- Credential offer transactions also return the `openid-credential-offer://` deep link for the offer.
- Issued credentials no longer carry `client_id` (claim or header) or `credential_configuration_id`, which revealed to every verifier which relying party requested the credential; CAS verifies its own credentials by the key’s `kid`.
- A wallet’s error response is stored bounded: `error` to 128 characters and `error_description` to 1024.
- Attribute values with a leading zero, such as postal codes, are issued as text. Previously they were read as octal numbers, so `0123` was issued as `83` and `08` failed issuance.
- Issued credentials carry the issuer signing key’s certificate chain as the `x5c` header, without the trust anchor, as HAIP 1.0 requires, when the key in the keystore has one.
- Signed presentation request objects (`X509_SAN_DNS`) no longer include the trust anchor in their `x5c` header, as HAIP 1.0 requires.
- A self-signed credential or request object signing certificate, which HAIP 1.0 forbids, is still used but now logs a warning.
- Refresh tokens keep the authorization details of the authorization code they were issued for, so access tokens obtained by refreshing may still request the credentials those details granted. Previously only scope-based grants survived a refresh.
- [Verifiable presentation responses](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Verifiable-Credentials.html) can be encrypted (`direct_post.jwt`), as the High Assurance Interoperability Profile requires, with `cas.authn.oidc.vc.presentation.response-mode=DIRECT_POST_JWT` or per request with `"response_mode": "direct_post.jwt"`: each request carries its own ephemeral `ECDH-ES` key, and presentations sent in the clear are refused.
- Verifiable presentation requests may identify CAS with the `x509_hash` client identifier prefix, which the High Assurance Interoperability Profile requires of verifiers that sign requests: `cas.authn.oidc.vc.presentation.client-identifier-prefix=X509_HASH`.
- Verifiable presentations may be requested through the [W3C Digital Credentials API](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Verifiable-Credentials.html) with the `dc_api` and `dc_api.jwt` response modes: CAS builds the request the relying party page passes to the browser, and verifies what the page posts back, bound to the page’s origin.
- Credential requests may carry [key attestations](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Verifiable-Credentials.html), as OpenID4VCI 1.0 and the High Assurance Interoperability Profile define: in the `key_attestation` header of a `jwt` proof, or as an `attestation` proof that yields one credential per attested key. Attestations must chain to trust anchors set with `cas.authn.oidc.vc.issuer.key-attestation.trust-anchors`, and a credential configuration may require them, along with the key storage and user authentication levels it accepts, which the issuer metadata advertises as `key_attestations_required`.
- Clients, wallets above all, may authenticate at the token and pushed authorization request endpoints with a client attestation and its proof of possession, per [OAuth 2.0 Attestation-Based Client Authentication](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-AccessToken-AuthMethods.html) (`attest_jwt_client_auth`), as the High Assurance Interoperability Profile requires of wallet attestations. Attestations must chain to trust anchors set with `cas.authn.oidc.client-attestation.trust-anchors`, which also advertises the method in the discovery document.
- Wallets may report what became of issued credentials at the new [notification endpoint](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Verifiable-Credentials.html): credential responses carry a `notification_id`, and `credential_accepted`, `credential_failure` and `credential_deleted` notifications are recorded in the audit log.
- Issued SD-JWT VC credentials may carry a `status` claim and be revoked or suspended, following the [Token Status List](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Verifiable-Credentials.html) specification: CAS publishes signed status list tokens, keeps entries in the ticket registry, offers an `oidcVcStatus` actuator endpoint to change a credential’s status, and as a verifier checks the status of its own credentials instead of refusing them. Turn it on with `cas.authn.oidc.vc.issuer.status-list.enabled=true`.
- Credential requests and responses may be [encrypted](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Verifiable-Credentials.html) on top of TLS, per OpenID4VCI 1.0: wallets encrypt requests to the encryption keys of the OpenID Connect keystore, published as `credential_request_encryption`, and receive the response encrypted to the key they send in `credential_response_encryption`. Turn it on with `cas.authn.oidc.vc.issuer.encryption.enabled=true`; either direction may also be required.
- The credential endpoint answers with the error codes of OpenID4VCI 1.0: an unpublished credential configuration is `unknown_credential_configuration` instead of the draft-era `unsupported_credential_type`, and a `credential_identifier` the token response did not return is `unknown_credential_identifier` instead of `credential_request_denied`.
- Once the token response returns `credential_identifiers`, credential requests must use `credential_identifier`, as OpenID4VCI 1.0 requires; a `credential_configuration_id` is then refused with `invalid_credential_request`. Otherwise a request must name a `credential_configuration_id`, and no longer falls back to the configuration recorded on the access token.
- Credential configurations may defer issuance (`deferred-issuance`): credential requests are answered with a `transaction_id`, an `oidcVcDeferred` actuator endpoint approves or denies the pending transactions, and wallets collect the credentials from the new [deferred credential endpoint](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Verifiable-Credentials.html), per OpenID4VCI 1.0.
- Client attestations may be bound to [short-lived challenges](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-AccessToken-AuthMethods.html) from the new challenge endpoint, advertised as `challenge_endpoint`, once turned on with `cas.authn.oidc.client-attestation.challenge.enabled=true`. With trust anchors set, clients may also use a DPoP proof in place of the attestation proof of possession (`attest_jwt_client_auth_dpop`).
- Status list tokens carry an `aggregation_uri`, and the new [status list aggregation endpoint](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Verifiable-Credentials.html), advertised as `status_list_aggregation_endpoint`, lists all published status lists.
- Wallets that ask for `application/jwt` receive the credential issuer metadata as [signed metadata](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Verifiable-Credentials.html), per OpenID4VCI 1.0.
- CAS can require [DPoP proofs to carry a nonce](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-DPoP.html) it handed out, per RFC 9449: turned on with `cas.authn.oidc.dpop.nonce.enabled=true`, the token endpoint and the DPoP combined mode answer with `400`, and protected resources with `401`, `use_dpop_nonce` and a fresh nonce in the `DPoP-Nonce` header. The OpenID4VCI nonce endpoint and the client attestation challenge endpoint hand out nonces as well.
- Authorization codes can be bound to the client’s [DPoP key](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-DPoP.html), per RFC 9449: with `dpop_jkt` at the authorization endpoint or in a pushed authorization request, or with a `DPoP` proof sent with the pushed authorization request. The token request must then carry a DPoP proof made with that key, or it is refused with `invalid_dpop_proof`.
- Clients can be required to use [DPoP for every token request](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-DPoP.html), per RFC 9449, with `dpopBoundAccessTokens` in the service definition or `dpop_bound_access_tokens` in a dynamic registration request. Polling with a device code and the JWT bearer grant now verify DPoP proofs too, and bind the tokens they issue.
- A [DPoP-bound access token](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-DPoP.html) is now accepted only in the `Authorization` header with the `DPoP` scheme; sent as a bearer token or as a request parameter, it is refused with `invalid_token`. Refresh tokens issued to public clients are bound to the [DPoP key](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-DPoP.html), per RFC 9449, and the stateless ticket registry keeps the DPoP binding of access and refresh tokens.
- [DPoP proofs](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-DPoP.html) are checked as RFC 9449 requires at every endpoint: a request with more than one `DPoP` header, or with one that is not a JWT, is refused with `invalid_dpop_proof`, which the token endpoint used to answer with `invalid_request`. The profile and userinfo endpoints now answer a missing or invalid proof with `401` and a `WWW-Authenticate: DPoP error="invalid_dpop_proof"` challenge, as the credential endpoint did, and every `DPoP` challenge, Heimdall’s included, lists the algorithms accepted for DPoP proofs in its `algs` parameter.
- Access tokens issued to a client that authenticates with [mutual TLS](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-AccessToken-AuthMethods.html#mutual-tls-client-authentication) are bound to its certificate for every grant, not only `client_credentials`, per RFC 8705. The profile, userinfo and credential endpoints now require the bound certificate, as Heimdall did, and answer `401` with `invalid_token` without it; the stateless ticket registry keeps the binding.
- Clients can be required to [always receive certificate-bound access tokens](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-AccessToken-AuthMethods.html#mutual-tls-client-authentication), per RFC 8705, with `tlsClientCertificateBoundAccessTokens` in the service definition or `tls_client_certificate_bound_access_tokens` in a dynamic registration request: a token request from such a client that is not authenticated with mutual TLS is refused with `invalid_request`.
- CAS issues and verifies [ISO/IEC 18013-5 mdocs](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Verifiable-Credentials.html), such as a mobile driving licence, in the `mso_mdoc` format of OpenID4VCI 1.0 and OpenID4VP 1.0, which HAIP also covers. A credential configuration with the `MSO_MDOC` format names its document type with `cas.authn.oidc.vc.issuer.credential-configurations[].doctype`, and each claim may name its name space with `cas.authn.oidc.vc.issuer.credential-configurations[].claims[].namespace`. The issuer signs the claims’ digests and the wallet’s device key in a mobile security object with an elliptic curve key that carries an X.509 chain. A presented mdoc must be signed by one of CAS’s keys and its device signature must cover the session transcript of the request. CBOR and COSE are handled by the Authlete `cbor` library, from the same vendor as the SD-JWT library CAS already uses.
- [Verifiable credentials](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Verifiable-Credentials.html) issued as `jwt_vc_json` or `jwt_vc_json-ld` no longer carry a `vct` claim, which belongs to SD-JWT VC; wallets such as walt.id classified such a credential as an SD-JWT VC and refused it as not matching the requested format. `dc+sd-jwt` credentials keep their `vct`.

[RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#openid-connect-verifiable-credentials)

Several improvements are now available for [OpenID Connect with Verifiable Credentials](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Verifiable-Credentials.html):

- CAS may require specific principal attributes to determine eligibility before a credential offer transaction can be created for a principal.
- Pre-authorization codes are now also properly updated and removed from the ticket registry after a credential offer transaction is completed once the ticket is expired.
- Access tokens used for credential issuance are now bound to the credential configuration id when the token is minted; this is then enforced when the token is used to request credentials.
- Transaction-code requirements are enforced during token exchange.
- A pre-authorized code is now redeemed atomically, before the access token is minted, rather than being marked used afterwards. Previously two concurrent exchanges of the same code could each receive an access token, since the code was only updated once the token already existed. OpenID4VCI requires the code to be single use.
- Credential batch issuance is limited and capped at a predefined limit.
- OpenID Connect discovery now advertises `none` among `token_endpoint_auth_methods_supported`, and `none` is understood as a client authentication method. CAS already accepted unauthenticated token requests for grants that carry their own proof of authorization, such as the OpenID4VCI pre-authorized code, but never said so; a client with no credentials had no way to discover that and would choose a method it could not satisfy.
- OpenID Connect discovery now publishes `token_endpoint_auth_signing_alg_values_supported`. [RFC 8414](https://www.rfc-editor.org/rfc/rfc8414) requires this whenever `private_key_jwt` or `client_secret_jwt` appears in `token_endpoint_auth_methods_supported`, which CAS advertises by default, and clients that enforce the requirement previously refused to use the authorization server at all. The published list is configurable and, as the specification requires, excludes `none`.
- Credential issuance now speaks the finalized OpenID4VCI 1.0 wire format rather than draft 13. The credential request takes `proofs` (plural) and accepts `credential_identifier`; the credential response is a `credentials` array and no longer carries `format`. The separate batch credential endpoint is removed, since a batch is now one request carrying several proofs, and the issuer metadata advertises `batch_credential_issuance` instead of `batch_credential_endpoint`. The proof challenge is no longer returned from the token endpoint, and the nonce endpoint returns `c_nonce` alone.
- The outcome of a [verifiable presentation](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Verifiable-Credentials.html#verifiable-presentations) now reaches the relying party that asked for it. Previously the wallet was told whether its presentation verified and the transaction was deleted, leaving the relying party with no way to learn the result or the disclosed claims. A new `oidcVcPresentationResult` endpoint, protected by the same client authentication as the request creation endpoint, reports `pending` until the wallet answers and then delivers the outcome together with the disclosed claims, once.
- Verifiable presentation requests are now delivered the way OpenID4VP requires. A request made under the `redirect_uri` client identifier prefix cannot be signed, and a request URI must serve a signed request object, so CAS now carries the whole authorization request by value in the wallet deep link and no longer offers a request URI in that mode. Setting `cas.authn.oidc.vc.presentation.client-identifier-prefix` to `X509_SAN_DNS` signs the request object and serves it by reference as `application/oauth-authz-req+jwt`. Previously CAS advertised a request URI that returned a plain JSON document.
- The credential endpoint now behaves as an OAuth protected resource. A missing, unknown or expired access token is answered with `401` and a `WWW-Authenticate` challenge in the scheme the wallet used, rather than `400 invalid_request`. Other failures now carry the credential error codes OpenID4VCI defines, such as `invalid_credential_request`, `unsupported_credential_type`, `credential_request_denied`, `invalid_proof` and `invalid_nonce`, instead of a single opaque `invalid_request`. The split between `invalid_proof` and `invalid_nonce` is what lets a wallet tell a stale nonce, which it can recover from by fetching a new one, from a proof it cannot fix.
- The authorization-code flow for verifiable credentials no longer ignores confidential clients or requests without PKCE. The response builder previously claimed the request only when the client had no secret *and* sent a code challenge; every other client had its `authorization_details` quietly dropped, and the failure surfaced much later as a refusal at the credential endpoint. OpenID4VCI 1.0 only recommends PKCE and places no restriction on confidential clients. A credential the client cannot be granted is now reported as `invalid_authorization_details` per [RFC 9396](https://www.rfc-editor.org/rfc/rfc9396) rather than dropped.
- Credential types are now [authorized per service](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Verifiable-Credentials.html#authorized-credential-types). Previously the offer endpoint and the authorization-details filter only checked that a credential configuration id existed in `cas.authn.oidc.vc.issuer.credential-configurations`, so any registered client could obtain any credential the deployment defined. An OpenID Connect service can now carry a `verifiableCredentialsPolicy` naming the credential configuration ids it may obtain, enforced when an offer transaction is created, when authorization details become an authorization code, and again at the credential endpoint when the token is spent. A service with no policy, or a policy that names no credential types, may obtain everything the issuer publishes, so existing deployments are unaffected.
- A service’s verifiable credentials policy may also narrow the algorithms its credentials are signed with, through `credentialSigningAlgValuesSupported`. Like the credential types on the same policy it can only narrow: the effective set is the intersection with what the credential configuration advertises.
- Verifiable credentials are no longer signed as if they were ID tokens. A credential was passed to the ID token signing service, so a client with `encryptIdToken` received a JWE rather than an SD-JWT, `signIdToken=false` produced an unsigned credential, and the algorithm came from the client’s `idTokenSigningAlg` instead of the advertised `credential-signing-alg-values-supported` which CAS’s own verifier then rejected. Credentials are now always signed, never encrypted, and use the first advertised algorithm the issuer key can perform, with the advertised list enforced as the permitted set.
- Issued credentials now honor a configurable lifetime per credential configuration, `cas.authn.oidc.vc.issuer.credential-configurations[].credential-validity`, which defaults to thirty days. Previously every credential expired five minutes after issuance, which left it unusable by the time a wallet had stored it.

[RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#openid-connect-verifiable-credentials)

[OpenID Connect with Verifiable Credentials](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Verifiable-Credentials.html) now supports the authorization code flow. There are also significant changes in place to support more formats with additional bug fixes and enhancements, and an improved test suite to verify credential issuance. There is also initial support for OpenID Connect with Verifiable Presentations (OpenID4VP).

#### OAuth and OpenID Connect

[RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#oauth-and-openid-connect)

[OAuth](https://apereo.github.io/cas/development/protocol/OAuth-Protocol.html) and [OpenID Connect](https://apereo.github.io/cas/development/authentication/OIDC-Authentication.html) security have been strengthened across several flows.

- Client assertions now require matching subject, issuer, and client identifiers.
- JWT IDs are tracked through the shared ticket registry to prevent replay across clustered deployments.
- Device and CIBA polling are bound to the authenticated client and completed poll-mode requests are consumed.
- Dynamic client key registrations are memory-bounded.
- [Token exchanges](https://apereo.github.io/cas/development/authentication/OAuth-ProtocolFlow-TokenExchange.html) are now authorized against the authenticated requesting client, and exchanged tokens cannot gain scopes beyond those granted to the subject token and allowed for the requesting client.
- Client secrets are now compared and enforced using a case sensitive strategy.
- Token introspection reports tokens from other clients as inactive, while revocation only affects the requesting client’s tokens and no longer reveals whether other tokens exist.
- [DPoP-bound access tokens](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-DPoP.html) can now be spent. CAS answers the token request with `token_type: DPoP` whenever a proof accompanies it, but protected resources recognized the `Bearer` authorization scheme alone, so a client following [RFC 9449](https://www.rfc-editor.org/rfc/rfc9449) section 7.1 and presenting the token as `Authorization: DPoP ...` was told the token was missing. The userinfo, JWKS registration and credential endpoints now accept either scheme.
- DPoP proofs are now verified at protected resources through a single `validateProtectedResourceRequest` operation on the proof-of-possession validator. The credential endpoint previously accepted a DPoP-bound access token on presentation alone, which left the sender constraint doing nothing; it now answers a missing or invalid proof with `401` and `WWW-Authenticate: DPoP error="invalid_dpop_proof"`. The userinfo endpoint keeps the same checks but no longer runs the token endpoint’s verifier over a resource request in addition to the correct one, and the resource-side check now includes the single-use `jti` check.
- DPoP proof validation no longer takes the client identifier from the unauthenticated `client_id` request parameter. The identifier is resolved from the authenticated profile first, then from the access token, and only then from the request, which matters for grants that carry their own proof of authorization rather than client credentials, such as the OpenID4VCI pre-authorized code. Validation also no longer overwrites the profile identifier when that identifier belongs to the subject rather than the client.
- Single-use checking for DPOP proofs is now enforced using the CAS ticket registry. Furthermore, DPOP requests are no longer treated as a form of client authentication and now sit on top of existing client authentication approaches such as client ID and client secret for confidential clients or PKCE.
- [User-Managed Access](https://apereo.github.io/cas/development/protocol/OAuth-UMA-Protocol.html) resources and policies are now bound to both the authenticated client and resource owner. New resource registrations also receive non-sequential, server-assigned identifiers.

#### OAuth & OpenID Connect Client Secrets

[RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#oauth--openid-connect-client-secrets)

OAuth and OIDC client applications may now define [multiple client secrets](https://apereo.github.io/cas/development/authentication/OAuth-ClientSecret-Management.html), allowing deployments to support secret expiration and smoother secret rotation. Existing single-secret configurations remain compatible, while new configurations can include additional secrets with expiration metadata so clients can transition to new secrets without immediate disruption. Client secret rotation may be carried out using a dedicated `oauthClientSecrets` actuator endpoint.

#### OpenID Connect Federation

[RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#openid-connect-federation)

Work on [OpenID Connect Federation](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Federation.html) is now roughly finalized. A number of test scenarios and minor edge cases are still being worked on and should be resolved in the next few releases.

### SAML2

#### SAML2

[RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#saml2)

- The [ECP profile](https://apereo.github.io/cas/development/authentication/Configuring-SAML2-Authentication.html) answers a successful login with the SAML2 response in the SOAP envelope again, instead of a SOAP fault.
- [Back-channel SAML2 logout](https://apereo.github.io/cas/development/installation/Configuring-SAML2-Logout.html) with the SOAP binding sends the SOAP envelope as the body of the request, instead of a URL-encoded copy of it.
- A `nameIdQualifier` or `serviceProviderNameIdQualifier` set to `none` on a [SAML2 service](https://apereo.github.io/cas/development/services/SAML2-Service-Management.html) leaves that qualifier out of the NameID, as documented, instead of sending the value `none`.
- The [attribute query profile](https://apereo.github.io/cas/development/installation/Configuring-SAML2-AttributeQuery.html) releases the attributes its release policy allows; the policy could not find the metadata of the service provider for a query and released none of them.
- For [unsolicited SSO](https://apereo.github.io/cas/development/authentication/Configuring-SAML2-Authentication-Unsolicited.html), the SAML2 identity provider no longer signs the authentication request it builds itself, nor checks it for a signature, and the `signUnsolicitedAuthnRequest` setting of SAML2 service definitions is removed. Unsolicited SSO also works when the session state is kept in browser storage. [Delegated authentication](https://apereo.github.io/cas/development/integration/Delegate-Authentication-SAML2.html) no longer reloads the metadata of a SAML2 identity provider for each request.
- [SAML2 service provider metadata](https://apereo.github.io/cas/development/installation/SAML2-ServiceProvider-Metadata.html#metadata-caching--resolution) that cannot be resolved is no longer retried with one-second pauses during the request, and a failure to load it is remembered for `cas.authn.saml-idp.metadata.core.failure-cache-expiration` instead of contacting the metadata source on every request. Metadata query servers keep one backup per entity, so an entity no longer falls back onto the metadata of another, and metadata stores such as JPA or MongoDB are cached per entity, so providers that share a service definition no longer evict each other’s metadata. Changes to the metadata settings of a service definition take effect on the next request on every node, and checking whether the JPA metadata store is available no longer opens a new connection pool each time.
- [SAML2 identity provider metadata](https://apereo.github.io/cas/development/installation/Configuring-SAML2-DynamicMetadata.html), keys and certificates are fetched again from their storage once `cas.authn.saml-idp.metadata.core.cache-expiration` has passed, even while in constant use, so a key rotation in a shared metadata store reaches every node.
- [SAML2 service provider metadata](https://apereo.github.io/cas/development/installation/SAML2-ServiceProvider-Metadata.html#default) downloaded over HTTP or from a [metadata query server](https://apereo.github.io/cas/development/installation/Configuring-SAML2-DynamicMetadata-MDQ.html) is fetched with conditional requests, and an unchanged document is no longer downloaded again. Comma-separated locations are all tried before the metadata backup file is used, and a failed load names every location and the reason it failed, in the logs and in the answer of the cached metadata endpoint. Validators are kept in a file next to the backup instead of an extended file attribute, which many file systems do not support.
- Service provider metadata kept in [Amazon S3](https://apereo.github.io/cas/development/installation/Configuring-SAML2-DynamicMetadata-AmazonS3.html) is no longer downloaded in full to find one provider: only matching documents are fetched, and buckets with more than 1000 documents are read completely. Metadata kept in [Git](https://apereo.github.io/cas/development/installation/Configuring-SAML2-DynamicMetadata-Git.html) pulls the repository at most once per `cas.authn.saml-idp.metadata.git.schedule.repeat-interval` instead of on every load. The metadata health indicator checks each metadata source once per run rather than once per service.
- [SAML2 service provider metadata](https://apereo.github.io/cas/development/installation/SAML2-ServiceProvider-Metadata.html#metadata-caching--resolution) read from files, directories or the JSON metadata file is loaded again once a file changes, checked at most once per `cas.authn.saml-idp.metadata.file-system.change-detection-interval`. The cached metadata endpoint lists statistics and entries of the metadata cache, including the failures it remembers, and reports whether each entity is still valid; the cache statistics are also published as metrics. A metadata backup file that its source has not confirmed for longer than `cas.authn.saml-idp.metadata.http.metadata-backup-max-age` is logged as an error when used and flagged by the metadata health indicator. Metadata criteria patterns are compiled once per load, metadata signature certificates are downloaded once per load, and downloaded metadata is no longer copied as text.
- [Cached SAML2 service provider metadata](https://apereo.github.io/cas/development/installation/SAML2-ServiceProvider-Metadata.html#metadata-caching--resolution) is loaded again once its `validUntil` passes, and a `cacheDuration` declared by the metadata is kept between `cas.authn.saml-idp.metadata.core.minimum-cache-duration` and `cas.authn.saml-idp.metadata.core.maximum-cache-duration`. A request for an entity that an aggregate does not hold no longer walks every entity of the aggregate. Metadata resolvers returned by [Groovy scripts](https://apereo.github.io/cas/development/installation/Configuring-SAML2-DynamicMetadata-Groovy.html) that run background tasks are destroyed once no longer used. Metadata kept in [Redis](https://apereo.github.io/cas/development/installation/Configuring-SAML2-DynamicMetadata-Redis.html) is indexed by entity, so a service provider is found without scanning every key. A [metadata query](https://apereo.github.io/cas/development/installation/Configuring-SAML2-DynamicMetadata-MDQ.html) location given as an expression, or as one of several comma-separated locations, is cached per entity like any other.
- [SAML2 identity provider metadata](https://apereo.github.io/cas/development/installation/Configuring-SAML2-DynamicMetadata.html) is cached once for all services that have no metadata of their own, instead of once per service, so deployments with many SAML2 services no longer read identity provider metadata from its storage on every response.
- [SAML2 identity provider metadata](https://apereo.github.io/cas/development/installation/Configuring-SAML2-DynamicMetadata.html) is served as `application/samlmetadata+xml` when asked for, carries an `ETag`, and answers `304 Not Modified` to a client that already holds it.
- A signed [SAML2 attribute query](https://apereo.github.io/cas/development/installation/Configuring-SAML2-AttributeQuery.html) is answered with the SOAP response alone. The identity provider also wrote the POST-binding form of the response before it, so the service provider received an HTML page and the SOAP response could not be sent.

#### SAML2 Identity Provider

[RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#saml2-identity-provider)

[SAML2 identity provider](https://apereo.github.io/cas/development/authentication/Configuring-SAML2-Authentication.html) support receives the following improvements:

- Presented SAML2 authentication request signatures are now validated independently of the metadata signing requirement. Only a successfully validated signature may authorize an assertion consumer service URL that is not registered in service provider metadata.
- IdP-initiated SSO now also requires a caller-provided `shire` to match a POST assertion consumer service registered in metadata.
- Inbound encrypted SAML2 identifiers now use CAS’s matching IdP encryption certificate and private key for decryption.
- Signature algorithm inclusion and exclusion policies are now enforced consistently across all inbound SAML bindings.
- The `Address` field of the SAML2 `SubjectConfirmationData` is now populated with the requester’s IP address when possible.
- SAML2 metadata cache entries are now isolated by registered service and resolved without scanning unrelated cached resolvers.
- SAML2 authentication request session entries are updated per session and consumed after callback correlation, avoiding controller-wide serialization and retained replicated-session state.
- Storage-backed SAML2 metadata resolution now uses the requested entity ID to limit document lookups and resolver rebuilds.
- SAML2 SOAP attribute queries and artifact resolutions now require independently validated signatures, message freshness, destinations, and replay protection; artifact tickets are relying-party bound and consumed atomically.
- [SAML2 single logout](https://apereo.github.io/cas/development/installation/Configuring-SAML2-Logout.html) now validates request freshness and replay, fully authenticates and correlates logout responses, and generates actuator logout requests with the correct IdP issuer and service-provider destination.
- [Identity provider metadata](https://apereo.github.io/cas/development/installation/Configuring-SAML2-DynamicMetadata.html#per-service) is now resolved atomically, so concurrent requests can no longer observe a metadata document, or the signing and encryption credentials that belong to it, that was installed on behalf of a different registered service.
- A [service provider metadata backup file](https://apereo.github.io/cas/development/installation/SAML2-ServiceProvider-Metadata.html) is no longer deleted ahead of a download attempt, and is used as a fallback when the remote metadata source or MDQ server cannot be reached, so a momentary outage there no longer takes the service offline.
- A service that defines a metadata signature location now fails to load its metadata when that certificate cannot be read, rather than loading the metadata unverified.
- Downloaded service provider metadata is written to its backup file in a single step, so services that share one metadata location can no longer read a partially written document.
- The JDBC and MongoDB identity provider metadata stores now scope the global document lookup to the global owner, so a service’s own document and keys can no longer answer in its place.
- Metadata query requests now ask for `application/samlmetadata+xml` via the `Accept` header, as the SAML profile for the Metadata Query Protocol requires, and are issued through the CAS HTTP client so they use the deployment’s TLS trust store and hostname verifier.
- The SAML2 metadata health indicator answers from the local metadata backup copy where one exists, instead of reaching out to every service’s metadata host on every health check.
- SAML2 metadata user interface information is resolved once per login attempt rather than twice, and dynamically-resolved entities are cached, so rendering the login page no longer issues a metadata query per request.
- Outbound HTTP calls now reuse their underlying HTTP client and its connection pool instead of building and discarding one per request, which recovers connection reuse and stops the server accumulating open sockets over time.
- Service names can no longer resolve per-service identity provider metadata directories outside the configured metadata root. Explicit `idpMetadataLocation` settings remain supported.
- HTTP response buffering now limits downloads, including decompressed bodies, to 256 MiB by default. The limit can be changed with the `org.apereo.cas.util.http.HttpRequestUtils.maximumResponseSize` system property or overridden per request.

### CAS & Other Protocols

#### CAS Protocol

[RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#cas-protocol)

- The CAS `1.0` [validation response](https://apereo.github.io/cas/development/ux/User-Interface-Views-CASv1.html) is line-delimited and carries no escaping mechanism. Principal identifiers and rendered attribute lines are now stripped of line breaks, so a value that contains a newline can no longer forge additional lines in the response.
- A [proxy-granting ticket](https://apereo.github.io/cas/development/authentication/Configuring-Proxy-Authentication.html#use-case) is now issued only after the service ticket, the validation specification and the authentication context have all been accepted. The `pgtUrl` callback used to be contacted and the proxy-granting ticket minted before any of those checks ran, so presenting a leaked service ticket could drive an outbound request and leave an unused proxy-granting ticket behind even though validation went on to fail.
- Validation responses now use the protocol’s own error codes for two cases that previously reported something else. A ticket that fails the validation specification without a `renew` request is reported as `INVALID_TICKET_SPEC` rather than `INVALID_TICKET`, and an unexpected failure during validation is reported as `INTERNAL_ERROR` rather than `INVALID_REQUEST`, which stays reserved for a request that is missing required parameters. A ticket that did not come from an initial login while `renew` was requested continues to be reported as `INVALID_TICKET`, as the protocol specifies.
- Attribute names are now sanitized into valid XML names before they are rendered as elements in the CAS `2.0`/`3.0` validation response, and proxy URLs in `<cas:proxy>` are XML-escaped. Previously an attribute name was only stripped of spaces and a proxy URL was written out verbatim, so either could carry markup or quoting characters into the response. Note that a name containing characters that are not legal in an XML name — a colon, for instance — now renders with those characters replaced by an underscore; such a name previously produced a response that was not well-formed XML.
- The SAML `1.1` [validation response](https://apereo.github.io/cas/development/protocol/SAML-v1-Protocol.html) now sets `Recipient` to the service URL the response is intended for, and reserves `InResponseTo` for the `RequestID` of the request being answered, omitting it when the caller did not send one. `InResponseTo` previously carried the hostname of the `TARGET` service unless a `RequestID` happened to be present, and `Recipient` was never set at all, so a relying party had no way to bind the response to its own request or endpoint.
- The `renew` parameter presented to the [CAS protocol](https://apereo.github.io/cas/development/protocol/CAS-Protocol.html) validation endpoints is now evaluated per request. Validation specifications are shared components and the requested value used to be assigned onto them for the duration of a request, which allowed a concurrent validation request to reset it. A service ticket that was not issued from a new login can no longer satisfy a `renew=true` validation because of another request that happened to be in flight at the same time.

### Authorization

#### Heimdall AuthZEN

[RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#heimdall-authzen)

[Heimdall](https://apereo.github.io/cas/development/authorization/Heimdall-Authorization-Overview.html) AuthZEN support is reworked to follow the [AuthZEN Authorization API 1.0](https://openid.net/specs/authorization-api-1_0.html) specification:

- The AuthZEN `resource.id` now identifies the resource instance rather than a policy namespace. AuthZEN requests are matched against [resources in all namespaces](https://apereo.github.io/cas/development/authorization/Heimdall-Authorization-Resources.html) by their new `resourceType`, `actions` and optional `resourceIdPattern` fields, and all matching resources must grant access. Existing AuthZEN resources must define these fields, or AuthZEN requests are denied; requests to `/heimdall/authorize` are unaffected.
- Evaluated denials return `200` with `"decision": false`, malformed requests `400`, and failed caller authentication `401`. The `X-Request-ID` header is echoed.
- Tokens must be issued to a registered OAuth or OpenID Connect application whose access strategy allows access, and a new [Heimdall access strategy](https://apereo.github.io/cas/development/authorization/Heimdall-Authorization-Principal.html) can prevent an application from calling Heimdall. [DPoP-bound](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-DPoP.html) and certificate-bound tokens now require their proof. On the AuthZEN endpoint, `Basic` credentials are the `client_id:client_secret` of a registered application rather than CAS user credentials.
- [JWT bearer assertions](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-JWT-Bearer.html) must carry `jti` and `iat` claims, are accepted once, and may not live longer than `cas.heimdall.jwt-assertion-max-lifetime` (five minutes by default).
- Failed caller authentication on `/heimdall/authorize` now returns `401` instead of `403`. Failed caller authentication on both Heimdall endpoints is subject to [authentication throttling](https://apereo.github.io/cas/development/authentication/Configuring-Authentication-Throttling-Failure.html).
- A resource with no policies now denies access instead of granting it, and the REST policy no longer sends the resource’s policies to its endpoint.
- [JDBC policies](https://apereo.github.io/cas/development/authorization/Heimdall-Authorization-Policies.html) use a shared connection pool, registered as an application context bean and optionally named via `dataSourceName`, instead of opening a new database connection for every decision. Queries time out after `queryTimeout` (five seconds by default). Policies are evaluated in order rather than on the shared thread pool.
- AuthZEN subjects are resolved from CAS [attribute repositories](https://apereo.github.io/cas/development/integration/Attribute-Resolution.html) only for the `user` subject type. Required and rejected attribute policies accept qualified names such as `subject.properties.department`, `resource.properties.owner`, `action.properties.method` and `context.channel`. HTTP request headers are no longer added to the AuthZEN request context; on `/heimdall/authorize` they no longer override body entries or include protocol headers.
- A resource that does not set `enforceAllPolicies` is now granted when any one of its policies grants access, as documented; previously every policy had to grant. Set `enforceAllPolicies` to `true` on resources that rely on the old behavior. In that mode, a policy that fails with an error no longer prevents a later policy from granting access.
- Heimdall supports the AuthZEN [access evaluations API](https://apereo.github.io/cas/development/authorization/Heimdall-Authorization-AuthZEN.html) at `/heimdall/authzen/evaluations`, with the `execute_all`, `deny_on_first_deny` and `permit_on_first_permit` semantics.
- Heimdall publishes AuthZEN [policy decision point metadata](https://apereo.github.io/cas/development/authorization/Heimdall-Authorization-AuthZEN.html) at `/heimdall/.well-known/authzen-configuration`; the well-known location defined by the specification needs a [rewrite rule](https://apereo.github.io/cas/development/installation/Servlet-Container-Embedded-Tomcat-RewriteValve.html).
- Denied AuthZEN decisions carry a [decision context](https://apereo.github.io/cas/development/authorization/Heimdall-Authorization-AuthZEN.html) with a `reason` code.
- [JDBC and OpenFGA policies](https://apereo.github.io/cas/development/authorization/Heimdall-Authorization-Policies.html) receive the AuthZEN subject, resource and action. [Palantir](https://apereo.github.io/cas/development/installation/Admin-Dashboard.html) can edit the AuthZEN fields of a resource and configure the Heimdall access strategy.

### Attributes & Consent

#### Attribute Release

[RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#attribute-release)

- The [pattern matching attribute release policy](https://apereo.github.io/cas/development/integration/Attribute-Release-Policy-PatternMatching.html) no longer releases the transformation template itself for values that do not match the pattern; such values are dropped, and the attribute is not released when none of its values match, as the inline Groovy transformation already did.
- The [return linked attribute release policy](https://apereo.github.io/cas/development/integration/Attribute-Release-Policy-ReturnLinked.html) releases the values of the first linked attribute that has a value, as documented; it used to release the last one.
- [SAML2 attribute release policies](https://apereo.github.io/cas/development/installation/Configuring-SAML2-Attribute-Release.html) that match metadata entity attributes, entity categories included (InCommon and REFEDS Research and Scholarship, anonymous, pseudonymous and personalized access), accept entity id patterns: a service provider whose metadata does not carry the entity attribute still receives the attributes when its entity id matches one of them. The Palantir service wizard can edit these patterns.

#### Attribute Consent

[RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#attribute-consent)

- [Consent decisions](https://apereo.github.io/cas/development/integration/Attribute-Release-Consent.html) are now assigned a unique identifier when they are created. Only the JDBC store generated one; every other store recorded the decision with an identifier of `0`, which is the key those stores use. LDAP and Redis therefore kept a single decision per user, and MongoDB and DynamoDB kept a single decision for the entire deployment, each new consent silently replacing the one before it. Decisions recorded by earlier versions continue to be honored.
- [DynamoDB](https://apereo.github.io/cas/development/integration/Attribute-Release-Consent-Storage-DynamoDb.html) consent lookups now match the service and the decision identifier for equality. They were matched as greater-than-or-equal, so consent granted to one service could answer the check for another, and revoking a single decision removed every decision whose identifier sorted at or above it.
- [Redis](https://apereo.github.io/cas/development/integration/Attribute-Release-Consent-Storage-Redis.html) consent lookups now treat the principal as a literal value. Glob characters such as `*` and `?`, and the `:` separator itself, were interpolated into the scan pattern, so a principal identifier that contained them could read and delete decisions that belong to other users.
- Revoking consent now removes every decision it should. Against the JDBC store, revoking all decisions for a user removed nothing at all once the user had more than one, and when several stores are configured together only the first store that reported success was asked to delete. The [REST](https://apereo.github.io/cas/development/integration/Attribute-Release-Consent-Storage-REST.html) store now sends the decision identifier as `decisionId`, the parameter its documented contract defines; it previously sent `id`, which a conforming server reads as a request to revoke everything for that user.
- A consent decision that cannot be deciphered no longer fails the login. Rotating the `cas.consent.core.crypto` keys, or a node that does not have them, produced an error that propagated out of the login flow; such a decision now counts as a mismatch, so the user is asked to consent again and the record is replaced using the current keys.
- Reminder options submitted with a consent decision are now validated. A missing or unparsable value falls back on the configured default, and a reminder that cannot be applied to a date, such as a negative amount or a time unit of `FOREVER`, is rejected rather than stored and then failing every subsequent login for that user.
- Consent decisions record their creation date in UTC, so a decision’s age is the same on every CAS node whatever time zone each node runs in. Dates shown on the [account profile](https://apereo.github.io/cas/development/registration/Account-Management-Overview.html) screen are labeled as UTC.
- Consent is now evaluated once per request. The [activation strategy](https://apereo.github.io/cas/development/integration/Attribute-Release-Consent-Activation.html) returns the full consent query result, carrying the decision and the consentable attributes, and the login flow reuses it rather than evaluating the attribute release policy and reading the consent store a second time to render the consent screen. Note that a Groovy activation script must now return a `ConsentQueryResult` instead of a boolean.

#### Attribute Definition Dependencies

[RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#attribute-definition-dependencies)

Attribute definitions may now [declare dependencies](https://apereo.github.io/cas/development/integration/Attribute-Definitions.html) on other attribute definitions. When an attribute is resolved, its declared dependencies are resolved first and their results are made available during resolution, allowing definitions to build on values produced by other definitions in a predictable, reusable way.

### Tickets & Sessions

#### Tickets & Sessions

[RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#tickets--sessions)

- [CAS 2.0 proxy validation](https://apereo.github.io/cas/development/protocol/CAS-Protocol-V2-Specification.html) responses list each proxy of a chain once; a ticket obtained through several proxies listed each of them as many times as there were proxies.
- New deployments of CAS, and upgrades to a new CAS version, should always start with a clean [JPA ticket registry](https://apereo.github.io/cas/development/ticketing/JPA-Ticket-Registry.html) table: drop the existing table and let CAS create it. Schema updates do not change the type or length of an existing column, and this version changes several of them.
- With PostgreSQL, the [JPA ticket registry](https://apereo.github.io/cas/development/ticketing/JPA-Ticket-Registry.html) stores ticket attributes as `jsonb`, and session lookups by attribute, as in delegated OpenID Connect and SAML2 logout, can be answered by a GIN index instead of reading every session. The index is not created by CAS: the administrator must create it once after the ticket table is created, with the statement on that page. A table created by an earlier version keeps a `json` column, on which those lookups fail; start with a clean table or convert the column.
- The [JPA ticket registry](https://apereo.github.io/cas/development/ticketing/JPA-Ticket-Registry.html) stores when each ticket expires, including its idle timeout, and indexes it, and the [ticket registry cleaner](https://apereo.github.io/cas/development/ticketing/Ticket-Registry-Cleaner.html) asks only for tickets whose time has passed, with a full scan at startup and hourly; it used to read and decode every ticket on every run. With MySQL, the registry streams scans through a server-side cursor (`useCursorFetch`) and leaves single-ticket lookups as they were; the defaults can be overridden with `cas.ticket.registry.jpa.properties`.
- Looking up sessions by attribute in the [JPA ticket registry](https://apereo.github.io/cas/development/ticketing/JPA-Ticket-Registry.html) no longer fails on MySQL when the caller runs outside a transaction, as delegated OpenID Connect and SAML2 logout do. Reading sessions and tickets without a transaction no longer fails on PostgreSQL or MySQL either, as the [single sign-on sessions](https://apereo.github.io/cas/development/authentication/Configuring-SSO.html#actuator-endpoints) and [ticket registry](https://apereo.github.io/cas/development/ticketing/Configuring-Ticketing-Components.html#actuator-endpoints) endpoints, the MCP server tools, JMX, OAuth token management and deferred verifiable credential issuance do; outside a transaction the registry reads the matching tickets in one of its own. Listing every ticket over JMX or every OAuth token through the management endpoint reads the registry in batches instead of loading all of its tickets first, and the JMX ticket count is a single count query rather than a read of every ticket. Updating a ticket encrypts it once instead of twice when ticket encryption is turned on.
- The [JPA ticket registry](https://apereo.github.io/cas/development/ticketing/JPA-Ticket-Registry.html) updates a ticket with a single statement instead of reading the row back first, no longer deletes service tickets twice when a session ends, pages ticket listings in the query instead of reading and skipping the rows before the page, and reads tickets without keeping copies of them for change tracking. Session queries by attribute bind the attribute values as parameters.
- With MySQL, the [JPA ticket registry](https://apereo.github.io/cas/development/ticketing/JPA-Ticket-Registry.html) declares shorter `type`, `principal_id` and `parent_id` columns so that its indexes fit MySQL’s key length limit; until now MySQL refused every one of them. Existing tables need the statements listed on that page. The index on the ticket type alone is gone on all databases.
- With the [stateless ticket registry](https://apereo.github.io/cas/development/ticketing/Stateless-Ticket-Registry.html), device codes keep their requested scopes, so tokens issued for an approved device code carry them; the scopes used to be dropped.
- The compact form of tickets kept by the [stateless ticket registry](https://apereo.github.io/cas/development/ticketing/Stateless-Ticket-Registry.html) now carries the resources of OAuth codes, access tokens, refresh tokens, device codes and pushed authorization requests, and the audience of exchanged access tokens; its format version changes: tickets and tokens issued before the upgrade can no longer be read, so users sign in again and clients obtain new tokens.
- The deprecated Azure CosmosDb ticket registry is removed, along with its settings. Deployments that still use it should switch to another [ticket registry](https://apereo.github.io/cas/development/ticketing/Configuring-Ticketing-Components.html).
- A [logout](https://apereo.github.io/cas/development/installation/Logout-Single-Signout.html) request now looks up its `service` parameter once for all the steps of the logout flow, where every step, and every single logout protocol in use, used to look it up again. [Redirecting logout to the service](https://apereo.github.io/cas/development/installation/Logout-Single-Signout.html#redirecting-logout-to-service) looks it up once more, and only when `cas.logout.follow-service-redirects` is turned on. Logout requests that name unknown services, such as those sent by crawlers, cost far less.

#### Stateless Ticket Registry

[RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#stateless-ticket-registry)

With the [stateless ticket registry](https://apereo.github.io/cas/development/ticketing/Stateless-Ticket-Registry.html), the ticket-granting ticket is now carried by the ticket-granting cookie, like with any other ticket registry, instead of being kept in browser storage. The single sign-on session therefore follows the [ticket-granting cookie settings](https://apereo.github.io/cas/development/authentication/Configuring-SSO-Cookie.html), and login pages no longer render a browser storage page before the login form. The `cas.ticket.registry.stateless.storage-type` setting no longer applies and is removed.

Stateless tickets now use a versioned format in which every field is length-prefixed, so values that contain separator characters, like distinguished names, round-trip correctly. Other changes:

- Tickets are compressed only when that makes them smaller.
- Each ticket is bound to the ticket type it was issued as, so changing its prefix (for example, presenting an access token as a refresh token, or a proxy ticket as a proxy-granting ticket) no longer yields a valid ticket.
- OAuth response and grant types are stored by name, so reordering them in a later release does not change existing tokens.
- Expanding a ticket no longer goes through the ticket factories, which skips id generation and service registry lookups.
- An expanded ticket keeps the id it was looked up by, and its authentication keeps the original authentication date.
- The ticket-granting ticket no longer carries its own id or the tickets it has granted, which the stateless registry does not track.
- The ticket-granting ticket no longer carries principal attributes. They are fetched from [attribute repositories](https://apereo.github.io/cas/development/integration/Attribute-Resolution.html) again each time the ticket-granting ticket is read, the same way they already were during ticket validation, which keeps the ticket-granting cookie small. Attributes that only authentication handlers produce, such as claims from Duo Security or delegated authentication, are no longer available to single sign-on decisions unless an attribute repository produces them as well.
- The ticket-granting ticket keeps only its authentication and is created through the ticket-granting ticket factory when read. It expires at the end of its maximum lifetime; an [idle timeout](https://apereo.github.io/cas/development/ticketing/Configuring-Ticket-Expiration-Policy-TGT.html) configured for it is not enforced.
- [Verifiable credentials](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Verifiable-Credentials.html) can be issued: OAuth authorization codes keep their authorization details, access tokens keep their credential configurations and authorization details, and the credential offer and nonce endpoints hand out the stored ids. The pre-authorized code and nonces are not single use there. Verifiable presentations are not supported.
- The OAuth [client credentials](https://apereo.github.io/cas/development/authentication/OAuth-ProtocolFlow-ClientCredentials.html) and [resource owner password](https://apereo.github.io/cas/development/authentication/OAuth-ProtocolFlow-ResourceOwner.html) grants issue access tokens; they failed while recording the use of the ticket-granting ticket they create, which the stateless registry hands back in encoded form.

The ticket-granting cookie can now be encrypted without being signed, using `cas.tgc.crypto.signing-enabled=false` (signing stays on while a signing key is defined). The cookie encryption is authenticated, so this keeps tamper detection and makes the cookie about a quarter smaller. This is recommended with the stateless ticket registry, where the cookie carries the ticket-granting ticket and can otherwise exceed the `4096` bytes browsers accept, for example after Duo Security multifactor authentication. See the [stateless ticket registry documentation](https://apereo.github.io/cas/development/ticketing/Stateless-Ticket-Registry.html) for details.

#### MongoDB Integrations

[RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#mongodb-integrations)

- A [configuration refresh](https://apereo.github.io/cas/development/configuration/Configuration-Management-Reload.html) no longer drops MongoDB collections. `drop-collection` and `drop-indexes` describe what happens at startup, but a refresh of the [MongoDB ticket registry](https://apereo.github.io/cas/development/ticketing/MongoDb-Ticket-Registry.html) or [service registry](https://apereo.github.io/cas/development/services/MongoDb-Service-Management.html) re-ran them and destroyed live tickets or registered services. Collection and index creation still runs on refresh, as it is repeatable.
- The MongoDB ticket registry no longer reports a storage failure as a missing ticket. Adding, updating or fetching a ticket now surfaces the underlying failure instead of logging it and carrying on, so a database outage, an oversized document or a ticket definition missing from the catalog can no longer look like a successful login followed by an invalid ticket.
- Result limits are now applied by MongoDB rather than after the fact, so paging through tickets and querying the ticket registry no longer fetch and deserialize whole collections only to discard them. Cursors opened across several collections are now closed even when the caller stops reading early, and collections shared by more than one ticket definition are read and counted once.
- An `IDX_PRINCIPAL` index is now created on every ticket collection. Queries by principal, such as removing all tickets issued to a user, run against all collections while only the ticket-granting ticket collection carried the index.
- Querying single sign-on sessions by an attribute whose name contains a dot now matches. Such names are escaped when the document is written, and the query did not apply the same escaping.

### Service Registry

#### Service Registries and Access Strategies

[RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#service-registries-and-access-strategies)

- The [JPA service registry](https://apereo.github.io/cas/development/services/JPA-Service-Management.html) reads only the identifier and body of each definition, keeps no entities for reads, and no longer deserializes a definition on reload when its stored body is unchanged. Deleting a service is a single statement, new services are no longer looked up before they are stored, bulk saves flush every `batch-size` definitions, and the service id and name columns are indexed.
- The [JPA service registry](https://apereo.github.io/cas/development/services/JPA-Service-Management.html) keeps the identifier of a service that carries one, such as an imported definition; a database sequence used to replace it. New services get a random identifier, as with other service registries.
- A service whose single sign-on participation policy does not set `createCookieOnRenewedAuthentication` (for example the last-used-time and authentication-date policies) no longer fails the login flow; such a policy behaves like one that leaves the setting undefined, and the single sign-on session is kept when the service does not take part in it.
- Attribute-based activation criteria for [access strategies](https://apereo.github.io/cas/development/services/Service-Access-Strategy-ABAC-Activation.html) and [attribute release policies](https://apereo.github.io/cas/development/integration/Attribute-Release-Policy-Activation.html) match required values against the entire attribute value, like required and rejected attributes; `admin` no longer activates a policy for `sysadmin`. Use `.*admin.*` to match part of a value.
- [Service replication](https://apereo.github.io/cas/development/services/Configuring-Service-Replication.html) no longer broadcasts every service definition from every node on each reload: a loaded definition is sent only when it differs from the replicated copy, and the replicated copies are kept across reloads instead of being cleared, so nodes started later still receive every definition. A replicated definition that differs from the local one replaces it instead of being listed twice.
- When several service registries are chained, a service URL that is not cached is matched against the best candidate from every registry by evaluation order, instead of the first registry’s match.
- The [REST service registry](https://apereo.github.io/cas/development/services/REST-Service-Management.html) no longer publishes a loaded event for every service when services are counted or looked up, and counting services while the registry is unreachable logs the failure and reports zero instead of failing (for example at startup).
- The [REST access strategy](https://apereo.github.io/cas/development/services/Service-Access-Strategy-REST.html) calls its endpoint once per access decision, without retries, and waits at most 5 seconds for the response; an endpoint that does not answer in time denies access. It used to retry up to three times on errors and timeouts, holding the login for the duration.
- Groovy single sign-on participation policies and Groovy activation criteria for access strategies and attribute release resolve their script once, and again only when the script changes or expires from the script cache, instead of resolving its location and expressions on every evaluation.
- [Service replication](https://apereo.github.io/cas/development/services/Configuring-Service-Replication.html) through Apache Kafka keeps one entry per service definition; an updated definition used to be stored next to its earlier versions.
- A service definition deleted on one node stays deleted across the cluster with [service replication](https://apereo.github.io/cas/development/services/Configuring-Service-Replication.html): the deletion marker is kept instead of being removed by the first node that applies it, so nodes that still have the definition remove it rather than sending it back. Deletions now also replicate through Apache Kafka, whose nodes used to drop the marker on arrival.
- With [service replication](https://apereo.github.io/cas/development/services/Configuring-Service-Replication.html) through Apache Kafka, a node reads the replication topic from the beginning when it starts, so it receives the service definitions that other nodes loaded before it joined; it used to rely on every node sending all definitions again on every reload.
- With the services cache turned off (`cas.service-registry.cache.cache-size` set to `0`), listing services no longer re-indexes every service it reads, which also made the next service lookup read the whole service registry again.
- The deprecated Azure CosmosDb service registry is removed, along with its settings. Definitions stored there should be exported and imported into another [service registry](https://apereo.github.io/cas/development/services/Service-Management.html).

### Interface & Accounts

#### Theme Builder

[RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#theme-builder)

The CAS header has a new palette button, next to the notifications button, that opens a [theme builder](https://apereo.github.io/cas/development/ux/User-Interface-Customization-CSSJS.html#theme-builder). It lists every theme property with its description and current value, and edits the colors, shapes, font, header, footer and background that `custom.css` usually controls. Changes are previewed live on the page without a reload, including elements that a theme property would turn off, and the result downloads as a `cas-theme-custom.properties` file and a `custom.css` file ready for an overlay. When the builder opens on a server that already serves such a `custom.css`, it reads its own block back so the file can keep evolving. Nothing is saved on the server; the builder is turned on by default and is controlled by the `cas.theme-builder.enabled` theme property.

#### Palantir Search, User Lookup, Service History, Interrupts & Drafts

[RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#palantir-search-user-lookup-service-history-interrupts--drafts)

[Palantir](https://apereo.github.io/cas/development/installation/Admin-Dashboard.html) has a search box, opened from the navigation bar or with `Ctrl+K` (`⌘K` on macOS) or `/`, that goes to any page or section, finds a registered application by name, identifier or service id, and runs common actions without leaving the keyboard. A new *Users* page looks up one user and gathers in one place their single sign-on sessions, multifactor and trusted devices, consent decisions, resolved attributes, impersonation accounts, passwordless account, password history, throttled login attempts, the interrupt they would see at login and recent audit events, each with its usual actions such as ending a session, removing a device or releasing a throttled attempt. Only sources with something recorded for the user are shown, and sources whose actuator endpoints are not enabled are left out. The change history of an application compares any recorded version side by side with the current definition or the version before it, and restores the selected version from the same dialog. Under *Interrupts*, the authentication section shows which [interrupt](https://apereo.github.io/cas/development/webflow/Webflow-Customization-Interrupt.html) a user would see at login and what each interrupt source answered.

Applications can be duplicated into the wizard as new applications, and the wizard and the definition editor ask before closing with unsaved changes, keeping a draft in the browser that can be resumed for up to thirty days. A keyboard shortcut overlay opens with `?`, and tables and dialogs are easier to use with a keyboard and a screen reader: tables are named, rows with actions take focus and open their actions with `Shift+F10`, modal dialogs announce themselves, and icon-only buttons are named.

#### Theme Properties

[RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#theme-properties)

Several screens can now be adjusted through [theme properties](https://apereo.github.io/cas/development/ux/User-Interface-Customization-Themes-Static.html), so they can differ per theme, per application and per tenant, and most of them preview live in the theme builder:

| Screen | Theme properties |
| --- | --- |
| Every page | `cas.main-content.position` (`topleft` … `bottomright`, default `topcenter`) |
| Login form | `cas.login-form.password-reveal.enabled`, `cas.login-form.caps-lock-warning.enabled` |
| Login card | `cas.login-side-panel.position` (`start`, `end`), `cas.login-providers.position` (`after`, `before`) |
| External identity providers | `cas.login-providers.layout` (`stacked`, `grid`, `inline`), `.select-threshold`, `.sort` |
| Password management links | `cas.pm-links.forgot-password.url` |
| Error pages | `cas.error.show-message`, `cas.error.show-details` |
| WebAuthn | `cas.webauthn.authentication.show-device-details`, `cas.webauthn.registration.recommended` |
| Google Authenticator | `cas.gauth.registration.show-secret-key`, `.scratch-codes` (`chips`, `list`), `.show-print` |
| QR codes | `cas.qr-code.size`, `cas.qr-code.foreground`, `cas.qr-code.background`, `cas.qr-code.margin` |
| Multifactor authentication | `cas.mfa.selection.layout` (`list`, `grid`, `cards`), `cas.mfa.device-name.prefill` |
| Attribute consent | `cas.consent.show-attribute-values`, `cas.consent.layout` (`table`, `list`) |

The main content position places the content of every page in a 3x3 grid on the screen: top, middle or bottom, and left, center or right, as in `middleleft` or `bottomright`. Pages taller than the screen still start at the top and scroll, and Palantir keeps its own layout.

Defaults keep the screens as they were, with two exceptions: external identity providers keep their configured order in the dropdown that replaces the buttons once there are more than ten, and the error page offers its details button only when `cas.error.show-details` is turned on, in which case it now shows the stack trace the server includes rather than an empty box. QR codes for Google Authenticator, WebAuthn and QR code login use the QR code properties of the theme that applies to the request; their size stays between `100` and `1000` pixels, and a foreground that is not darker than the background falls back to black on white so the code remains scannable. Device and account names suggested when registering trusted devices, Google Authenticator, YubiKey or WebAuthn devices can start empty, random or with the username.

- [Themes](https://apereo.github.io/cas/development/ux/User-Interface-Customization-Themes.html) other than the default one, including themes chosen per application or per tenant, now inherit `cas-theme-custom.properties` before falling back to `cas-theme-default.properties`. Theme files found under the template prefixes now apply regardless of the browser language; they used to be ignored for languages other than English, and the aggregate theme source only honored them for an exact `en` request.

#### Account Profile

[RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#account-profile)

The [account profile](https://apereo.github.io/cas/development/registration/Account-Management-Overview.html) shows more of what CAS knows about the user:

| Section | What it shows |
| --- | --- |
| Overview | Days left before the password expires, from the password policy warning, with a shortcut to change it. |
| Sign-ins | Successful, failed and risky sign-in attempts of the last 30 days from the authentication events. |
| Contact | Email addresses and phone numbers for one-time codes and for passwordless sign-in. |
| Tokens | Refresh tokens (offline access) next to access tokens; revoking one revokes its access tokens. |
| Usage policy | Whether the acceptable usage policy is accepted, and the policy terms. |
| Impersonation | Accounts the user may impersonate, and on whose behalf an impersonation session runs. |
| QR devices | Devices authorized for QR code login, which the user can remove. |
| MFA devices | YubiKey registration, new Google Authenticator scratch codes, renaming WebAuthn passkeys. |

YubiKey registration is offered when the user has no device yet or when `cas.authn.mfa.yubikey.multiple-device-registration-enabled` is turned on, and an email address for one-time codes can be added when `cas.authn.mfa.simple.mail.accepted-email-pattern` is set.

- The account profile only ends single sign-on sessions, revokes OpenID Connect tokens and removes trusted devices that belong to the signed-in user; it used to act on any session, token or device whose identifier was submitted.
- The account profile and account management pages follow the shared card, text and border colors of the theme, and the light Palantir theme follows the theme’s primary and secondary colors.

#### Authentication Interrupts

[RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#authentication-interrupts)

- [Interrupt inquirers](https://apereo.github.io/cas/development/webflow/Webflow-Customization-Interrupt-Custom.html) no longer receive the Spring Webflow request context, so they can be asked outside the login flow. Custom inquirers must drop that parameter, and [Groovy interrupt scripts](https://apereo.github.io/cas/development/webflow/Webflow-Customization-Interrupt-Groovy.html) now receive the logger as their fifth argument: scripts that read the sixth argument or destructure six arguments must be updated. The REST strategy still passes the language preferences of the current request along to the interrupt endpoint.
- The new `interrupts` [actuator endpoint](https://apereo.github.io/cas/development/webflow/Webflow-Customization-Interrupt.html#actuator-endpoints) determines the interrupt payload produced for a user, optionally signing in to an application: it asks every interrupt strategy in order and reports what each one produced, or why it failed, along with the interrupt the login flow would present.

#### User Interface

[RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#user-interface)

- When password management is turned off, the login page no longer links to `https://pwd.example.org`; the address of an external password recovery page is set with the `cas.pm-links.forgot-password.url` theme property, and nothing is shown when it is blank.
- Date fields in the [Palantir](https://apereo.github.io/cas/development/installation/Admin-Dashboard.html) service wizard open a calendar to pick from: the expiration date, client secret expiration dates, and the start and end of a time-based access strategy, which take the start and the end of the picked day. Values of the access strategy types (remote, HTTP request, time-based and others) and of the proxy and token exchange policies now reach the generated definition; they used to be left out.
- Generated passwords on the password update form and client secrets generated in Palantir use the browser’s cryptographic random number generator. WebAuthn no longer shows the session token after authentication, and the account unlock page only blocks the context menu on the captcha.
- Browser storage pages show their error panel when progress display is turned off, instead of leaving a blank page.
- Labels and messages that were written in English inside templates and scripts (generic success page, logout confirmation, error pages, QR code login, WebAuthn progress, attribute consent, interrupts, delegated profile selection, impersonation, device code approval, account profile and sign-up dialogs) now come from message bundles.
- Turning on CAS login for [Palantir](https://apereo.github.io/cas/development/installation/Admin-Dashboard.html) no longer sends other protected endpoints, such as actuator endpoints and the [MCP server](https://apereo.github.io/cas/development/installation/Admin-MCP-Server.html), to the CAS login page; they challenge for their own credentials again, as API clients expect, and only Palantir redirects to CAS.

#### Views and Themes

[RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#views-and-themes)

- The [REST-based view resolver](https://apereo.github.io/cas/development/ux/User-Interface-Views-External.html) no longer forwards credential-bearing request headers, such as `Cookie`, `Authorization` and `Proxy-Authorization`, to the external template endpoint.
- Every resolved theme name is now matched against a real [theme definition](https://apereo.github.io/cas/development/ux/User-Interface-Customization-Themes-Static.html) before it is honored, no matter how the name was chosen. A name without a matching `[theme].properties` file, either at the root of a configured template prefix or at the root of the classpath, is ignored and the next resolver in the chain applies. This closes a gap where names taken from a request header, cookie or session value were used without any check, and it makes the [Groovy](https://apereo.github.io/cas/development/ux/User-Interface-Customization-Themes-Groovy.html) and [REST](https://apereo.github.io/cas/development/ux/User-Interface-Customization-Themes-REST.html) theme sources hold to the same contract as every other theme: a script or endpoint that returns a name with no theme definition behind it now falls back to the default theme rather than selecting a theme that does not exist.
- A service theme is now resolved once per request rather than once per template lookup, so service resolution, access strategy evaluation and theme file lookups no longer repeat for every fragment rendered on a page.
- [Thymeleaf template caching](https://apereo.github.io/cas/development/ux/User-Interface-Thymeleaf.html) now takes effect. The chaining template resolver previously declared every resolution non-cacheable regardless of `spring.thymeleaf.cache`, so each page and each fragment was re-resolved and re-parsed on every request. Resolutions are now cached per the configured setting, except for those produced by a theme-aware resolver: those map one template name to different files depending on the request’s theme, and Thymeleaf’s cache key does not carry the theme, so they remain non-cacheable. Deployments with no themed template overrides cache every template; deployments with themed overrides cache everything except the overridden templates.

#### Interrupt Notifications

[RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#interrupt-notifications)

- Following a link on a [blocking interrupt](https://apereo.github.io/cas/development/webflow/Webflow-Customization-Interrupt.html) no longer records the interrupt as acknowledged, and a blocking response is never skipped by [interrupt tracking](https://apereo.github.io/cas/development/webflow/Webflow-Customization-Interrupt-Tracking.html). Tracking cookies are now bound to the principal; cookies issued by earlier versions are ignored, so users may see an acknowledged interrupt once more.
- Interrupt notifications no longer render during passive requests (CAS `gateway`, OpenID Connect `prompt=none`, SAML2 `IsPassive`); CAS returns to the application without a ticket instead, as required by the respective specifications.
- [JSON interrupt notifications](https://apereo.github.io/cas/development/webflow/Webflow-Customization-Interrupt-JSON.html) now load their policies once and reload file changes atomically, so concurrent logins can no longer skip an interrupt while the file is being re-read.
- [REST interrupt notifications](https://apereo.github.io/cas/development/webflow/Webflow-Customization-Interrupt-REST.html) now read the response payload only for successful status codes; error responses no longer interrupt every login with a generic message.

#### Acceptable Usage Policy & Multitenancy

[RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#acceptable-usage-policy--multitenancy)

[Acceptable Usage Policy (AUP) support](https://apereo.github.io/cas/development/webflow/Webflow-Customization-AUP.html) has been extended to support multitenancy. A number of storage mechanisms are now available to support multitenancy, specifically [MongoDb](https://apereo.github.io/cas/development/webflow/Webflow-Customization-AUP-MongoDb.html) and more will be added in the future.

### Platform & Operations

#### OpenRewrite Recipes

[RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#openrewrite-recipes) [RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#openrewrite-recipes) [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#openrewrite-recipes) [RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#openrewrite-recipes)

CAS continues to produce and publish [OpenRewrite](https://docs.openrewrite.org/) recipes that allow the project to upgrade installations in place from one version to the next. [See this guide](https://apereo.github.io/cas/development/installation/OpenRewrite-Upgrade-Recipes.html) to learn more.

#### Graal VM Native Images

[RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#graal-vm-native-images) [RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#graal-vm-native-images) [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#graal-vm-native-images) [RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#graal-vm-native-images)

A CAS server installation and deployment process can be tuned to build and run as a [Graal VM native image](https://apereo.github.io/cas/development/installation/GraalVM-NativeImage-Installation.html). We continue to polish native runtime hints. The collection of end-to-end [browser tests based on Puppeteer](https://apereo.github.io/cas/developer/Test-Process.html) have selectively switched to build and verify Graal VM native images and we plan to extend the coverage to all such scenarios in the coming releases.

#### Model Context Protocol (MCP)

[RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#model-context-protocol-mcp)

- The [MCP server](https://apereo.github.io/cas/development/installation/Admin-MCP-Server.html) reports the audience and allowed resources of OAuth and OpenID Connect clients, and the new `checkOidcClientResource` tool tells whether a client may request a resource, which allowed expression it matches or why it would be refused, and the audience of the token.
- The [MCP server](https://apereo.github.io/cas/development/installation/Admin-MCP-Server.html#oauth-access-tokens) accepts OAuth access tokens issued by CAS, alongside Basic authentication. It publishes RFC 9728 protected resource metadata that names CAS as the authorization server, challenges unauthenticated clients with both `Bearer` and `Basic`, and accepts a token only when it was issued for the MCP endpoint with the `mcp:read` scope to an application that is still allowed. The `MCP_ADMIN` role comes from the principal attributes listed in `cas.mcp.oauth.required-attributes`, resolved for the person when the token is used rather than taken from the attributes released to the client; they match nobody by default, and the audit record of a call names the OAuth client. The `AI.mcp-oauth` feature toggle turns it off.
- The [MCP server](https://apereo.github.io/cas/development/installation/Admin-MCP-Server.html#tools-that-change-cas) can offer tools that change CAS: end the single sign-on sessions of a person with single logout, release throttled logins, turn an application on or off, change a log level, revoke OAuth tokens and consent decisions, and remove trusted devices, one-time password devices and WebAuthn devices. They are turned off by default, are turned on with the `AI.mcp-write` feature toggle, and require the new `MCP_OPERATOR` role. With an OAuth access token, that role also requires the `mcp:write` scope and the principal attributes listed in `cas.mcp.oauth.operator-required-attributes`, which match nobody by default.

#### MCP Server

[RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#mcp-server)

CAS can now act as a [Model Context Protocol (MCP) server](https://apereo.github.io/cas/development/installation/Admin-MCP-Server.html), so AI assistants and agents can examine a CAS deployment through read-only tools. They find the registered service that matches an application URL, evaluate whether a person may access an application and explain a denial, preview the username and attributes released to an application, determine which multifactor authentication provider is required and why, preview single logout notifications, search registered services, look up single sign-on sessions by person or attribute, tickets, tenants, audit records and people along with their resolved attributes. Tools that examine optional features are offered only when that feature is included in the build; these include attribute consent decisions, SAML2 service provider metadata resolution with certificate expiration dates, and OpenID Connect client registrations and keystore keys. The server uses the Streamable HTTP transport at `/cas/mcp`; every request must be authenticated with the same user stores that protect actuator endpoints, and tool calls require the `MCP_ADMIN` role.

The server can also return the recorded authentication events of a person, with the client IP address, user agent and location of each, and, when those features are included in the build, the devices a person trusts for multifactor authentication and the accounts a person may impersonate. Device fingerprints are never returned. It can also preview the interrupt notification a person would see after logging into an application, and score how risky a login attempt from a given client IP address and user agent would be. It lists the one-time password devices and WebAuthn security keys or passkeys a person has registered, without secret keys, codes or key material, and the multifactor authentication trigger evaluation now accepts the client IP address, user agent and location of the login attempt so that adaptive triggers can be checked. Passwordless accounts (with masked contact details), delegated identity providers and Heimdall resources can be looked up as well, and the CAS settings catalog is offered as the MCP resource `cas://settings/{name}`, which describes a setting without revealing its value; the resource capability is now turned on with `spring.ai.mcp.server.capabilities.resource=true`. A `searchSettings` tool finds settings by name or description. Tickets are now summarized rather than returned in full, so attribute values no longer leave CAS through the MCP server, and every MCP tool call and resource read is recorded in the audit log under the MCP user who made it. Single sign-on sessions are identified by a one-way session reference rather than their ticket-granting ticket id, and service definitions are returned with client secrets, passwords and keys redacted.

Agents can now start with `describeDeployment`, which reports the CAS version, enabled features, registries, authentication handlers, attribute repositories and active MCP tool groups, and use `explainLogin` to walk through access, risk, multifactor authentication, interrupts and attribute release for a person and an application in one call. `validateServiceDefinition` checks a proposed service definition and which service would win for given URLs without saving it, `findThrottledLogins` shows throttled login attempts, and `findAttributeSources` and `findAttributeDefinitions` show which attribute repository returns which attribute and how attribute definitions transform them. Tools now publish an output schema and return structured content, registered services are offered as the resource `cas://services/{id}`, and setting names and service ids can be completed by clients, with `spring.ai.mcp.server.capabilities.completion=true`. The documentation now explains how to let browser-based MCP clients reach the server through CAS CORS support.

#### JMX Management

[RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#jmx-management)

[JMX management](https://apereo.github.io/cas/development/integration/JMX-Integration.html) now includes service reload and lookup, ticket and session counts with filtered listings, expired-ticket cleanup, authentication and MFA diagnostics, and principal attribute cache invalidation. Service listings also release backend resources correctly.

#### Palantir

[RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#palantir)

The OpenID Connect service wizard now includes an advanced verifiable credentials policy section for allowed credential types and signing algorithms.

The [service history endpoint](https://apereo.github.io/cas/development/services/Configuring-Service-Version-History.html) can now restore a selected registered-service revision to the service registry and live service cache, while retaining its history. [Palantir](https://apereo.github.io/cas/development/installation/Admin-Dashboard.html) offers a **Restore Version** action in the revision table’s context menu under ** View Change History**.

#### Groovy Scripting

[RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#groovy-scripting)

- [Groovy scripts](https://apereo.github.io/cas/development/integration/Apache-Groovy-Scripting.html) no longer abandon an execution when the script is already busy on another thread. Previously a script that stayed busy for more than five seconds caused queued executions to return no result at all, which could silently skip a multifactor authentication trigger or an acceptable usage policy check. Executions now wait for their turn.
- Bindings assigned to an inline `groovy { ... }` script are now scoped to the assigning thread and to a single execution, so variables belonging to one request can no longer be observed by the next execution of that script or by an unrelated script.
- An inline `groovy { ... }` script that is executed without variables, such as a scripted multifactor authentication trigger, now keeps working past its first execution. The script’s variables were previously reset to an immutable map once it had run, so every later execution failed internally and produced no result.
- [OpenID Connect claim mappings](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Claims-Mapping.html#mapping-claims-per-service) defined as scripts no longer discard the shared Groovy script cache after every claim. Compiled scripts, and the reloading of scripts as their files change, are now retained across token requests.
- Inline scripts used by [allowed attributes](https://apereo.github.io/cas/development/integration/Attribute-Release-Policy-InlineGroovy.html), pattern-matching attribute transformations and service access strategy required attributes are compiled once and served from the script cache. Previously each of these compiled a fresh script for every attribute on every request.
- The [surrogate access strategy](https://apereo.github.io/cas/development/authentication/Surrogate-Authentication.html) and [Groovy SAML2 metadata resolution](https://apereo.github.io/cas/development/installation/Configuring-SAML2-DynamicMetadata-Groovy.html) no longer compile their script, and register a new file watcher, on every request.
- The multifactor authentication [principal attribute predicate](https://apereo.github.io/cas/development/mfa/Configuring-Multifactor-Authentication-Triggers-PrincipalAttribute-PerApplication.html) compiles its Groovy class once and recompiles it only after the file changes, instead of on every authentication attempt.
- A Groovy script that fails to compile, or that throws while running, now reports that failure to the CAS component which asked for it rather than reporting no result at all. Components designed to carry on without a script result, such as mapped attribute release and the Groovy username provider, keep their existing behavior.
- A Groovy access strategy activation criteria that produces no result now fails the request with an explanation. Previously it raised an unexplained error, and treating the missing result as an inactive criteria would have granted access without evaluating the service’s required attributes.

#### Palantir Admin Dashboard

[RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#palantir-admin-dashboard)

Inlined table buttons in [Palantir Admin Dashboard](https://apereo.github.io/cas/development/installation/Admin-Dashboard.html) are replaced with proper context menus triggered by right clicks. The configuration tab is also extended to display cached scripted resources with the ability to either remove or recompute the cache entry.

Furthermore, [Heimdall authorization policies](https://apereo.github.io/cas/development/authorization/Heimdall-Authorization-Policies.html) can now be created, edited and removed from the [Palantir Admin Dashboard](https://apereo.github.io/cas/development/installation/Admin-Dashboard.html). There is also dedicated simulation support to experiment with authorization requests.

Configuration for almost all attribute repositories can also be viewed in better detail. The dashboard is also able to register new attribute repositories for LDAP, JDBC and Stub repositories.

Finally, fields that do support [inline Groovy scripts](https://apereo.github.io/cas/development/integration/Apache-Groovy-Scripting.html) are also allowed to better receive their value from a dedicated editor.

#### Palantir User vs. Admin Roles

[RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#palantir-user-vs-admin-roles)

The [Palantir Admin Dashboard](https://apereo.github.io/cas/development/installation/Admin-Dashboard.html) now supports basic user roles and permissions. By default, all authenticated users are assigned a `ROLE_USER` role/authority. To access critical functionality as an admin, you will need to resolve and release a `role` attribute to Palantir with a value of `ADMIN` or `ROLE_ADMIN` if you are accessing Palantir via external CAS authentication, or your configuration needs to assign the authenticated user role via `spring.security.user.roles=ADMIN`.

At this moment, all Palantir functionality is disabled and hidden for non-admin users, except for the ability to manage the list of registered applications.

#### Cluster Topology

[RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#cluster-topology)

A new `clusterTopology` actuator endpoint is available to report on the current cluster topology and the status of each node in the cluster, particularly relevant when CAS is running in high-availability mode. Cluster topology support is available for the following features:

- [MongoDb Ticket Registry](https://apereo.github.io/cas/development/ticketing/MongoDb-Ticket-Registry.html)
- [Redis Ticket Registry](https://apereo.github.io/cas/development/ticketing/Redis-Ticket-Registry.html)
- [Hazelcast Ticket Registry](https://apereo.github.io/cas/development/ticketing/Hazelcast-Ticket-Registry.html)
- [Apache Ignite Ticket Registry](https://apereo.github.io/cas/development/ticketing/Ignite-Ticket-Registry.html)
- [Apache Kafka Ticket Registry](https://apereo.github.io/cas/development/ticketing/Kafka-Ticket-Registry.html)
- [Apache Pulsar Ticket Registry](https://apereo.github.io/cas/development/ticketing/Pulsar-Ticket-Registry.html)
- [Apache Geode Ticket Registry](https://apereo.github.io/cas/development/ticketing/Geode-Ticket-Registry.html)
- [AMQP Ticket Registry](https://apereo.github.io/cas/development/ticketing/Messaging-AMQP-Ticket-Registry.html)

This capability is also supported and available for the [Palantir Admin Dashboard](https://apereo.github.io/cas/development/installation/Admin-Dashboard.html).

### Documentation

#### Documentation

[RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#documentation)

- The [SPNEGO](https://apereo.github.io/cas/development/authentication/SPNEGO-Authentication.html) settings explain that `cas.authn.spnego.properties[0].jcifs-service-password` is only used when `cas.authn.spnego.system.use-subject-creds-only` is `true` (otherwise the service keys come from a keytab), and that setting both the Kerberos KDC and realm makes the JDK ignore the Kerberos configuration file.
- Setting lists show default values in full instead of cutting them off; where a list is narrow, the value moves to a second row under the setting name. Each setting in the configuration catalog also has a *Copy link* button.
- Every page of the documentation has a Markdown copy at the same address ending in `.md`, which the page toolbar copies to the clipboard, and each version publishes an `llms.txt` index of its pages for AI assistants and other tools.
- The new [Configuration Properties Changes](https://apereo.github.io/cas/development/configuration/Configuration-Properties-Changes.html) page lists the CAS settings added, removed, replaced or given a new default since the previous version, generated from the configuration catalogs and also published as JSON.

[RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#documentation)

The CAS documentation site has received a visual and functional overhaul. Notable changes include:

- A refreshed theme with light and dark modes, a bold grotesque typeface for headings, a monospaced sidebar, and quiet icons for recurring sections such as configuration, actuator endpoints and troubleshooting.
- Configuration settings are presented as a searchable, filterable reference list. Each setting can be expanded to show its description, type, default value and deprecation status, and copied as `.properties`, YAML or environment variables. The chosen format is remembered across pages.
- [Actuator endpoints](https://apereo.github.io/cas/development/monitoring/Monitoring-Statistics.html) are grouped by endpoint, with each operation listing its parameters, response and a working `curl` example inline. Instructions to enable, expose and secure the endpoint along with related settings and troubleshooting notes are shown once per endpoint rather than once per operation, which makes pages considerably lighter.
- The [configuration properties](https://apereo.github.io/cas/development/configuration/Configuration-Properties.html) search is rewritten. It matches setting names regardless of how they are written (property names, environment variables including indexed ones such as `CAS_AUTHN_LDAP_0_LDAP_URL`, or pasted assignments), supports exact name matches, searches names or descriptions, filters CAS or third-party and deprecated settings, and keeps the search in the page address so results can be shared.
- Pressing `Shift` twice on any documentation page opens a quick search for configuration settings.
- [Feature toggles](https://apereo.github.io/cas/development/configuration/Configuration-Feature-Toggles.html) are grouped by area and can be searched or filtered to those that are off by default. Each feature can be expanded to show its modules, their default state, the auto-configuration classes they control and a link to the relevant documentation, and every toggle can be copied as `.properties`, YAML or environment variables.
- The documentation build and validation time is significantly reduced and changes are now published up to `75%` faster. External links are checked on a weekly schedule rather than on every change, and broken external links no longer block publishing.
- Every page has its own title, overview pages carry a written description, and other pages describe themselves in search results with their first paragraph rather than a shared boilerplate sentence.
- The **Versions** menu opens the current page in the selected release, and lists development and the releases still under [maintenance](https://apereo.github.io/cas/developer/Maintenance-Policy.html).
- Comparison tables help choose between [ticket registries](https://apereo.github.io/cas/development/ticketing/Configuring-Ticketing-Components.html), [service registries](https://apereo.github.io/cas/development/services/Service-Management.html), [multifactor providers](https://apereo.github.io/cas/development/mfa/Configuring-Multifactor-Authentication.html) and [protocols](https://apereo.github.io/cas/development/protocol/Protocol-Overview.html).
- Text in setting lists and feature toggles meets contrast requirements in both themes, the dependency tabs are announced correctly by screen readers, and third-party scripts and stylesheets are pinned and loaded with integrity checks.
- On phones, the site header scrolls away to leave only the navigation bar, and the development notice shrinks to a single line, so the page heading appears on the first screen.
- [Heimdall](https://apereo.github.io/cas/development/authorization/Heimdall-Authorization-Overview.html) and [multitenancy](https://apereo.github.io/cas/development/multitenancy/Multitenancy-Overview.html) documentation is split into focused pages with their own menu entries.
- Every page opens with a short summary, which also serves as its description in search results.
- [Docker installation](https://apereo.github.io/cas/development/installation/Docker-Installation.html) names an explicit image tag, and [Getting Started](https://apereo.github.io/cas/development/planning/Getting-Started.html) points to it for a quick local trial.
- A short [Quick Start](https://apereo.github.io/cas/development/planning/Quick-Start.html) recipe, featured on the home page and at the top of Getting Started, takes a new deployer from the Docker image to a configured overlay with a registered application and an optional LDAP connection. Every setting it mentions is a link: clicking one opens its description, default value, module and `.properties`, YAML or environment variable form in place, without leaving the page. Settings mentioned throughout the documentation and release notes are marked the same way, and the `Shift` `Shift` search shows a setting’s details directly instead of moving to the configuration catalog. Features are chosen before the overlay is generated: the Initializr command, pinned to the same release as the Docker preview, carries the selected modules, and the build step assembles the overlay’s `dependencies` block and the complete `cas.properties` for the selection. The selection now survives the browser’s Back button, and WebAuthn runs on the same `http://localhost:8080` server as every other recipe. New recipes cover Groovy scripting and the Hazelcast ticket registry, and interrupt notifications now come from a Groovy script. The wizard keeps the selection consistent: interrupt notifications bring in Groovy, and Redis and Hazelcast replace each other.
- The site loads two web fonts instead of four; code and the sidebar use the system monospace font. Images hosted on third-party sites are removed, and the [logout](https://apereo.github.io/cas/development/installation/Logout-Single-Signout.html) session example is now a table.
- Release notes open with a digest of each release candidate: what to review before upgrading, the highlights and, where a release has screenshots, a spotlight. Every change below it is tagged by type and grouped by area, and can be searched and filtered; the [overview](https://apereo.github.io/cas/development/release_notes/Overview.html) lists every release candidate with its summary, and [the combined release candidate page](https://apereo.github.io/cas/development/release_notes/RC.html) gathers every change from every release candidate on one page, tagged with the release candidate it shipped in.
- The documentation sidebar can be filtered by page name, groups its sections under six headings (start, sign-in, protocols and applications, identity data and access, experience and operations), keeps only the current path open with its parent sections pinned at the top while scrolling, and opens a section’s overview from the section name itself instead of a separate *Overview* entry.

### Build & Project

#### Testing Strategy

[RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#testing-strategy)

The collection of end-to-end [browser tests based on Puppeteer](https://apereo.github.io/cas/developer/Test-Process.html) continue to grow to cover more use cases and scenarios. At the moment, total number of jobs stands at approximately `628` distinct scenarios. The overall test coverage of the CAS codebase is approximately `94%`.

- MariaDB integration tests and functional test scenarios use port `3307`, so they can run alongside the MySQL tests on `3306`.
- Browser tests now cover service matching strategies (literal, starts with, full and partial regular expressions), throttled ticket-granting ticket timeouts, per-service service ticket, proxy ticket and proxy-granting ticket expiration policies, single sign-on participation policies (never, last used time, authentication date), per-service security headers, encrypted, pattern matching, linked, environment and Groovy script attribute release policies, the anonymous username provider, multifactor bypass by remote address, request header and credential type, and the REST multifactor trigger (`cas.authn.mfa.triggers.rest.url`).
- Browser tests now cover OpenID Connect front-channel logout, refused hybrid and unauthorized response types, encrypted user info and introspection responses, JWT refresh tokens, optional ID token encryption, token revocation with its cascade from refresh tokens to access tokens, WebAuthn signature counters, user verification and attestation, and Google Authenticator replay protection with custom code length, time step and window.
- Browser tests now sign in with [SPNEGO](https://apereo.github.io/cas/development/authentication/SPNEGO-Authentication.html) against a real Kerberos KDC: the browser presents a Kerberos ticket and is signed in without a password, with and without `cas.authn.spnego.principal-with-domain-name`, and falls back to the login form when it has no ticket.
- Browser tests now cover refusal paths of the CAS protocol and its security controls: validation error codes and per-service supported protocols, refused and mismatched proxy callbacks and multi-hop proxy chains, plain gateway requests, time-based access strategies, global authentication policies (any with try-all, all handlers, required handler, not prevented, Groovy and REST), request parameter filtering (`cas.http-web-request.pattern-to-block`), actuator endpoint `DENY`, `PERMIT` and `AUTHORITY` access, audit action filters with `cas.audit.engine.ignore-audit-failures`, and CAPTCHA verification on the login form.
- Browser tests now cover OAuth refresh token rotation and expiration, rejected client assertions and enforced client authentication methods, PKCE on the OAuth endpoints, expired client secrets, tampered request objects and logout redirect validation; trusted device fingerprints, expiration and per-service bypass, multifactor bypass by request attributes, simple multifactor token expiration and rate limiting, YubiKey verification against a mock server, REST authentication account states, password history (`cas.authn.pm.history.core.enabled`), passwordless token expiration, and SAML2 signed logout requests, AuthnRequest rejection, artifact resolution, attribute queries and ECP.
- Browser tests now cover OpenID Connect client attestation and JWKS rotation and revocation across issued tokens, SAML2 back-channel logout, offline metadata signature trust, persistent NameIDs, signing and encryption algorithms, Active Directory and OpenLDAP password policy warnings, X.509 revocation through CRLs, the ticket registry cleaner, concurrent ticket validation across nodes, ticket encryption at rest in MongoDb, masking of tickets in logs, and strict audit failure handling (`cas.audit.engine.ignore-audit-failures`).

[RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#testing-strategy)

The collection of end-to-end [browser tests based on Puppeteer](https://apereo.github.io/cas/developer/Test-Process.html) continue to grow to cover more use cases and scenarios. At the moment, total number of jobs stands at approximately `609` distinct scenarios. The overall test coverage of the CAS codebase is approximately `94%`.

[RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#testing-strategy) [RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#testing-strategy)

The collection of end-to-end [browser tests based on Puppeteer](https://apereo.github.io/cas/developer/Test-Process.html) continue to grow to cover more use cases and scenarios. At the moment, total number of jobs stands at approximately `556` distinct scenarios. The overall test coverage of the CAS codebase is approximately `94%`.

#### JSpecify & NullAway

[RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#jspecify--nullaway) [RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#jspecify--nullaway) [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#jspecify--nullaway) [RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#jspecify--nullaway)

CAS codebase is now annotated with [JSpecify](https://jspecify.dev/) annotations to indicate nullness contracts on method parameters, return types and fields. We will gradually extend the coverage of such annotations across the entire codebase in future releases and will integrate the Gradle build tool with tools such as [NullAway](https://github.com/uber/NullAway) to prevent nullness contract violations during compile time.

#### Gradle 9.9

[RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#gradle-99)

CAS is now built with Gradle `9.9` and the build process has been updated to use the latest Gradle features and capabilities.

#### Spring Boot 4.2

[RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#spring-boot-42) [RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#spring-boot-42)

CAS is now built on top of Spring Boot `4.2.x`. This is an in-progress ongoing minor platform upgrade that affects almost all aspects of the codebase including many of the third-party core libraries used by CAS as well as some CAS functionality.

Please refer to the [Spring Boot Wiki](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.2-Release-Notes) for more information on the changes and updates in this release.

[RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#spring-boot-42)

CAS is now built on top of Spring Boot `4.2.x`. This is an in-progress ongoing minor platform upgrade that affects almost all aspects of the codebase including many of the third-party core libraries used by CAS as well as some CAS functionality.

Please refer to the [Spring Boot Wiki](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.2-Release-Notes) for more information on the changes and updates in this release. The biggest change to CAS would be support for AMQP 1.0.

REST password management, trusted-device storage, Clickatell SMS and Spring Boot Admin registration now use Spring’s `RestClient` in place of deprecated `RestTemplate` APIs.

[RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#spring-boot-42)

CAS is now built on top of Spring Boot `4.2.x`. This is an in-progress ongoing minor platform upgrade that affects almost all aspects of the codebase including many of the third-party core libraries used by CAS as well as some CAS functionality.

Please refer to the [Spring Boot Wiki](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.2-Release-Notes) for more information on the changes and updates in this release. The biggest change to CAS would be support for AMQP 1.0.

#### Java 27

[RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#java-27)

CAS may be built and run using Java `27` and the build process has been updated to use the latest Java `27` features and capabilities. Please note that this is only a preparatory step for future releases and the baseline requirement will remain as it was.

#### Gradle 9.8

[RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#gradle-98)

CAS is now built with Gradle `9.8` and the build process has been updated to use the latest Gradle features and capabilities.

#### Gradle 9.7

[RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#gradle-97)

CAS is now built with Gradle 9.7 and the build process has been updated to use the latest Gradle features and capabilities.

## Other Changes

### Authentication

- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#authentication) Hazelcast maps of all CAS modules no longer evict entries to make room by default: `cas.ticket.registry.hazelcast.cluster.core.eviction-policy` and its counterparts now default to `NONE`, since evicting a ticket or session that is still valid logs its user out without notice; entries leave when they expire. After a network split heals, the copy of an entry that was updated last now wins (`cas.ticket.registry.hazelcast.cluster.core.map-merge-policy` defaults to `LATEST_UPDATE`), so changes made on either side, such as applications added to a session, are kept. Choosing a merge policy that compares update, access or expiration times or hits no longer stops the Hazelcast member from starting.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#authentication) A CAS Hazelcast member with TCP/IP and multicast turned off no longer joins other Hazelcast members on the network through Hazelcast’s auto-detection, which fell back to multicast; it runs on its own, and other members can join it only through the TCP/IP, multicast or discovery settings CAS exposes. Removing failed login records of [authentication throttling with Hazelcast](https://apereo.github.io/cas/development/authentication/Configuring-Authentication-Throttling-Hazelcast.html) no longer fails when some of the records are kept on another member of the cluster.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#authentication) [Delegated authentication](https://apereo.github.io/cas/development/integration/Delegate-Authentication.html) no longer fails intermittently when concurrent requests reach an identity provider that is still being initialized, typically right after startup. Such requests now wait for the initialization in progress instead of failing, which also affects [SAML2 identity providers](https://apereo.github.io/cas/development/integration/Delegate-Authentication-SAML2.html) when building SAML2 responses, metadata and logout requests.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#authentication) CAS now logs a warning when a cookie it writes, such as the [ticket-granting cookie](https://apereo.github.io/cas/development/authentication/Configuring-SSO-Cookie.html), is larger than the 4 KB that browsers are guaranteed to accept.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#authentication) With the [acceptable usage policy](https://apereo.github.io/cas/development/webflow/Webflow-Customization-AUP.html) kept in [MongoDb](https://apereo.github.io/cas/development/webflow/Webflow-Customization-AUP-MongoDb.html) or [Redis](https://apereo.github.io/cas/development/webflow/Webflow-Customization-AUP-Redis.html), an accepted policy is remembered at later logins; the decision used to be stored but never read back, so the policy was asked for at every login. MongoDb now creates the user’s document when there is none.
- [RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#authentication) When removing cookies, particularly during logout, the existing cookie value is no longer echoed back for remove operations.

### Multifactor & Passkeys

- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#multifactor--passkeys) [WebAuthn](https://apereo.github.io/cas/development/mfa/FIDO2-WebAuthn-Authentication.html) registration and login pages no longer show a broken image when the device has no icon in its attestation metadata, and fall back to the credential nickname for the device name.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#multifactor--passkeys) [WebAuthn](https://apereo.github.io/cas/development/mfa/FIDO2-WebAuthn-Authentication.html) pages now report browsers that cannot run them; the support check was never applied, and it no longer requires a platform authenticator.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#multifactor--passkeys) [WebAuthn](https://apereo.github.io/cas/development/mfa/FIDO2-WebAuthn-Authentication.html) authentication pages now send the CSRF token rendered by CAS instead of reading it from the `XSRF-TOKEN` cookie, which failed with `403` whenever the page could not read that cookie.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#multifactor--passkeys) Browser storage used by [Duo Security](https://apereo.github.io/cas/development/mfa/DuoSecurity-Authentication.html) and the [SAML2 identity provider](https://apereo.github.io/cas/development/authentication/Configuring-SAML2-Authentication.html) now falls back to cookies when the browser cannot use local or session storage.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#multifactor--passkeys) [Google Authenticator](https://apereo.github.io/cas/development/mfa/GoogleAuthenticator-Authentication.html) devices can only be removed or confirmed by the user that owns them; a device id that belongs to another user is now refused.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#multifactor--passkeys) Google Authenticator device repositories: removing a device or a user from [DynamoDB](https://apereo.github.io/cas/development/mfa/GoogleAuthenticator-Authentication-Registration-DynamoDb.html) no longer removes other records with it; registering or updating a device in [LDAP](https://apereo.github.io/cas/development/mfa/GoogleAuthenticator-Authentication-Registration-LDAP.html) no longer replaces the user’s other devices; and removing or confirming a device with [JPA](https://apereo.github.io/cas/development/mfa/GoogleAuthenticator-Authentication-Registration-JPA.html) works again and no longer re-encrypts the device secret when encryption is enabled.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#multifactor--passkeys) Google Authenticator device removal and confirmation now expire if not completed within a few minutes of the code being verified. The [REST device repository](https://apereo.github.io/cas/development/mfa/GoogleAuthenticator-Authentication-Registration-Rest.html) now sends deletions as `DELETE` and includes the device `id` and `properties` when saving. A REST or JSON repository that cannot be read now fails the login instead of offering device registration, and LDAP removes the right device when several users’ device ids share leading digits.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#multifactor--passkeys) More Google Authenticator repository fixes: [MongoDB](https://apereo.github.io/cas/development/mfa/GoogleAuthenticator-Authentication-Registration-MongoDb.html) no longer treats usernames that differ only by accents (such as `jose` and `josé`) as the same user; the tenant of a device is kept when it is registered and stored, including in DynamoDB and JPA; DynamoDB stores encrypted scratch codes and an emptied list of scratch codes; saving a device no longer alters the object passed in; and every repository now counts devices rather than users and replaces a device saved twice instead of storing a duplicate.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#multifactor--passkeys) Google Authenticator devices now record when they were last used, in every repository. Updating a device that is not stored now adds it in every repository. A code used to verify a device removal or confirmation is no longer restored afterwards. A device id that is not a number is refused instead of failing with a server error. The [JSON repository](https://apereo.github.io/cas/development/mfa/GoogleAuthenticator-Authentication-Registration-JSON.html) replaces its file in one atomic step and is documented as single-node.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#multifactor--passkeys) Faster Google Authenticator lookups: [DynamoDB](https://apereo.github.io/cas/development/mfa/GoogleAuthenticator-Authentication-Registration-DynamoDb.html) reads a device by key and finds a user’s devices through a new `useridIndex` global secondary index instead of scanning the table (CAS adds the index at startup; create it yourself if table creation on startup is turned off), and counts without reading records back. [MongoDB](https://apereo.github.io/cas/development/mfa/GoogleAuthenticator-Authentication-Registration-MongoDb.html) creates an index on usernames with the collation its lookups use.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#multifactor--passkeys) Google Authenticator [LDAP](https://apereo.github.io/cas/development/mfa/GoogleAuthenticator-Authentication-Registration-LDAP.html) device lookups by id no longer read every entry, and saving or removing a device no longer decrypts and re-encrypts the user’s other devices. [Redis](https://apereo.github.io/cas/development/mfa/GoogleAuthenticator-Authentication-Registration-Redis.html) loads all devices with batched `MGET` instead of one request per device, and saving a device no longer decrypts the user’s other devices.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#multifactor--passkeys) The Google Authenticator [JPA repository](https://apereo.github.io/cas/development/mfa/GoogleAuthenticator-Authentication-Registration-JPA.html) can now save a device whose id is not already in the database, such as one imported through the actuator endpoint; the database assigns it a new id. Removing a user’s devices is now a single bulk delete.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#multifactor--passkeys) WebAuthn falls back to the cached [FIDO metadata BLOB](https://apereo.github.io/cas/development/mfa/FIDO2-WebAuthn-Authentication-Attestation.html) when downloading a fresh one fails at startup, for example when the FIDO metadata service rate-limits the request, instead of failing to start.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#multifactor--passkeys) Each [WebAuthn](https://apereo.github.io/cas/development/mfa/FIDO2-WebAuthn-Authentication.html) storage module (JPA, MongoDb, Redis, LDAP, DynamoDb, REST) can now be switched off with `CasFeatureModule.WebAuthn.<module>.enabled=false`, as the Google Authenticator, trusted device and consent storage modules already could, so that several can ship in one build while only one keeps device registrations.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#multifactor--passkeys) With [trusted devices kept in Redis](https://apereo.github.io/cas/development/mfa/Multifactor-TrustedDevice-Authentication-Storage-Redis.html), logins continue with multifactor authentication while Redis cannot be reached, as with the JDBC storage, instead of failing with a server error.
- [RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#multifactor--passkeys) Multifactor authentication may also be activated using [SAML2 metadata entity attributes](https://apereo.github.io/cas/development/mfa/Configuring-Multifactor-Authentication-Triggers-EntityId.html).

### OAuth & OpenID Connect

- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) OAuth and OpenID Connect requests find the application definition for a `client_id` through the service index instead of examining every definition, falling back to examining them when the index has no match.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) Listing application definitions, for example through the `registeredServices` actuator endpoint or for the [OpenID Connect JWKS endpoint](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-JWKS.html), no longer includes expired definitions that are set to be deleted when they expire. A definition that is renewed and expires again notifies its contacts again.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) Reloading application definitions no longer empties the cache and the service index before filling them again. New and updated definitions are added first and removed ones are dropped last, so requests that arrive during a reload no longer miss an application, match a broader definition by mistake, or fail to find an OAuth or OpenID Connect client.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) Matching a request to an application definition no longer sorts every cached definition on each lookup, and listing definitions, as the [OpenID Connect JWKS endpoint](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-JWKS.html) and the `registeredServices` actuator endpoint do, no longer re-indexes every cached definition.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) An OAuth/OpenID Connect token request now saves the user’s single sign-on session once instead of once per token it issues; exchanging an authorization code for an access and a refresh token used to write it three times.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) The `at_hash` claim of an [OpenID Connect](https://apereo.github.io/cas/development/authentication/OIDC-Authentication.html) ID token is now computed over the access token returned in the same response. CAS used to build and sign the access token a second time for the hash, which doubled the work for JWT access tokens and produced an `at_hash` that did not match when the access token was signed with ES* or PS* algorithms, encrypted, or requested as a JWT through token exchange.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) Reading the [OpenID Connect keystore](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-JWKS.html) from a file, as every request to the JWKS endpoint does, no longer fails an attempt to parse the file path as an inline keystore first, and the keystore generated event is published only when CAS actually generates a new keystore instead of on every read.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) JWT access tokens, JWT response modes and other tokens signed or encrypted with keys defined per application now reuse the prepared signing and encryption keys instead of parsing them again for every token issued or read; a changed application definition or a rotated key is picked up the first time it is used. Each such token also looks up the application’s keys once instead of three times; for an OpenID Connect application, a look-up reads its keystore and serializes the keys.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) [OpenID Connect](https://apereo.github.io/cas/development/authentication/OIDC-Authentication.html) ID tokens issued for `response_type=id_token` no longer carry an `at_hash` claim, since no access token is issued with them. For `response_type=id_token token`, the `at_hash` claim is now computed over the access token returned in the same response; for JWT access tokens it used to be computed over a different token and did not match.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) An OAuth/OpenID Connect [authorization code](https://apereo.github.io/cas/development/authentication/OAuth-ProtocolFlow-AuthorizationCode.html) is now redeemed by removing it from the ticket registry before any token is issued, and only the request that removes it receives tokens. Two token requests presenting the same code at the same moment could previously both receive tokens. The code is also no longer written back to the ticket registry right before it is redeemed.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) An approved [device code](https://apereo.github.io/cas/development/authentication/OAuth-ProtocolFlow-DeviceAuthorization.html) and a [CIBA](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-CIBA.html) request in poll or ping mode are now redeemed the same way: only the token request that removes them from the ticket registry receives tokens. Two polls arriving at the same moment could previously both receive tokens. A CIBA request is now removed before its tokens are issued rather than after, and a request that lost the race gets `invalid_grant`, as CIBA Core 1.0 requires once the `auth_req_id` has been redeemed.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) The [OpenID Connect JWKS endpoint](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-JWKS.html) now caches the published key set instead of loading and parsing the keystore for every request, whichever keystore is used. The cached set expires along with the keys CAS signs with (`cas.authn.oidc.jwks.core.jwks-cache-expiration`), and never later than a day. Rotating or revoking keys, or changing a keystore file, publishes the new key set right away on that CAS node; other nodes publish it once their cached set expires.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) OAuth and OpenID Connect client authentication at the token endpoint no longer builds a throw-away access token to decide which claims to release, and the authorization code or refresh token in the request is read from the ticket registry once per request instead of once by every authenticator and validator that inspects it.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) Authorization responses that return their parameters in the URL fragment, such as those of the implicit flow, no longer repeat every query parameter of the client’s redirect URI. When CAS includes the issuer in authorization responses (RFC 9207), it now sends it once, with the other response parameters: it used to be sent twice with the `query` response mode, and also added to the query of `fragment` and `form_post` responses.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) JWT access tokens returned from the OpenID Connect authorization endpoint, with the `token` and `id_token token` response types, now name the OpenID Connect issuer, as the ID token and the token endpoint do. They used to name the CAS server, so resource servers that check the issuer of a JWT access token rejected them.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) When a request to an [OAuth](https://apereo.github.io/cas/development/authentication/OAuth-Authentication.html) endpoint carries no `client_id` parameter, as when a client authenticates at the token endpoint with HTTP Basic, CAS now takes the client from those credentials instead of matching the redirect URI against every OAuth application each time the authenticated profile is read. Where the redirect URI still has to be matched, that happens once per request. Clients whose redirect URI patterns overlap are no longer mistaken for one another in this case.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) A request object passed with the `request` parameter is now unpacked and verified once per request instead of once for every authorization parameter read from it.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) With `cas.authn.oauth.code.remove-related-access-tokens` enabled, presenting an [authorization code](https://apereo.github.io/cas/development/authentication/OAuth-Authentication.html) again after it was redeemed now revokes the access and refresh tokens issued from it, and the access tokens later issued from that refresh token, as RFC 6749 recommends. Previously only access tokens were looked for, by searching every ticket in the registry for each unknown or expired code, and a replay rejected during client authentication, such as with PKCE or `private_key_jwt`, revoked nothing. CAS now keeps a short-lived record of the tokens issued from each redeemed code, for as long as transient session tickets live, and an unknown code no longer triggers a search of the registry. This comes with trade-offs, which is why the setting remains off by default: each code exchange writes one extra transient session ticket to the registry; a code presented again after that record has expired revokes nothing, and the record’s lifetime follows the transient session ticket expiration settings shared with other transient session tickets; tokens issued before upgrading have no record and are not revoked, where the previous search would have found their access tokens; and a code presented again while its first redemption is still issuing tokens is rejected, but the tokens being issued at that moment are not revoked.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) A client that authenticates at the token endpoint with `private_key_jwt` or `client_secret_jwt` and sends no `client_id` parameter is now identified from its client assertion, whose issuer and subject are the client identifier, instead of by matching the redirect URI against every OAuth application.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) [PKCE](https://apereo.github.io/cas/development/authentication/OAuth-ProtocolFlow-AuthorizationCode.html) is now verified for every authorization code exchange, whatever the client authentication method. A client that authenticated at the token endpoint with `private_key_jwt`, `client_secret_jwt` or mutual TLS could previously redeem a code issued with a `code_challenge` without a `code_verifier`, or with one that did not match. As RFC 7636 and RFC 9700 require, the code verifier is now compared exactly rather than ignoring case, and a `code_verifier` sent for a code that was issued without a challenge is refused with `invalid_grant`. A `code_challenge` and `code_challenge_method` sent inside a request object are now recorded with the authorization code as well; previously only the query parameters were read, so the code was issued without the challenge and its verifier was then refused.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) When an [OAuth application](https://apereo.github.io/cas/development/authentication/OAuth-Authentication-Clients.html) renews refresh tokens, the refresh token presented is now removed before new tokens are issued for it, and only the request that removes it is issued tokens. Concurrent requests presenting the same refresh token could previously each receive a new refresh token, so one refresh token turned into several valid ones. A client that sends the same refresh token in parallel now receives `invalid_grant` for all but one request. The refresh token being renewed is also no longer written back to the ticket registry during the request. Tokens already issued from a refresh token presented again are not revoked.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) [Client secrets](https://apereo.github.io/cas/development/authentication/OAuth-ClientSecret-Management.html) are now compared in constant time, and expired client secrets are no longer removed while a request is authenticated: CAS used to change the shared application definition and save it to the service registry during the request, and once every secret of an application had expired and been removed, the application was treated as having no client secret and was accepted with any secret, or none. Expired client secrets are now ignored when a request is authenticated, and removed from the application definition in the background each time CAS loads applications from the service registry. When all secrets of an application have expired, the one that expired last is kept, so the application keeps refusing requests until it is given a new secret. Updating a [dynamically registered](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Dynamic-Registration.html) client now replaces an expired client secret with a new one instead of extending the expiration of the expired secret.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) A client that authenticates at the token endpoint with [`client_secret_jwt`](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-AccessToken-AuthMethods.html) is now verified with its `clientSecret`, as OpenID Connect requires, whenever the application’s `tokenEndpointAuthenticationMethod` is `client_secret_jwt` or left blank. A raw secret in the application’s `jwks` is still accepted, but that fallback is deprecated for removal. A secret shorter than the signing algorithm requires (32 bytes for `HS256`, 64 for `HS512`) is skipped with a warning that names the application.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) When CAS works out which application a login is for, a request object carried by the OAuth or OpenID Connect authorization request is now unpacked and verified once, instead of once for each of the client identifier, redirect URI and grant type read from it.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) Rotating or revoking keys in the [OpenID Connect keystore](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-JWKS.html) now also clears the keys CAS signs with on that node, so the new current key is used and a revoked key stops being used right away instead of once the cached keys expire.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) Changes to an [OpenID Connect keystore](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-JWKS.html) file are announced again once CAS notices them. The file watcher has never broadcast them, so CAS kept signing with the keys it had loaded until its key cache expired.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) Ed25519 keys presented to the OpenID Connect [client JWKS registration endpoint](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-JWKS-Clients.html) are now verified with the JDK’s own EdDSA support. Verification previously relied on Google Tink, which CAS does not ship, so such registrations failed at runtime.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) CIBA token requests answer `authorization_pending` while the user has not yet approved the request, `expired_token` for an expired `auth_req_id`, and `invalid_grant` for one that is unknown or already redeemed, as CIBA Core 1.0 requires; they used to return `invalid_grant` and `invalid_request`.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) Device code token requests answer `expired_token` for an expired device or user code, and `invalid_grant` for a device code that is unknown, already redeemed or issued to another client, as RFC 8628 requires; they used to return `access_denied`, which RFC 8628 reserves for a user who denied the request.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#oauth--openid-connect) Approving consent for an authorization request with `prompt=consent` now completes the request instead of showing the consent screen again.
- [RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#oauth--openid-connect) Custom ID token claims can also be constructed using [Apache Groovy](https://apereo.github.io/cas/development/authentication/OIDC-Authentication-Claims-Custom.html).

### SAML2

- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#saml2) The [SAML2 identity provider](https://apereo.github.io/cas/development/authentication/Configuring-SAML2-Authentication.html) no longer matches an entity id longer than `cas.service-registry.core.maximum-service-id-length` against application definitions.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#saml2) The [SAML2 identity provider](https://apereo.github.io/cas/development/authentication/Configuring-SAML2-Authentication.html) parses its signing key once instead of for every signed response or assertion; a rotated or replaced key is picked up the first time it is used.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#saml2) Back-channel [SAML2 single logout](https://apereo.github.io/cas/development/installation/Logout-Single-Signout.html) notifications sent with the HTTP-POST or HTTP-Redirect bindings now honor `cas.slo.asynchronous` the way SOAP and CAS protocol notifications already did: by default they are sent in the background, so one slow service provider no longer holds up the logout of the others, and the logout reports them as sent without waiting for a response. The incoming logout request is also parsed once per logout instead of once per service provider.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#saml2) Cached [SAML2 service provider metadata](https://apereo.github.io/cas/development/installation/Configuring-SAML2-DynamicMetadata.html) is reloaded in the background once three quarters of its cache lifetime has passed, so the request that finds it nearly expired no longer waits for the metadata to be fetched, parsed and validated. The current metadata stays in use until the reload completes.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#saml2) The [SAML2 identity provider](https://apereo.github.io/cas/development/authentication/Configuring-SAML2-Authentication.html) builds the resolver that locates its signing credentials once instead of for every signed response or assertion.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#saml2) The [SAML2 identity provider](https://apereo.github.io/cas/development/authentication/Configuring-SAML2-Authentication.html) decodes and parses the incoming `SAMLRequest` or `SAMLResponse` once when matching it to a service provider, instead of once for every SAML2 application definition it compares against.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#saml2) The [SAML2 identity provider](https://apereo.github.io/cas/development/authentication/Configuring-SAML2-Authentication.html) locates and verifies the service provider behind an authentication request, and resolves its metadata, once per request instead of up to four times while validating the request, choosing the binding and building the response.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#saml2) The SAML2 identity provider now looks up a service provider’s entity in its metadata once per request: the entity found while checking that the cached metadata is still usable is handed to the request, instead of being looked up a second time.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#saml2) Templates the SAML2 identity provider loads from the classpath, such as the page that posts a SAML2 response or logout request with the HTTP-POST binding, are now parsed once and cached instead of being read and parsed for every response.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#saml2) SAML2 service provider metadata committed by hand to a [Git repository](https://apereo.github.io/cas/development/installation/Configuring-SAML2-DynamicMetadata-Git.html), such as `sp-metadata/SP.xml`, keeps the same identifier across loads and can be looked up and removed by it; documents whose names contain a hyphen are no longer skipped.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#saml2) With identity provider metadata kept in [Redis](https://apereo.github.io/cas/development/installation/Configuring-SAML2-DynamicMetadata-Redis.html), service providers without metadata of their own use the identity provider’s global metadata and keys, as with the other metadata stores; SAML2 responses used to fail with “Unable to locate signing credentials”.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#saml2) With identity provider metadata kept in [DynamoDb](https://apereo.github.io/cas/development/installation/Configuring-SAML2-DynamicMetadata-DynamoDb.html) or [Google Cloud Storage](https://apereo.github.io/cas/development/installation/Configuring-SAML2-DynamicMetadata-GCP-Storage.html), service providers without metadata of their own use the identity provider’s global metadata and keys, as with Redis, JPA and MongoDb.

### CAS & Other Protocols

- [RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#cas--other-protocols) [CAS REST APIs](https://apereo.github.io/cas/development/protocol/REST-Protocol.html) now return a `403` status code instead of a `500` type of error when unauthorized application requests are identified.

### Authorization

- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#authorization) [Attribute-based access strategies](https://apereo.github.io/cas/development/services/Service-Access-Strategy-ABAC.html) no longer deny a principal only because it carries fewer attributes than the strategy lists as rejected, or, when `requireAllAttributes` is `false`, fewer than it lists as required. The documentation now spells out that `requireAllAttributes` also governs rejected attributes: by default access is refused only when every rejected attribute is present with a matching value, so a strategy that should refuse access on any one rejected attribute must set `requireAllAttributes` to `false`.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#authorization) The [HTTP request access strategy](https://apereo.github.io/cas/development/services/Service-Access-Strategy-Http.html) now denies access when a configured header is missing from the request, matches header names case-insensitively, and requires the IP address, user agent and header patterns to match the entire value. Previously a missing header satisfied its rule and patterns matched any part of a value, so `10.0.0.1` also matched `110.0.0.15`. Patterns written to match only part of a value, such as `Chrome.+` for a user agent, must now be written as `.*Chrome.+`.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#authorization) [Attribute-based access strategies](https://apereo.github.io/cas/development/services/Service-Access-Strategy-ABAC.html), including those used by [Grouper](https://apereo.github.io/cas/development/services/Service-Access-Strategy-Grouper.html) and SCIM access strategies, now require a required or rejected attribute value pattern to match the entire attribute value. Previously a pattern matched any part of a value, so a required `admin` also accepted `sysadmin-disabled`. Patterns written to match only part of a value must now be written as, for example, `.*admin.*`.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#authorization) The [REST access strategy](https://apereo.github.io/cas/development/services/Service-Access-Strategy-REST.html) no longer sends the application definition to the remote endpoint, since it may carry client secrets, keys and other credentials. The request body now carries the principal id and attributes, the requested service id and the id, name, service id and type of the application definition.

### Attributes & Consent

- [RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#attributes--consent) Releasing attributes via [pattern matching](https://apereo.github.io/cas/development/integration/Attribute-Release-Policy-PatternMatching.html) accepts Groovy transformation rules.
- [RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#attributes--consent) Attributes requested for [Consent](https://apereo.github.io/cas/development/integration/Attribute-Release-Consent.html) may now be localized using language bundles and a prefixed language key that is `screen.consent.attributes.attribute.[attribute-name]`.

### Tickets & Sessions

- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#tickets--sessions) Ticket registries that replicate tickets through a message queue, such as [Kafka](https://apereo.github.io/cas/development/ticketing/Kafka-Ticket-Registry.html), [AMQP](https://apereo.github.io/cas/development/ticketing/Messaging-AMQP-Ticket-Registry.html), [Pulsar](https://apereo.github.io/cas/development/ticketing/Pulsar-Ticket-Registry.html) and [Google Cloud Pub/Sub](https://apereo.github.io/cas/development/ticketing/GCP-PubSub-Ticket-Registry.html), now send one message per ticket update instead of two, and a node that applies an update received from another node no longer sends it back out. Those echoed copies cost one extra message per node for every update and could overwrite a newer version of the ticket on the node that made the change.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#tickets--sessions) When a single sign-on session ends, the [JPA ticket registry](https://apereo.github.io/cas/development/ticketing/JPA-Ticket-Registry.html) removes the session’s service tickets with one statement instead of reading and deleting them one at a time. Service and OAuth tickets that were updated after they were issued, which lost the link to their session when ticket encryption was enabled, now keep it and are removed with the session.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#tickets--sessions) When a single sign-on session ends, the remaining ticket registries, such as [Redis](https://apereo.github.io/cas/development/ticketing/Redis-Ticket-Registry.html), [Hazelcast](https://apereo.github.io/cas/development/ticketing/Hazelcast-Ticket-Registry.html), [DynamoDb](https://apereo.github.io/cas/development/ticketing/DynamoDb-Ticket-Registry.html), [Cassandra](https://apereo.github.io/cas/development/ticketing/Cassandra-Ticket-Registry.html), [Memcached](https://apereo.github.io/cas/development/ticketing/Memcached-Ticket-Registry.html) and the default in-memory registry, remove the session’s service and proxy tickets by their identifier instead of reading and decoding each one first. With registries that cannot tell whether a ticket was still present, the number of removed tickets reported for the session may include service tickets that were already validated. Removing a ticket that is already gone from the Apache Geode and the deprecated CosmosDb registries no longer fails.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#tickets--sessions) The [ticket registry cleaner](https://apereo.github.io/cas/development/ticketing/Ticket-Registry-Cleaner.html) now looks for expired tickets first, in batches of up to ten thousand so a large backlog is never held in memory at once, and then removes up to eight of them at a time, where it used to remove them one by one while the scan was still running. With the [JPA ticket registry](https://apereo.github.io/cas/development/ticketing/JPA-Ticket-Registry.html), the database cursor, connection and transaction used for the scan are released before any ticket is removed, and rows read by the scan are no longer all kept in memory until it ends.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#tickets--sessions) A failed attempt to validate a service or proxy ticket for a different service, or for the wrong tenant, now counts as a use of the ticket, as the [CAS protocol](https://apereo.github.io/cas/development/protocol/CAS-Protocol-Specification.html) expects. Such an attempt used to leave the ticket untouched, so it could be presented again. The ticket’s expiration policy still decides what remains: a single-use ticket, the default, is removed, so a client that retries with a corrected service URL needs a new ticket, while a ticket allowed several uses keeps the rest.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#tickets--sessions) [Validating a single-use service ticket](https://apereo.github.io/cas/development/authentication/CAS-Protocol-TicketValidation.html) now removes it in one step, and only the request that actually removes it receives a successful response. Two requests validating the same ticket at the same moment, on the same node or on different nodes, could previously both succeed with ticket registries that store copies of tickets, such as JPA, MongoDB, Redis or Hazelcast, or with ticket encryption enabled. Each validation also costs fewer trips to the ticket registry, and a validation that cannot obtain the ticket’s lock is now rejected instead of being allowed without consuming the ticket.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#tickets--sessions) When a [ticket is validated](https://apereo.github.io/cas/development/authentication/CAS-Protocol-TicketValidation.html), the service URL must match the one the ticket was issued for exactly up to its query, so an encoded character, such as an encoded `#` that hides user information in the host, no longer makes two different URLs match. The query is compared once decoded, so a parameter that the application and the validating client encode differently, such as a space sent as `+` or as `%20`, still matches.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#tickets--sessions) The [Memcached](https://apereo.github.io/cas/development/ticketing/Memcached-Ticket-Registry.html), [DynamoDB](https://apereo.github.io/cas/development/ticketing/DynamoDb-Ticket-Registry.html) and [Google Cloud Firestore](https://apereo.github.io/cas/development/ticketing/GCP-Firestore-Ticket-Registry.html) ticket registries now report a ticket that was already gone when asked to remove it. Service tickets, authorization codes, device codes and CIBA requests are redeemed by whichever request removes them, and these registries reported every removal as successful, so two requests presenting the same ticket at the same moment could both succeed. A Memcached removal that fails, such as on a timeout, is no longer counted as removed either. The [Cassandra](https://apereo.github.io/cas/development/ticketing/Cassandra-Ticket-Registry.html) ticket registry still reports every removal as successful and keeps this limitation.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#tickets--sessions) Removing a proxy-granting ticket no longer writes back the copy of the single sign-on session it was issued from, which undid any change made to the session since, such as applications the user logged into afterwards. When a single sign-on session ends, it is no longer rewritten once for each of its proxy-granting tickets and once more right before it is removed.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#tickets--sessions) The [Hazelcast ticket registry](https://apereo.github.io/cas/development/ticketing/Hazelcast-Ticket-Registry.html) keeps each ticket for as long as the expiration policy that applies to it, so remember-me single sign-on sessions are no longer removed after the default session lifetime, and updating a session no longer restarts its lifetime in Hazelcast. Looking up sessions by attribute, such as for logout requests sent by external identity providers, no longer builds a query from the attribute values, so values that contain quotes are matched instead of failing. Listing tickets page by page, such as through the `ssoSessions` actuator endpoint, now skips and counts tickets across the whole registry rather than within each ticket map, and the registry no longer shuts down the Hazelcast instance it shares with other modules, such as authentication throttling.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#tickets--sessions) The [Hazelcast ticket registry](https://apereo.github.io/cas/development/ticketing/Hazelcast-Ticket-Registry.html) counts, finds and removes the tickets of a user through its indexes in one pass, with or without Jet, instead of running two SQL statements for each ticket map or reading the whole registry, and reports the number of tickets it actually removed. Users are matched regardless of case, like the other ticket registries; tickets stored before the upgrade for user names that are not in lower case are found again once they are renewed or expire. Session and service ticket counts read the size of their maps, only the indexes the registry queries are kept, and an encrypted ticket read back after encryption is turned off is removed instead of failing. Ending a single sign-on session removes its service tickets with one call per ticket map, listing all tickets reads them in batches instead of copying each ticket map into memory at once, and the ticket maps are configured before the Hazelcast member starts rather than added to the running cluster afterwards.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#tickets--sessions) The [Hazelcast ticket registry](https://apereo.github.io/cas/development/ticketing/Hazelcast-Ticket-Registry.html) no longer runs SQL queries, so Hazelcast Jet is no longer started by any CAS Hazelcast module and the `core.enable-jet` Hazelcast setting, such as `cas.ticket.registry.hazelcast.core.enable-jet`, is removed. Remove it from your configuration; CAS refuses to start with settings it does not recognize. The `hazelcast-sql` library is no longer included.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#tickets--sessions) The [Hazelcast ticket registry](https://apereo.github.io/cas/development/ticketing/Hazelcast-Ticket-Registry.html) stores tickets in a new, smaller format: entries use Hazelcast compact serialization, so looking up tickets by user, by service or by attribute reads only those fields instead of deserializing every ticket, and authentication attributes are kept only with single sign-on sessions rather than with every ticket. Nodes running this version cannot read tickets stored by earlier versions, so upgrade the whole cluster at once; existing tickets and single sign-on sessions are not carried over. With encryption turned on, the application a ticket was issued for is now stored as a digest too, like the ticket id, user and attributes, instead of as its plain URL. Since tickets no longer go through Hazelcast’s Java serialization, `cas.ticket.registry.hazelcast.core.enable-compression` no longer applies to them.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#tickets--sessions) When Hazelcast expires a single sign-on session of the [Hazelcast ticket registry](https://apereo.github.io/cas/development/ticketing/Hazelcast-Ticket-Registry.html), CAS now sends single logout requests, removes the tickets the session still holds and reports the session as destroyed, like the ticket registry cleaner does for other ticket registries; expired sessions used to disappear silently. Each expired session is handled once, by the node that owns it.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#tickets--sessions) The `cluster.core.replicated` and `cluster.core.async-fillup` Hazelcast settings, such as `cas.ticket.registry.hazelcast.cluster.core.replicated`, are removed from every Hazelcast module: no module ever used a Hazelcast replicated map, so turning them on only left the actual map, such as that of the [Hazelcast ticket registry](https://apereo.github.io/cas/development/ticketing/Hazelcast-Ticket-Registry.html), authentication throttling or Spring Session, without its expiration, eviction, backup settings and indexes. Remove them from your configuration; CAS refuses to start with settings it does not recognize.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#tickets--sessions) The [MongoDB ticket registry](https://apereo.github.io/cas/development/ticketing/MongoDb-Ticket-Registry.html) reads only the stored ticket when fetching tickets, counts tickets from collection metadata instead of scanning collections, and removes the service tickets of a ticket-granting ticket with one request per collection instead of reading each one back first. Ticket identifiers are now stored as the document `_id`, which removes the separate `IDX_ID` index and makes duplicate identifiers impossible; tickets stored by earlier versions are converted at startup, so all nodes sharing the registry should be upgraded together. Only ticket-granting tickets store principal attributes and carry the attribute index, and only tickets linked to a service carry the service index, so issuing and validating a service ticket no longer writes an index entry per attribute value; indexes left on other collections by earlier versions are removed at startup.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#tickets--sessions) MongoDB connection pools now default to 100 connections, keep connections for 30 minutes and idle connections for 5 minutes, and wait at most 10 seconds for a free connection instead of a minute. The server heartbeat uses the driver’s default instead of the connection `timeout`. Pool settings do not apply when `client-uri` is used; set the connection string’s own options instead. If an index CAS replaces cannot be rebuilt, the previous index is restored instead of the collection being left without it.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#tickets--sessions) Redis-backed counts and key lookups, such as the [ticket](https://apereo.github.io/cas/development/ticketing/Redis-Ticket-Registry.html) and [service registry](https://apereo.github.io/cas/development/services/Redis-Service-Management.html) counts, now return their connection when done. With connection pooling enabled, every call used to keep a pooled connection until the pool ran out.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#tickets--sessions) The [Redis ticket registry](https://apereo.github.io/cas/development/ticketing/Redis-Ticket-Registry.html) query for ticket ids and loading all [YubiKey](https://apereo.github.io/cas/development/mfa/YubiKey-Authentication.html) devices from Redis also return their connection when done.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#tickets--sessions) The in-memory cache of the [Redis ticket registry](https://apereo.github.io/cas/development/ticketing/Redis-Ticket-Registry.html) no longer keeps tickets that have changed or been removed on another CAS node. A cached ticket used to stay cached for as long as it was read, so a session ended on one node could stay valid on another and could even be written back to Redis. Tickets are now kept for at most 30 seconds, a ticket read while another node announces a change to it is not kept, and changes are announced to other nodes without the ticket itself, which used to be sent unencrypted to every node on every change even with ticket encryption enabled. Nodes still on an earlier version handle the new notifications correctly. Reading all tickets, as the ticket registry cleaner does, or querying tickets no longer fills the cache with every ticket read; a ticket first seen by such a scan is read from Redis the next time it is needed, instead of being served from the cache.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#tickets--sessions) The Apache Pulsar ticket registry subscribes each CAS node under a subscription of its own, named after `cas.ticket.registry.pulsar.subscription-name` and the node’s `cas.ticket.registry.core.queue-identifier`, so every node receives every ticket change; nodes used to share one subscription. Ticket registry commands now travel as JSON strings written by CAS, as they do with the Kafka ticket registry, rather than through Pulsar’s JSON schema, which could not carry them.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#tickets--sessions) The [Google Cloud Pub/Sub](https://apereo.github.io/cas/development/ticketing/GCP-PubSub-Ticket-Registry.html) ticket registry subscribes each CAS node under a subscription of its own, named `CasTicketRegistryTopicSubscription-` followed by the node’s `cas.ticket.registry.core.queue-identifier`, so every node receives every ticket change; nodes used to share one subscription.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#tickets--sessions) A login from a client that sends no `User-Agent` header no longer fails with a server error when the ticket-granting cookie is pinned to the client session; the cookie is pinned to the absence of a user agent instead.
- [RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#tickets--sessions) [RediSearch](https://apereo.github.io/cas/development/ticketing/Redis-Ticket-Registry-RediSearch.html) functionality now supports Redis clustering.
- [RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#tickets--sessions) The maximum lifetime of a transient session ticket (i.e. `TST`) is by default reduced from `15` minutes to `5` minutes.
- [RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#tickets--sessions) [LettuceMod](https://apereo.github.io/cas/development/ticketing/Redis-Ticket-Registry-RediSearch.html) is removed from CAS and its functionality is directly provided by Lettuce itself.

### Service Registry

- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#service-registry) When the background reload is enabled, a request for an application that matches no cached service definition no longer reads the whole service registry; definitions added outside CAS, for example by another node, are recognized at the next scheduled reload. Changes picked up by the [JSON](https://apereo.github.io/cas/development/services/JSON-Service-Management.html) and [YAML](https://apereo.github.io/cas/development/services/YAML-Service-Management.html) registry watchers now update the cache right away, fetching services by type through the [`registeredServices` actuator endpoint](https://apereo.github.io/cas/development/services/Service-Management.html) no longer empties the cache, and CAS warns when the cache expires definitions before the scheduler reloads them. See [this guide](https://apereo.github.io/cas/development/services/Service-Management-Caching-Reloading.html).
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#service-registry) Exporting a single service definition through the [`registeredServices` actuator endpoint](https://apereo.github.io/cas/development/services/Service-Management.html) now answers `404` for an unknown identifier instead of failing with an error.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#service-registry) Saving or deleting a service definition while the service registry is being reloaded now waits for the reload to finish. Previously, a save or delete that waited more than five seconds was dropped without an error; the `registeredServices` actuator endpoint reported success and a dynamically registered client could receive credentials that were never stored.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#service-registry) A service registry that fails to load, such as a [REST service registry](https://apereo.github.io/cas/development/services/REST-Service-Management.html) whose endpoint is unavailable or answers with an error, no longer empties the cache of service definitions; CAS logs the failure and keeps the definitions it already loaded until a later reload succeeds.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#service-registry) Groovy scripts referenced by service definitions as files, for [access strategies](https://apereo.github.io/cas/development/services/Service-Access-Strategy-Groovy.html), their activation criteria, attribute release activation criteria, single sign-on participation policies, attribute filters and surrogate access strategies, are now compiled and watched once per script location instead of once per service definition on every reload or evaluation. Previously each reload leaked a file watcher, which could exhaust the operating system’s limit on file watchers.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#service-registry) [Expired application definitions](https://apereo.github.io/cas/development/services/Configuring-Service-Expiration-Policy.html) are processed once each time service definitions are loaded instead of on every request. Contacts of an expired application are notified once per expiration date rather than on every request and every reload, a request for an expired application no longer sends notifications, and definitions set to be deleted when they expire are no longer found by requests and are deleted at the next load instead of on the request thread.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#service-registry) Lookups in the [JSON](https://apereo.github.io/cas/development/services/JSON-Service-Management.html) and [YAML](https://apereo.github.io/cas/development/services/YAML-Service-Management.html) service registries no longer read a map that a reload, a save or the file watcher is modifying at the same time.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#service-registry) Service registry listeners that replace a loaded application definition now take effect; the [REST service registry](https://apereo.github.io/cas/development/services/REST-Service-Management.html), among others, used to return the definitions as they were before the listeners ran. Single sign-on checks no longer fail with an error when no service is given.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#service-registry) Inline Groovy scripts used by access strategy and attribute release activation criteria and by single sign-on participation policies are compiled once and shared, instead of being compiled again on every evaluation. Groovy script files used by service definitions resume picking up changes after the script cache expires them, and compiling one script for the first time no longer holds up compiling a different one.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#service-registry) A [REST service registry](https://apereo.github.io/cas/development/services/REST-Service-Management.html) that fails to load no longer reports credentials or query parameters of its endpoint URL in the error.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#service-registry) Application definitions that CAS reads straight from the service registry, because they are not cached yet, now get their [templates](https://apereo.github.io/cas/development/services/Configuring-Service-Template-Definitions.html) applied, like definitions that are loaded in bulk. Previously settings that a template supplies, such as access or multifactor authentication rules, were missing from such definitions until the next reload.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#service-registry) Values that only resemble a whole number, such as `1.5`, `1e3`, `0x1F` or one too large to store, no longer fail the request with a server error. A `service` parameter, including the one used to apply an application’s [HTTP security headers](https://apereo.github.io/cas/development/services/Configuring-Service-Http-Security-Headers.html) and to fetch [SAML2 identity provider metadata](https://apereo.github.io/cas/development/authentication/Configuring-SAML2-Authentication.html), is matched as a service URL instead of an identifier; the issue instant of an IdP-initiated SAML2 request, the WS-Federation `wfresh` parameter, the single sign-on revocation attribute, numeric durations, cookie lifetimes and the actuator endpoints that accept a service identifier handle such values the same way. Cookie lifetimes too large for a cookie are capped instead of overflowing.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#service-registry) When the service registry is consulted for an application that matches no cached definition, the [JPA](https://apereo.github.io/cas/development/services/JPA-Service-Management.html) and the deprecated CosmosDb service registries now evaluate every definition the way other registries do. Both used to search for definitions whose pattern contained the requested URL, which never found definitions written as regular expressions; CosmosDb also placed the requested URL in its query text unescaped and no longer sends it to the database at all.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#service-registry) Service lookups against the [Git service registry](https://apereo.github.io/cas/development/services/Git-Service-Management.html) use the definitions from the last load instead of pulling from the remote repository on each lookup; definitions are pulled when service definitions are reloaded.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#service-registry) Application definitions that are saved through CAS, picked up by the JSON and YAML registry watchers, or announced by [Google Cloud Storage](https://apereo.github.io/cas/development/services/GCP-Storage-Service-Management.html) change notifications now have their [templates](https://apereo.github.io/cas/development/services/Configuring-Service-Template-Definitions.html) applied before they are cached, as they are when definitions are loaded, instead of being matched without them until the next reload.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#service-registry) The [MongoDB service registry](https://apereo.github.io/cas/development/services/MongoDb-Service-Management.html) deletes a service definition in one request instead of reading it back first, counts definitions from collection metadata instead of scanning the collection, and, when several definitions share a service id or name, returns the one that comes first in evaluation order instead of an arbitrary match. Definitions it looks up by id or name, or reads while the cache is empty, now get the same post-processing as those it loads in bulk, so OpenID Connect applications keep their scope-based attribute release policies.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#service-registry) When MongoDB runs as a replica set, the [MongoDB service registry](https://apereo.github.io/cas/development/services/MongoDb-Service-Management.html) watches its collection through a change stream and reloads service definitions on every CAS node shortly after they change, including changes made by another node or directly in MongoDB. Deployments that are not replica sets keep relying on the scheduled reload.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#service-registry) The [Amazon S3 service registry](https://apereo.github.io/cas/development/services/AmazonS3-Service-Management.html) names buckets after service definitions with only the characters S3 accepts, so definitions whose names carry spaces or other punctuation can be saved; it saves definitions that have no description, and renaming a definition no longer leaves a copy behind in its old bucket.

### Interface & Accounts

- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#interface--accounts) The [account profile](https://apereo.github.io/cas/development/registration/Account-Management-Overview.html) and the account management screens (sign-up, password reset, forgot username, expired or must-change password) are redesigned: the profile gets a section rail, an overview with summary cards and lists with inline details, filtering and keyboard support, and every account management flow shows its steps beside a single form card. Features are unchanged; colors follow the CAS theme.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#interface--accounts) Buttons and text fields on CAS pages share one [look](https://apereo.github.io/cas/development/ux/User-Interface-Customization-CSSJS.html): labels sit above rounded inputs, buttons are pill-shaped and password fields get round reveal and generate buttons, matching the account screens and following the CAS theme colors. Alert boxes, tables and progress bars follow the same style, and page layouts stay as they were.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#interface--accounts) [WebAuthn](https://apereo.github.io/cas/development/mfa/FIDO2-WebAuthn-Authentication.html) and consent screens are redesigned. WebAuthn login, registration and QR code pages sit in a centered card with a status icon, and registration offers passkeys and security keys side by side. OpenID Connect, OAuth and CIBA consent show the application up front, list each scope and claim with its own icon, and show the CIBA binding message as a large code. Consent no longer shows two *Deny* buttons, CIBA lists scope names again, OAuth scopes without a description no longer show a raw message key, and the WebAuthn status heading now updates during login and registration.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#interface--accounts) [Interrupt](https://apereo.github.io/cas/development/webflow/Webflow-Customization-Interrupt.html) and [attribute consent](https://apereo.github.io/cas/development/integration/Attribute-Release-Consent.html) screens follow the same style: a centered card with a status icon, the interrupt message as a callout with its links, attribute consent tabs as a segmented control and consent options as selectable cards. Tabs no longer show a focus ring when a page loads.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#interface--accounts) The rest of the CAS screens follow the same style: error, account status, logout and confirmation pages show a status icon in a centered card, multifactor, passwordless, delegated, surrogate and other sign-in steps show the provider’s icon above their form, provider and option choices appear as cards, and the login card picks up the same surface.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#interface--accounts) [Palantir](https://apereo.github.io/cas/development/installation/Admin-Dashboard.html) dialogs, including the service wizard and editor, use the same look as the rest of CAS: rounded dialogs with a plain title bar, matching buttons (the main action highlighted), select menus and multi-value fields that look like the other inputs, round row buttons, and restyled menus, tooltips and confirmation prompts. Dialogs no longer fail to position themselves when opened before any right-click menu was set up.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#interface--accounts) [Palantir](https://apereo.github.io/cas/development/installation/Admin-Dashboard.html) has a navigation bar with grouped menus (Identity, Sessions, Platform) in place of the side rail, and each page is headed by the section it shows. Applications can be shown as cards or as the table, and a card’s menu offers the same actions as right-clicking a row. Palantir follows the light or dark appearance of the operating system until another is chosen from the navigation bar or the settings dialog. The service wizard starts from a choice of described application types and then walks the definition in steps (Basics, Protocol, Access & attributes, Policies) to a review of what the definition holds: each step shows its sections as tiles that say whether they hold values, with Back and Next to move along. Choosing the Policies step, whose sections are all advanced options, shows the advanced options. The *Feature Catalog* lists each feature as a card with its modules, with a search over feature and module names.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#interface--accounts) [Palantir](https://apereo.github.io/cas/development/installation/Admin-Dashboard.html) shows its *SAML2 Metadata Sources* tab whenever a SAML2 service provider is registered; the tab stayed hidden, or appeared only when the last definition read was a SAML2 one.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#interface--accounts) [Palantir](https://apereo.github.io/cas/development/installation/Admin-Dashboard.html) service wizard dropdowns react to a choice again: picking a username attribute provider, attribute release policy or another type now shows that type’s fields and adds it to the definition.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#interface--accounts) Pressing Enter in a [Palantir](https://apereo.github.io/cas/development/installation/Admin-Dashboard.html) search or test form, such as surrogate accounts or trusted multifactor devices, runs the form’s main action; some forms used to ignore it and others reloaded the dashboard.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#interface--accounts) A login form submitted with a malformed `execution` value now restarts the login flow instead of failing with a server error.

### Platform & Operations

- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#platform--operations) [Groovy scripts](https://apereo.github.io/cas/development/integration/Apache-Groovy-Scripting.html), whether inline or in a file, that keep no state between executions no longer run one request at a time: each execution gets its own instance of the compiled script, so concurrent requests through the same script run in parallel. Scripts that declare fields other than `final` ones holding plain values, such as counters or caches, still run one execution at a time so that their state stays consistent.
- [RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#platform--operations) CAS [actuator endpoints](https://apereo.github.io/cas/development/monitoring/Monitoring-Statistics.html#actuator-endpoints) that are built on Spring MVC request mappings now honor `management.endpoint.<id>.access`. A `READ_ONLY` declaration registers only the endpoint’s read mappings, so its `POST`, `PUT`, `PATCH` and `DELETE` mappings are no longer reachable, and a `NONE` declaration registers no mappings at all.
- [RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#platform--operations) [CloudWatch logging](https://apereo.github.io/cas/development/logging/Logging-Cloudwatch.html) now avoids recursive logging initialization when reporting appender startup or delivery failures.
- [RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#platform--operations) Several optimizations are in place to assist with faster startup time, allowing for more components to be lazily initialized.

### Build & Project

- [RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#build--project) The vulnerability report of the [dependencies endpoint](https://apereo.github.io/cas/development/installation/Dependency-Management-Versions.html#actuator-endpoints) follows OSV pagination for each dependency, so vulnerabilities past the first page of a result are no longer left out, and vulnerability details that cannot be read are reported as errors instead of being dropped silently.
- [RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#build--project) [RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#build--project) [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#build--project) [RC4](https://apereo.github.io/cas/development/release_notes/RC4.html#build--project) A large number of dependencies and libraries have been updated to their latest versions.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#build--project) The unit, integration and functional test workflows, like the native image tests, can be started manually to run only the test categories or browser scenarios that failed in the previous run, against the latest changes. See [this guide](https://apereo.github.io/cas/developer/Test-Process.html).
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#build--project) Almost all CAS unit tests are internally reworked to allow maximum parallelization and speed up the overall test execution time.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#build--project) MongoDb integration tests have now switched to using MongoDb `9.x`.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#build--project) The project [README](https://github.com/apereo/cas) is reworked as a landing page, with a one-minute Docker try-out, a link to the new [Quick Start](https://apereo.github.io/cas/development/planning/Quick-Start.html), features grouped by area and the maintained documentation versions.
- [RC2](https://apereo.github.io/cas/development/release_notes/RC2.html#build--project) Authentication history, theme caching and CloudWatch shutdown now use concurrent collections and explicit coordination in place of Java monitor locking.
- [RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#build--project) Groovy integration tests have now switched to use Groovy `5.1.x`.
- [RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#build--project) Redis integration tests have now switched to use Redis `8.10.x`.

### General

- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#general) CAS no longer tries to match a service identifier, such as the `service` parameter, that is longer than 4096 characters against application definitions, which guards against expensive regular expression matching on oversized input. The limit is controlled by `cas.service-registry.core.maximum-service-id-length`; zero or a negative value disables it.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#general) New application definitions without an identifier receive a random positive identifier instead of one derived from the system clock, so definitions created at the same time, on the same or different nodes, no longer collide.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#general) Checking access to an application now looks up the additional access strategy enforcers once instead of on every request.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#general) Queries against the service index return their results in full instead of reading from a result set that is already closed.
- [RC3](https://apereo.github.io/cas/development/release_notes/RC3.html#general) Looking up an application definition by name or by identifier and type no longer examines every cached definition. A lookup by identifier for a different type of definition now finds nothing instead of returning the definition of the other type.
- [RC1](https://apereo.github.io/cas/development/release_notes/RC1.html#general) A number of date-formatting operations have switched their base timezone from system default to `UTC`.

## Test Release Candidates Early

CAS releases are strictly time-based, and a `GA` release is [a tag and nothing more](https://apereo.github.io/2017/03/08/the-myth-of-ga-rel/). Trying each release candidate and the snapshots that follow it is the best way to gain confidence in an upgrade.

### Support the Project

Development is almost entirely voluntary. If CAS serves you well, [join the Apereo Foundation](https://www.apereo.org/content/apereo-membership) to fund dedicated time for long-term support, maintenance and timely security fixes.

### Get Involved

- [Share feedback](https://apereo.github.io/cas/Mailing-Lists.html) on the features you try
- [Contribute patches](https://apereo.github.io/cas/developer/Contributor-Guidelines.html) and documentation improvements

### Resources

- [Release schedule](https://github.com/apereo/cas/milestones)
- [Release policy](https://apereo.github.io/cas/developer/Release-Policy.html)
- [Maintenance policy](https://apereo.github.io/cas/developer/Maintenance-Policy.html)
