Release notes · 8.1.0 development
8.1.0-RC3
The third release candidate reworks Heimdall around the AuthZEN 1.0 specification, brings WebAuthn Level 3 and passkeys to passwordless and multifactor login, and carries the stateless ticket-granting ticket in its cookie.
- Requires JDK 25
- Built on Spring Boot 4.2
- 609 browser test scenarios
- 94% test coverage
- 14New & noteworthy topics
- 9Changes to review before upgrading
- 129Other changes
- 64Fixes
Before You Upgrade
These changes alter existing behavior. Each one links to its full description below.
-
Action
Certificate-bound access tokens issued before the upgrade are rejected; thumbprints now follow RFC 8705.OAuth & OpenID Connect
-
Action
Heimdall AuthZEN resources must define
resourceType,actionsand optionallyresourceIdPattern, or AuthZEN requests are denied.Authorization -
Changed
Heimdall policies deny access when a resource has none, and one granting policy is enough unless
enforceAllPoliciesis set.Authorization -
Removed
cas.ticket.registry.stateless.storage-typeno longer applies; the ticket-granting ticket is carried by the ticket-granting cookie.Tickets & Sessions -
Changed
Stateless ticket-granting tickets no longer carry principal attributes; attribute repositories are consulted each time instead.Tickets & Sessions
-
Changed
Authentication throttling counts a failed attempt only on a
401response, once per request.Authentication -
Changed
WebAuthn requires browsers that support the WebAuthn Level 3 JSON serialization.Multifactor & Passkeys
-
Changed
Passwordless tokens arrive as
PasswordlessTokenCredential, the only credential the handler accepts; the recorded credential type changes.Passwordless -
Action
Google Authenticator on DynamoDB needs a new
useridIndexglobal secondary index; create it yourself if tables are not created at startup.Multifactor & Passkeys
Spotlight · User interface
A Refreshed Look Across CAS
- The account profile gets a section rail, an overview with summary cards and lists with inline details.
- Login, multifactor, consent, interrupt and error screens share one style, with pill-shaped buttons and rounded inputs.
- Palantir gets a navigation bar with grouped menus, application cards, a step-by-step service wizard and a dark theme.
Highlights
-
AuthorizationChanged
Heimdall AuthZEN
Follows the AuthZEN Authorization API 1.0, with resource instances, the access evaluations API, decision point metadata and decision context.
-
Multifactor & PasskeysNew
WebAuthn Level 3
Related origins, the Signal API, passkey provider names and icons, and a passkey upgrade after password login.
-
PasswordlessNew
Passwordless Authentication
Single-use tokens under load, one-time-code autofill, origin-bound SMS codes and passkeys from the autofill menu.
-
Tickets & SessionsAction
Stateless Ticket Registry
The ticket-granting ticket now rides in the ticket-granting cookie, with a versioned format and type-bound tickets.
-
OAuth & OpenID ConnectNew
OpenID Connect Verifiable Credentials
Proofs follow each credential configuration, and holder keys may be
x5c,did:jwkor Ed25519. -
DocumentationNew
Documentation
A refreshed theme, searchable settings, grouped actuator endpoints and feature toggles, and a Quick Start recipe.
New & Noteworthy
The following items are new improvements and enhancements presented in this release.
OpenRewrite Recipes
CAS continues to produce and publish OpenRewrite recipes that allow the project to upgrade installations in place from one version to the next. See this guide to learn more.
Graal VM Native Images
A CAS server installation and deployment process can be tuned to build and run as a Graal VM native image. We continue to polish native runtime hints. The collection of end-to-end browser tests based on Puppeteer 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.
Testing Strategy
The collection of end-to-end browser tests based on Puppeteer 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%.
JSpecify & NullAway
CAS codebase is now annotated with JSpecify 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 to prevent nullness contract violations during compile time.
Spring Boot 4.2
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 for more information on the changes and updates in this release.
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 are grouped by endpoint, with each operation listing its parameters, response and a working
curlexample 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 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 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.
- Comparison tables help choose between ticket registries, service registries, multifactor providers and protocols.
- 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 and multitenancy 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 names an explicit image tag, and Getting Started points to it for a quick local trial.
- A short Quick Start 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’sdependenciesblock and the completecas.propertiesfor the selection. The selection now survives the browser’s Back button, and WebAuthn runs on the samehttp://localhost:8080server 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 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 lists every release candidate with its summary, and the combined release candidate page 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.
Heimdall AuthZEN
Heimdall AuthZEN support is reworked to follow the AuthZEN Authorization API 1.0 specification:
- The AuthZEN
resource.idnow identifies the resource instance rather than a policy namespace. AuthZEN requests are matched against resources in all namespaces by their newresourceType,actionsand optionalresourceIdPatternfields, and all matching resources must grant access. Existing AuthZEN resources must define these fields, or AuthZEN requests are denied; requests to/heimdall/authorizeare unaffected. - Evaluated denials return
200with"decision": false, malformed requests400, and failed caller authentication401. TheX-Request-IDheader 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 can prevent an application from calling Heimdall. DPoP-bound and certificate-bound tokens
now require their proof. On the AuthZEN endpoint,
Basiccredentials are theclient_id:client_secretof a registered application rather than CAS user credentials. - JWT bearer assertions must carry
jtiandiatclaims, are accepted once, and may not live longer thancas.heimdall.jwt-assertion-max-lifetime(five minutes by default). - Failed caller authentication on
/heimdall/authorizenow returns401instead of403. Failed caller authentication on both Heimdall endpoints is subject to authentication throttling. - 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 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 afterqueryTimeout(five seconds by default). Policies are evaluated in order rather than on the shared thread pool. - AuthZEN subjects are resolved from CAS attribute repositories only for the
usersubject type. Required and rejected attribute policies accept qualified names such assubject.properties.department,resource.properties.owner,action.properties.methodandcontext.channel. HTTP request headers are no longer added to the AuthZEN request context; on/heimdall/authorizethey no longer override body entries or include protocol headers. - A resource that does not set
enforceAllPoliciesis now granted when any one of its policies grants access, as documented; previously every policy had to grant. SetenforceAllPoliciestotrueon 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 at
/heimdall/authzen/evaluations, with theexecute_all,deny_on_first_denyandpermit_on_first_permitsemantics. - Heimdall publishes AuthZEN policy decision point metadata at
/heimdall/.well-known/authzen-configuration; the well-known location defined by the specification needs a rewrite rule. - Denied AuthZEN decisions carry a decision context
with a
reasoncode. - JDBC and OpenFGA policies receive the AuthZEN subject, resource and action. Palantir can edit the AuthZEN fields of a resource and configure the Heimdall access strategy.
Certificate-Bound Access Tokens
The certificate thumbprint that CAS records for mutual TLS client authentication
and emits as the cnf x5t#S256 claim of access tokens and introspection responses is now computed as specified
by RFC 8705: 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.
Authentication Throttling
Authentication throttling 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 attempts, which answer with a SOAP fault, are now counted as well.
Redis authentication throttling 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 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.
Passwordless Authentication
- Passwordless authentication 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.
- Tokens kept in JPA or MongoDb are now removed once used, and their cleaner removes expired tokens; it used to remove the valid ones.
- MongoDb and REST stores no longer return expired
tokens, and a REST endpoint must answer a single-token
DELETEwith a2xxstatus only when it removed the token. - A token that could not be delivered by email or SMS 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 line (
@host #token), which the token page reads through the WebOTP API where available. - When WebAuthn primary authentication 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 offers a passkey option. Both hand the passkey assertion to the existing WebAuthn primary authentication flow.
WebAuthn Level 3
- FIDO2 WebAuthn publishes its origins at
/.well-known/webauthnfor 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, 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 withdirect; browsers without this WebAuthn Level 3 support can no longer use WebAuthn in CAS. - CAS also publishes
/.well-known/passkey-endpointsso password managers can link users to the pages where passkeys are created and managed; by default both point to the account profile 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, turned on with
cas.authn.mfa.web-authn.core.passkey-upgrade-enabledalongside 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.
OpenID Connect Verifiable Credentials
- Credential proofs now follow what each credential configuration
advertises: a proof signed with an algorithm outside
proof-signing-alg-values-supported, or naming its key by a binding method outsidecryptographic-binding-methods-supported, is refused withinvalid_proof. Holder keys may now also be given as anx5ccertificate or adid:jwkkey identifier, and Ed25519 (EdDSA) keys are accepted. - Authorization server metadata advertises
pre-authorized_grant_anonymous_access_supportedwhenever 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 by the
scopeits 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
request can now say so: its error response is accepted, answered as OpenID4VP requires, and reported to the relying party
as an
erroroutcome 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_jsonandjwt_vc_json-ldconfigurations publishcredential_definitioninstead ofvct. JSON-LD credentials no longer reference a context document CAS never served, and a configuration without a scope no longer issues anullcredential 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 addjwt-vc-issuerto the well-known rewrite rule. - 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-jwtcredential configurations instead of a fixed list. - A presentation request may carry a registered
redirect_urifor a same-device flow: the wallet is sent back to it with aresponse_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": falsein a presentation request are now optional: they are requested through DCQLclaim_setsand 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) orcredential_configuration_id, which revealed to every verifier which relying party requested the credential; CAS verifies its own credentials by the key’skid. - A wallet’s error response is stored bounded:
errorto 128 characters anderror_descriptionto 1024. - Attribute values with a leading zero, such as postal codes, are issued as text. Previously they were read as octal numbers,
so
0123was issued as83and08failed issuance. - Issued credentials carry the issuer signing key’s certificate chain as the
x5cheader, 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 theirx5cheader, 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 can be
encrypted (
direct_post.jwt), as the High Assurance Interoperability Profile requires, withcas.authn.oidc.vc.presentation.response-mode=DIRECT_POST_JWTor per request with"response_mode": "direct_post.jwt": each request carries its own ephemeralECDH-ESkey, and presentations sent in the clear are refused. - Verifiable presentation requests may identify CAS with the
x509_hashclient 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
with the
dc_apianddc_api.jwtresponse 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, as
OpenID4VCI 1.0 and the High Assurance Interoperability Profile define: in the
key_attestationheader of ajwtproof, or as anattestationproof that yields one credential per attested key. Attestations must chain to trust anchors set withcas.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 askey_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
(
attest_jwt_client_auth), as the High Assurance Interoperability Profile requires of wallet attestations. Attestations must chain to trust anchors set withcas.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:
credential responses carry a
notification_id, andcredential_accepted,credential_failureandcredential_deletednotifications are recorded in the audit log. - Issued SD-JWT VC credentials may carry a
statusclaim and be revoked or suspended, following the Token Status List specification: CAS publishes signed status list tokens, keeps entries in the ticket registry, offers anoidcVcStatusactuator 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 withcas.authn.oidc.vc.issuer.status-list.enabled=true. - Credential requests and responses may be encrypted 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 incredential_response_encryption. Turn it on withcas.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_configurationinstead of the draft-eraunsupported_credential_type, and acredential_identifierthe token response did not return isunknown_credential_identifierinstead ofcredential_request_denied. - Once the token response returns
credential_identifiers, credential requests must usecredential_identifier, as OpenID4VCI 1.0 requires; acredential_configuration_idis then refused withinvalid_credential_request. Otherwise a request must name acredential_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 atransaction_id, anoidcVcDeferredactuator endpoint approves or denies the pending transactions, and wallets collect the credentials from the new deferred credential endpoint, per OpenID4VCI 1.0. - Client attestations may be bound to short-lived challenges
from the new challenge endpoint, advertised as
challenge_endpoint, once turned on withcas.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, advertised asstatus_list_aggregation_endpoint, lists all published status lists. - Wallets that ask for
application/jwtreceive the credential issuer metadata as signed metadata, per OpenID4VCI 1.0. - CAS can require DPoP proofs to carry a nonce 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 with400, and protected resources with401,use_dpop_nonceand a fresh nonce in theDPoP-Nonceheader. 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,
per RFC 9449: with
dpop_jktat the authorization endpoint or in a pushed authorization request, or with aDPoPproof sent with the pushed authorization request. The token request must then carry a DPoP proof made with that key, or it is refused withinvalid_dpop_proof. - Clients can be required to use DPoP for every token request,
per RFC 9449, with
dpopBoundAccessTokensin the service definition ordpop_bound_access_tokensin 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 is now accepted only in the
Authorizationheader with theDPoPscheme; sent as a bearer token or as a request parameter, it is refused withinvalid_token. Refresh tokens issued to public clients are bound to the DPoP key, per RFC 9449, and the stateless ticket registry keeps the DPoP binding of access and refresh tokens. - DPoP proofs are checked as RFC 9449 requires at every endpoint: a request
with more than one
DPoPheader, or with one that is not a JWT, is refused withinvalid_dpop_proof, which the token endpoint used to answer withinvalid_request. The profile and userinfo endpoints now answer a missing or invalid proof with401and aWWW-Authenticate: DPoP error="invalid_dpop_proof"challenge, as the credential endpoint did, and everyDPoPchallenge, Heimdall’s included, lists the algorithms accepted for DPoP proofs in itsalgsparameter. - Access tokens issued to a client that authenticates with mutual TLS
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 answer401withinvalid_tokenwithout it; the stateless ticket registry keeps the binding. - Clients can be required to always receive certificate-bound access tokens,
per RFC 8705, with
tlsClientCertificateBoundAccessTokensin the service definition ortls_client_certificate_bound_access_tokensin a dynamic registration request: a token request from such a client that is not authenticated with mutual TLS is refused withinvalid_request. - CAS issues and verifies ISO/IEC 18013-5 mdocs,
such as a mobile driving licence, in the
mso_mdocformat of OpenID4VCI 1.0 and OpenID4VP 1.0, which HAIP also covers. A credential configuration with theMSO_MDOCformat names its document type withcas.authn.oidc.vc.issuer.credential-configurations[].doctype, and each claim may name its name space withcas.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 Authletecborlibrary, from the same vendor as the SD-JWT library CAS already uses. - Verifiable credentials issued as
jwt_vc_jsonorjwt_vc_json-ldno longer carry avctclaim, 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-jwtcredentials keep theirvct.
Stateless Ticket Registry
With the stateless ticket registry, 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, 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 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 configured for it is not enforced.
- Verifiable credentials 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 and resource owner password 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 for details.
MCP Server
CAS can now act as a Model Context Protocol (MCP) server, 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.
Other Stuff
Authentication
- Hazelcast maps of all CAS modules no longer evict entries to make room by default:
cas.ticket.registry.hazelcast.cluster.core.eviction-policyand its counterparts now default toNONE, 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-policydefaults toLATEST_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. - 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 no longer fails when some of the records are kept on another member of the cluster.
- Delegated authentication 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 when building SAML2 responses, metadata and logout requests.
- CAS now logs a warning when a cookie it writes, such as the ticket-granting cookie, is larger than the 4 KB that browsers are guaranteed to accept.
- With the acceptable usage policy kept in MongoDb or Redis, 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.
Multifactor & Passkeys
- WebAuthn 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.
- WebAuthn pages now report browsers that cannot run them; the support check was never applied, and it no longer requires a platform authenticator.
- WebAuthn authentication pages now send the CSRF token rendered by CAS instead of reading it from the
XSRF-TOKENcookie, which failed with403whenever the page could not read that cookie. - Browser storage used by Duo Security and the SAML2 identity provider now falls back to cookies when the browser cannot use local or session storage.
- Google Authenticator devices can only be removed or confirmed by the user that owns them; a device id that belongs to another user is now refused.
- Google Authenticator device repositories: removing a device or a user from DynamoDB no longer removes other records with it; registering or updating a device in LDAP no longer replaces the user’s other devices; and removing or confirming a device with JPA works again and no longer re-encrypts the device secret when encryption is enabled.
- Google Authenticator device removal and confirmation now expire if not completed within a few minutes of the code being verified.
The REST device repository now sends deletions as
DELETEand includes the deviceidandpropertieswhen 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. - More Google Authenticator repository fixes:
MongoDB no longer treats usernames that differ
only by accents (such as
joseandjosé) 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. - 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 replaces its file in one atomic step and is documented as single-node.
- Faster Google Authenticator lookups: DynamoDB
reads a device by key and finds a user’s devices through a new
useridIndexglobal 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 creates an index on usernames with the collation its lookups use. - Google Authenticator LDAP 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 loads all devices with batched
MGETinstead of one request per device, and saving a device no longer decrypts the user’s other devices. - The Google Authenticator JPA repository 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.
- WebAuthn falls back to the cached FIDO metadata BLOB when downloading a fresh one fails at startup, for example when the FIDO metadata service rate-limits the request, instead of failing to start.
- Each WebAuthn 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. - With trusted devices kept in Redis, logins continue with multifactor authentication while Redis cannot be reached, as with the JDBC storage, instead of failing with a server error.
OAuth & OpenID Connect
- OAuth and OpenID Connect requests find the application definition for a
client_idthrough the service index instead of examining every definition, falling back to examining them when the index has no match. - Listing application definitions, for example through the
registeredServicesactuator endpoint or for the OpenID Connect JWKS endpoint, 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. - 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.
- 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 and the
registeredServicesactuator endpoint do, no longer re-indexes every cached definition. - 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.
- The
at_hashclaim of an OpenID Connect 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 anat_hashthat did not match when the access token was signed with ES* or PS* algorithms, encrypted, or requested as a JWT through token exchange. - Reading the OpenID Connect keystore 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.
- 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.
- OpenID Connect ID tokens issued for
response_type=id_tokenno longer carry anat_hashclaim, since no access token is issued with them. Forresponse_type=id_token token, theat_hashclaim 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. - An OAuth/OpenID Connect authorization code 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.
- An approved device code and a
CIBA 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 theauth_req_idhas been redeemed. - The OpenID Connect JWKS endpoint 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. - 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.
- 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
queryresponse mode, and also added to the query offragmentandform_postresponses. - JWT access tokens returned from the OpenID Connect authorization endpoint, with the
tokenandid_token tokenresponse 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. - When a request to an OAuth endpoint carries
no
client_idparameter, 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. - A request object passed with the
requestparameter is now unpacked and verified once per request instead of once for every authorization parameter read from it. - With
cas.authn.oauth.code.remove-related-access-tokensenabled, presenting an authorization code 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 orprivate_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. - A client that authenticates at the token endpoint with
private_key_jwtorclient_secret_jwtand sends noclient_idparameter 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. - PKCE 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_jwtor mutual TLS could previously redeem a code issued with acode_challengewithout acode_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 acode_verifiersent for a code that was issued without a challenge is refused withinvalid_grant. Acode_challengeandcode_challenge_methodsent 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. - When an OAuth application 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_grantfor 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. - Client secrets 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 client now replaces an expired client secret with a new one instead of extending the expiration of the expired secret.
- A client that authenticates at the token endpoint with
client_secret_jwtis now verified with itsclientSecret, as OpenID Connect requires, whenever the application’stokenEndpointAuthenticationMethodisclient_secret_jwtor left blank. A raw secret in the application’sjwksis still accepted, but that fallback is deprecated for removal. A secret shorter than the signing algorithm requires (32 bytes forHS256, 64 forHS512) is skipped with a warning that names the application. - 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.
- Rotating or revoking keys in the OpenID Connect keystore 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.
- Changes to an OpenID Connect keystore 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.
- Ed25519 keys presented to the OpenID Connect client JWKS registration endpoint 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.
- CIBA token requests answer
authorization_pendingwhile the user has not yet approved the request,expired_tokenfor an expiredauth_req_id, andinvalid_grantfor one that is unknown or already redeemed, as CIBA Core 1.0 requires; they used to returninvalid_grantandinvalid_request. - Device code token requests answer
expired_tokenfor an expired device or user code, andinvalid_grantfor a device code that is unknown, already redeemed or issued to another client, as RFC 8628 requires; they used to returnaccess_denied, which RFC 8628 reserves for a user who denied the request. - Approving consent for an authorization request with
prompt=consentnow completes the request instead of showing the consent screen again.
SAML2
- The SAML2 identity provider no longer
matches an entity id longer than
cas.service-registry.core.maximum-service-id-lengthagainst application definitions. - The SAML2 identity provider 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.
- Back-channel SAML2 single logout notifications sent with the HTTP-POST or
HTTP-Redirect bindings now honor
cas.slo.asynchronousthe 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. - Cached SAML2 service provider metadata 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.
- The SAML2 identity provider builds the resolver that locates its signing credentials once instead of for every signed response or assertion.
- The SAML2 identity provider decodes and
parses the incoming
SAMLRequestorSAMLResponseonce when matching it to a service provider, instead of once for every SAML2 application definition it compares against. - The SAML2 identity provider 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.
- 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.
- 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.
- SAML2 service provider metadata committed by hand to a Git repository, 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. - With identity provider metadata kept in Redis, 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”.
- With identity provider metadata kept in DynamoDb or Google Cloud Storage, service providers without metadata of their own use the identity provider’s global metadata and keys, as with Redis, JPA and MongoDb.
Authorization
- Attribute-based access strategies no longer deny a
principal only because it carries fewer attributes than the strategy lists as rejected, or, when
requireAllAttributesisfalse, fewer than it lists as required. The documentation now spells out thatrequireAllAttributesalso 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 setrequireAllAttributestofalse. - The HTTP request access strategy 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.1also matched110.0.0.15. Patterns written to match only part of a value, such asChrome.+for a user agent, must now be written as.*Chrome.+. - Attribute-based access strategies, including
those used by Grouper 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
adminalso acceptedsysadmin-disabled. Patterns written to match only part of a value must now be written as, for example,.*admin.*. - The REST access strategy 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.
Tickets & Sessions
- Ticket registries that replicate tickets through a message queue, such as Kafka, AMQP, Pulsar and Google Cloud Pub/Sub, 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.
- When a single sign-on session ends, the JPA ticket registry 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.
- When a single sign-on session ends, the remaining ticket registries, such as Redis, Hazelcast, DynamoDb, Cassandra, Memcached 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.
- The ticket registry cleaner 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, 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.
- 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 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.
- Validating a single-use service ticket 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.
- When a ticket is validated, 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. - The Memcached, DynamoDB and Google Cloud Firestore 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 ticket registry still reports every removal as successful and keeps this limitation.
- 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.
- The Hazelcast ticket registry 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
ssoSessionsactuator 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. - The Hazelcast ticket registry 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.
- The Hazelcast ticket registry no longer runs SQL
queries, so Hazelcast Jet is no longer started by any CAS Hazelcast module and the
core.enable-jetHazelcast setting, such ascas.ticket.registry.hazelcast.core.enable-jet, is removed. Remove it from your configuration; CAS refuses to start with settings it does not recognize. Thehazelcast-sqllibrary is no longer included. - The Hazelcast ticket registry 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-compressionno longer applies to them. - When Hazelcast expires a single sign-on session of the Hazelcast ticket registry, 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.
- The
cluster.core.replicatedandcluster.core.async-fillupHazelcast settings, such ascas.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, 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. - The MongoDB ticket registry 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 separateIDX_IDindex 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. - 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 whenclient-uriis 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. - Redis-backed counts and key lookups, such as the ticket and service registry counts, now return their connection when done. With connection pooling enabled, every call used to keep a pooled connection until the pool ran out.
- The Redis ticket registry query for ticket ids and loading all YubiKey devices from Redis also return their connection when done.
- The in-memory cache of the Redis ticket registry 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.
- The Apache Pulsar ticket registry subscribes each CAS node under a subscription of its own, named after
cas.ticket.registry.pulsar.subscription-nameand the node’scas.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. - The Google Cloud Pub/Sub ticket registry subscribes each CAS node under a subscription of its own, named
CasTicketRegistryTopicSubscription-followed by the node’scas.ticket.registry.core.queue-identifier, so every node receives every ticket change; nodes used to share one subscription. - A login from a client that sends no
User-Agentheader 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.
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 and
YAML registry watchers now update the cache right away, fetching services
by type through the
registeredServicesactuator endpoint no longer empties the cache, and CAS warns when the cache expires definitions before the scheduler reloads them. See this guide. - Exporting a single service definition through the
registeredServicesactuator endpoint now answers404for an unknown identifier instead of failing with an error. - 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
registeredServicesactuator endpoint reported success and a dynamically registered client could receive credentials that were never stored. - A service registry that fails to load, such as a REST service registry 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.
- Groovy scripts referenced by service definitions as files, for access strategies, 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.
- Expired application definitions 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.
- Lookups in the JSON and YAML service registries no longer read a map that a reload, a save or the file watcher is modifying at the same time.
- Service registry listeners that replace a loaded application definition now take effect; the REST service registry, 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.
- 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.
- A REST service registry that fails to load no longer reports credentials or query parameters of its endpoint URL in the error.
- Application definitions that CAS reads straight from the service registry, because they are not cached yet, now get their templates 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.
- Values that only resemble a whole number, such as
1.5,1e3,0x1For one too large to store, no longer fail the request with a server error. Aserviceparameter, including the one used to apply an application’s HTTP security headers and to fetch SAML2 identity provider metadata, is matched as a service URL instead of an identifier; the issue instant of an IdP-initiated SAML2 request, the WS-Federationwfreshparameter, 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. - When the service registry is consulted for an application that matches no cached definition, the JPA 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.
- Service lookups against the Git service registry 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.
- Application definitions that are saved through CAS, picked up by the JSON and YAML registry watchers, or announced by Google Cloud Storage change notifications now have their templates applied before they are cached, as they are when definitions are loaded, instead of being matched without them until the next reload.
- The MongoDB service registry 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.
- When MongoDB runs as a replica set, the MongoDB service registry 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.
- The Amazon S3 service registry 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
- The account profile 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.
- Buttons and text fields on CAS pages share one look: 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.
- WebAuthn 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.
- Interrupt and attribute consent 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.
- 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.
- Palantir 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.
- Palantir 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.
- Palantir 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.
- Palantir 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.
- Pressing Enter in a Palantir 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.
- A login form submitted with a malformed
executionvalue now restarts the login flow instead of failing with a server error.
Platform & Operations
- Groovy scripts, 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
finalones holding plain values, such as counters or caches, still run one execution at a time so that their state stays consistent.
Build & Project
- A large number of dependencies and libraries have been updated to their latest versions.
- 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.
- Almost all CAS unit tests are internally reworked to allow maximum parallelization and speed up the overall test execution time.
- MongoDb integration tests have now switched to using MongoDb
9.x. - The project README is reworked as a landing page, with a one-minute Docker try-out, a link to the new Quick Start, features grouped by area and the maintained documentation versions.
General
- CAS no longer tries to match a service identifier, such as the
serviceparameter, that is longer than 4096 characters against application definitions, which guards against expensive regular expression matching on oversized input. The limit is controlled bycas.service-registry.core.maximum-service-id-length; zero or a negative value disables it. - 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.
- Checking access to an application now looks up the additional access strategy enforcers once instead of on every request.
- Queries against the service index return their results in full instead of reading from a result set that is already closed.
- 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.
- DynamoDb audit records are kept one per entry instead of one per principal, so a user’s earlier records are no longer replaced by the latest one, and the time of each record is kept in milliseconds; records used to read back with dates in 1970 and to match date filters only by accident. Tables created by earlier versions are keyed by principal and must be dropped, for example with
cas.audit.dynamo-db.drop-tables-on-startup, to keep every record. - The DynamoDb configuration source reads its settings under
cas.spring.cloud.dynamo-db, as documented, and creates its table on startup unlesscas.spring.cloud.dynamo-db.prevent-table-creation-on-startupis turned on; CAS used to fail to start when the table did not exist yet. Large tables are read page by page. - With trusted devices kept in DynamoDb, each device remembers the multifactor provider it was trusted for, and logins continue with multifactor authentication while DynamoDb cannot be reached instead of failing with a server error.
- YubiKey devices kept in DynamoDb keep the tenant of the account they were first registered for.
- The Amazon S3 configuration source keeps the other settings of an S3 object when one of them is changed or removed, instead of overwriting or deleting the whole object.
- The AWS Secrets Manager configuration source loads every secret rather than only the first page of the listing, and removing all settings no longer fails when there is more than one secret. Settings from AWS Secrets Manager and AWS SSM are loaded once per refresh and kept in memory, so lookups no longer call AWS each time and keep working after a refresh.
- Logging to SQS sends the logging context as typed message attributes, within the SQS limit of 10, so messages written while a request is handled are no longer rejected.
- Audit records sent to Firehose end with a newline, so delivered objects can be read as newline-delimited JSON.
- Password management over REST sends the email address when looking up a forgotten username, so the forgot-username flow can find the account.
- The notifications dialog no longer shares an element id with the Google Authenticator confirmation dialog, which left the dialog descriptions ambiguous to screen readers. The notifications button takes its accessible name from its own text, “Notifications” and the visible count, rather than from a separate label that did not contain the count as shown.
- The AWS Secrets Manager, AWS SSM and Amazon S3 configuration sources open a client for each refresh or change and close it afterwards, so settings can still be changed through CAS after a refresh; the client they used to share was closed when the refresh finished. A new SSM parameter written with no active profile is named
/cas/<setting>rather than/cas//<setting>. - REST audit records handed back by the endpoint as CAS posted them, with type information, are read back correctly; records without type information are still accepted.
- The connection, socket and client execution timeouts of AWS clients (DynamoDb, S3, SQS, SES, Firehose and the rest) default to 5, 5 and 10 seconds. The defaults were written as
5000and10000, which CAS reads as seconds, so a backend that stopped answering held a request for well over an hour. Values given as plain numbers are still read as seconds; prefer the duration syntax, such asPT5S. - Trusted devices kept behind a REST API are saved when the endpoint answers with a plain-text or empty body; registering a device used to fail with a server error unless the response was JSON.
- The MongoDb modules for the acceptable usage policy and passwordless authentication no longer stop CAS from starting: their templates are only injected by name, so they no longer clash with the template Spring Boot’s MongoDb support expects.
- The Google Authenticator, trusted device, WebAuthn, YubiKey, event, service registry, SAML2 metadata and OpenID Connect keystore MongoDb modules can be used together and with the others: their templates are only injected by name, so Spring Boot’s MongoDb support no longer finds none or several candidates and stops CAS from starting. Only the MongoDb ticket registry template stays the primary one.
- YubiKey devices kept in Redis keep the tenant of the account they were first registered for, and removing a device for an unknown account in MongoDb no longer fails.
- Cassandra authentication no longer releases the stored password as a principal attribute.
- The Cassandra ticket registry creates its secondary indexes only when they are missing, instead of dropping and recreating them each time a node starts, so a node joining a cluster no longer removes the indexes the other nodes are querying.
- Authentication events sent to Kafka are keyed by their id, so the compacted events topic accepts them; records without a key used to be rejected.
- YubiKey devices kept in JPA or MongoDb are saved under the user’s existing account instead of adding a second account each time, and removing the account of a user who has none no longer fails with a server error in JPA.
- The MongoDb client JWKS store is set up before the OpenID Connect core, so the core’s in-memory store backs off for it instead of being replaced by bean overriding.
- The Cassandra service registry creates its
casservicestable when it does not exist, and definitions looked up by id pass through the same service registry listeners as those loaded in bulk. - Looking up authentication events of a type since a given date now applies the date; every event of that type used to match.
- Accessibility checks with axe-core now cover the login, login error, account status (locked, disabled, expired and must-change password, bad hours, bad workstation), password reset and forgot-username, unauthorized service, page not found, login success, attribute consent, logout confirmation and logout, Google Authenticator registration and token, and trusted device registration pages in one scenario, and the interrupt, simple MFA, passwordless, WebAuthn, YubiKey, graphical user authentication, acceptable usage policy, delegated provider selection and error, MFA provider selection and unavailable, surrogate selection, account sign-up, account profile, risk-blocked and QR code login pages in the scenarios that test those features. Each fails on serious or critical WCAG 2.1 A/AA violations.
- OAuth and OpenID Connect client authentication, the password grant, token introspection and token revocation identify the client by its client id when checking service access, rather than by its
serviceIdpattern. Patterns such ashttps://app.example.org.*are not URLs, and with HTTP service URLs now checked for a host and for user information they were rejected as unauthorized services. - Accessibility checks with axe-core now cover the login, login error, account status (locked, disabled, expired and must-change password, bad hours, bad workstation), password reset and forgot-username, unauthorized service, page not found, login success, attribute consent, logout confirmation and logout, Google Authenticator registration and token, and trusted device registration pages in one scenario, and the interrupt, simple MFA, passwordless, WebAuthn, YubiKey, graphical user authentication, acceptable usage policy, delegated provider selection and error, MFA provider selection and unavailable, surrogate selection, account sign-up, account profile, risk-blocked, QR code login, security questions, password reset and change, weak password, authentication warnings, service access warning, risky authentication verification, simple MFA recipient selection and email registration, passwordless method selection and Google Authenticator registration confirmation pages in the scenarios that test those features. Each fails on serious or critical WCAG 2.1 A/AA violations.
- The account sign-up form gives its fields valid
autocompletevalues (given-name,emailand so on) instead of their field names, and the QR code login field’s label points at the field it describes. - The Palantir SAML2 metadata and OpenID Connect configuration dialogs have accessible names and descriptions; they referred to element ids that did not exist. The account unlock field uses a valid
autocomplete="off"instead offalse. - The Memcached ticket registry no longer needs
java.utilopened to the application (--add-opens java.base/java.util=ALL-UNNAMED). Its Kryo serializers forEnumMap,EnumSet, calendars,Arrays.asListand the unmodifiable collection views read private JDK fields through reflection, which a plainjava -jarlaunch refuses, so no ticket-granting ticket could be stored and every login failed. They are replaced by serializers that do not use reflection, and the system clock is written as its zone instead of through its private field, which neededjava.timeopened. - A request whose
serviceis refused outright, such as an HTTP URL with user information, is answered with the CAS service error page and a403status. The response headers filter and the locale interceptor used to let the refusal escape, which left the browser with an empty error response. The check reads the host and user information of the URL directly instead of parsing it as ajava.net.URI, so service URLs CAS has always accepted, such as a space in the query or an underscore in a host name, are not refused. - The tabs on the login success page sit directly in their tab list, so screen readers announce them as tabs. The default account sign-up patterns for email and phone are valid in browsers that compile the
patternattribute with thevflag; they were ignored with a console error, so the fields were not checked before submitting. - A request that carries a badly formatted webflow
execution, in the form or in the query string, restarts the flow like any other execution CAS cannot use: it is redirected to the same page with theexecutionparameter removed and its other parameters, such asservice, kept. It used to end in a server error, because the redirect kept the bad key and would have looped. - Browser tests close the browser within a bounded time and kill it when it stops answering, so a stuck browser fails the scenario instead of holding the job until its timeout.


