• Skip to primary navigation
  • Skip to main content
  • Skip to primary sidebar
  • Skip to footer

Mister PKI

SSL Certificates * SSL Tools * Certificate Decoder

  • PKI Training
  • OpenSSL
  • Keytool
  • PKI Tools
    • Apereo CAS
    • SAML
    • OpenID Connect
  • Get expert help

Configure OpenID Connect (OIDC) in Apereo CAS 7.3 and 8

September 18, 2026 by Mister PKI Leave a Comment

Overview

Apereo CAS is best known as a Central Authentication Service server, but it can also act as a full OpenID Connect Provider, allowing applications to authenticate users with standard OAuth 2.0 and OpenID Connect flows.

A typical deployment looks like this:

User
  |
  v
OIDC Application
  |
  | Authorization request
  v
Apereo CAS
  |
  | Authentication
  v
Identity Source
  |
  v
CAS issues authorization code
  |
  v
Application exchanges code for tokens

Once configured, CAS can provide OpenID Connect endpoints for:

  • discovery;
  • authorization;
  • token issuance;
  • JSON Web Keys;
  • user information;
  • token introspection;
  • token revocation;
  • logout;
  • client registration.

This guide focuses on a practical system-administration scenario:

An internal web application needs to use Apereo CAS as its OpenID Connect identity provider. The application will use Authorization Code flow and request the standard openid, profile, and email scopes along with a custom groups scope derived from directory group membership.

The configuration will use:

  • Apereo CAS 7.3.8.3 as the primary example;
  • compatibility notes for CAS 8.x;
  • cas.groovy for CAS properties;
  • a JSON service registry;
  • Jasypt-encrypted OIDC client secrets;
  • a filesystem JWKS;
  • encrypted JWKS contents;
  • Authorization Code flow;
  • custom groups scope and claim;
  • Groovy claim mapping.

The goal is to produce an OIDC provider with a discovery document similar to:

https://cas.example.org/cas/oidc/.well-known/openid-configuration

and an application registration whose redirect URI resembles:

https://app.example.org/oauth/oidc/callback

All domains, group names, IDs, paths, and secrets in this article are generic examples.


CAS 7.3.8.3 versus CAS 8.x

The overall OpenID Connect configuration model is very similar between CAS 7.3 and CAS 8.

Both versions use:

cas-server-support-oidc

and both use:

org.apereo.cas.services.OidcRegisteredService

for statically registered OIDC clients.

CAS 8 continues to expose the same major concepts:

OIDC issuer
Discovery
JWKS
Authorization endpoint
Token endpoint
Profile/UserInfo endpoint
Scopes
Claims
Registered OIDC services

Most CAS 7.3 OIDC service definitions therefore migrate to CAS 8 with relatively small changes.

The biggest upgrade concern is not usually the fundamental OIDC flow. It is validating:

  • renamed or migrated configuration properties;
  • deprecated CAS properties;
  • Java and Spring Boot requirements;
  • service-definition serialization;
  • custom Groovy logic;
  • token behavior;
  • new security defaults.

Do not copy an entire CAS 7 configuration into CAS 8 without testing it against the CAS 8 configuration catalog.


Step 1: Add OpenID Connect support to the CAS overlay

OIDC provider support is not enabled merely by setting a property.

The CAS overlay must include:

cas-server-support-oidc

For a Gradle overlay:

dependencies {
    implementation "org.apereo.cas:cas-server-support-oidc:${project.'cas.version'}"
}

If your overlay uses the CAS BOM:

dependencies {
    implementation "org.apereo.cas:cas-server-support-oidc"
}

For Maven:

<dependency>
    <groupId>org.apereo.cas</groupId>
    <artifactId>cas-server-support-oidc</artifactId>
    <version>${cas.version}</version>
</dependency>

After rebuilding CAS, OIDC server support becomes available.

The official CAS documentation notes that enabling OpenID Connect also enables the underlying OAuth support because OIDC extends OAuth.


Step 2: Understand the CAS OIDC endpoints

CAS publishes a number of OIDC endpoints.

Typical endpoints include:

/cas/oidc/.well-known
/cas/oidc/.well-known/openid-configuration
/cas/oidc/jwks
/cas/oidc/authorize
/cas/oidc/token
/cas/oidc/profile
/cas/oidc/logout
/cas/oidc/introspect
/cas/oidc/revoke

Do not configure applications by manually copying these URLs unless absolutely necessary.

OIDC clients should normally use the discovery document:

https://cas.example.org/cas/oidc/.well-known/openid-configuration

The discovery document tells clients which URLs CAS currently exposes.

For example:

curl \
  https://cas.example.org/cas/oidc/.well-known/openid-configuration

Pretty-print with jq:

curl -s \
  https://cas.example.org/cas/oidc/.well-known/openid-configuration |
jq

A client can discover values such as:

{
  "issuer": "https://cas.example.org/cas/oidc",
  "authorization_endpoint": "...",
  "token_endpoint": "...",
  "userinfo_endpoint": "...",
  "jwks_uri": "...",
  "scopes_supported": [],
  "claims_supported": []
}

Using discovery makes the integration less dependent on CAS-specific endpoint naming.


Step 3: Configure the OIDC issuer

Every OpenID Provider has an issuer identifier.

For this example:

https://cas.example.org/cas/oidc

Configure it in:

/etc/cas/config/cas.groovy

with:

cas.authn.oidc.core.issuer="https://cas.example.org/cas/oidc"

This value is important.

CAS uses it as the OIDC issuer, and ID tokens contain the same issuer in their:

iss

claim.

A relying party should expect:

{
  "iss": "https://cas.example.org/cas/oidc"
}

The issuer should be HTTPS in a production deployment.

It should also match the externally visible CAS URL.

Do not accidentally configure an internal load-balancer hostname such as:

https://cas-node01.internal:8443/cas/oidc

if clients actually access:

https://cas.example.org/cas/oidc

Issuer mismatches commonly cause OIDC validation failures.


Step 4: Configure the OIDC JWKS

OIDC tokens are normally digitally signed.

Clients need CAS’s public signing keys so they can verify those signatures.

CAS publishes those public keys from:

/oidc/jwks

but CAS itself needs access to the corresponding private key material.

A common configuration is a filesystem JSON Web Key Set.

In cas.groovy:

cas.authn.oidc.jwks.fileSystem.jwksFile="file:/etc/cas/config/keystore.jwks"

CAS 8 documentation explicitly shows the resource-style path:

file:/etc/cas/config/keystore.jwks

Some CAS 7 deployments may also accept a plain filesystem path such as:

cas.authn.oidc.jwks.fileSystem.jwksFile="/etc/cas/config/keystore.jwks"

Using the explicit file: resource form makes the intent clearer:

cas.authn.oidc.jwks.fileSystem.jwksFile="file:/etc/cas/config/keystore.jwks"

What is stored in keystore.jwks?

An unencrypted JWKS resembles:

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "cas",
      "use": "sig",
      "n": "...",
      "e": "AQAB",
      "d": "..."
    }
  ]
}

The important distinction is:

/oidc/jwks endpoint
        ↓
public key information
keystore.jwks on CAS
        ↓
may contain private key material

The filesystem JWKS therefore deserves private-key-level protection.

Use restrictive permissions:

chown cas:cas /etc/cas/config/keystore.jwks
chmod 600 /etc/cas/config/keystore.jwks

Do not publish the server-side JWKS file.

Only the public JWKS endpoint is meant for relying parties.


Step 5: Generate the OIDC JWKS

CAS provides tooling capable of generating an OIDC JWKS.

Depending on your CAS overlay tooling, the CAS command-line shell exposes a command similar to:

generate-oidc-jwks

A typical generated key uses:

RSA
2048-bit or larger key
kid=cas

You can also manage the JWKS independently as long as CAS receives a valid keystore structure.

After CAS starts, verify the public endpoint:

curl -s \
  https://cas.example.org/cas/oidc/jwks |
jq

You should see public key material similar to:

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "cas",
      "use": "sig",
      "n": "...",
      "e": "AQAB"
    }
  ]
}

You should not see the RSA private exponent:

d

at the public endpoint.


Step 6: Encrypt the JWKS with CAS configuration security

The filesystem JWKS may contain OIDC signing private keys.

Leaving it as plain JSON means anyone with read access to the file can obtain those keys.

CAS documentation supports encrypting JWKS contents using the same configuration-security mechanism used for other encrypted CAS settings.

CAS uses values prefixed with:

{cas-cipher}

For example:

{cas-cipher}ENCRYPTED_VALUE

A practical workflow is:

Plain JWKS JSON
      ↓
Jasypt encryption
      ↓
{cas-cipher}...
      ↓
/etc/cas/config/keystore.jwks

Keep an unencrypted working copy only long enough to perform the encryption operation, then securely remove it according to your organization’s key-handling procedures.


Step 7: Configure Jasypt bootstrap settings

CAS needs the encryption password before it can decrypt encrypted configuration data.

The important configuration-security bootstrap values should therefore be supplied outside the normal CAS configuration file.

CAS documentation specifically warns that these values are needed very early during startup.

Depending on your deployment method, they might be supplied as environment variables or JVM system properties.

For example:

export CAS_STANDALONE_CONFIGURATION_SECURITY_PSW='strong-decryption-secret'

If you select a non-default Jasypt algorithm, also configure the matching algorithm.

For example:

export CAS_STANDALONE_CONFIGURATION_SECURITY_ALG='YOUR_SELECTED_PBE_ALGORITHM'

Your Jasypt encryption operation and CAS decryption settings must agree on:

  • password;
  • algorithm;
  • provider;
  • iterations;
  • IV behavior where applicable.

Do not place the Jasypt master password inside the same encrypted configuration that depends on it.

That creates a bootstrapping problem and provides little security benefit.


Step 8: Encrypt the JWKS

Using the Jasypt command-line utilities, encrypt the complete JWKS content with the same password and algorithm CAS will use.

Conceptually:

encrypt.sh \
  input="$(cat keystore.jwks.plain)" \
  password="$JASYPT_PASSWORD" \
  algorithm="$JASYPT_ALGORITHM"

The exact Jasypt options depend on your selected algorithm and provider.

The result should be stored as:

{cas-cipher}ENCRYPTED_JWKS_DATA

inside:

/etc/cas/config/keystore.jwks

Do not include the original JSON around the encrypted value unless your selected CAS/JWKS implementation specifically expects that structure.

The goal is for CAS to decrypt the keystore contents at runtime.


Step 9: Configure discovery claims

OIDC discovery tells applications which claims the provider supports.

In cas.groovy:

cas.authn.oidc.discovery.claims="sub,acr,name,preferred_username,family_name,given_name,middle_name,profile,picture,nickname,website,zoneinfo,locale,updated_at,birthdate,email,email_verified,phone_number,phone_number_verified,address,gender,groups"

The important addition for this example is:

groups

Without advertising a custom claim, relying parties may not see it represented correctly in discovery or CAS may not treat it as a recognized custom claim in the expected processing path.

After restart, check:

curl -s \
  https://cas.example.org/cas/oidc/.well-known/openid-configuration |
jq '.claims_supported'

Confirm:

groups

appears.


Step 10: Configure OIDC discovery scopes

Likewise, advertise the scopes that CAS supports:

cas.authn.oidc.discovery.scopes="openid,profile,email,address,phone,offline_access,device_sso,client_configuration_scope,uma_authorization,uma_protection,client_registration_scope,groups"

Again, the relevant custom addition is:

groups

Check the discovery document:

curl -s \
  https://cas.example.org/cas/oidc/.well-known/openid-configuration |
jq '.scopes_supported'

You should see:

[
  "openid",
  "profile",
  "email",
  "...",
  "groups"
]

This is an important part of configuring custom scopes.

CAS documentation explicitly notes that custom scopes and their custom claims must be advertised through OIDC discovery.


Why the groups scope must appear in discovery

Assume the application sends:

scope=openid profile email groups

CAS must know that:

groups

is an accepted scope.

It must also recognize:

groups

as a supported claim.

The relationship becomes:

groups scope
      ↓
OIDC service accepts groups
      ↓
CAS custom scope policy executes
      ↓
directory attribute transformed
      ↓
groups claim returned

Simply mapping an attribute in the JSON service is not enough if the requested custom scope itself is not recognized.

That is why the discovery configuration is important.


Step 11: Consider user-defined scope configuration

CAS also provides:

cas.authn.oidc.core.user-defined-scopes

for globally defining custom scope-to-claim bundles.

For example, conceptually:

groups → groups

may be registered globally.

This is particularly useful when many applications should use the same custom scope.

In this article, however, we will demonstrate a per-service custom scope policy, which gives finer control over exactly how the groups claim is produced for one client.

Whichever method you use, custom scopes and claims should also be represented in discovery.


Step 12: Configure OAuth/OIDC session replication

OIDC builds on CAS’s OAuth support.

In clustered environments, OAuth/OIDC flows may temporarily depend on state stored in the HTTP session.

CAS provides session replication support:

cas.authn.oauth.sessionReplication.replicateSessions="true"
cas.authn.oauth.sessionReplication.cookie.crypto.enabled="true"

The first property allows CAS to replicate OAuth-related session information using its own infrastructure.

The second enables cryptographic protection for the associated session replication cookie.

This becomes particularly important when:

Authorization request
      ↓
CAS node 1
Token or callback processing
      ↓
CAS node 2

without guaranteed load-balancer stickiness.

Important CAS 7.3 / CAS 8 note

The:

cas.authn.oauth.session-replication.replicate-sessions

property is marked as deprecated beginning with CAS 7.3.0.

It remains documented in CAS 7.3 and the CAS 8 documentation, but administrators planning a CAS 8 migration should specifically review the current session-storage and replication model rather than assuming this property will remain indefinitely.

For an existing CAS 7.3.8.3 deployment where OIDC flows require it, the configuration may still be necessary.


Step 13: Complete cas.groovy example

A practical CAS 7.3.8.3 configuration might therefore contain:

cas.authn.oidc.core.issuer="https://cas.example.org/cas/oidc"
cas.authn.oauth.sessionReplication.replicateSessions="true"
cas.authn.oauth.sessionReplication.cookie.crypto.enabled="true"
cas.authn.oidc.discovery.claims="sub,acr,name,preferred_username,family_name,given_name,middle_name,profile,picture,nickname,website,zoneinfo,locale,updated_at,birthdate,email,email_verified,phone_number,phone_number_verified,address,gender,groups"
cas.authn.oidc.discovery.scopes="openid,profile,email,address,phone,offline_access,device_sso,client_configuration_scope,uma_authorization,uma_protection,client_registration_scope,groups"
cas.authn.oidc.jwks.fileSystem.jwksFile="file:/etc/cas/config/keystore.jwks"

You do not need to copy every OIDC property from the CAS configuration catalog.

CAS documentation consistently recommends configuring only the values you actually need to override.

That makes upgrades much easier.


Step 14: Create the OIDC registered service

OIDC clients are CAS registered services.

For a JSON service registry, create a file such as:

/etc/cas/services/example-oidc-1001.json

A basic Authorization Code client looks like:

{
  "@class": "org.apereo.cas.services.OidcRegisteredService",
  "clientId": "example-oidc-client",
  "clientSecret": "{cas-cipher}ENCRYPTED_CLIENT_SECRET",
  "serviceId": "^https://app.example.org/oauth/oidc/callback$",
  "name": "Example OIDC Application",
  "id": 1001,
  "bypassApprovalPrompt": true,
  "evaluationOrder": 1001,
  "scopes": [
    "java.util.HashSet",
    [
      "openid",
      "email",
      "profile",
      "groups"
    ]
  ],
  "supportedResponseTypes": [
    "java.util.HashSet",
    [
      "code"
    ]
  ],
  "supportedGrantTypes": [
    "java.util.HashSet",
    [
      "authorization_code"
    ]
  ]
}

This explicitly restricts the client to:

Authorization Code flow

instead of leaving unnecessary grant and response types available.

CAS documentation recommends explicitly declaring allowed grant and response types because permissive backward-compatible defaults may change in future releases.

You may create an optional attribute release policy for the groups.

"attributeReleasePolicy": {
    "@class": "org.apereo.cas.services.ChainingAttributeReleasePolicy",
    "policies": [
      "java.util.ArrayList",
      [
        {
          "@class": "org.apereo.cas.oidc.claims.OidcCustomScopeAttributeReleasePolicy",
          "order": 1,
          "scopeName": "groups",
          "allowedAttributes": ["java.util.ArrayList", ["groups"]],
          "claimMappings": {
            "@class": "java.util.TreeMap",
            "groups": "groovy { return attributes['memberOf'] ? attributes['memberOf'].collect { it.split(',')[0].replaceFirst('^CN=', '') }.findAll { it.startsWith('test_') } : [] }"
          }
        }
      ]
    ]
  }

Step 15: Understand serviceId

For an OIDC service:

"serviceId": "^https://app.example.org/oauth/oidc/callback$"

represents the allowed redirect URI.

The client will send:

redirect_uri=https://app.example.org/oauth/oidc/callback

CAS matches that value against:

serviceId

Because serviceId supports regular expressions, avoid patterns that are broader than necessary.

Bad:

"serviceId": ".*"

Better:

"serviceId": "^https://app\\.example\\.org/oauth/oidc/callback$"

Or, depending on how the JSON regex is represented in your environment:

"serviceId": "^https://app.example.org/oauth/oidc/callback$"

A narrowly scoped redirect URI reduces the chance of redirect-based attacks.


Step 16: Generate the client secret

Generate a strong random secret using openssl rand.

For example:

openssl rand -base64 48

Or:

openssl rand -hex 32

Example:

f8c61c...random-data...9b25

Do not put the plaintext value directly into the JSON registry:

"clientSecret": "plain-text-secret"

Instead, encrypt it using the same CAS configuration-security system.


Step 17: Encrypt the OIDC client secret with Jasypt

Use the Jasypt CLI with the same master password and algorithm CAS uses.

Conceptually:

encrypt.sh \
  input='THE_OIDC_CLIENT_SECRET' \
  password="$JASYPT_PASSWORD" \
  algorithm="$JASYPT_ALGORITHM"

Store the result as:

"clientSecret": "{cas-cipher}ENCRYPTED_CLIENT_SECRET"

The service registry then contains encrypted data rather than the usable client credential.

Protect the Jasypt master key separately.

If someone obtains both:

service JSON files
+
Jasypt decryption password

the encrypted client secrets can still be recovered.

Encryption is therefore one layer of secret-management defense, not a substitute for filesystem security.


Step 18: Add the custom groups scope

Now add the custom claim policy.

The complete service becomes:

{
  "@class": "org.apereo.cas.services.OidcRegisteredService",
  "clientId": "example-oidc-client",
  "clientSecret": "{cas-cipher}ENCRYPTED_CLIENT_SECRET",
  "serviceId": "^https://app.example.org/oauth/oidc/callback$",
  "name": "Example OIDC Application",
  "id": 1001,
  "bypassApprovalPrompt": true,
  "evaluationOrder": 1001,
  "scopes": [
    "java.util.HashSet",
    [
      "openid",
      "email",
      "profile",
      "groups"
    ]
  ],
  "supportedResponseTypes": [
    "java.util.HashSet",
    [
      "code"
    ]
  ],
  "supportedGrantTypes": [
    "java.util.HashSet",
    [
      "authorization_code"
    ]
  ],
  "attributeReleasePolicy": {
    "@class": "org.apereo.cas.services.ChainingAttributeReleasePolicy",
    "policies": [
      "java.util.ArrayList",
      [
        {
          "@class": "org.apereo.cas.oidc.claims.OidcCustomScopeAttributeReleasePolicy",
          "order": 1,
          "scopeName": "groups",
          "allowedAttributes": [
            "java.util.ArrayList",
            [
              "groups"
            ]
          ],
          "claimMappings": {
            "@class": "java.util.TreeMap",
            "groups": "groovy { return attributes['memberOf'] ? attributes['memberOf'].collect { it.split(',')[0].replaceFirst('^CN=', '') }.findAll { it.startsWith('app-') } : [] }"
          }
        }
      ]
    ]
  }
}

This is where the integration becomes more interesting.


Step 19: Understand the groups claim mapping

Assume CAS resolves an LDAP-style:

memberOf

attribute containing values such as:

CN=app-admins,OU=Groups,DC=example,DC=org
CN=app-users,OU=Groups,DC=example,DC=org
CN=finance-users,OU=Groups,DC=example,DC=org

The Groovy mapping:

groovy {
    return attributes['memberOf']
        ? attributes['memberOf']
            .collect {
                it.split(',')[0]
                    .replaceFirst('^CN=', '')
            }
            .findAll {
                it.startsWith('app-')
            }
        : []
}

transforms those values into:

[
  "app-admins",
  "app-users"
]

The unrelated:

finance-users

group is filtered out.

That is generally preferable to exposing an entire enterprise group-membership attribute to every OIDC application.


Why filter groups per application?

Directory groups frequently reveal far more information than an application needs.

For example, a user might belong to:

Payroll-Administrators
HR-Investigations
VPN-Privileged
Database-DBA
Application-A-Users
Application-B-Users

If Application A needs only:

Application-A-Users

there is no reason to disclose every other group.

Use a service-specific policy:

Directory memberOf
       ↓
CAS filtering
       ↓
Only application-relevant groups
       ↓
OIDC groups claim

This follows the principle of least information disclosure.


Step 20: Why use OidcCustomScopeAttributeReleasePolicy?

The policy:

org.apereo.cas.oidc.claims.OidcCustomScopeAttributeReleasePolicy

ties a set of claims to a specific requested OIDC scope.

In this example:

"scopeName": "groups"

means the policy applies when the application requests:

groups

and:

"allowedAttributes": [
  "java.util.ArrayList",
  [
    "groups"
  ]
]

defines the claim that may be returned.

This is better than releasing the attribute unconditionally when the desired behavior is:

Application requests groups
          ↓
CAS releases groups

rather than:

Application does not request groups
          ↓
CAS releases groups anyway

Step 21: Make sure memberOf exists before mapping it

The Groovy mapping can only transform an attribute that CAS has already resolved.

Before debugging OIDC, verify the authenticated principal actually contains:

memberOf

If your LDAP or attribute repository does not resolve that attribute, this:

attributes['memberOf']

will be empty or absent.

The OIDC configuration cannot manufacture directory membership that CAS never resolved.

The flow is:

Directory
   ↓
CAS attribute repository
   ↓
memberOf
   ↓
OIDC attribute release policy
   ↓
groups

Troubleshoot from left to right.


Step 22: Do standard scopes need explicit attribute policies?

Usually, no.

CAS automatically understands standard OIDC scopes such as:

openid
profile
email
address
phone

and maps them to the standard OIDC claims associated with those scopes.

For example:

email

typically corresponds to:

email
email_verified

while:

profile

covers claims such as:

name
family_name
given_name
preferred_username
locale
picture

Custom scopes such as:

groups

need explicit handling.


Step 23: Remember where claims normally appear

A common OIDC misconception is that every requested claim automatically appears in the ID token.

That is not necessarily how OIDC is intended to work.

CAS documentation notes that claims associated with scopes such as:

profile
email

are generally obtained from the user-info/profile endpoint using the access token.

For an Authorization Code flow:

Authorization Code
      ↓
Token endpoint
      ↓
ID Token + Access Token
      ↓
UserInfo/Profile endpoint
      ↓
Additional claims

Do not immediately force every directory attribute into the ID token because an application fails to call UserInfo correctly.

Prefer standards-compliant client behavior where possible.


Step 24: Restart CAS and inspect discovery

After enabling OIDC and configuring the properties, restart CAS.

Then request:

curl -s \
  https://cas.example.org/cas/oidc/.well-known/openid-configuration |
jq

Check at minimum:

curl -s \
  https://cas.example.org/cas/oidc/.well-known/openid-configuration |
jq '{
  issuer,
  authorization_endpoint,
  token_endpoint,
  userinfo_endpoint,
  jwks_uri,
  scopes_supported,
  claims_supported
}'

Verify:

issuer

matches:

https://cas.example.org/cas/oidc

and confirm both:

groups

appear in:

scopes_supported
claims_supported

Step 25: Inspect the CAS JWKS endpoint

Run:

curl -s \
  https://cas.example.org/cas/oidc/jwks |
jq

You should see at least one public signing key.

Record its:

kid

For example:

{
  "kty": "RSA",
  "kid": "cas",
  "use": "sig"
}

Later, an ID token header may contain:

{
  "alg": "RS256",
  "kid": "cas"
}

The relying party uses:

kid

to select the matching key from the JWKS document.


Step 26: Test the authorization request

A typical authorization request resembles:

https://cas.example.org/cas/oidc/authorize
    ?client_id=example-oidc-client
    &response_type=code
    &redirect_uri=https%3A%2F%2Fapp.example.org%2Foauth%2Foidc%2Fcallback
    &scope=openid%20profile%20email%20groups
    &state=...
    &nonce=...

Normally, your application library creates this URL automatically.

The important values are:

client_id
response_type=code
redirect_uri
scope
state
nonce

The redirect URI must match the registered:

serviceId

pattern.


Step 27: Exchange the authorization code for tokens

The relying party sends the authorization code to the token endpoint.

CAS supports standard client authentication methods, with:

client_secret_basic

documented as the default.

Conceptually:

curl \
  -u 'example-oidc-client:CLIENT_SECRET' \
  -X POST \
  https://cas.example.org/cas/oidc/token \
  -d 'grant_type=authorization_code' \
  -d 'code=AUTHORIZATION_CODE' \
  -d 'redirect_uri=https://app.example.org/oauth/oidc/callback'

A successful response may contain:

{
  "access_token": "...",
  "token_type": "Bearer",
  "expires_in": 28800,
  "id_token": "..."
}

Never put real production OIDC secrets into public shell history or documentation.


Step 28: Decode the ID token

An ID token is a JWT:

header.payload.signature

You can inspect its decoded content with a JWT decoder.

For troubleshooting from Linux, split the token carefully or use an appropriate JWT utility.

Typical claims include:

{
  "iss": "https://cas.example.org/cas/oidc",
  "sub": "user123",
  "aud": "example-oidc-client",
  "iat": 1789750000,
  "exp": 1789753600
}

Depending on scope, response type, and CAS configuration, additional claims may appear in either the ID token or UserInfo response.

Do not publish real tokens.

Even decoded examples can expose:

  • usernames;
  • email addresses;
  • groups;
  • internal client IDs;
  • authentication context;
  • token timestamps.

Step 29: Call the UserInfo/Profile endpoint

Using the access token:

curl \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  https://cas.example.org/cas/oidc/profile

A response might contain:

{
  "sub": "user123",
  "email": "user@example.org",
  "preferred_username": "user123",
  "groups": [
    "app-users",
    "app-admins"
  ]
}

This is where the custom group scope becomes visible to a standards-compliant OIDC application.


Step 30: Verify scope filtering

Repeat authorization without:

groups

For example:

scope=openid profile email

The custom:

groups

claim should not be released through the custom scope policy.

Then test:

scope=openid profile email groups

and confirm the claim appears.

This verifies the entire policy chain:

Requested scope
      ↓
Registered-service allowed scope
      ↓
Custom OIDC scope policy
      ↓
Claim mapping
      ↓
Released claim

Step 31: Understand bypassApprovalPrompt

The example includes:

"bypassApprovalPrompt": true

OIDC normally allows CAS to ask the user whether they approve the scopes requested by an application.

Setting:

bypassApprovalPrompt=true

removes that interactive approval for the service.

That may be appropriate for:

  • trusted first-party applications;
  • tightly controlled enterprise integrations.

It may be less appropriate for:

  • third-party clients;
  • dynamically registered clients;
  • applications requesting sensitive claims.

Consider whether users should explicitly approve disclosure of claims such as:

email
phone
address
groups

before globally bypassing consent.


Step 32: Keep grant types narrow

For a normal server-side web application:

"supportedGrantTypes": [
  "java.util.HashSet",
  [
    "authorization_code"
  ]
]

is usually a sensible baseline.

Likewise:

"supportedResponseTypes": [
  "java.util.HashSet",
  [
    "code"
  ]
]

Avoid enabling grant types merely because CAS supports them.

For example:

password
client_credentials
refresh_token
device_code
token-exchange

serve different purposes.

A registered service should list only what the application actually needs.


Step 33: Consider PKCE

Modern OIDC clients should generally support Proof Key for Code Exchange, or PKCE, particularly when the client cannot strongly protect a secret.

PKCE adds:

code_verifier

and:

code_challenge

to the Authorization Code flow.

The conceptual flow becomes:

Client creates code_verifier
        ↓
Creates code_challenge
        ↓
Authorization request contains challenge
        ↓
CAS issues code
        ↓
Token request supplies verifier
        ↓
CAS confirms verifier matches challenge

Whether PKCE is mandatory depends on the client type and your CAS configuration.

For new integrations, prefer modern OIDC libraries that support PKCE rather than building the authorization flow manually.


Step 34: Configure multiple redirect URIs carefully

Some applications have multiple legitimate callbacks.

For example:

https://app.example.org/oauth/oidc/callback
https://app-dr.example.org/oauth/oidc/callback

You may use a carefully designed regular expression in:

serviceId

but avoid overly broad patterns.

For example:

"serviceId": "^https://app(-dr)?\\.example\\.org/oauth/oidc/callback$"

is safer than:

"serviceId": "^https://.*example\\.org.*"

Redirect URI validation is a security boundary.


Step 35: CAS cluster considerations

For a single CAS node:

OIDC request
     ↓
same JVM/session

is straightforward.

For multiple CAS nodes:

Browser
   |
Load Balancer
   |
+------+------+
|             |
CAS 1       CAS 2

consider:

  • ticket registry replication;
  • OAuth/OIDC session state;
  • load-balancer affinity;
  • shared encryption keys;
  • shared JWKS.

The JWKS point is especially important.

Every CAS node serving the same OIDC issuer must use the same signing keys.

If:

CAS node 1 signs ID token

but:

CAS node 2 publishes different JWKS

a relying party may fail token signature validation intermittently.

CAS documentation explicitly warns clustered deployments to share an identical JWKS.


Step 36: Do not let each CAS node generate a different JWKS

CAS can generate a JWKS if the configured file does not exist.

That is useful during initial setup, but dangerous in an improperly configured cluster.

Imagine:

CAS 1
/etc/cas/config/keystore.jwks
Key A
CAS 2
/etc/cas/config/keystore.jwks
Key B

Both nodes advertise the same issuer:

https://cas.example.org/cas/oidc

but they do not share signing keys.

Users may experience random failures depending on which node:

  • issues the token;
  • serves the JWKS request.

For a cluster, distribute the same protected JWKS to every node or use a supported centralized JWKS storage strategy.


Step 37: CAS 8.x differences to review

When moving from CAS 7.3.8.3 to CAS 8.x, the fundamental configuration remains recognizable.

You will still use:

cas-server-support-oidc
OidcRegisteredService
cas.authn.oidc.core.issuer
cas.authn.oidc.discovery.*
cas.authn.oidc.jwks.*

and CAS 8 continues to document filesystem JWKS storage.

However, explicitly review these areas during the upgrade.

1. Session replication

The OAuth session replication setting was already marked deprecated beginning with CAS 7.3.

Do not design a new CAS 8 architecture around deprecated session-replication behavior without reviewing CAS 8’s current alternatives.

2. JWKS resource syntax

CAS 8 documentation explicitly demonstrates:

file:/etc/cas/config/keystore.jwks

Prefer the explicit resource prefix.

3. Service JSON compatibility

Revalidate:

@class
attributeReleasePolicy
claimMappings
supportedGrantTypes
supportedResponseTypes

against the CAS 8 service schema.

Do not assume every serialized Java type remains forever unchanged.

4. Groovy expressions

If your OIDC claim mapping contains:

groovy { ... }

test it after the upgrade.

Framework, Groovy, and CAS implementation changes can expose assumptions that were harmless in CAS 7.

5. Discovery document

Diff the CAS 7 and CAS 8 discovery documents:

curl -s \
  https://cas7.example.org/cas/oidc/.well-known/openid-configuration |
jq -S . \
> cas7-discovery.json
curl -s \
  https://cas8.example.org/cas/oidc/.well-known/openid-configuration |
jq -S . \
> cas8-discovery.json

Then:

diff -u \
  cas7-discovery.json \
  cas8-discovery.json

This catches OIDC capability changes before applications do.


Step 38: Validate the service registry after upgrading

Before sending real users through CAS 8, verify the service definition loads correctly.

Look for startup errors related to:

OidcRegisteredService
ChainingAttributeReleasePolicy
OidcCustomScopeAttributeReleasePolicy
claimMappings

Then confirm the service appears in CAS’s loaded registry.

A syntactically valid JSON document is not necessarily a valid CAS registered service.


Step 39: Troubleshoot invalid_client

A token request may fail with:

invalid_client

Check:

clientId
clientSecret

in the JSON service.

Make sure the application is using exactly:

example-oidc-client

and that CAS successfully decrypted:

"clientSecret": "{cas-cipher}..."

Also confirm the application’s client authentication method.

CAS documents:

client_secret_basic

as the default method for access-token requests.


Step 40: Troubleshoot invalid_redirect_uri

Check the application request:

redirect_uri

against:

"serviceId": "..."

Remember that CAS treats:

serviceId

as a regular expression.

Test the exact callback URL rather than assuming two visually similar URLs are identical.

Differences might include:

http vs https
hostname
port
path
trailing slash
case

Step 41: Troubleshoot invalid_scope

If CAS rejects:

groups

check three places.

Discovery

cas.authn.oidc.discovery.scopes="...,groups"

Service registration

"scopes": [
  "java.util.HashSet",
  [
    "openid",
    "profile",
    "email",
    "groups"
  ]
]

Custom release policy

"scopeName": "groups"

Also advertise the custom claim:

cas.authn.oidc.discovery.claims="...,groups"

A mismatch between any of these may produce confusing results.


Step 42: Troubleshoot missing groups

If authentication works but:

groups

is absent, check this order:

1. Is memberOf resolved?
2. Does the client request groups?
3. Does the service allow groups?
4. Does discovery advertise groups?
5. Does the custom scope policy execute?
6. Does the Groovy expression return values?
7. Is the application reading UserInfo?

Start by temporarily simplifying the mapping:

groovy {
    return attributes['memberOf'] ?: []
}

If that works, the problem is in the transformation or filtering logic.

Then add the transformation:

.collect {
    it.split(',')[0].replaceFirst('^CN=', '')
}

Then filtering:

.findAll {
    it.startsWith('app-')
}

Debug incrementally.


Step 43: Troubleshoot groups appearing in the wrong place

An application may expect:

groups

inside the ID token even though CAS exposes it through UserInfo.

Before modifying CAS to force the claim into the ID token, check the client documentation.

OIDC normally expects many scope-based claims to be retrieved from the UserInfo endpoint using the access token.

A broken client implementation should not automatically dictate a less standards-compliant identity-provider configuration.


Step 44: Troubleshoot token signature failures

If the application reports:

Unable to verify ID token signature

check:

Issuer

"iss": "https://cas.example.org/cas/oidc"

JWKS URL

From discovery:

jwks_uri

Token header

Look for:

{
  "kid": "..."
}

JWKS

Confirm the same:

kid

exists.

Cluster

Confirm all nodes use the identical:

keystore.jwks

Key rotation

If you rotate JWKS keys, make sure clients have enough overlap to validate tokens signed with the prior key.


Step 45: Troubleshoot CAS failing to load the JWKS

Check:

ls -l /etc/cas/config/keystore.jwks

Confirm:

  • CAS can read the file;
  • the resource URI is correct;
  • Jasypt settings are available before CAS configuration loads;
  • encryption algorithm and password match;
  • file contents have not been truncated.

If using:

cas.authn.oidc.jwks.fileSystem.jwksFile="file:/etc/cas/config/keystore.jwks"

make sure the service account running CAS can access:

/etc/cas/config

and the file itself.


Step 46: Troubleshoot {cas-cipher} values that do not decrypt

If CAS cannot decrypt:

{cas-cipher}...

verify the runtime configuration-security settings.

For example:

env |
grep CAS_STANDALONE_CONFIGURATION_SECURITY

Confirm the same settings were used when the ciphertext was created.

Common mistakes include:

  • wrong Jasypt password;
  • different encryption algorithm;
  • incompatible provider;
  • mismatched iteration count;
  • copied/truncated ciphertext;
  • putting the bootstrap password inside cas.groovy.

Remember:

CAS must build the decryptor
        ↓
before
        ↓
CAS can decrypt cas.groovy/JWKS/service values

Step 47: Test the complete flow before production

A useful deployment checklist is:

1. CAS starts without OIDC errors
2. Discovery endpoint responds
3. Issuer is correct
4. groups appears in scopes_supported
5. groups appears in claims_supported
6. JWKS endpoint responds
7. JWKS does not expose private key material
8. OIDC service loads
9. Authorization request succeeds
10. Authorization code is returned
11. Token exchange succeeds
12. ID token signature validates
13. UserInfo request succeeds
14. email/profile claims are correct
15. groups contains only intended memberships
16. Logout behavior is tested
17. Both CAS nodes behave identically if clustered

Do this before handing the integration to application owners.


Example complete cas.groovy OIDC section

For CAS 7.3.8.3:

cas.authn.oidc.core.issuer="https://cas.example.org/cas/oidc"
cas.authn.oauth.sessionReplication.replicateSessions="true"
cas.authn.oauth.sessionReplication.cookie.crypto.enabled="true"
cas.authn.oidc.discovery.claims="sub,acr,name,preferred_username,family_name,given_name,middle_name,profile,picture,nickname,website,zoneinfo,locale,updated_at,birthdate,email,email_verified,phone_number,phone_number_verified,address,gender,groups"
cas.authn.oidc.discovery.scopes="openid,profile,email,address,phone,offline_access,device_sso,client_configuration_scope,uma_authorization,uma_protection,client_registration_scope,groups"
cas.authn.oidc.jwks.fileSystem.jwksFile="file:/etc/cas/config/keystore.jwks"

Keep the configuration limited to settings your deployment actually requires.


Example complete OIDC JSON service

{
  "@class": "org.apereo.cas.services.OidcRegisteredService",
  "clientId": "example-oidc-client",
  "clientSecret": "{cas-cipher}ENCRYPTED_CLIENT_SECRET",
  "serviceId": "^https://app.example.org/oauth/oidc/callback$",
  "name": "Example OIDC Application",
  "id": 1001,
  "bypassApprovalPrompt": true,
  "evaluationOrder": 1001,
  "scopes": [
    "java.util.HashSet",
    [
      "openid",
      "email",
      "profile",
      "groups"
    ]
  ],
  "supportedResponseTypes": [
    "java.util.HashSet",
    [
      "code"
    ]
  ],
  "supportedGrantTypes": [
    "java.util.HashSet",
    [
      "authorization_code"
    ]
  ],
  "attributeReleasePolicy": {
    "@class": "org.apereo.cas.services.ChainingAttributeReleasePolicy",
    "policies": [
      "java.util.ArrayList",
      [
        {
          "@class": "org.apereo.cas.oidc.claims.OidcCustomScopeAttributeReleasePolicy",
          "order": 1,
          "scopeName": "groups",
          "allowedAttributes": [
            "java.util.ArrayList",
            [
              "groups"
            ]
          ],
          "claimMappings": {
            "@class": "java.util.TreeMap",
            "groups": "groovy { return attributes['memberOf'] ? attributes['memberOf'].collect { it.split(',')[0].replaceFirst('^CN=', '') }.findAll { it.startsWith('app-') } : [] }"
          }
        }
      ]
    ]
  }
}

The example intentionally filters group membership rather than returning every value from:

memberOf

to the client.


OIDC security recommendations for CAS

For production OIDC integrations:

  • use HTTPS for the CAS issuer;
  • keep redirect URI patterns narrow;
  • encrypt client secrets;
  • protect the Jasypt bootstrap password separately;
  • protect the filesystem JWKS as private key material;
  • use identical JWKS across CAS cluster nodes;
  • explicitly configure grant and response types;
  • release only required claims;
  • filter group memberships per application;
  • use Authorization Code flow for normal web applications;
  • prefer PKCE-capable client libraries;
  • review approval/consent before setting bypassApprovalPrompt;
  • avoid forcing unnecessary attributes into ID tokens;
  • verify discovery and JWKS after every upgrade;
  • test custom Groovy mappings after CAS upgrades.

Frequently asked questions

Can Apereo CAS act as an OpenID Connect provider?

Yes.

Include:

cas-server-support-oidc

in the CAS overlay and configure the OIDC issuer, JWKS, and registered OIDC clients.

What is the CAS OIDC discovery URL?

For a typical /cas context:

https://cas.example.org/cas/oidc/.well-known/openid-configuration

Applications should use discovery rather than hard-coding individual OIDC endpoint URLs.

What is OidcRegisteredService?

It is the CAS registered-service type used to define an OpenID Connect relying party.

Example:

{
  "@class": "org.apereo.cas.services.OidcRegisteredService"
}

Where is the OIDC client redirect URI configured?

In:

"serviceId": "^https://app.example.org/oauth/oidc/callback$"

The value is a CAS service pattern matched against the requested OIDC redirect URI.

How do I encrypt an OIDC client secret?

Encrypt it with CAS/Jasypt configuration security and store:

"clientSecret": "{cas-cipher}..."

rather than plaintext.

How do I encrypt the CAS OIDC JWKS?

CAS supports configuration-security encryption for filesystem JWKS contents.

Encrypt the JWKS content with the same Jasypt configuration CAS uses and store the encrypted value using the:

{cas-cipher}

prefix.

What is the JWKS used for?

CAS uses its JSON Web Key Set to sign and potentially encrypt OIDC token material.

Applications retrieve the public portion through:

/oidc/jwks

and use it to verify token signatures.

Does every CAS node need the same JWKS?

Yes, when nodes represent the same OIDC issuer.

Different JWKS files across cluster nodes can cause intermittent token-validation failures.

How do I add a groups claim to CAS OIDC?

A practical design is:

1. Advertise groups as a supported scope.
2. Advertise groups as a supported claim.
3. Add groups to the OIDC registered service scopes.
4. Add an OidcCustomScopeAttributeReleasePolicy.
5. Map a resolved directory attribute such as memberOf to groups.

Why was adding groups to discovery necessary?

CAS documentation requires custom scopes and claims to be advertised through OIDC discovery so they are recognized as supported during claim processing.

Can I transform LDAP memberOf into shorter group names?

Yes.

A Groovy claim mapping can convert:

CN=app-admins,OU=Groups,DC=example,DC=org

into:

app-admins

and filter unrelated group memberships.

Should groups be in the ID token?

Not necessarily.

OIDC clients should normally retrieve many scope-based claims from UserInfo using the access token.

Do not force every claim into the ID token unless the integration specifically requires it and you understand the interoperability tradeoff.

Should I enable every OIDC scope?

No.

Allow only the scopes an application requires.

For example:

[
  "openid",
  "profile",
  "email",
  "groups"
]

may be sufficient.

Should I enable every grant type?

No.

For a traditional server-side web application, explicitly allowing:

authorization_code

may be all that is needed.

Is the CAS OAuth sessionReplication property required?

It depends on your deployment.

It can be useful in clustered OAuth/OIDC deployments where session state needs to survive movement between nodes.

However, CAS marks the setting deprecated beginning with 7.3.0, so review the current CAS 8 session-storage model when upgrading.

Does this configuration work on CAS 8?

The primary OIDC architecture does.

CAS 8 continues to use:

cas-server-support-oidc
OidcRegisteredService
OIDC discovery
OIDC JWKS
OIDC scopes and claims

but service definitions, deprecated settings, Groovy mappings, and configuration-property names should always be retested against the specific CAS 8 release being deployed.


CAS 7.3.8.3 to CAS 8.x migration checklist

Before migrating an OIDC provider from CAS 7.3.8.3:

[ ] Build CAS 8 with cas-server-support-oidc
[ ] Confirm issuer configuration
[ ] Reuse or safely migrate the existing JWKS
[ ] Do not generate a new signing key accidentally
[ ] Verify Jasypt decryption
[ ] Load OIDC JSON services
[ ] Validate encrypted client secrets
[ ] Test Groovy claim mappings
[ ] Confirm groups in discovery scopes
[ ] Confirm groups in discovery claims
[ ] Compare CAS 7 and CAS 8 discovery documents
[ ] Verify JWKS output
[ ] Verify ID token signatures
[ ] Verify UserInfo claims
[ ] Test Authorization Code flow
[ ] Review deprecated session replication properties
[ ] Test clustered behavior
[ ] Test logout

Preserving the OIDC signing keys is particularly important.

If CAS 8 starts with an entirely new JWKS without a planned rotation, applications may be unable to validate tokens issued during the transition.


Related Mister PKI articles

  • Learn more about CAS logout behavior in Apereo CAS SLO.
  • Review certificate fundamentals in What Are SSL Certificates?.
  • Use the OpenSSL Commands Guide when generating or troubleshooting cryptographic keys and certificates used alongside CAS.

External references

  • Apereo CAS 7.3 OpenID Connect Provider documentation
  • Apereo CAS 8.0 OpenID Connect Provider documentation
  • Apereo CAS 8.0 JWKS storage documentation
  • Apereo CAS OIDC scope-based claims documentation
  • Apereo CAS OIDC claim-release documentation
  • Apereo CAS OIDC client documentation
  • Apereo CAS configuration security documentation
  • OpenID Connect Core
  • OpenID Connect Discovery

Conclusion

Configuring Apereo CAS as an OpenID Connect provider requires more than simply adding:

cas-server-support-oidc

A complete deployment needs:

OIDC issuer
     ↓
JWKS signing keys
     ↓
Discovery scopes and claims
     ↓
Registered OIDC client
     ↓
Encrypted client secret
     ↓
Attribute/claim release
     ↓
Authorization Code flow
     ↓
Token validation

For a custom groups claim, the critical pieces are:

cas.authn.oidc.discovery.claims="...,groups"
cas.authn.oidc.discovery.scopes="...,groups"

combined with:

"scopes": [
  "java.util.HashSet",
  [
    "openid",
    "email",
    "profile",
    "groups"
  ]
]

and an:

OidcCustomScopeAttributeReleasePolicy

that converts the application’s required directory memberships into a constrained:

"groups": []

claim.

That approach keeps the OIDC integration standards-based while allowing CAS administrators to control exactly which identity information each relying party receives.

For CAS 7.3.8.3, this architecture works well with cas.groovy, a JSON service registry, Jasypt-protected client secrets, and an encrypted filesystem JWKS.

When moving to CAS 8, the same overall OIDC model remains, but treat the upgrade as an identity-provider migration: validate deprecated properties, preserve signing keys, retest registered services and Groovy mappings, and compare the OIDC discovery document before directing production clients to the new version.

Apereo CAS,  Identity Management,  OIDC

Reader Interactions

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Primary Sidebar

Popular Posts

PKCS12

openssl s_client

Keytool

Keytool list

ECDSA vs RSA

OpenSSL

PKCS7

Certificate Decoder

PKI for DevOps Engineers – Free Training Series

PKI for DevOps Training Hub

Lesson 1 – How TLS and PKI Work

Lesson 2 – Understanding X.509 Certificates

Lesson 3 – Certificate Chains Explained

Lesson 4 – Debug TLS with OpenSSL

Lesson 5 – Verify Certificate Chains

Lesson 6 – Creating CSRs with OpenSSL

Lesson 7 – Working with PKCS12 Certificates

Lesson 8 – Java Keystores and keytool

Lesson 9 – Certificate Expiration Monitoring

Lesson 10 – Automating Certificate Renewal

Lesson 11 – Common TLS Errors

Lesson 12 – PKI Architecture for DevOps

Recent Posts

  • Apache mod_auth_cas: Protect Web Applications with Apereo CAS and Docker
  • Apache mod_auth_mellon: Configure SAML SSO & Docker
  • Configure OpenID Connect (OIDC) in Apereo CAS 7.3 and 8
  • Certificate Ownership Is an Operating Model Problem, Not a Tooling Problem
  • OpenSSL dgst: Create Checksums and Verify Digital Signatures

Footer

  • Twitter
  • YouTube
Mister PKI

Practical security engineering for the systems that establish trust.

Learn OpenSSL Keytool Free training
Work together PKI consulting CAS consulting Sponsored contributions
Site About Support Mister PKI Cookie policy Search
© 2026 Mister PKI Secure systems. Clear guidance.