<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://fawnoos.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://fawnoos.com/" rel="alternate" type="text/html" /><updated>2026-07-09T16:01:42+00:00</updated><id>https://fawnoos.com/feed.xml</id><title type="html">Fawnoos</title><subtitle>Fawnoos || Open-Source Identity and Access Management Consulting Services</subtitle><author><name>Misagh Moayyed</name></author><entry><title type="html">Apereo CAS - SAML2 Metadata Entity Attributes</title><link href="https://fawnoos.com/2026/07/10/cas81x-saml2-metadata-entity-attributes/" rel="alternate" type="text/html" title="Apereo CAS - SAML2 Metadata Entity Attributes" /><published>2026-07-10T00:00:00+00:00</published><updated>2026-07-10T00:00:00+00:00</updated><id>https://fawnoos.com/2026/07/10/cas81x-saml2-metadata-entity-attributes</id><content type="html" xml:base="https://fawnoos.com/2026/07/10/cas81x-saml2-metadata-entity-attributes/"><![CDATA[<p>SAML2 metadata has always contained more than certificates and endpoints. It can also describe how a service provider expects to interact with an identity provider. Apereo CAS now understands several well-known entity attributes that allow metadata to control response signing, assertion encryption, authentication context selection (including MFA), and even attribute renaming. The result is less configuration with fewer opportunities for drift.</p>

<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>This is what this blog focuses on. Our main focus is on:</p>

<ul>
  <li>CAS <code class="language-plaintext highlighter-rouge">8.1.x</code></li>
  <li>Java <code class="language-plaintext highlighter-rouge">25</code></li>
</ul>

<h1 id="using-entity-attributes">Using Entity Attributes</h1>

<p>Once service metadata is processed by CAS successfully, there is not a whole lot more than is done with it and yet it is capable of much more. Apereo CAS has introduced support for consuming a number of well-known SAML entity attributes directly from service provider metadata. Rather than configuring signing behavior, encryption requirements, authentication context preferences, or attribute naming rules inside the CAS service registry, these preferences can now travel with the metadata itself.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>SAML metadata has traditionally been viewed as a collection of endpoints, bindings, and certificates. While those are certainly important, the SAML specification also defines <em>Entity Attributes</em>, allowing a service provider to advertise behavioral requirements alongside its technical configuration. Many Shibboleth deployments have relied on these attributes for a while now. CAS now understands several of these well-known attributes, allowing administrators to let metadata influence runtime behavior automatically.</p>

<h1 id="signing-operations">Signing Operations</h1>

<p>Some service providers expect signed SAML responses. Others only require signed assertions. Some want both! Rather than configuring these requirements manually for every registered service, metadata can advertise them directly.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<div class="highlight">
    <div class="xml-code-block">
      <pre><code data-lang="xml">
&lt;saml:Attribute
    Name=&quot;http://shibboleth.net/ns/profiles/saml2/sso/browser/signResponses&quot;
    NameFormat=&quot;urn:oasis:names:tc:SAML:2.0:attrname-format:uri&quot;&gt;
    &lt;saml:AttributeValue&gt;true&lt;/saml:AttributeValue&gt;
&lt;/saml:Attribute&gt;

&lt;saml:Attribute
    Name=&quot;http://shibboleth.net/ns/profiles/saml2/sso/browser/signAssertions&quot;
    NameFormat=&quot;urn:oasis:names:tc:SAML:2.0:attrname-format:uri&quot;&gt;
    &lt;saml:AttributeValue&gt;true&lt;/saml:AttributeValue&gt;
&lt;/saml:Attribute&gt;
</code></pre>
    </div>
</div>

<p>When these entity attributes are present, CAS automatically enables signing of responses and/or assertions for that relying party. No additional service registry settings are required.</p>

<h1 id="assertion-encryption">Assertion Encryption</h1>

<p>Encryption requirements can also be expressed directly in metadata.</p>

<div class="highlight">
    <div class="xml-code-block">
      <pre><code data-lang="xml">
&lt;saml:Attribute
    Name=&quot;http://shibboleth.net/ns/profiles/encryptAssertions&quot;
    NameFormat=&quot;urn:oasis:names:tc:SAML:2.0:attrname-format:uri&quot;&gt;
    &lt;saml:AttributeValue&gt;true&lt;/saml:AttributeValue&gt;
&lt;/saml:Attribute&gt;
</code></pre>
    </div>
</div>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>Once detected, CAS encrypts assertions before they’re delivered to the service provider, assuming the appropriate encryption certificates are available.</p>

<h1 id="mfa">MFA</h1>

<p>One particularly useful entity attribute is:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://shibboleth.net/ns/profiles/defaultAuthenticationMethods
</code></pre></div></div>

<p>This allows a service provider to advertise the authentication context it expects. For example, CAS can translate this authentication context directly into a multifactor authentication flow using Duo Security:</p>

<div class="highlight">
    <div class="xml-code-block">
      <pre><code data-lang="xml">
&lt;saml:Attribute
    Name=&quot;http://shibboleth.net/ns/profiles/defaultAuthenticationMethods&quot;
    NameFormat=&quot;urn:oasis:names:tc:SAML:2.0:attrname-format:uri&quot;&gt;
    &lt;saml:AttributeValue&gt;https://refeds.org/profile/mfa&lt;/saml:AttributeValue&gt;
&lt;/saml:Attribute&gt;
</code></pre>
    </div>
</div>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>This of course requires that you teach CAS how such authentication methods are mapped internally:</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">cas.authn.saml-idp.core.context.authentication-context-class-mappings</span><span class="p">=</span><span class="se">\
</span>  <span class="s">https://refeds.org/profile/mfa-&gt;mfa-duo</span>
</code></pre></div></div>

<h1 id="remapping-attributes">Remapping Attributes</h1>

<p>Rather than creating custom release policies for every variation, metadata can declare the desired mapping for attributes.</p>

<div class="highlight">
    <div class="xml-code-block">
      <pre><code data-lang="xml">
&lt;mdattr:EntityAttributes
    xmlns:mdattr=&quot;urn:oasis:names:tc:SAML:metadata:attribute&quot;&gt;

    &lt;saml:Attribute
        xmlns:saml=&quot;urn:oasis:names:tc:SAML:2.0:assertion&quot;
        Name=&quot;http://shibboleth.net/ns/attributes/naming/saml2&quot;
        NameFormat=&quot;urn:oasis:names:tc:SAML:2.0:attrname-format:uri&quot;&gt;

        &lt;saml:AttributeValue&gt;mail email&lt;/saml:AttributeValue&gt;
        &lt;saml:AttributeValue&gt;givenName first_name&lt;/saml:AttributeValue&gt;
        &lt;saml:AttributeValue&gt;sn last_name&lt;/saml:AttributeValue&gt;

    &lt;/saml:Attribute&gt;
&lt;/mdattr:EntityAttributes&gt;
</code></pre>
    </div>
</div>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>During attribute release, CAS automatically applies these mappings (i.e. <em>Take mail and release it as email</em>) when using the appropriate release policy:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"@class"</span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apereo.cas.support.saml.services.SamlRegisteredService"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"serviceId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"entity-id"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"SAML"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w">
  </span><span class="nl">"metadataLocation"</span><span class="p">:</span><span class="w"> </span><span class="s2">"path/to/metadata.xml"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"attributeReleasePolicy"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"@class"</span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apereo.cas.support.saml.services.MappedEntityAttributesAttributeReleasePolicy"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The original attribute remains the source of truth, while the metadata dictates how it should appear to the service provider.</p>

<h2 id="supported-entity-attributes">Supported Entity Attributes</h2>

<p>CAS currently recognizes the following entity attributes:</p>

<table>
  <thead>
    <tr>
      <th>Entity Attribute</th>
      <th>Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">http://shibboleth.net/ns/profiles/saml2/sso/browser/signAssertions</code></td>
      <td>Sign SAML assertions</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">http://shibboleth.net/ns/profiles/saml2/sso/browser/signResponses</code></td>
      <td>Sign SAML responses</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">http://shibboleth.net/ns/profiles/encryptAssertions</code></td>
      <td>Encrypt assertions</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">http://shibboleth.net/ns/profiles/defaultAuthenticationMethods</code></td>
      <td>Request an authentication context for MFA</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">http://shibboleth.net/ns/attributes/naming/saml2</code></td>
      <td>Rename released SAML attributes</td>
    </tr>
  </tbody>
</table>

<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>By understanding these well-known entity attributes, CAS moves closer to treating metadata as a behavioral contract and not simply a collection of URLs and certificates. Signing preferences, encryption requirements, authentication context selection, and attribute naming conventions can all be expressed once, published through metadata, and honored automatically by the identity provider.</p>

<h1 id="need-help">Need Help?</h1>

<p>If you have questions about the contents and the topic of this blog post, or if you need additional guidance and support, feel free to <a href="/#contact-section-header">send us a note </a> and ask about consulting and support services.</p>

<div class="followit-subscribe-wrapper">
  
    <button type="button" class="followit-subscribe-toggle" onclick="
        const wrapper = this.closest('.followit-subscribe-wrapper');
        wrapper.classList.add('is-visible');
      ">
      Subscribe by email
    </button>
  

  <div class="followit--follow-form-container followit-subscribe-box " attr-a="" attr-b="" attr-c="" attr-d="" attr-e="" attr-f="">

    <form data-v-c497cbba="" action="https://api.follow.it/subscription-form/TDFQVjQzZTk2czNPRkF6RUZSUmQ0SE0wYnBnS2VkQjdZeXpsZDFQNWRETjVGc0JyL1FqL001SlFzUlVhWkpJdFZ4SUl3OEU0Sm1CK2pZUjlCbG1PcG9oNDRadjhKeldCSmlLdkpZQnRqUC8xMTJTWmxxQTFwWEpjc3FkVGI4WGx8MEJXeEV6ZTlkaUhUOXBJdFlmMXU0Q3hCQXI0anpveVlMVnNlN2VPRC9uaz0=/8" method="post">

      <div data-v-c497cbba="" class="form-preview" style="background-color: rgb(255, 255, 255); position: relative; border-style: solid; border-width: 1px; border-color: rgb(204, 204, 204);">

        <div data-v-c497cbba="" class="preview-heading">
          <h5 data-v-c497cbba="" style="font-family: Arial; font-weight: bold; color: rgb(0, 0, 0); font-size: 16px; text-align: center; text-transform: none !important;">
            Get new posts by email:
          </h5>
        </div>

        <div data-v-c497cbba="" class="preview-input-field">
          <input data-v-c497cbba="" type="email" name="email" required="" placeholder="Enter your email" spellcheck="false" style="font-family: Arial; font-weight: normal; color: rgb(0, 0, 0); font-size: 14px; text-align: center; background-color: rgb(255, 255, 255); text-transform: none !important;" />
        </div>

        <div data-v-c497cbba="" class="preview-submit-button">
          <button data-v-c497cbba="" type="submit" class="contact100-form-btn">
            Subscribe
          </button>
        </div>
      </div>
    </form>

    <a href="https://follow.it" class="powered-by-line">
      <img src="https://follow.it/images/colored-logo.svg" alt="follow.it" height="17px" />
    </a>
  </div>
</div>

<h1 id="so">So…</h1>

<p>I hope the content here was of some help to you and I am sure that both this post as well as the functionality it attempts to explain can be improved in any number of ways. If you have suggestions or ideas on how to improve this post, please feel free to reach out to us.</p>

<p><a href="https://fawnoos.com">Misagh Moayyed</a></p>]]></content><author><name>Misagh Moayyed</name></author><category term="CAS 8.1.x" /><category term="SAML" /><summary type="html"><![CDATA[SAML2 metadata has always contained more than certificates and endpoints. It can also describe how a service provider expects to interact with an identity provider. Apereo CAS now understands several well-known entity attributes that allow metadata to control response signing, assertion encryption, authentication context selection (including MFA), and even attribute renaming. The result is less configuration with fewer opportunities for drift.]]></summary></entry><entry><title type="html">Apereo CAS - Directory-based SAML2 Metadata Registration</title><link href="https://fawnoos.com/2026/07/09/cas80x-saml2-metadata-from-directories/" rel="alternate" type="text/html" title="Apereo CAS - Directory-based SAML2 Metadata Registration" /><published>2026-07-09T00:00:00+00:00</published><updated>2026-07-09T00:00:00+00:00</updated><id>https://fawnoos.com/2026/07/09/cas80x-saml2-metadata-from-directories</id><content type="html" xml:base="https://fawnoos.com/2026/07/09/cas80x-saml2-metadata-from-directories/"><![CDATA[<p>Managing many SAML2 service provider registrations individually quickly becomes tedious and difficult to maintain. Apereo CAS supports using an entire directory of SAML2 metadata as the source of truth, allowing service providers to be discovered dynamically without creating one registered service per metadata file. Simply drop new metadata into the directory and let CAS do the rest.</p>

<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>This is what this blog focuses on. Our main focus is on:</p>

<ul>
  <li>CAS <code class="language-plaintext highlighter-rouge">8.1.x</code></li>
  <li>Java <code class="language-plaintext highlighter-rouge">25</code></li>
</ul>

<h1 id="overview">Overview</h1>

<p>If you’ve ever maintained a large SAML2 deployment, you’ve probably experienced the gradual accumulation of registered services. It usually starts innocently enough with maybe a few service providers and then someone adds another, and then another, etc. Before long, the service registry ends up collecting  nearly identical SAML2 service definitions, differing only in metadata location and perhaps a handful of policies.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>Apereo CAS supports registering an entire directory of SAML2 metadata using a single registered service. New service providers can then be onboarded simply by dropping their metadata into the configured directory. Sometimes the best configuration is the one you never have to write!</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<h1 id="metadata-management">Metadata Management</h1>

<p>Instead of registering every SAML2 service provider individually, define a single <code class="language-plaintext highlighter-rouge">SamlRegisteredService</code> whose metadata location points to a directory.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"@class"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apereo.cas.support.saml.services.SamlRegisteredService"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"serviceId"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">".*"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"name"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"Sample"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"id"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
  </span><span class="nl">"evaluationOrder"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
  </span><span class="nl">"metadataLocation"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"/path/to/metadata/directory"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"attributeReleasePolicy"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"@class"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apereo.cas.services.ReturnAllAttributeReleasePolicy"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>The <code class="language-plaintext highlighter-rouge">serviceId</code> field is interesting. Normally, such a regular expression would be a terrible way to identify SAML2 service providers. In this scenario, however, the actual matching is delegated to the metadata itself. CAS searches the configured metadata directory and attempts to locate metadata whose entity ID matches the incoming SAML request. If matching metadata is found, the service is considered registered. If not, the request is rejected.</p>

<p>The second interesting property is <code class="language-plaintext highlighter-rouge">metadataLocation</code>. Instead of referencing a single metadata document, this location points to an entire directory. Behind the scenes, CAS delegates metadata resolution to OpenSAML’s <code class="language-plaintext highlighter-rouge">LocalDynamicMetadataResolver</code>, which understands how to locate and resolve metadata documents from a filesystem directory.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<h1 id="registration">Registration</h1>

<p>Onboarding a new SAML2 application is somewhat straightforward. You obtain the service provider’s metadata and you’ll have to save it as as <code class="language-plaintext highlighter-rouge">.xml</code> file using a particular strategy. By default, OpenSAML generates a deterministic key from the entity ID, commonly based on a <code class="language-plaintext highlighter-rouge">SHA-1</code> digest. That means the service provider metadata file is not usually named after the raw entity ID. Instead, the entity ID is transformed into a stable local filename such as <code class="language-plaintext highlighter-rouge">$sha1-of-entity-id.xml</code>.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<h1 id="remember">Remember</h1>

<p>This approach works best when participating service providers share common policy. Since all metadata files resolve through the same <code class="language-plaintext highlighter-rouge">SamlRegisteredService</code>, they inherit the same configuration, including attribute release policy, signing behavior, access strategy, NameID configuration, and other service settings. If certain service providers require substantially different behavior, separate registered services pointing at different metadata directories or even individual metadata files may still be the better choice.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>I hope you and your future-self (and probably your colleagues) find this approach simpler!</p>

<h1 id="need-help">Need Help?</h1>

<p>If you have questions about the contents and the topic of this blog post, or if you need additional guidance and support, feel free to <a href="/#contact-section-header">send us a note </a> and ask about consulting and support services.</p>

<div class="followit-subscribe-wrapper">
  
    <button type="button" class="followit-subscribe-toggle" onclick="
        const wrapper = this.closest('.followit-subscribe-wrapper');
        wrapper.classList.add('is-visible');
      ">
      Subscribe by email
    </button>
  

  <div class="followit--follow-form-container followit-subscribe-box " attr-a="" attr-b="" attr-c="" attr-d="" attr-e="" attr-f="">

    <form data-v-c497cbba="" action="https://api.follow.it/subscription-form/TDFQVjQzZTk2czNPRkF6RUZSUmQ0SE0wYnBnS2VkQjdZeXpsZDFQNWRETjVGc0JyL1FqL001SlFzUlVhWkpJdFZ4SUl3OEU0Sm1CK2pZUjlCbG1PcG9oNDRadjhKeldCSmlLdkpZQnRqUC8xMTJTWmxxQTFwWEpjc3FkVGI4WGx8MEJXeEV6ZTlkaUhUOXBJdFlmMXU0Q3hCQXI0anpveVlMVnNlN2VPRC9uaz0=/8" method="post">

      <div data-v-c497cbba="" class="form-preview" style="background-color: rgb(255, 255, 255); position: relative; border-style: solid; border-width: 1px; border-color: rgb(204, 204, 204);">

        <div data-v-c497cbba="" class="preview-heading">
          <h5 data-v-c497cbba="" style="font-family: Arial; font-weight: bold; color: rgb(0, 0, 0); font-size: 16px; text-align: center; text-transform: none !important;">
            Get new posts by email:
          </h5>
        </div>

        <div data-v-c497cbba="" class="preview-input-field">
          <input data-v-c497cbba="" type="email" name="email" required="" placeholder="Enter your email" spellcheck="false" style="font-family: Arial; font-weight: normal; color: rgb(0, 0, 0); font-size: 14px; text-align: center; background-color: rgb(255, 255, 255); text-transform: none !important;" />
        </div>

        <div data-v-c497cbba="" class="preview-submit-button">
          <button data-v-c497cbba="" type="submit" class="contact100-form-btn">
            Subscribe
          </button>
        </div>
      </div>
    </form>

    <a href="https://follow.it" class="powered-by-line">
      <img src="https://follow.it/images/colored-logo.svg" alt="follow.it" height="17px" />
    </a>
  </div>
</div>

<h1 id="so">So…</h1>

<p>I hope the content here was of some help to you and I am sure that both this post as well as the functionality it attempts to explain can be improved in any number of ways. If you have suggestions or ideas on how to improve this post, please feel free to reach out to us.</p>

<p><a href="https://fawnoos.com">Misagh Moayyed</a></p>]]></content><author><name>Misagh Moayyed</name></author><category term="CAS 8.0.x" /><category term="SAML" /><summary type="html"><![CDATA[Managing many SAML2 service provider registrations individually quickly becomes tedious and difficult to maintain. Apereo CAS supports using an entire directory of SAML2 metadata as the source of truth, allowing service providers to be discovered dynamically without creating one registered service per metadata file. Simply drop new metadata into the directory and let CAS do the rest.]]></summary></entry><entry><title type="html">Apereo CAS - Teaching the Browser via Puppeteer to Test Flows</title><link href="https://fawnoos.com/2026/07/06/cas8-gradle-overlay-puppeteer/" rel="alternate" type="text/html" title="Apereo CAS - Teaching the Browser via Puppeteer to Test Flows" /><published>2026-07-06T00:00:00+00:00</published><updated>2026-07-06T00:00:00+00:00</updated><id>https://fawnoos.com/2026/07/06/cas8-gradle-overlay-puppeteer</id><content type="html" xml:base="https://fawnoos.com/2026/07/06/cas8-gradle-overlay-puppeteer/"><![CDATA[<p>There are many ways to test your Apereo CAS deployment. Configuration, components, endpoints, ticket validity, protocol flows, etc. But at some point, after all the unit tests are green and the integration tests are smiling politely, you might still wonder: <em>Can an actual user using a real browser really log in?</em> Apereo CAS Gradle overlays attempt to assist with this question via <a href="https://pptr.dev/">Puppeteer</a> and this is what this blog focuses on. Our main focus is on:</p>

<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<ul>
  <li>CAS <code class="language-plaintext highlighter-rouge">8.0.x</code></li>
  <li>Java <code class="language-plaintext highlighter-rouge">25</code></li>
</ul>

<h1 id="cas--puppeteer">CAS &amp; Puppeteer</h1>

<p><a href="https://pptr.dev/">Puppeteer is a Node.js library</a> that provides a high-level API to control Chrome or Chromium over the DevTools Protocol. In practical terms, Puppeteer lets you write JavaScript code that opens a browser, navigates to pages, clicks buttons, fills forms, waits for responses, inspects content, etc. By default, Puppeteer runs in headless mode, which means there is no visible browser window. This makes it ideal for automation, CI pipelines, and acceptance testing. It can also run in full, non-headless mode when you need to see what is happening on screen, which is particularly useful when the browser is doing something mysterious!</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>Of course, CAS is not simply a collection of REST endpoints. As an authentication platform, authentication is almost always experienced through a browser. A user visits an application. The application redirects to CAS. CAS presents a login screen. The user submits credentials. CAS validates the request, creates tickets, redirects back, releases attributes, satisfies policy, maybe invokes MFA, maybe delegates to another identity provider, and so on. That entire flow is difficult to validate with simple HTTP tests alone.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>Puppeteer gives you a way to verify the flow from the browser’s point of view. This is especially useful for a CAS Gradle overlay because the overlay is where real deployments take shape. It contains your configuration, customizations, extensions, themes, service definitions, authentication handlers, attribute release rules, and all the little deployment-specific choices that make your CAS server yours. Testing the overlay with Puppeteer means you are not just testing CAS in theory; you are testing the thing you are actually going to run.</p>

<h1 id="test-scenarios">Test Scenarios</h1>

<p>In the CAS overlay template, Puppeteer scenarios are used as a form of acceptance testing. A scenario is a small Node.js script that launches a browser and walks through a particular behavior or authentication flow. The template provides a puppeteer directory that contains the supporting scripts and scenario structure:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./puppeteer
├── package.json
├── run.sh
└── scenarios
    └── basic.js
</code></pre></div></div>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>Each scenario is intentionally small and focused and the goal is to capture a single meaningful flow. The provided <code class="language-plaintext highlighter-rouge">basic.js</code> scenario in the overlay template is a good starting point for understanding the structure. From there, you can create additional scenarios that match your own deployment requirements.</p>

<h1 id="running-tests">Running Tests</h1>

<p>To execute Puppeteer scenarios from the CAS overlay, run:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./puppeteer/run.sh
</code></pre></div></div>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>The script is designed to take care of the usual preparation work. It will attempt to build the CAS overlay, install Puppeteer and the required Node.js dependencies, launch the CAS server, wait until it is available, and then execute the defined scenarios one at a time. The script also assumes that the overlay is prepared with an embedded servlet container such as Apache Tomcat. This is the common overlay model: build the CAS web application, start it locally, and then let Puppeteer drive a browser against it.</p>

<h1 id="designing-tests">Designing Tests</h1>

<p>A basic Puppeteer scenario might verify that the CAS login page loads and accepts credentials. That is a good starting point but you should not stop there. You can pretty much <strong>TEST ANYTHING</strong> that you prefer, can and should. Some good example categories would be:</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<ul>
  <li>Service redirects</li>
  <li>Attribute release behavior</li>
  <li>Multifactor authentication prompts</li>
  <li>Delegated authentication</li>
  <li>OIDC authorization flows</li>
  <li>SAML2 login flows</li>
  <li>Password management screens</li>
  <li>Logout behavior</li>
  <li>Custom themes and UI changes</li>
  <li>Consent screens</li>
  <li>Interrupt flows</li>
  <li>Account status warnings</li>
</ul>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>In other words, any feature or functionality that your CAS deployment is capable of performing and is of importance to you should be tested. For example, a custom scenario might verify that a themed login page renders the expected banner, or that a specific service redirects correctly after successful authentication. Another scenario might verify that an MFA provider is triggered only when the correct service policy applies.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>A CAS feature may work perfectly in isolation. A configuration property may be documented correctly. A service definition may be syntactically valid. But the real question is whether they all work together once assembled into your overlay.</p>

<h1 id="recommendations">Recommendations</h1>

<p>The following list is based on my experience and is a direct summation of many many conversations I have had with customers over the years on how to test CAS and software in general.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<ul>
  <li>Keep scenarios focused. A scenario should verify one meaningful flow and fail with a clear reason.</li>
  <li>Use stable selectors. Prefer IDs, names, or test-friendly selectors over brittle CSS paths that depend on page layout.</li>
  <li>Account for timing carefully. Authentication flows involve redirects, network calls, and page transitions.</li>
  <li>Treat scenarios as deployment tests. Run via CI/CD automated pipelines.</li>
</ul>

<h1 id="read-the-fine-print">Read the Fine Print</h1>

<p>While CAS itself runs a comprehensive suite of browser-based acceptance tests, including hundreds of Puppeteer scenarios across many authentication flows, protocols, and deployment conditions, that should not be interpreted as a guarantee that every CAS overlay in the wild will behave exactly as expected. The CAS project’s own test coverage is valuable, but it cannot possibly know the unique shape of every institution’s deployment.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>Also, as with most open-source software, CAS is provided under its project license without a promise that it will perfectly satisfy every production requirement, environment, policy, or security expectation. The license used by CAS includes the usual “as is” warranty disclaimer, which is a shorthand way of indicating that you should not assume the universe has personally tested your exact configuration.</p>

<h1 id="what-about-playwright">What About Playwright?</h1>

<p>Puppeteer is a natural fit here because it is simple, direct, and already familiar to many JavaScript users. It provides a high-level API to automate Chrome and Firefox, runs headlessly by default, and is very effective for browser automation tasks. But that is of course not the only choice.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>The most obvious alternative here would be <a href="https://playwright.dev/">Playwright</a>, which is a browser automation and testing framework, with support for many browsers using a single API. In many cases, it may be the stronger technical choice, especially for projects that need richer cross-browser coverage and a more complete end-to-end testing framework out of the box. That said, the CAS overlay use case does not necessarily need all of that. The goal is to let deployers write small, readable scenarios that confirm their CAS server behaves correctly and plain JavaScript is often more than enough for that. Simple JavaScript keeps the barrier to entry low, the examples approachable, and the focus where it belongs</p>

<h1 id="need-help">Need Help?</h1>

<p>If you have questions about the contents and the topic of this blog post, or if you need additional guidance and support, feel free to <a href="/#contact-section-header">send us a note </a> and ask about consulting and support services.</p>

<div class="followit-subscribe-wrapper">
  
    <button type="button" class="followit-subscribe-toggle" onclick="
        const wrapper = this.closest('.followit-subscribe-wrapper');
        wrapper.classList.add('is-visible');
      ">
      Subscribe by email
    </button>
  

  <div class="followit--follow-form-container followit-subscribe-box " attr-a="" attr-b="" attr-c="" attr-d="" attr-e="" attr-f="">

    <form data-v-c497cbba="" action="https://api.follow.it/subscription-form/TDFQVjQzZTk2czNPRkF6RUZSUmQ0SE0wYnBnS2VkQjdZeXpsZDFQNWRETjVGc0JyL1FqL001SlFzUlVhWkpJdFZ4SUl3OEU0Sm1CK2pZUjlCbG1PcG9oNDRadjhKeldCSmlLdkpZQnRqUC8xMTJTWmxxQTFwWEpjc3FkVGI4WGx8MEJXeEV6ZTlkaUhUOXBJdFlmMXU0Q3hCQXI0anpveVlMVnNlN2VPRC9uaz0=/8" method="post">

      <div data-v-c497cbba="" class="form-preview" style="background-color: rgb(255, 255, 255); position: relative; border-style: solid; border-width: 1px; border-color: rgb(204, 204, 204);">

        <div data-v-c497cbba="" class="preview-heading">
          <h5 data-v-c497cbba="" style="font-family: Arial; font-weight: bold; color: rgb(0, 0, 0); font-size: 16px; text-align: center; text-transform: none !important;">
            Get new posts by email:
          </h5>
        </div>

        <div data-v-c497cbba="" class="preview-input-field">
          <input data-v-c497cbba="" type="email" name="email" required="" placeholder="Enter your email" spellcheck="false" style="font-family: Arial; font-weight: normal; color: rgb(0, 0, 0); font-size: 14px; text-align: center; background-color: rgb(255, 255, 255); text-transform: none !important;" />
        </div>

        <div data-v-c497cbba="" class="preview-submit-button">
          <button data-v-c497cbba="" type="submit" class="contact100-form-btn">
            Subscribe
          </button>
        </div>
      </div>
    </form>

    <a href="https://follow.it" class="powered-by-line">
      <img src="https://follow.it/images/colored-logo.svg" alt="follow.it" height="17px" />
    </a>
  </div>
</div>

<h1 id="so">So…</h1>

<p>I hope the content here was of some help to you and I am sure that both this post as well as the functionality it attempts to explain can be improved in any number of ways. If you have suggestions or ideas on how to improve this post, please feel free to reach out to us.</p>

<p><a href="https://fawnoos.com">Misagh Moayyed</a></p>]]></content><author><name>Misagh Moayyed</name></author><category term="CAS 8.0.x" /><category term="Puppeteer" /><category term="Gradle" /><category term="Getting Started" /><category term="UI/UX" /><summary type="html"><![CDATA[There are many ways to test your Apereo CAS deployment. Configuration, components, endpoints, ticket validity, protocol flows, etc. But at some point, after all the unit tests are green and the integration tests are smiling politely, you might still wonder: Can an actual user using a real browser really log in? Apereo CAS Gradle overlays attempt to assist with this question via Puppeteer and this is what this blog focuses on. Our main focus is on:]]></summary></entry><entry><title type="html">Apereo CAS - Rotating OpenID Connect Client Secrets</title><link href="https://fawnoos.com/2026/07/02/cas8-oidc-client-secret-rotation/" rel="alternate" type="text/html" title="Apereo CAS - Rotating OpenID Connect Client Secrets" /><published>2026-07-02T00:00:00+00:00</published><updated>2026-07-02T00:00:00+00:00</updated><id>https://fawnoos.com/2026/07/02/cas8-oidc-client-secret-rotation</id><content type="html" xml:base="https://fawnoos.com/2026/07/02/cas8-oidc-client-secret-rotation/"><![CDATA[<p>In the context of OpenID Connect, client secrets (though a discouraged security practice) are useful and usually boring until you lose one, accidentally commit one to GitHub, or somehow leave it inside a shell history file, etc. In OpenID Connect, confidential clients commonly authenticate to the token endpoint using a client secret. That secret may be passed using <code class="language-plaintext highlighter-rouge">client_secret_basic</code>, <code class="language-plaintext highlighter-rouge">client_secret_post</code>, or used indirectly with methods such as <code class="language-plaintext highlighter-rouge">client_secret_jwt</code>. However it is used, the secret remains a credential until it leaks to places where it should not be, or you may decide (for security reasons) to rotate it out once in a while. So just like milk, client secrets need an expiration date too.</p>

<p>CAS supports this lifecycle by allowing registered OIDC applications to have client secrets that expire. Once a secret reaches its expiration time, CAS treats it as no longer valid for token endpoint authentication. The client must then obtain a renewed secret through the registration management endpoint, using its registration access token.</p>

<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>This is what this blog focuses on. Our main focus is on:</p>

<ul>
  <li>CAS <code class="language-plaintext highlighter-rouge">8.0.x</code></li>
  <li>Java <code class="language-plaintext highlighter-rouge">25</code></li>
</ul>

<h1 id="the-flow">The Flow</h1>

<p>The flow is as such:</p>

<ol>
  <li>A client dynamically registers with CAS.</li>
  <li>CAS issues a <code class="language-plaintext highlighter-rouge">client_id</code>, <code class="language-plaintext highlighter-rouge">client_secret</code>, <code class="language-plaintext highlighter-rouge">registration_access_token</code>, and <code class="language-plaintext highlighter-rouge">registration_client_uri</code>.</li>
  <li>The client uses the secret while it is valid.</li>
  <li>When the secret expires, token requests using the old secret fail.</li>
  <li>The client calls the registration client URI using the registration access token.</li>
  <li>CAS renews the secret and returns the updated client configuration.</li>
  <li>The client starts using the new secret.</li>
</ol>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>As you likely note, this is an automated renewal path that does not require manual efforts or coordinations with security or IAM teams with extra support tickets, etc. You may instruct CAS that client applications registered this way may be associated with a client secret expiration policy:</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Generated secrets are considered expired in two weeks
</span><span class="py">cas.authn.oidc.registration.client-secret-expiration</span><span class="p">=</span><span class="s">PT14D</span>
</code></pre></div></div>

<p>A client may register itself with CAS using the OIDC dynamic registration endpoint. The registration request describes the client, its redirect URIs, supported grant types, token endpoint authentication method, contacts, signing/encryption preferences, and optionally its JSON Web Key Set.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-k</span> <span class="nt">-X</span> POST <span class="s1">'https://sso.example.org/cas/oidc/register'</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{
    ...
  }'</span>
</code></pre></div></div>

<p>CAS responds with the newly registered client metadata. The important fields are:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"client_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"client_secret"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"client_secret_expires_at"</span><span class="p">:</span><span class="w"> </span><span class="mi">1234567890</span><span class="p">,</span><span class="w">
  </span><span class="nl">"registration_client_uri"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://sso.example.org/cas/oidc/clientConfig?client_id=..."</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>The <code class="language-plaintext highlighter-rouge">client_secret</code> is the credential the client uses at the token endpoint. The <code class="language-plaintext highlighter-rouge">client_secret_expires_at</code> value tells the client when that credential should be considered expired. While the secret is valid, the client can call the token endpoint. For client_secret_basic, the client authenticates with HTTP Basic authentication:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">CLIENT_ID</span><span class="o">=</span><span class="s1">'...'</span>
<span class="nv">CLIENT_SECRET</span><span class="o">=</span><span class="s1">'...'</span>

curl <span class="nt">-k</span> <span class="nt">-X</span> POST <span class="s1">'https://sso.example.org/cas/oidc/token'</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Authorization: Basic </span><span class="si">$(</span><span class="nb">printf</span> <span class="s1">'%s:%s'</span> <span class="s2">"</span><span class="nv">$CLIENT_ID</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$CLIENT_SECRET</span><span class="s2">"</span> | <span class="nb">base64</span><span class="si">)</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/x-www-form-urlencoded'</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'grant_type=client_credentials&amp;scope=openid'</span>
</code></pre></div></div>

<p>If the secret has expired, CAS rejects the request. This is the important part where we have to renew the client secret.</p>

<h1 id="secret-renewals">Secret Renewals</h1>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>Once the secret has expired, you may use the registration management endpoint for to obtain a renewed secret. The client calls its <code class="language-plaintext highlighter-rouge">registration_client_uri</code> and authenticates using its registration access token. When CAS processes this request, it evaluates the client’s secret expiration policy. If the configured expiration window is active and the existing secret has expired, CAS renews the secret, updates the expiration timestamp, and returns the updated client configuration. The client can then retry its token request with the renewed secret.</p>

<p>A few things to point out:</p>

<p>As a recommended practice, note that clients should not wait until the exact final second to renew their secret. Clocks drift, deployments restart, etc. Clients should watch <code class="language-plaintext highlighter-rouge">client_secret_expires_at</code> and renew before the deadline. Your client applications should be designed with rotation and automatic renewals in mind if you are concerned about client secret management.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>Then, it should go without saying client applications should store secrets in a real secret store. Kubernetes secrets, Vault, AWS Secrets Manager, Azure Key Vault, and similar systems are all better choices than <code class="language-plaintext highlighter-rouge">config-final-REAL-this-time.json</code>!</p>

<p>Client secret rotation is one of those features that sounds simple until you ask how clients are supposed to get the new secret. CAS itself does present a background job or some out-of-band process to review application records and update secrets as they expire. Instead, it handles this by leaning on the OIDC registration management model: the client uses its registration access token to retrieve its updated configuration, and CAS renews the secret when expiration rules require it.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>It’s a kind of boring security feature, and that is how it should be!</p>

<h1 id="need-help">Need Help?</h1>

<p>If you have questions about the contents and the topic of this blog post, or if you need additional guidance and support, feel free to <a href="/#contact-section-header">send us a note </a> and ask about consulting and support services.</p>

<div class="followit-subscribe-wrapper">
  
    <button type="button" class="followit-subscribe-toggle" onclick="
        const wrapper = this.closest('.followit-subscribe-wrapper');
        wrapper.classList.add('is-visible');
      ">
      Subscribe by email
    </button>
  

  <div class="followit--follow-form-container followit-subscribe-box " attr-a="" attr-b="" attr-c="" attr-d="" attr-e="" attr-f="">

    <form data-v-c497cbba="" action="https://api.follow.it/subscription-form/TDFQVjQzZTk2czNPRkF6RUZSUmQ0SE0wYnBnS2VkQjdZeXpsZDFQNWRETjVGc0JyL1FqL001SlFzUlVhWkpJdFZ4SUl3OEU0Sm1CK2pZUjlCbG1PcG9oNDRadjhKeldCSmlLdkpZQnRqUC8xMTJTWmxxQTFwWEpjc3FkVGI4WGx8MEJXeEV6ZTlkaUhUOXBJdFlmMXU0Q3hCQXI0anpveVlMVnNlN2VPRC9uaz0=/8" method="post">

      <div data-v-c497cbba="" class="form-preview" style="background-color: rgb(255, 255, 255); position: relative; border-style: solid; border-width: 1px; border-color: rgb(204, 204, 204);">

        <div data-v-c497cbba="" class="preview-heading">
          <h5 data-v-c497cbba="" style="font-family: Arial; font-weight: bold; color: rgb(0, 0, 0); font-size: 16px; text-align: center; text-transform: none !important;">
            Get new posts by email:
          </h5>
        </div>

        <div data-v-c497cbba="" class="preview-input-field">
          <input data-v-c497cbba="" type="email" name="email" required="" placeholder="Enter your email" spellcheck="false" style="font-family: Arial; font-weight: normal; color: rgb(0, 0, 0); font-size: 14px; text-align: center; background-color: rgb(255, 255, 255); text-transform: none !important;" />
        </div>

        <div data-v-c497cbba="" class="preview-submit-button">
          <button data-v-c497cbba="" type="submit" class="contact100-form-btn">
            Subscribe
          </button>
        </div>
      </div>
    </form>

    <a href="https://follow.it" class="powered-by-line">
      <img src="https://follow.it/images/colored-logo.svg" alt="follow.it" height="17px" />
    </a>
  </div>
</div>

<h1 id="so">So…</h1>

<p>I hope the content here was of some help to you and I am sure that both this post as well as the functionality it attempts to explain can be improved in any number of ways. If you have suggestions or ideas on how to improve this post, please feel free to reach out to us.</p>

<p><a href="https://fawnoos.com">Misagh Moayyed</a></p>]]></content><author><name>Misagh Moayyed</name></author><category term="CAS 8.0.x" /><category term="OpenID Connect" /><category term="OAuth" /><summary type="html"><![CDATA[In the context of OpenID Connect, client secrets (though a discouraged security practice) are useful and usually boring until you lose one, accidentally commit one to GitHub, or somehow leave it inside a shell history file, etc. In OpenID Connect, confidential clients commonly authenticate to the token endpoint using a client secret. That secret may be passed using client_secret_basic, client_secret_post, or used indirectly with methods such as client_secret_jwt. However it is used, the secret remains a credential until it leaks to places where it should not be, or you may decide (for security reasons) to rotate it out once in a while. So just like milk, client secrets need an expiration date too.]]></summary></entry><entry><title type="html">Apereo CAS - Gradle’s Isolated Projects</title><link href="https://fawnoos.com/2026/06/26/cas8-gradle-isolated-projects/" rel="alternate" type="text/html" title="Apereo CAS - Gradle’s Isolated Projects" /><published>2026-06-26T00:00:00+00:00</published><updated>2026-06-26T00:00:00+00:00</updated><id>https://fawnoos.com/2026/06/26/cas8-gradle-isolated-projects</id><content type="html" xml:base="https://fawnoos.com/2026/06/26/cas8-gradle-isolated-projects/"><![CDATA[<p>Apereo CAS has used Gradle as its main primary build tool for a long time now, and given the size and complexity of the project and its architectural composure, the build has sort of developed its own personality. Small projects that only carry a few modules and Gradle tasks with some shared covensions and dependency management magic are typically quite fine. Over time and as the project grows, asking and waiting for the build to finish its <em>configuration</em> can have you respond with the emotional range of a steam engine!</p>

<p>Apereo CAS of course is not exactly your typical “hello world” application. It is a large, modular, identity and single sign-on platform with many moving parts, integrations, extensions, test fixtures, deployment styles, etc. Build performance matters a lot for developers, contributors, release engineering, IDEs and CLIs.</p>

<p>So, CAS has been moving toward <a href="https://docs.gradle.org/current/userguide/isolated_projects.html">Gradle’s Isolated Projects feature</a>. This is what this blog focuses on. Our main focus is on:</p>

<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<ul>
  <li>CAS <code class="language-plaintext highlighter-rouge">8.0.x</code></li>
  <li>Java <code class="language-plaintext highlighter-rouge">25</code></li>
  <li>Gradle <code class="language-plaintext highlighter-rouge">9.6.x</code></li>
</ul>

<h1 id="gradle-isolated-projects">Gradle Isolated Projects</h1>

<p><a href="https://docs.gradle.org/current/userguide/isolated_projects.html">Gradle’s Isolated Projects feature</a> is an experimental build feature designed to improve scalability and performance in large multi-project builds. The idea is that each project (submodule) in a multi-project Gradle build should configure itself without casually reaching across the field and poking at another project’s configuration and state. In other words, project <code class="language-plaintext highlighter-rouge">:cas-server-core-something</code> should not be rummaging through project <code class="language-plaintext highlighter-rouge">:cas-server-support-something-else</code> during configuration time.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>By enforcing stronger boundaries between projects, Gradle can unlock two very important capabilities:</p>

<ol>
  <li>Parallel project configuration</li>
  <li>Finer-grained caching of configuration results</li>
</ol>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>That means Gradle can do more work safely in parallel, cache configuration work more effectively, and avoid reconfiguring the universe every time one tiny corner of the build changes. This is absolutely a critical advantage for a project like Apereo CAS. With a build of its size, configuration time can become expensive. Isolated Projects helps CAS move toward a cleaner and more scalable build architecture by encouraging each module to declare what it needs, avoid hidden cross-project coupling, and behave more like a well-mannered citizen.</p>

<h1 id="gradle-configuration-cache">Gradle Configuration Cache</h1>

<p>The Gradle configuration cache is already an important part of making repeated builds faster. Isolated Projects takes that idea further by making configuration results more granular. When projects are truly isolated, Gradle has a better chance of caching configuration work at the project level and invalidating only what actually changed. That is a big deal for CAS. A change in one module should not necessarily require Gradle to re-evaluate every distant relative in the build tree.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<h1 id="spring-boot-plugin-wrinkle">Spring Boot Plugin Wrinkle</h1>

<p>In the Spring Boot Gradle plugin, there is an <a href="https://github.com/spring-projects/spring-boot/issues/43755">open issue</a> tracking support for Gradle’s Isolated Projects feature. The issue notes that some parts of the Spring Boot Gradle plugin are not yet compatible with isolated projects, particularly around usage of resolvable dependencies for layered jars and GraalVM metadata detection. This matters to CAS because CAS builds on Spring Boot and uses Gradle heavily. When the underlying ecosystem is still adapting to isolated projects, large projects like CAS need to account for those limitations carefully.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>While we wait for Spring Boot to fix the original problem upstream and provide official support, the problem has for now been solved by providing an override fix for the problematic <code class="language-plaintext highlighter-rouge">ResolvedDependencies</code> component from Spring Boot.</p>

<h1 id="going-forward">Going Forward</h1>

<p>CAS has always had to balance a broad feature set with maintainability and the build system is part of that story. As CAS continues to grow, the build must become more modular, more cacheable, more parallel, and more predictable. Gradle’s Isolated Projects feature helps move the project in that direction.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>It’s worth pointing out that this capability only applies to the CAS project’s own build system. WAR overlays that build with Gradle are not affected and remain as they were.</p>

<h1 id="need-help">Need Help?</h1>

<p>If you have questions about the contents and the topic of this blog post, or if you need additional guidance and support, feel free to <a href="/#contact-section-header">send us a note </a> and ask about consulting and support services.</p>

<div class="followit-subscribe-wrapper">
  
    <button type="button" class="followit-subscribe-toggle" onclick="
        const wrapper = this.closest('.followit-subscribe-wrapper');
        wrapper.classList.add('is-visible');
      ">
      Subscribe by email
    </button>
  

  <div class="followit--follow-form-container followit-subscribe-box " attr-a="" attr-b="" attr-c="" attr-d="" attr-e="" attr-f="">

    <form data-v-c497cbba="" action="https://api.follow.it/subscription-form/TDFQVjQzZTk2czNPRkF6RUZSUmQ0SE0wYnBnS2VkQjdZeXpsZDFQNWRETjVGc0JyL1FqL001SlFzUlVhWkpJdFZ4SUl3OEU0Sm1CK2pZUjlCbG1PcG9oNDRadjhKeldCSmlLdkpZQnRqUC8xMTJTWmxxQTFwWEpjc3FkVGI4WGx8MEJXeEV6ZTlkaUhUOXBJdFlmMXU0Q3hCQXI0anpveVlMVnNlN2VPRC9uaz0=/8" method="post">

      <div data-v-c497cbba="" class="form-preview" style="background-color: rgb(255, 255, 255); position: relative; border-style: solid; border-width: 1px; border-color: rgb(204, 204, 204);">

        <div data-v-c497cbba="" class="preview-heading">
          <h5 data-v-c497cbba="" style="font-family: Arial; font-weight: bold; color: rgb(0, 0, 0); font-size: 16px; text-align: center; text-transform: none !important;">
            Get new posts by email:
          </h5>
        </div>

        <div data-v-c497cbba="" class="preview-input-field">
          <input data-v-c497cbba="" type="email" name="email" required="" placeholder="Enter your email" spellcheck="false" style="font-family: Arial; font-weight: normal; color: rgb(0, 0, 0); font-size: 14px; text-align: center; background-color: rgb(255, 255, 255); text-transform: none !important;" />
        </div>

        <div data-v-c497cbba="" class="preview-submit-button">
          <button data-v-c497cbba="" type="submit" class="contact100-form-btn">
            Subscribe
          </button>
        </div>
      </div>
    </form>

    <a href="https://follow.it" class="powered-by-line">
      <img src="https://follow.it/images/colored-logo.svg" alt="follow.it" height="17px" />
    </a>
  </div>
</div>

<h1 id="so">So…</h1>

<p>I hope the content here was of some help to you and I am sure that both this post as well as the functionality it attempts to explain can be improved in any number of ways. If you have suggestions or ideas on how to improve this post, please feel free to reach out to us.</p>

<p><a href="https://fawnoos.com">Misagh Moayyed</a></p>]]></content><author><name>Misagh Moayyed</name></author><category term="CAS 8.0.x" /><category term="Gradle" /><category term="Spring Boot" /><summary type="html"><![CDATA[Apereo CAS has used Gradle as its main primary build tool for a long time now, and given the size and complexity of the project and its architectural composure, the build has sort of developed its own personality. Small projects that only carry a few modules and Gradle tasks with some shared covensions and dependency management magic are typically quite fine. Over time and as the project grows, asking and waiting for the build to finish its configuration can have you respond with the emotional range of a steam engine!]]></summary></entry><entry><title type="html">Apereo CAS - SSO Session Revocation via Attributes</title><link href="https://fawnoos.com/2026/06/24/cas8-attr-based-session-revocation/" rel="alternate" type="text/html" title="Apereo CAS - SSO Session Revocation via Attributes" /><published>2026-06-24T00:00:00+00:00</published><updated>2026-06-24T00:00:00+00:00</updated><id>https://fawnoos.com/2026/06/24/cas8-attr-based-session-revocation</id><content type="html" xml:base="https://fawnoos.com/2026/06/24/cas8-attr-based-session-revocation/"><![CDATA[<p>There are scenarios and use cases when an administrator or some external authority decides that a user’s current authentication and SSO state should no longer be trusted. Maybe their account was disabled and re-enabled, or maybe a risk engine detected something suspicious. Whatever the case may be, in Apereo CAS, there is a subtle and useful mechanism that helps with this exact class of problem: SSO session revocation based on an attribute.</p>

<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>This is what this blog focuses on. Our main focus is on:</p>

<ul>
  <li>CAS <code class="language-plaintext highlighter-rouge">8.0.x</code></li>
  <li>Java <code class="language-plaintext highlighter-rouge">25</code></li>
</ul>

<h1 id="setup">Setup</h1>

<p>The key idea is simple:</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">cas.sso.revocation-attribute-name</span><span class="p">=</span><span class="s">...</span>
</code></pre></div></div>

<p>CAS can be configured to look at a specific attribute on the authenticated principal. The value of that attribute is expected to represent a Unix epoch timestamp in seconds. This timestamp becomes a demarcation point. Any CAS authentication event created before that time is considered revoked for SSO purposes. The attribute basically says: <em>“Any SSO authentication older than this timestamp should no longer count.”</em>.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>When the user is sent back to CAS and CAS evaluates the existing SSO session, it compares the creation time of the original authentication with the timestamp found in the configured attribute. If the authentication was created before that timestamp, CAS treats that authentication as revoked. The existing SSO state is ignored, and CAS proceeds as though that SSO session is no longer usable for that request. This is especially useful when the revocation decision lives outside CAS.</p>

<h1 id="reads--updates">Reads &amp; Updates</h1>

<p>One important detail here to point out is that CAS only reads this attribute. CAS does not modify or update it. CAS does not write back a new revocation timestamp when something happens. The attribute is an external signal that CAS consumes. That means the system of record is somewhere else. It might be LDAP, a database, an attribute repository, or a custom integration. Whatever owns the attribute is responsible for updating it when the user should be forced through CAS again.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>In other words, the CAS logic simply is asking: <em>“Does this principal have a revocation timestamp, and is my current SSO authentication older than that?”</em></p>

<h1 id="slo">SLO</h1>

<p>Also worth pointing out that this mechanism should not be confused with Single Logout, or SLO. If SSO is disregarded using this approach, CAS does not notify applications and it does not (and cannot anyway) destroy application sessions. The setting only affects what happens the next time the user is sent to CAS.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>This means that if the user already has an active session inside an application, that application session continues to exist unless the application itself expires it, invalidates it, or participates in some other logout mechanism. CAS is not retroactively reaching into those applications and removing their sessions.</p>

<h1 id="utility--admin-logouts">Utility &amp; Admin Logouts</h1>

<p>The common administrative logout feature is deceptively hard, which should be no surprise for you dear Reader. An administrator might want to click <em>Log out user everywhere</em>. In a perfect world, that would immediately terminate every session across every application, browser, device, proxy, token, and integration. However, in the real world that depends on protocol support, application behavior, session management, logout callbacks, browser state, network timing, and whether every participating application has implemented logout correctly.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>So the revocation-attribute approach gives you a practical and centralized control point for SSO. If your applications have an exceptionally long session timeout and the user never sees the need to be sent to CAS to re-authenticate again, or if you want immediate logouts with SLO to follow, this option may not be the right fit for you.</p>

<h1 id="caching">Caching</h1>

<p>There is one big operational detail that should not be overlooked: CAS deployments commonly cache principal attributes for performance and availability reasons. Attribute repositories in particular introduce some form of caching or delayed visibility. If you depend on this feature for administrative logout or security-sensitive revocation, you need to understand where your attributes come from and how (if at all) they are cached, how long they are cached, and what it takes for CAS to see the most recent value.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<h1 id="need-help">Need Help?</h1>

<p>If you have questions about the contents and the topic of this blog post, or if you need additional guidance and support, feel free to <a href="/#contact-section-header">send us a note </a> and ask about consulting and support services.</p>

<div class="followit-subscribe-wrapper">
  
    <button type="button" class="followit-subscribe-toggle" onclick="
        const wrapper = this.closest('.followit-subscribe-wrapper');
        wrapper.classList.add('is-visible');
      ">
      Subscribe by email
    </button>
  

  <div class="followit--follow-form-container followit-subscribe-box " attr-a="" attr-b="" attr-c="" attr-d="" attr-e="" attr-f="">

    <form data-v-c497cbba="" action="https://api.follow.it/subscription-form/TDFQVjQzZTk2czNPRkF6RUZSUmQ0SE0wYnBnS2VkQjdZeXpsZDFQNWRETjVGc0JyL1FqL001SlFzUlVhWkpJdFZ4SUl3OEU0Sm1CK2pZUjlCbG1PcG9oNDRadjhKeldCSmlLdkpZQnRqUC8xMTJTWmxxQTFwWEpjc3FkVGI4WGx8MEJXeEV6ZTlkaUhUOXBJdFlmMXU0Q3hCQXI0anpveVlMVnNlN2VPRC9uaz0=/8" method="post">

      <div data-v-c497cbba="" class="form-preview" style="background-color: rgb(255, 255, 255); position: relative; border-style: solid; border-width: 1px; border-color: rgb(204, 204, 204);">

        <div data-v-c497cbba="" class="preview-heading">
          <h5 data-v-c497cbba="" style="font-family: Arial; font-weight: bold; color: rgb(0, 0, 0); font-size: 16px; text-align: center; text-transform: none !important;">
            Get new posts by email:
          </h5>
        </div>

        <div data-v-c497cbba="" class="preview-input-field">
          <input data-v-c497cbba="" type="email" name="email" required="" placeholder="Enter your email" spellcheck="false" style="font-family: Arial; font-weight: normal; color: rgb(0, 0, 0); font-size: 14px; text-align: center; background-color: rgb(255, 255, 255); text-transform: none !important;" />
        </div>

        <div data-v-c497cbba="" class="preview-submit-button">
          <button data-v-c497cbba="" type="submit" class="contact100-form-btn">
            Subscribe
          </button>
        </div>
      </div>
    </form>

    <a href="https://follow.it" class="powered-by-line">
      <img src="https://follow.it/images/colored-logo.svg" alt="follow.it" height="17px" />
    </a>
  </div>
</div>

<h1 id="so">So…</h1>

<p>I hope the content here was of some help to you and I am sure that both this post as well as the functionality it attempts to explain can be improved in any number of ways. If you have suggestions or ideas on how to improve this post, please feel free to reach out to us.</p>

<p><a href="https://fawnoos.com">Misagh Moayyed</a></p>]]></content><author><name>Misagh Moayyed</name></author><category term="CAS 8.0.x" /><summary type="html"><![CDATA[There are scenarios and use cases when an administrator or some external authority decides that a user’s current authentication and SSO state should no longer be trusted. Maybe their account was disabled and re-enabled, or maybe a risk engine detected something suspicious. Whatever the case may be, in Apereo CAS, there is a subtle and useful mechanism that helps with this exact class of problem: SSO session revocation based on an attribute.]]></summary></entry><entry><title type="html">Apereo CAS - Recognizing MFA from External Identity Providers</title><link href="https://fawnoos.com/2026/06/16/cas8-delegated-authn-with-mfa/" rel="alternate" type="text/html" title="Apereo CAS - Recognizing MFA from External Identity Providers" /><published>2026-06-16T00:00:00+00:00</published><updated>2026-06-16T00:00:00+00:00</updated><id>https://fawnoos.com/2026/06/16/cas8-delegated-authn-with-mfa</id><content type="html" xml:base="https://fawnoos.com/2026/06/16/cas8-delegated-authn-with-mfa/"><![CDATA[<p>This post looks at a scenario where Apereo CAS is acting as a proxy to Microsoft Entra ID (or any external identity provider in the general sense), Entra ID performing MFA, and CAS later deciding that it too should recognize MFA on the return trip to elevate and mark its own SSO session accordingly. Our primary goal is to achieve the following:</p>

<blockquote>
  <p>When a user signs in through an Entra ID-protected service and Entra ID has already performed MFA, the resulting CAS SSO session should recognize that fact and not prompt the user for MFA for subsequent application requests that may not make it over to Entra ID.</p>
</blockquote>

<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>Our main focus is on:</p>

<ul>
  <li>CAS <code class="language-plaintext highlighter-rouge">8.0.x</code></li>
  <li>Java <code class="language-plaintext highlighter-rouge">25</code></li>
</ul>

<h1 id="the-setup">The Setup</h1>

<p>Our environment has CAS integrated with Duo Security for multifactor authentication already. The specifics of this are not quite relevant yet; CAS normally triggers Duo based on a principal attribute, specifically an LDAP entitlement. CAS is also acting as a proxy to Microsoft Entra ID for selected services. The CAS-to-Entra integration is done using SAML2. For those services, Entra ID is responsible for authentication and Entra ID itself is configured to trigger MFA through its own machinery.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>This is intentional and not too uncommon, specially when you’re in pilot mode and/or you selectively want to hand off authentication to an external identity provider for some users and applications and letting that handle MFA for you, while a whole separate group of applications and users would be interacting with CAS directly and letting it handle MFA for you.</p>

<p>MFA here. MFA there. We have MFA everywhere!</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>The problem however is that when the user accesses another CAS-protected service, one that is not exclusively proxied over to Entra ID after having logged into Entra ID once with MFA, CAS sees the SSO session, skips username and password but continues to prompt for MFA because the very fact that MFA did happen in the first original attempt when CAS SSO was established was ignored. At this point, from the CAS point of view MFA needs to happen again (because the rules say user/application qualifies), and Duo appears again.</p>

<p>So, once CAS establishes an SSO session based on the Entra ID login, it needs to know that the authentication event already included MFA. Otherwise, when the user accesses another service that normally requires Duo, CAS has no reason to believe MFA has already happened.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>The missing piece is authentication context. CAS needs a signal that says: “This authentication event satisfied MFA.” and conveniently, Entra ID can provide such a signal.</p>

<h1 id="authn-method-references">AuthN Method References</h1>

<p>In the SAML response from Entra ID, the following attribute is available:</p>

<div class="highlight">
    <div class="xml-code-block">
      <pre><code data-lang="xml">
&amp;lt;Attribute Name=&amp;quot;http://schemas.microsoft.com/claims/authnmethodsreferences&amp;quot;&amp;gt;
  &amp;lt;AttributeValue&amp;gt;http://schemas.microsoft.com/claims/multipleauthn&amp;lt;/AttributeValue&amp;gt;
&amp;lt;/Attribute&amp;gt;
</code></pre>
    </div>
</div>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>That attribute tells us that multiple authentication methods were used. In this scenario, that means Entra ID has already performed MFA. Now we need to translate that Entra ID claim into something CAS can use during its own authentication processing.Specifically, we want CAS to add an authentication attribute that indicates the Duo MFA context has already been satisfied.</p>

<h1 id="post-processing-authentications">Post Processing Authentications</h1>

<p>CAS supports authentication post-processing using Groovy. This gives us a hook into the authentication flow where we can inspect the principal attributes produced by the delegated SAML authentication event and add additional attributes to the CAS authentication result. To do this, you need the following pieces:</p>

<ul>
  <li>First, make sure the CAS scripting module is included in your build.</li>
  <li>Then configure CAS to use a Groovy authentication post-processor:</li>
</ul>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">cas.authn.core.engine.groovy-post-processor.location</span><span class="p">=</span><span class="s">file:/path/to/GroovyAuthnPostProcessor.groovy</span>
</code></pre></div></div>

<ul>
  <li>…and now the script itself:</li>
</ul>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">org.apereo.cas.authentication.principal.*</span>

<span class="kt">def</span> <span class="nf">run</span><span class="o">(</span><span class="n">Object</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
    <span class="kt">def</span> <span class="o">(</span><span class="n">builder</span><span class="o">,</span> <span class="n">transaction</span><span class="o">,</span> <span class="n">logger</span><span class="o">)</span> <span class="o">=</span> <span class="n">args</span>

    <span class="kt">def</span> <span class="n">AUTHN_METHODS_REFERENCES</span> <span class="o">=</span>
        <span class="s1">'http://schemas.microsoft.com/claims/authnmethodsreferences'</span>
    <span class="kt">def</span> <span class="n">MULTIPLE_AUTHN</span> <span class="o">=</span>
        <span class="s1">'http://schemas.microsoft.com/claims/multipleauthn'</span>

    <span class="n">logger</span><span class="o">.</span><span class="na">debug</span><span class="o">(</span><span class="s2">"Current principal attributes: ${builder.principal.attributes}"</span><span class="o">)</span>

    <span class="kt">def</span> <span class="n">authnMethods</span> <span class="o">=</span>
        <span class="n">builder</span><span class="o">.</span><span class="na">principal</span><span class="o">.</span><span class="na">attributes</span><span class="o">[</span><span class="n">AUTHN_METHODS_REFERENCES</span><span class="o">]</span> <span class="o">?:</span> <span class="o">[]</span>

    <span class="k">if</span> <span class="o">(</span><span class="n">MULTIPLE_AUTHN</span> <span class="k">in</span> <span class="n">authnMethods</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">logger</span><span class="o">.</span><span class="na">debug</span><span class="o">(</span><span class="s1">'Entra ID reports MFA was satisfied; adding CAS MFA context'</span><span class="o">)</span>
        <span class="n">builder</span><span class="o">.</span><span class="na">addAttribute</span><span class="o">(</span><span class="s1">'authnContextClass'</span><span class="o">,</span> <span class="o">[</span><span class="s1">'mfa-duo'</span><span class="o">])</span>
    <span class="o">}</span>

    <span class="kc">true</span>
<span class="o">}</span>

<span class="kt">def</span> <span class="nf">supports</span><span class="o">(</span><span class="n">Object</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
    <span class="kt">def</span> <span class="o">(</span><span class="n">credential</span><span class="o">,</span> <span class="n">logger</span><span class="o">)</span> <span class="o">=</span> <span class="n">args</span>
    <span class="n">logger</span><span class="o">.</span><span class="na">debug</span><span class="o">(</span><span class="s2">"Current credential is ${credential}"</span><span class="o">)</span>
    <span class="n">credential</span> <span class="k">instanceof</span> <span class="n">ClientCredential</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Note that the script only applies to delegated authentication credentials. That matters because we only want this logic to run for delegated logins, such as the SAML2 login coming back from Entra ID. We do not want to inject the MFA context into every authentication attempt.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>Happy Coding!</p>

<h1 id="need-help">Need Help?</h1>

<p>If you have questions about the contents and the topic of this blog post, or if you need additional guidance and support, feel free to <a href="/#contact-section-header">send us a note </a> and ask about consulting and support services.</p>

<div class="followit-subscribe-wrapper">
  
    <button type="button" class="followit-subscribe-toggle" onclick="
        const wrapper = this.closest('.followit-subscribe-wrapper');
        wrapper.classList.add('is-visible');
      ">
      Subscribe by email
    </button>
  

  <div class="followit--follow-form-container followit-subscribe-box " attr-a="" attr-b="" attr-c="" attr-d="" attr-e="" attr-f="">

    <form data-v-c497cbba="" action="https://api.follow.it/subscription-form/TDFQVjQzZTk2czNPRkF6RUZSUmQ0SE0wYnBnS2VkQjdZeXpsZDFQNWRETjVGc0JyL1FqL001SlFzUlVhWkpJdFZ4SUl3OEU0Sm1CK2pZUjlCbG1PcG9oNDRadjhKeldCSmlLdkpZQnRqUC8xMTJTWmxxQTFwWEpjc3FkVGI4WGx8MEJXeEV6ZTlkaUhUOXBJdFlmMXU0Q3hCQXI0anpveVlMVnNlN2VPRC9uaz0=/8" method="post">

      <div data-v-c497cbba="" class="form-preview" style="background-color: rgb(255, 255, 255); position: relative; border-style: solid; border-width: 1px; border-color: rgb(204, 204, 204);">

        <div data-v-c497cbba="" class="preview-heading">
          <h5 data-v-c497cbba="" style="font-family: Arial; font-weight: bold; color: rgb(0, 0, 0); font-size: 16px; text-align: center; text-transform: none !important;">
            Get new posts by email:
          </h5>
        </div>

        <div data-v-c497cbba="" class="preview-input-field">
          <input data-v-c497cbba="" type="email" name="email" required="" placeholder="Enter your email" spellcheck="false" style="font-family: Arial; font-weight: normal; color: rgb(0, 0, 0); font-size: 14px; text-align: center; background-color: rgb(255, 255, 255); text-transform: none !important;" />
        </div>

        <div data-v-c497cbba="" class="preview-submit-button">
          <button data-v-c497cbba="" type="submit" class="contact100-form-btn">
            Subscribe
          </button>
        </div>
      </div>
    </form>

    <a href="https://follow.it" class="powered-by-line">
      <img src="https://follow.it/images/colored-logo.svg" alt="follow.it" height="17px" />
    </a>
  </div>
</div>

<h1 id="so">So…</h1>

<p>I hope the content here was of some help to you and I am sure that both this post as well as the functionality it attempts to explain can be improved in any number of ways. If you have suggestions or ideas on how to improve this post, please feel free to reach out to us.</p>

<p><a href="https://fawnoos.com">Misagh Moayyed</a></p>]]></content><author><name>Misagh Moayyed</name></author><category term="CAS 8.0.x" /><category term="MFA" /><summary type="html"><![CDATA[This post looks at a scenario where Apereo CAS is acting as a proxy to Microsoft Entra ID (or any external identity provider in the general sense), Entra ID performing MFA, and CAS later deciding that it too should recognize MFA on the return trip to elevate and mark its own SSO session accordingly. Our primary goal is to achieve the following:]]></summary></entry><entry><title type="html">Apereo CAS - Managing Service Definitions with Google Cloud Storage</title><link href="https://fawnoos.com/2026/06/14/cas8-gcp-storage-service-registry/" rel="alternate" type="text/html" title="Apereo CAS - Managing Service Definitions with Google Cloud Storage" /><published>2026-06-14T00:00:00+00:00</published><updated>2026-06-14T00:00:00+00:00</updated><id>https://fawnoos.com/2026/06/14/cas8-gcp-storage-service-registry</id><content type="html" xml:base="https://fawnoos.com/2026/06/14/cas8-gcp-storage-service-registry/"><![CDATA[<p>Application registration records and service definitions need a proper storage backend to be recognized by CAS. You may start with JSON files on disk, perhaps lovingly copied into the right directory by hand, perhaps with a shell script that has seen things no shell script should ever see. Eventually, the deployment grows larger. As nodes multiply, it becomes more challenging to keep files in sync and/or apply updates.</p>

<p>One attractive option to store records centrally, specially if your deployment environment is backed by Google, is Google Cloud Storage-backed CAS service registry. Instead of treating service registry files as local artifacts tied to a single node with fancy replication techniques, CAS can store and retrieve them from GCS buckets. Applications may be grouped by type and stored in their own buckets, with those buckets managed or created automatically depending on configuration. This helps your CAS deployment to move from “that directory on that one server” to a cloud-managed storage layer with fewer late-night <code class="language-plaintext highlighter-rouge">scp</code> rituals and that is the target for this blog post.</p>

<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>Our main focus is on:</p>

<ul>
  <li>CAS <code class="language-plaintext highlighter-rouge">8.0.x</code></li>
  <li>Java <code class="language-plaintext highlighter-rouge">25</code></li>
</ul>

<h1 id="why">Why</h1>

<p>The Google Cloud Storage service registry allows CAS to store service definitions in GCS. This is especially useful when CAS is deployed in environments where local storage is either temporary, shared awkwardly, or simply not the right abstraction.</p>

<p>Typical use cases include:</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<ul>
  <li>CAS deployments running in Google Cloud.</li>
  <li>Clustered CAS nodes that need a shared source of truth for service definitions.</li>
  <li>Cloud-native deployments where local disk is disposable.</li>
  <li>Environments where service definitions should be externalized from the application runtime.</li>
  <li>…</li>
</ul>

<p>The basic idea is straightforward: CAS reads and writes service registry objects using Google Cloud Storage APIs. The service definitions remain JSON-based, but their persistence layer becomes GCS.</p>

<h1 id="how">How</h1>

<p>Once <a href="https://apereo.github.io/cas/development/services/GCP-Storage-Service-Management.html">the extension module is included</a> in your build, CAS can be configured with the relevant Spring Cloud GCP Storage settings. The integration is backed by the Spring Cloud GCP project, which provides the foundation for connecting Spring-based applications to Google Cloud APIs. CAS builds on top of that support so the service registry can interact with Google Cloud Storage in a familiar Spring-oriented way.</p>

<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>Before CAS can speak to Google Cloud Storage, it needs credentials. The recommended approach is to use Google Cloud Application Default Credentials. This allows the runtime environment to provide credentials in a standard way, whether CAS is running locally, in a container, in Google Cloud, or in another deployment model where credentials are mounted or otherwise made available.</p>

<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>Optionally, you may also need:</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Not needed; enabled by default...
</span><span class="py">spring.cloud.gcp.storage.enabled</span><span class="p">=</span><span class="s">true</span>

<span class="py">spring.cloud.gcp.storage.project-id</span><span class="p">=</span><span class="s">apereo-cas-gcp</span>
</code></pre></div></div>

<p>The important bit is that CAS itself does not need to reinvent authentication to Google Cloud. It delegates that responsibility to the Google Cloud client libraries and Spring Cloud GCP integration so you don’t have to end up reading IAM documentation at 2:17 AM while questioning earlier life choices.</p>

<h1 id="change-management">Change Management</h1>

<p>CAs typically refreshes its copy of service definitions using one (or a combination) of the following options:</p>

<ol>
  <li>A background job that runs on a schedule to periodically fetch a new copy of the applications.</li>
  <li>An application definition that is expired in the CAS locale cache can be fetched again on demand and cached again.</li>
</ol>

<p>The scheduler, enabled by default, can be tuned via:</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Enable auto-initialization from local JSON service files...
</span><span class="py">cas.service-registry.core.init-from-json</span><span class="p">=</span><span class="s">true</span>

<span class="py">cas.service-registry.schedule.repeat-interval</span><span class="p">=</span><span class="s">PT2S</span>
<span class="py">cas.service-registry.schedule.start-delay</span><span class="p">=</span><span class="s">PT2S</span>
</code></pre></div></div>

<p>Local caching, on by default, can be tuned via:</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">cas.service-registry.cache.duration</span><span class="p">=</span><span class="s">PT60S</span>
<span class="py">cas.service-registry.cache.cache-size</span><span class="p">=</span><span class="s">100</span>
</code></pre></div></div>

<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>In this particular case, CAS can also, optionally, listen for Google Cloud Storage object-change events through Google Pub/Sub. This allows the GCS-backed service registry to react when service definitions or related registry objects are created, updated, or deleted. Instead of relying only on restarts, polling, or manual refreshes, CAS nodes can receive notifications and refresh or invalidate local registry state accordingly. When a registry object changes, Pub/Sub can help propagate that change to running nodes with lower latency and less operational overhead.</p>

<p>You should note that CAS consumes messages from the configured Pub/Sub subscription, but it does not create the surrounding infrastructure for you.  The Cloud Storage bucket notification configuration must be set up by you in advance. With Pub/Sub integration, changes can flow through a more event-driven path:</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<ol>
  <li>A service definition is created, updated, or deleted in GCS.</li>
  <li>Google Cloud Storage emits an object-change event such as <code class="language-plaintext highlighter-rouge">OBJECT_FINALIZE</code> or <code class="language-plaintext highlighter-rouge">OBJECT_DELETE</code>.</li>
  <li>Pub/Sub receives the notification.</li>
  <li>CAS consumes the message.</li>
  <li>CAS refreshes or invalidates local registry state as needed.</li>
</ol>

<h1 id="need-help">Need Help?</h1>

<p>If you have questions about the contents and the topic of this blog post, or if you need additional guidance and support, feel free to <a href="/#contact-section-header">send us a note </a> and ask about consulting and support services.</p>

<div class="followit-subscribe-wrapper">
  
    <button type="button" class="followit-subscribe-toggle" onclick="
        const wrapper = this.closest('.followit-subscribe-wrapper');
        wrapper.classList.add('is-visible');
      ">
      Subscribe by email
    </button>
  

  <div class="followit--follow-form-container followit-subscribe-box " attr-a="" attr-b="" attr-c="" attr-d="" attr-e="" attr-f="">

    <form data-v-c497cbba="" action="https://api.follow.it/subscription-form/TDFQVjQzZTk2czNPRkF6RUZSUmQ0SE0wYnBnS2VkQjdZeXpsZDFQNWRETjVGc0JyL1FqL001SlFzUlVhWkpJdFZ4SUl3OEU0Sm1CK2pZUjlCbG1PcG9oNDRadjhKeldCSmlLdkpZQnRqUC8xMTJTWmxxQTFwWEpjc3FkVGI4WGx8MEJXeEV6ZTlkaUhUOXBJdFlmMXU0Q3hCQXI0anpveVlMVnNlN2VPRC9uaz0=/8" method="post">

      <div data-v-c497cbba="" class="form-preview" style="background-color: rgb(255, 255, 255); position: relative; border-style: solid; border-width: 1px; border-color: rgb(204, 204, 204);">

        <div data-v-c497cbba="" class="preview-heading">
          <h5 data-v-c497cbba="" style="font-family: Arial; font-weight: bold; color: rgb(0, 0, 0); font-size: 16px; text-align: center; text-transform: none !important;">
            Get new posts by email:
          </h5>
        </div>

        <div data-v-c497cbba="" class="preview-input-field">
          <input data-v-c497cbba="" type="email" name="email" required="" placeholder="Enter your email" spellcheck="false" style="font-family: Arial; font-weight: normal; color: rgb(0, 0, 0); font-size: 14px; text-align: center; background-color: rgb(255, 255, 255); text-transform: none !important;" />
        </div>

        <div data-v-c497cbba="" class="preview-submit-button">
          <button data-v-c497cbba="" type="submit" class="contact100-form-btn">
            Subscribe
          </button>
        </div>
      </div>
    </form>

    <a href="https://follow.it" class="powered-by-line">
      <img src="https://follow.it/images/colored-logo.svg" alt="follow.it" height="17px" />
    </a>
  </div>
</div>

<h1 id="so">So…</h1>

<p>I hope the content here was of some help to you and I am sure that both this post as well as the functionality it attempts to explain can be improved in any number of ways. If you have suggestions or ideas on how to improve this post, please feel free to reach out to us.</p>

<p><a href="https://fawnoos.com">Misagh Moayyed</a></p>]]></content><author><name>Misagh Moayyed</name></author><category term="CAS 8.0.x" /><category term="Service Integrations" /><summary type="html"><![CDATA[Application registration records and service definitions need a proper storage backend to be recognized by CAS. You may start with JSON files on disk, perhaps lovingly copied into the right directory by hand, perhaps with a shell script that has seen things no shell script should ever see. Eventually, the deployment grows larger. As nodes multiply, it becomes more challenging to keep files in sync and/or apply updates.]]></summary></entry><entry><title type="html">Apereo CAS - Spring Boot Admin Integration</title><link href="https://fawnoos.com/2026/06/12/cas8-springboot-admin-server/" rel="alternate" type="text/html" title="Apereo CAS - Spring Boot Admin Integration" /><published>2026-06-12T00:00:00+00:00</published><updated>2026-06-12T00:00:00+00:00</updated><id>https://fawnoos.com/2026/06/12/cas8-springboot-admin-server</id><content type="html" xml:base="https://fawnoos.com/2026/06/12/cas8-springboot-admin-server/"><![CDATA[<p>There comes a time in every CAS deployment when you look at your running CAS server and think:</p>

<blockquote>
  <p>“This is great. It authenticates people, speaks protocols from three different geological eras, exposes actuator endpoints, emits logs, answers health checks, and probably knows what I had for breakfast. But can I see all of that in a shiny dashboard?”</p>
</blockquote>

<p>Yes! In this post, we will look at how to run Spring Boot Admin Server directly inside Apereo CAS, how to configure CAS as both the admin server and the admin client, and how to make the UI feel CAS-flavored.</p>

<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>Our main focus is on:</p>

<ul>
  <li>CAS <code class="language-plaintext highlighter-rouge">8.0.x</code></li>
  <li>Java <code class="language-plaintext highlighter-rouge">25</code></li>
</ul>

<h2 id="overview">Overview</h2>

<p>The goal is to have Apereo CAS host the Spring Boot Admin Server under a dedicated path, such as <code class="language-plaintext highlighter-rouge">/sba</code>. The arrangement looks roughly like this:</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Browser
  |
  |  https://cas.example.org/sba
  v
Apereo CAS
  |
  |-- Spring Boot Admin Server
  |
  |-- Spring Boot Admin Client
  |
  |-- Spring Boot Actuator Endpoints
</code></pre></div></div>

<p>This is particularly useful when you want operational visibility into CAS without having to deploy a separate Spring Boot Admin application somewhere else. For smaller deployments, development environments, demos, or environments where CAS already has the necessary security model wrapped around it, embedding the admin server directly can be quite convenient.</p>

<h2 id="configuration">Configuration</h2>

<p>Once you have included the right module into your CAS build, the default configuration allows for the following:</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">spring.boot.admin.server.enabled</span><span class="p">=</span><span class="s">true</span>
<span class="py">spring.boot.admin.context-path</span><span class="p">=</span><span class="s">/sba</span>
<span class="py">spring.boot.admin.notify.mail.enabled</span><span class="p">=</span><span class="s">false</span>
</code></pre></div></div>

<p>The important property here is:</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">spring.boot.admin.context-path</span><span class="p">=</span><span class="s">/sba</span>
</code></pre></div></div>

<p>This tells Spring Boot Admin to mount itself under <code class="language-plaintext highlighter-rouge">/sba</code>, keeping it separate from the normal CAS web application routes. Disabling mail notifications is optional, but useful unless you actually want the admin server to send email notifications. In many CAS deployments, alerting is already handled elsewhere by Prometheus, Grafana, Datadog, Splunk, CloudWatch, carrier pigeons, etc.</p>

<h2 id="security">Security</h2>

<p>Spring Boot Admin needs to talk to registered applications and read their actuator endpoints. Since CAS actuator endpoints are usually protected, the admin server needs credentials. Here is the relevant configuration:</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">spring.boot.admin.instance-auth.enabled</span><span class="p">=</span><span class="s">true</span>
<span class="py">spring.boot.admin.instance-auth.service-map.CAS.user-name</span><span class="p">=</span><span class="s">${spring.security.user.name}</span>
<span class="py">spring.boot.admin.instance-auth.service-map.CAS.user-password</span><span class="p">=</span><span class="s">${spring.security.user.password}</span>
</code></pre></div></div>

<p>This enables instance authentication and provides a credential mapping for the registered service named <code class="language-plaintext highlighter-rouge">CAS</code>. The name matters. Later, the client registers itself as:</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">spring.boot.admin.client.instance.name</span><span class="p">=</span><span class="s">CAS</span>
</code></pre></div></div>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>That name is then matched by the <code class="language-plaintext highlighter-rouge">service-map.CAS</code> configuration. If the names do not line up, Spring Boot Admin may register the instance but fail to access protected actuator endpoints. This is the sort of problem that causes dashboards to look healthy from far away and deeply disappointing up close.</p>

<p>The credentials are reused from:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>${spring.security.user.name}
${spring.security.user.password}
</code></pre></div></div>

<h2 id="user-interface">User Interface</h2>

<p>Spring Boot Admin lets you customize the UI, which is nice because nothing says “integrated platform” like replacing the default title and icons with project branding.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>spring.boot.admin.ui.cache-templates=false
spring.boot.admin.ui.cache.no-cache=true
spring.boot.admin.ui.title=Apereo CAS
spring.boot.admin.ui.login-icon=assets/img/cas-logo.png
spring.boot.admin.ui.external-views[0].label=Documentation
spring.boot.admin.ui.external-views[0].url=https://apereo.github.io/cas
spring.boot.admin.ui.available-languages=en
</code></pre></div></div>

<p>There are a few things happening here. First, template caching is disabled:</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">spring.boot.admin.ui.cache-templates</span><span class="p">=</span><span class="s">false</span>
<span class="py">spring.boot.admin.ui.cache.no-cache</span><span class="p">=</span><span class="s">true</span>
</code></pre></div></div>

<p>This is useful when you are customizing the UI and want changes to show up without arguing with caches. In production, you may decide differently depending on how often you expect to change these assets. Next, the UI title becomes:</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">spring.boot.admin.ui.title</span><span class="p">=</span><span class="s">Apereo CAS</span>
</code></pre></div></div>

<p>This is what users will see in the browser and UI header areas. It is a small thing, but small things matter. Otherwise, six months later someone opens <code class="language-plaintext highlighter-rouge">/sba</code>, sees “Spring Boot Admin,” and opens a ticket asking who installed this mystery application on the CAS server. Then we customize the login icon, brand, and favicon:</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">spring.boot.admin.ui.login-icon</span><span class="p">=</span><span class="s">assets/img/cas-logo.png</span>
<span class="py">spring.boot.admin.ui.favicon</span><span class="p">=</span><span class="s">assets/img/favicon.ico</span>
</code></pre></div></div>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>The brand configuration includes both Apereo and CAS logos. This is appropriate because we want the UI to feel like it belongs to the CAS ecosystem, not like a random dashboard that wandered into the deployment and decided to live there. Finally, we add a documentation link:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>spring.boot.admin.ui.external-views[0].label=Documentation
spring.boot.admin.ui.external-views[0].url=https://apereo.github.io/cas
</code></pre></div></div>

<p>This gives operators an easy jump point to the CAS documentation. We remain optimistic that some day someone might read it. Of course, hope is an important part of open source.</p>

<h2 id="registering-cas">Registering CAS</h2>

<p>Now that the admin server is enabled, CAS also needs to register itself as a client.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>spring.boot.admin.client.enabled=true
spring.boot.admin.client.period=5000
spring.boot.admin.client.instance.metadata.name=Apereo CAS ${cas.host.name:}
spring.boot.admin.client.instance.metadata.tags.name=Apereo CAS ${cas.host.name:}
spring.boot.admin.client.username=${spring.security.user.name}
spring.boot.admin.client.password=${spring.security.user.password}
spring.boot.admin.client.url=${cas.server.prefix}${spring.boot.admin.context-path}
spring.boot.admin.client.instance.management-base-url=${cas.server.prefix}
spring.boot.admin.client.instance.name=CAS
</code></pre></div></div>

<p>Let’s walk through this carefully.</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">spring.boot.admin.client.enabled</span><span class="p">=</span><span class="s">true</span>
</code></pre></div></div>

<p>This enables the Spring Boot Admin client.</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">spring.boot.admin.client.period</span><span class="p">=</span><span class="s">5000</span>
</code></pre></div></div>

<p>This tells the client to register periodically every five seconds. In practice, this means CAS will keep announcing itself to the admin server. This is useful during startup and restarts, especially in environments where services may not become available in a perfectly reasonable order.</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">spring.boot.admin.client.url</span><span class="p">=</span><span class="s">${cas.server.prefix}${spring.boot.admin.context-path}</span>
</code></pre></div></div>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>This is the URL of the Spring Boot Admin Server. Since the admin server is embedded in CAS and mounted at <code class="language-plaintext highlighter-rouge">/sba</code>, the final value becomes something like:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://cas.example.org/cas/sba
</code></pre></div></div>

<p>…or whatever your <code class="language-plaintext highlighter-rouge">cas.server.prefix</code> is configured to be.</p>

<p>The client credentials are:</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>spring.boot.admin.client.username=${spring.security.user.name}
spring.boot.admin.client.password=${spring.security.user.password}
</code></pre></div></div>

<p>These are used by the client when registering with the admin server, assuming the admin server is protected by Spring Security. The management base URL is:</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">spring.boot.admin.client.instance.management-base-url</span><span class="p">=</span><span class="s">${cas.server.prefix}</span>
</code></pre></div></div>

<p>This tells Spring Boot Admin where CAS actuator endpoints live. Depending on your CAS configuration, actuator endpoints are typically available relative to the CAS server prefix. Finally:</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">spring.boot.admin.client.instance.name</span><span class="p">=</span><span class="s">CAS</span>
</code></pre></div></div>

<p>This is the registered instance name. Remember the server-side authentication mapping?</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">spring.boot.admin.instance-auth.service-map.CAS.user-name</span><span class="p">=</span><span class="s">${spring.security.user.name}</span>
<span class="py">spring.boot.admin.instance-auth.service-map.CAS.user-password</span><span class="p">=</span><span class="s">${spring.security.user.password}</span>
</code></pre></div></div>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>That <code class="language-plaintext highlighter-rouge">CAS</code> key corresponds to this instance name. This is the little handshake between the client registration and the admin server’s ability to authenticate back into the CAS instance.</p>

<h2 id="operational-notes">Operational Notes</h2>

<p>Once this is running, you should be able to visit this url and log in using the configured Spring Security credentials:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>${cas.server.prefix}/sba
</code></pre></div></div>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>This is the initial landing page:</p>
<div class="image-wrapper rounded mx-auto d-block text-center">
    
    <a data-lightbox="image-fawnoos" data-title="Apereo CAS - Spring BootAdmin - Fawnoos || Open-Source Identity and Access Management Consulting Services" href="/images/blog/assets/boot-admin.png" title="Apereo CAS - Spring BootAdmin" target="_blank">
    <img src="/images/blog/assets/boot-admin.png" style="width:70%;" title="Apereo CAS - Spring BootAdmin - Fawnoos || Open-Source Identity and Access Management Consulting Services" />
    </a>
</div>
<p />

<p>…and how you can then drill down into the app:</p>

<div class="image-wrapper rounded mx-auto d-block text-center">
    
    <a data-lightbox="image-fawnoos" data-title="Apereo CAS - Spring BootAdmin - Fawnoos || Open-Source Identity and Access Management Consulting Services" href="/images/blog/assets/boot-admin-1.png" title="Apereo CAS - Spring BootAdmin" target="_blank">
    <img src="/images/blog/assets/boot-admin-1.png" style="width:70%;" title="Apereo CAS - Spring BootAdmin - Fawnoos || Open-Source Identity and Access Management Consulting Services" />
    </a>
</div>
<p />

<p>After authentication, CAS should appear as a registered application named <code class="language-plaintext highlighter-rouge">CAS</code>. From there, Spring Boot Admin can inspect actuator endpoints, health information, environment details, metrics, loggers, and other operational views depending on what endpoints are exposed and authorized in your CAS deployment. Be careful with actuator exposure. Spring Boot Admin is only as safe as the endpoints it can reach. If you expose powerful actuator endpoints, make sure access is intentional, authenticated, and appropriate for the environment.</p>

<p>You may also be interested in this <a href="https://github.com/codecentric/spring-boot-admin/issues/5438">login button issue</a> in Spring Boot Admin <code class="language-plaintext highlighter-rouge">4.1</code>.</p>

<h1 id="need-help">Need Help?</h1>

<p>If you have questions about the contents and the topic of this blog post, or if you need additional guidance and support, feel free to <a href="/#contact-section-header">send us a note </a> and ask about consulting and support services.</p>

<div class="followit-subscribe-wrapper">
  
    <button type="button" class="followit-subscribe-toggle" onclick="
        const wrapper = this.closest('.followit-subscribe-wrapper');
        wrapper.classList.add('is-visible');
      ">
      Subscribe by email
    </button>
  

  <div class="followit--follow-form-container followit-subscribe-box " attr-a="" attr-b="" attr-c="" attr-d="" attr-e="" attr-f="">

    <form data-v-c497cbba="" action="https://api.follow.it/subscription-form/TDFQVjQzZTk2czNPRkF6RUZSUmQ0SE0wYnBnS2VkQjdZeXpsZDFQNWRETjVGc0JyL1FqL001SlFzUlVhWkpJdFZ4SUl3OEU0Sm1CK2pZUjlCbG1PcG9oNDRadjhKeldCSmlLdkpZQnRqUC8xMTJTWmxxQTFwWEpjc3FkVGI4WGx8MEJXeEV6ZTlkaUhUOXBJdFlmMXU0Q3hCQXI0anpveVlMVnNlN2VPRC9uaz0=/8" method="post">

      <div data-v-c497cbba="" class="form-preview" style="background-color: rgb(255, 255, 255); position: relative; border-style: solid; border-width: 1px; border-color: rgb(204, 204, 204);">

        <div data-v-c497cbba="" class="preview-heading">
          <h5 data-v-c497cbba="" style="font-family: Arial; font-weight: bold; color: rgb(0, 0, 0); font-size: 16px; text-align: center; text-transform: none !important;">
            Get new posts by email:
          </h5>
        </div>

        <div data-v-c497cbba="" class="preview-input-field">
          <input data-v-c497cbba="" type="email" name="email" required="" placeholder="Enter your email" spellcheck="false" style="font-family: Arial; font-weight: normal; color: rgb(0, 0, 0); font-size: 14px; text-align: center; background-color: rgb(255, 255, 255); text-transform: none !important;" />
        </div>

        <div data-v-c497cbba="" class="preview-submit-button">
          <button data-v-c497cbba="" type="submit" class="contact100-form-btn">
            Subscribe
          </button>
        </div>
      </div>
    </form>

    <a href="https://follow.it" class="powered-by-line">
      <img src="https://follow.it/images/colored-logo.svg" alt="follow.it" height="17px" />
    </a>
  </div>
</div>

<h1 id="so">So…</h1>

<p>I hope the content here was of some help to you and I am sure that both this post as well as the functionality it attempts to explain can be improved in any number of ways. If you have suggestions or ideas on how to improve this post, please feel free to reach out to us.</p>

<p><a href="https://fawnoos.com">Misagh Moayyed</a></p>]]></content><author><name>Misagh Moayyed</name></author><category term="CAS 8.0.x" /><category term="Monitoring" /><summary type="html"><![CDATA[There comes a time in every CAS deployment when you look at your running CAS server and think:]]></summary></entry><entry><title type="html">Apereo CAS - Stateless Ticket Registry</title><link href="https://fawnoos.com/2026/06/09/cas80x-stateless-ticket-registry/" rel="alternate" type="text/html" title="Apereo CAS - Stateless Ticket Registry" /><published>2026-06-09T00:00:00+00:00</published><updated>2026-06-09T00:00:00+00:00</updated><id>https://fawnoos.com/2026/06/09/cas80x-stateless-ticket-registry</id><content type="html" xml:base="https://fawnoos.com/2026/06/09/cas80x-stateless-ticket-registry/"><![CDATA[<p>Apereo CAS deployments generally need a ticket registry: a place where CAS can store, find, update, and eventually forget tickets. Depending on your mood, budget, infrastructure team, and tolerance for YAML, this registry may be backed by Redis, Hazelcast, MongoDB, JDBC, Cassandra, Kafka, or any other technology that promises clustering nirvana just after you finish reading its 93-page production guide.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>The <strong>stateless ticket registry</strong> takes a different path. Instead of storing tickets in a backend system, CAS produces tickets that carry their own state. The ticket itself becomes the suitcase, the passport, the boarding pass, the hotel reservation, and possibly the tiny shampoo bottle you were not supposed to take. CAS encrypts and compresses the ticket payload, and any CAS node that can decrypt and understand the ticket can process it without needing to ask a central storage service.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>This post takes a closer look at the stateless ticket registry, where it shines, where it politely declines to participate, and what caveats you should understand before inviting it to production.</p>

<p>Our main focus is on:</p>

<ul>
  <li>CAS <code class="language-plaintext highlighter-rouge">8.0.x</code></li>
  <li>Java <code class="language-plaintext highlighter-rouge">25</code></li>
</ul>

<h2 id="why-stateless">Why Stateless?</h2>

<p>A traditional ticket registry stores ticket state somewhere. That somewhere must be installed, maintained, monitored, upgraded, backed up, secured, tuned, and eventually blamed during an outage. A stateless ticket registry removes that moving part.</p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dependencies</span> <span class="o">{</span>
    <span class="n">implementation</span> <span class="s2">"org.apereo.cas:cas-server-support-stateless-ticket-registry"</span>
<span class="o">}</span>
</code></pre></div></div>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>With the module included in the CAS WAR overlay, generated tickets become self-contained and portable across CAS nodes. This can be attractive in environments where:</p>

<ul>
  <li>CAS runs in Kubernetes or another cloud-native platform.</li>
  <li>You want to reduce infrastructure dependencies.</li>
  <li>You do not want Redis, JDBC, Hazelcast, or another backend just to manage tickets.</li>
  <li>Your deployment needs horizontal scaling without registry replication gymnastics.</li>
  <li>You enjoy deleting entire sections from architecture diagrams.</li>
</ul>

<p>The result is a deployment model where CAS nodes do not have to coordinate ticket state through a shared store. The ticket carries what CAS needs, and CAS validates it by decoding and decrypting the ticket itself.</p>

<h2 id="what-you-get">What You Get</h2>

<p>The obvious win is operational simplicity. There is no schema to migrate. There is no cache cluster to babysit. There is no expired-ticket cleanup process to tune. There is no “node A created the ticket but node B cannot find it” incident to enjoy at 2:17 AM while developing a sincere appreciation for coffee! You also get easier portability across clustered deployments. As long as CAS nodes share compatible configuration and cryptographic material, one node may issue a ticket and another node may validate it.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<h2 id="supported-protocols">Supported Protocols</h2>

<p>The stateless ticket registry is designed to support common CAS use cases, but it is not a universal drop-in replacement for every feature that has ever touched a ticket object. At the time of this writing, the following protocol families are generally supported:</p>

<ul>
  <li>CAS protocol.</li>
  <li>SAML1.</li>
  <li>SAML2, with selected exceptions such as SAML2 attribute queries.</li>
  <li>OAuth2, with selected exceptions such as device authorization and token exchange.</li>
  <li>OpenID Connect, with selected exceptions such as DPoP.</li>
</ul>

<p>This is a good time to remember that a ticket registry is not just a bucket of strings. Many CAS features create, update, retrieve, remove, track, count, correlate, revoke, or inspect tickets. A stateless registry changes the rules of that game.</p>

<h2 id="ticket-size-the-suitcase-has-limits">Ticket Size: The Suitcase Has Limits</h2>

<p>Stateless tickets carry their own state. This means the ticket value may become larger than what you are used to seeing with a traditional registry. CAS attempts to keep generated tickets compact, compressed, and constrained to a reasonable size. Still, you should assume ticket size is now part of your architecture.</p>

<p>For example, a traditional service ticket might look like:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ST-abc123
</code></pre></div></div>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>A stateless service ticket may need to carry encoded, encrypted ticket state and therefore may be longer. This becomes important when tickets travel through:</p>

<ul>
  <li>HTTP query parameters.</li>
  <li>Form posts.</li>
  <li>Response headers.</li>
  <li>Reverse proxies.</li>
  <li>Application servers.</li>
  <li>Older CAS clients.</li>
</ul>

<p>You may need to review and adjust container and proxy limits. For example, with embedded Apache Tomcat in Spring Boot:</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">server.max-http-request-header-size</span><span class="p">=</span><span class="s">16KB</span>
<span class="py">server.tomcat.max-http-form-post-size</span><span class="p">=</span><span class="s">2MB</span>
</code></pre></div></div>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>If CAS is behind nginx, you may need something like:</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">large_client_header_buffers</span> <span class="mi">4</span> <span class="mi">16k</span><span class="p">;</span>
<span class="k">client_header_buffer_size</span> <span class="mi">16k</span><span class="p">;</span>
</code></pre></div></div>

<p>The actual values depend on your environment, ticket size, client behavior, and proxy chain. The important lesson is not “copy these values and declare victory.” The important lesson is: <strong>test the entire path</strong>.</p>

<h2 id="long-service-urls-bring-a-smaller-backpack">Long Service URLs: Bring a Smaller Backpack</h2>

<p>Service tickets often need to remember the service URL for which they were issued. If your service URL looks like a complete autobiography with query parameters, tracking codes, tenant identifiers, UI state, campaign data, and the emotional history of the front-end team, your ticket size may suffer.</p>

<p>For example:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://app.example.org/callback?tenant=alpha&amp;theme=blue&amp;returnUrl=https%3A%2F%2F...
</code></pre></div></div>

<p>CAS can apply a modest shortening strategy for long application URLs, and validation logic can account for that behavior. Still, shorter URLs are strongly recommended. A better pattern is:</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://app.example.org/cas/callback
</code></pre></div></div>

<p>Then let the application manage local routing state after authentication. This is not just a stateless-ticket-registry recommendation. It is a general life recommendation. Short URLs are easier to validate, easier to log, easier to compare, easier to troubleshoot, and less likely to become a novel.</p>

<h2 id="ticket-expiration">Ticket Expiration</h2>

<p>Because stateless tickets need to be decoded and decrypted, extremely aggressive expiration settings may become counterproductive. If your service tickets expire in the blink of an eye, a ticket might be issued, redirected, received, decoded, and rejected because the expiration policy was configured by someone who apparently benchmarks authentication flows using a stopwatch and a grudge.</p>

<p>A reasonable suggestion is to allow service tickets to live for around 30 seconds:</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">cas.ticket.st.time-to-kill-in-seconds</span><span class="p">=</span><span class="s">30</span>
</code></pre></div></div>

<p>The exact property names may vary depending on the CAS version and policy type, so check the configuration metadata for your release. The intent is what matters:</p>

<ul>
  <li>Service tickets should remain short-lived.</li>
  <li>Service tickets should generally remain single-use.</li>
  <li>The timeout should still allow realistic redirect and validation flows to complete.</li>
</ul>

<p>This is especially important when applications, proxies, or clients add latency. The ticket should not live forever, but it should live long enough to reach adulthood, which in service-ticket years is approximately a few seconds.</p>

<h2 id="reusability-and-idle-expiration">Reusability and Idle Expiration</h2>

<p>In a traditional ticket registry, CAS can update ticket state. It can track usage counts, idle timestamps, last-used moments, and other mutable state. In a stateless model, mutation is limited by design. The ticket is self-contained. Once issued, it is not sitting in a central registry waiting to be updated. This means expiration policies for generated tickets are generally reduced to enforcing an expiration instant. Reusability limits and idle/inactivity semantics are not handled in quite the same way.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>For example, with a stateful registry like Redis, CAS may say:</p>

<blockquote>
  <p>“This TGT has not been used for 30 minutes. Expire it.”</p>
</blockquote>

<p>With a stateless registry, there is no central TGT record whose idle timestamp can be continuously updated by all nodes. The ticket must be evaluated based on what it carries. You are trading centralized mutability for portability and operational simplicity.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>The practical takeaway:</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">cas.ticket.tgt.primary.max-time-to-live-in-seconds</span><span class="p">=</span><span class="s">28800</span>
</code></pre></div></div>

<p>Prefer hard expiration windows that match your risk profile. Avoid designing policies that require CAS to constantly update and remember server-side ticket state.</p>

<h2 id="encryption">Encryption</h2>

<p>Stateless tickets are encrypted to protect their contents. This is essential because the ticket carries state that would otherwise have lived inside a backend registry. This also means cryptographic configuration becomes operationally significant. All CAS nodes that participate in issuing and validating stateless tickets must agree on the relevant cryptographic settings and keys. If node A encrypts a ticket using one key and node B tries to decrypt it using another, the ticket validation ceremony will end quickly and without applause.</p>

<p>In containerized environments, you might externalize secrets:</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">cas.ticket.registry.crypto.encryption.key</span><span class="p">=</span><span class="s">${CAS_TICKET_REGISTRY_ENCRYPTION_KEY}</span>
<span class="py">cas.ticket.registry.crypto.signing.key</span><span class="p">=</span><span class="s">${CAS_TICKET_REGISTRY_SIGNING_KEY}</span>
</code></pre></div></div>

<p>The exact property path may differ by CAS version and registry implementation, so use your release’s configuration catalog as the final authority. Operationally, treat these keys like production secrets:</p>

<ul>
  <li>Store them in a secret manager.</li>
  <li>Keep them consistent across CAS nodes.</li>
  <li>Rotate them carefully.</li>
  <li>Understand whether old tickets should survive rotation.</li>
  <li>Never generate fresh random keys independently per node unless you enjoy distributed authentication roulette.</li>
</ul>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>A stateless ticket registry does not remove state from the universe. It moves some of that responsibility into cryptographic material and ticket payloads.</p>

<h2 id="attribute-resolution">Attribute Resolution</h2>

<p>One of the most important caveats is attribute handling. During the first leg of authentication, CAS may collect attributes from authentication handlers. In a stateful model, ticket state can preserve what happened during authentication. In a stateless model, particularly during back-channel ticket validation, attributes produced only during the original authentication transaction may not be available later.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>This matters when an authentication handler produces attributes that are not otherwise resolvable. For example, imagine an authentication handler that returns:</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"uid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"casuser"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"department"</span><span class="p">:</span><span class="w"> </span><span class="s2">"engineering"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"favoriteDatabase"</span><span class="p">:</span><span class="w"> </span><span class="s2">"the one that is currently up"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>If <code class="language-plaintext highlighter-rouge">department</code> only exists because the authentication handler produced it during login, back-channel validation may not see it unless CAS can resolve it again from an attribute repository. The fix is to configure an attribute repository that can reproduce the required attributes during validation.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>For example, using a JDBC-backed attribute repository:</p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">cas</span><span class="o">.</span><span class="na">authn</span><span class="o">.</span><span class="na">attribute</span><span class="o">-</span><span class="n">repository</span><span class="o">.</span><span class="na">jdbc</span><span class="o">[</span><span class="mi">0</span><span class="o">].</span><span class="na">url</span><span class="o">=</span><span class="nl">jdbc:postgresql:</span><span class="c1">//db.example.org/iam</span>
<span class="n">cas</span><span class="o">.</span><span class="na">authn</span><span class="o">.</span><span class="na">attribute</span><span class="o">-</span><span class="n">repository</span><span class="o">.</span><span class="na">jdbc</span><span class="o">[</span><span class="mi">0</span><span class="o">].</span><span class="na">user</span><span class="o">=</span><span class="n">cas</span>
<span class="n">cas</span><span class="o">.</span><span class="na">authn</span><span class="o">.</span><span class="na">attribute</span><span class="o">-</span><span class="n">repository</span><span class="o">.</span><span class="na">jdbc</span><span class="o">[</span><span class="mi">0</span><span class="o">].</span><span class="na">password</span><span class="o">=</span><span class="n">$</span><span class="o">{</span><span class="n">CAS_ATTR_REPO_PASSWORD</span><span class="o">}</span>
<span class="n">cas</span><span class="o">.</span><span class="na">authn</span><span class="o">.</span><span class="na">attribute</span><span class="o">-</span><span class="n">repository</span><span class="o">.</span><span class="na">jdbc</span><span class="o">[</span><span class="mi">0</span><span class="o">].</span><span class="na">sql</span><span class="o">=</span><span class="n">select</span> <span class="o">*</span> <span class="n">from</span> <span class="n">user_attributes</span> <span class="n">where</span> <span class="n">uid</span><span class="o">=?</span>
<span class="n">cas</span><span class="o">.</span><span class="na">authn</span><span class="o">.</span><span class="na">attribute</span><span class="o">-</span><span class="n">repository</span><span class="o">.</span><span class="na">jdbc</span><span class="o">[</span><span class="mi">0</span><span class="o">].</span><span class="na">username</span><span class="o">=</span><span class="n">uid</span>
</code></pre></div></div>

<p>Or with LDAP:</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">cas</span><span class="o">.</span><span class="na">authn</span><span class="o">.</span><span class="na">attribute</span><span class="o">-</span><span class="n">repository</span><span class="o">.</span><span class="na">ldap</span><span class="o">[</span><span class="mi">0</span><span class="o">].</span><span class="na">ldap</span><span class="o">-</span><span class="n">url</span><span class="o">=</span><span class="nl">ldaps:</span><span class="c1">//ldap.example.org</span>
<span class="n">cas</span><span class="o">.</span><span class="na">authn</span><span class="o">.</span><span class="na">attribute</span><span class="o">-</span><span class="n">repository</span><span class="o">.</span><span class="na">ldap</span><span class="o">[</span><span class="mi">0</span><span class="o">].</span><span class="na">base</span><span class="o">-</span><span class="n">dn</span><span class="o">=</span><span class="n">ou</span><span class="o">=</span><span class="n">people</span><span class="o">,</span><span class="n">dc</span><span class="o">=</span><span class="n">example</span><span class="o">,</span><span class="n">dc</span><span class="o">=</span><span class="n">org</span>
<span class="n">cas</span><span class="o">.</span><span class="na">authn</span><span class="o">.</span><span class="na">attribute</span><span class="o">-</span><span class="n">repository</span><span class="o">.</span><span class="na">ldap</span><span class="o">[</span><span class="mi">0</span><span class="o">].</span><span class="na">search</span><span class="o">-</span><span class="n">filter</span><span class="o">=</span><span class="n">uid</span><span class="o">={</span><span class="n">user</span><span class="o">}</span>
<span class="n">cas</span><span class="o">.</span><span class="na">authn</span><span class="o">.</span><span class="na">attribute</span><span class="o">-</span><span class="n">repository</span><span class="o">.</span><span class="na">ldap</span><span class="o">[</span><span class="mi">0</span><span class="o">].</span><span class="na">bind</span><span class="o">-</span><span class="n">dn</span><span class="o">=</span><span class="n">cn</span><span class="o">=</span><span class="n">cas</span><span class="o">,</span><span class="n">ou</span><span class="o">=</span><span class="n">services</span><span class="o">,</span><span class="n">dc</span><span class="o">=</span><span class="n">example</span><span class="o">,</span><span class="n">dc</span><span class="o">=</span><span class="n">org</span>
<span class="n">cas</span><span class="o">.</span><span class="na">authn</span><span class="o">.</span><span class="na">attribute</span><span class="o">-</span><span class="n">repository</span><span class="o">.</span><span class="na">ldap</span><span class="o">[</span><span class="mi">0</span><span class="o">].</span><span class="na">bind</span><span class="o">-</span><span class="n">credential</span><span class="o">=</span><span class="n">$</span><span class="o">{</span><span class="n">CAS_LDAP_PASSWORD</span><span class="o">}</span>
</code></pre></div></div>

<h2 id="single-logout">Single Logout</h2>

<p>Back-channel single logout generally requires CAS to know which services participated in a single sign-on session. In a stateful registry, CAS can track that relationship. It can know which service tickets were issued under a TGT and which applications should receive logout notifications. In a stateless registry, there is no central backend holding that session graph. As a result, back-channel single logout is not supported in the same way. Similarly, administrative features that ask CAS to list active SSO sessions, inspect who is logged in, or forcibly terminate a user’s session are outside the scope of this model.</p>

<p>For example, this sort of operational question becomes difficult:</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<blockquote>
  <p>“Show me every active session for <code class="language-plaintext highlighter-rouge">casuser</code> and terminate them now.”</p>
</blockquote>

<p>There is no central registry to query. CAS cannot inspect a storage backend that does not exist. If your environment requires central session visibility, administrative termination, or reliable back-channel logout, you should strongly consider a stateful registry option. If you mostly need short-lived ticket validation, horizontal portability, and fewer infrastructure dependencies, stateless may be a good fit.</p>

<h2 id="the-browser-has-a-job">The Browser Has a Job</h2>

<p>The stateless ticket registry leans heavily on browser session management features during ticket exchanges. This means the browser must support local or session storage as required by the flow. In most modern environments this is fine. In more restrictive environments, you should test carefully:</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<ul>
  <li>Locked-down kiosk or Privacy-enhanced browsers.</li>
  <li>Embedded web views and mobile in-app browsers.</li>
</ul>

<p>If browser storage is blocked or aggressively cleared, some flows may behave poorly. This is not unique to CAS. Many modern identity flows depend on browser-side state to correlate requests, prevent forgery, and complete redirects. Still, stateless ticketing makes it especially important to test real clients rather than only your favorite developer browser with all shields down and a warm cup of optimism.</p>

<h2 id="final-thoughts">Final Thoughts</h2>

<p>The stateless ticket registry is a powerful option for CAS deployments that want to reduce operational dependencies and simplify clustered ticket validation. It allows tickets to carry their own encrypted state, making them portable across CAS nodes without a shared backend registry.</p>
<div id="adscode" style="width:100%">
     <script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
     <ins class="adsbygoogle" style="display:block; text-align:center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-8081398210264173" data-ad-slot="3789603713"></ins>
     <script>
          (adsbygoogle = window.adsbygoogle || []).push({});
     </script>
</div>

<p>That simplicity comes with trade-offs. You give up certain kinds of centralized tracking. You need to care about ticket size. You need to test proxy and client limits. You need to configure attribute repositories properly. You need to understand that single logout and administrative session management do not work the same way when there is no central session record to inspect.</p>

<h1 id="need-help">Need Help?</h1>

<p>If you have questions about the contents and the topic of this blog post, or if you need additional guidance and support, feel free to <a href="/#contact-section-header">send us a note </a> and ask about consulting and support services.</p>

<div class="followit-subscribe-wrapper">
  
    <button type="button" class="followit-subscribe-toggle" onclick="
        const wrapper = this.closest('.followit-subscribe-wrapper');
        wrapper.classList.add('is-visible');
      ">
      Subscribe by email
    </button>
  

  <div class="followit--follow-form-container followit-subscribe-box " attr-a="" attr-b="" attr-c="" attr-d="" attr-e="" attr-f="">

    <form data-v-c497cbba="" action="https://api.follow.it/subscription-form/TDFQVjQzZTk2czNPRkF6RUZSUmQ0SE0wYnBnS2VkQjdZeXpsZDFQNWRETjVGc0JyL1FqL001SlFzUlVhWkpJdFZ4SUl3OEU0Sm1CK2pZUjlCbG1PcG9oNDRadjhKeldCSmlLdkpZQnRqUC8xMTJTWmxxQTFwWEpjc3FkVGI4WGx8MEJXeEV6ZTlkaUhUOXBJdFlmMXU0Q3hCQXI0anpveVlMVnNlN2VPRC9uaz0=/8" method="post">

      <div data-v-c497cbba="" class="form-preview" style="background-color: rgb(255, 255, 255); position: relative; border-style: solid; border-width: 1px; border-color: rgb(204, 204, 204);">

        <div data-v-c497cbba="" class="preview-heading">
          <h5 data-v-c497cbba="" style="font-family: Arial; font-weight: bold; color: rgb(0, 0, 0); font-size: 16px; text-align: center; text-transform: none !important;">
            Get new posts by email:
          </h5>
        </div>

        <div data-v-c497cbba="" class="preview-input-field">
          <input data-v-c497cbba="" type="email" name="email" required="" placeholder="Enter your email" spellcheck="false" style="font-family: Arial; font-weight: normal; color: rgb(0, 0, 0); font-size: 14px; text-align: center; background-color: rgb(255, 255, 255); text-transform: none !important;" />
        </div>

        <div data-v-c497cbba="" class="preview-submit-button">
          <button data-v-c497cbba="" type="submit" class="contact100-form-btn">
            Subscribe
          </button>
        </div>
      </div>
    </form>

    <a href="https://follow.it" class="powered-by-line">
      <img src="https://follow.it/images/colored-logo.svg" alt="follow.it" height="17px" />
    </a>
  </div>
</div>

<h1 id="so">So…</h1>

<p>I hope the content here was of some help to you and I am sure that both this post as well as the functionality it attempts to explain can be improved in any number of ways. If you have suggestions or ideas on how to improve this post, please feel free to reach out to us.</p>

<p><a href="https://fawnoos.com">Misagh Moayyed</a></p>]]></content><author><name>Misagh Moayyed</name></author><category term="CAS 8.0.x" /><summary type="html"><![CDATA[Apereo CAS deployments generally need a ticket registry: a place where CAS can store, find, update, and eventually forget tickets. Depending on your mood, budget, infrastructure team, and tolerance for YAML, this registry may be backed by Redis, Hazelcast, MongoDB, JDBC, Cassandra, Kafka, or any other technology that promises clustering nirvana just after you finish reading its 93-page production guide. The stateless ticket registry takes a different path. Instead of storing tickets in a backend system, CAS produces tickets that carry their own state. The ticket itself becomes the suitcase, the passport, the boarding pass, the hotel reservation, and possibly the tiny shampoo bottle you were not supposed to take. CAS encrypts and compresses the ticket payload, and any CAS node that can decrypt and understand the ticket can process it without needing to ask a central storage service. This post takes a closer look at the stateless ticket registry, where it shines, where it politely declines to participate, and what caveats you should understand before inviting it to production.]]></summary></entry></feed>