cas-auth
Authenticates requests against a CAS (Central Authentication Service) server. The node runs in one of two modes:
- Stateless ticket validation (default). When a request carries a CAS
ticketquery parameter, the plugin validates it against the CAS server's/serviceValidateendpoint and, on success, attaches the authenticated user to the request. This is the pre-existing behavior and is unchanged. - Interactive SSO login (opt-in). Configure a session secret to enable the full browser login flow: unauthenticated browsers are redirected to the CAS
/loginendpoint, the returned ticket is consumed at the callback, and the authenticated user is sealed into an encrypted client-side session cookie. Subsequent requests authenticate straight from the cookie — no server-side session store is required.
With no session secret configured the node behaves exactly as the stateless ticket validator described above. Setting session.secret (or session_secret) switches on the interactive flow.
Configuration
| Key | Type | Default | Description |
|---|---|---|---|
idp_uri | string | — (required) | CAS server base URI. Validation goes to <idp_uri>/serviceValidate; interactive login goes to <idp_uri>/login. |
service | string | — | Service URL passed to /serviceValidate and /login. When omitted it is derived from the request as scheme://host/path. Because CAS requires the validated service to match the login service, set this explicitly whenever the gateway sits behind a proxy. |
ticket_param | string | ticket | Query parameter carrying the CAS service ticket. |
ssl_verify | boolean | true | Verify the CAS server's TLS certificate. |
timeout_ms | number | 3000 | Whole-call deadline for the validation callout. |
Interactive-mode keys
Setting a session secret enables the interactive login flow.
| Key | Type | Default | Description |
|---|---|---|---|
session_secret / session.secret | string | — | Secret used to encrypt+authenticate the session cookie. Presence enables interactive mode. A key is derived from it (SHA-256), so any length is accepted; the same value must be configured on every gateway instance. |
session.storage (or session_storage) | string | cookie | cookie seals the session into the encrypted browser cookie. redis shrinks the cookie to a bare random 128-bit id and stores the sealed payload server-side in the named session.store, enabling listing/revocation via the Admin API (GET/DELETE /api/sessions). |
session.store (or session_store) | string | — | Name of a declared stores: entry (redis/valkey). Required when session.storage: redis; resolved at policy-compile time — an unknown name fails compilation, never a request. |
session.cookie.name (or session_cookie_name) | string | cas_session | Session cookie name. |
session.cookie.path (or session_cookie_path) | string | / | Session cookie Path. Scope it to the app's subpath (e.g. /app_a) so nodes on different subpaths keep independent sessions. |
session.cookie.lifetime (or session_cookie_lifetime) | number (seconds) | 3600 | Session cookie lifetime (also the sealed payload's expiry). |
logout_path | string | — | Optional. A request to this path clears the session cookie and redirects to /. |
# Stateless ticket validation (unchanged default)
- id: auth
type: cas-auth
config:
idp_uri: https://cas.example.org/cas
service: https://app.example.org/
ssl_verify: true
# Interactive SSO login
- id: auth
type: cas-auth
config:
idp_uri: https://cas.example.org/cas
service: https://app.example.org/
session:
secret: ${CAS_SESSION_SECRET}
cookie:
name: cas_session
lifetime: 3600
logout_path: /logout
idp_uri is required; a missing or blank value is rejected at config load.
Behavior
Stateless mode (no session secret)
- The CAS ticket is read from the
ticket_paramquery parameter. A missing ticket rejects immediately. - The plugin calls
GET <idp_uri>/serviceValidate?ticket=<ticket>&service=<service>, whereserviceis the configured value or the request-derivedscheme://host/path. - The response is parsed for the authenticated user. Both the default CAS 2.0 XML (
<cas:authenticationSuccess>/<cas:user>, with or without thecas:prefix) and the CAS 3.0 JSON (serviceResponse.authenticationSuccess.user) formats are supported.
On success the context passes through the success port, with the username exposed as context.message["user"] / context.message["user_id"] and injected onto the request as the X-CAS-User header.
Otherwise the outcome depends on whether CAS gave a verdict:
- Missing ticket, or an authentication-failure (or unparseable)
/serviceValidatebody — CAS said no, so this is a deliberate rejection: thedeniedport, withcontext.response.status_code = 401and body{"error": "unauthorized", "message": "<reason>"}. - Callout failure (CAS unreachable, timed out) or a non-200
/serviceValidatereply — no verdict was obtained, so this is an infrastructure failure: theerrorport, error codeCAS_AUTH_PROVIDER_ERROR. The prepared response is a502{"error": "provider_error", "message": "<reason>"}with no challenge header — a CAS outage must not read as a refused ticket.
Before v0.8 this provider-failure response reused the denied shape (401 {"error": "unauthorized"}), so an unreachable CAS server was indistinguishable from a refused ticket. It is now the shared 502 {"error": "provider_error", "message": "<reason>"} response with no challenge header, the same shape every provider-backed auth plugin prepares (openid-connect, cas-auth, ldap-auth, authz-keycloak, authz-casdoor). Match on the error port / the error code, or on the 502, instead of the old status.
Interactive mode (session secret set)
Each request is resolved through three branches:
- Valid session cookie. If the
<session.cookie.name>cookie opens successfully, the sealed username is attached (context.message["user"]/["user_id"]and theX-CAS-Userheader) and the request continues through the success port. - Callback (ticket present). A request carrying the CAS
ticketis validated via/serviceValidate(same logic as stateless mode). On success the username is sealed into a fresh session cookie and the browser is 302-redirected to the ticket-free service URL with aSet-Cookie, exiting on theredirectport. On failure the request is rejected with401on thedeniedport. - No session, not a callback. The browser is 302-redirected to
<idp_uri>/login?service=<service-url>to begin CAS login, exiting on theredirectport. (CAS returns the ticket to the same service URL, so no flow cookie is needed.)
If logout_path is configured and the request path matches, the session cookie is deleted and the browser is redirected to / on the redirect port.
Server-side sessions (session.storage: redis)
Setting session.storage: redis (+ session.store: <name>) moves the sealed session payload into the named store; the browser only ever holds a bare 128-bit id. Sessions become listable and revocable through the Admin API (GET/DELETE /api/sessions) — storage: cookie sessions (the default) remain unrevocable by design, since nothing server-side tracks them. A session-store failure on any operation (open, establish, revoke) never falls back to 401: it exits through the ordinary error port as a 503 (error code SESSION_STORE_ERROR), because treating a store outage as "logged out" would just redirect the user into a CAS login loop the store also can't complete. Unlike openid-connect/authz-casdoor, cas-auth has no separate transient flow cookie — CAS returns the ticket directly to the service URL — so session.storage only ever affects the one session cookie.
Redirect wiring (important)
Every 302 in interactive mode (login redirect, post-callback redirect, logout) exits through the dedicated redirect output port, carrying the prepared 302 response. This follows the same convention as the standalone redirect node. Wire the node's redirect edge to client.in so the redirect (and its Set-Cookie) reaches the browser; wire denied to client.in too (or a custom denial handler) for deliberate rejections; wire the success edge onward to the upstream for authenticated requests.
Ports
cas-auth declares four output ports:
| Port | When it fires |
|---|---|
success | The request is authenticated (valid ticket, or a valid session cookie in interactive mode). |
denied | A deliberate 401 is prepared: no ticket at all, or the CAS server answered and refused the ticket. |
redirect | A 302 browser move is prepared — login, post-callback, or logout. Interactive mode only, but the port is always declared. |
error | The ticket-validation callout failed: the CAS server was unreachable, timed out, or answered /serviceValidate with a non-200. The node never obtained a verdict, so this is not a denial. Error code CAS_AUTH_PROVIDER_ERROR; the client-visible response is a 502 provider_error, not the 401 denied shape. In session.storage: redis mode, a session-store failure also exits here, as a 503 with error code SESSION_STORE_ERROR. |
Like success, denied and redirect are both mandatory ports: the policy compiler rejects any policy that leaves either unwired, even in stateless mode where redirect is never actually taken. Wire both straight to client; error is optional and falls back to the policy catch-all:
edges:
- from: cas-auth.success
to: upstream.in
- from: cas-auth.denied
to: client.in
- from: cas-auth.redirect
to: client.in
- from: cas-auth.error # optional; omit to use the policy catch-all
to: error-handler.in
Session cookie attributes
The session cookie is set with Path=<session.cookie.path> (default /), HttpOnly, SameSite=Lax, and Max-Age=<session.cookie.lifetime>. Secure is added only when the request scheme is https, so plain-HTTP local development works; run behind HTTPS in production so the cookie is marked Secure.
Deviations / Limitations
- Interactive login is now supported via an encrypted, authenticated client-side session cookie (AES-256-GCM) — no server-side session store is required, and any gateway instance sharing the secret can open any cookie, so this works across a horizontally-scaled deployment.
- No server-side session revocation before expiry — in
session.storage: cookiemode (the default). Because those sessions live entirely in the client cookie, there is no way to invalidate an individual one before itslifetimeelapses (short of rotating the secret, which invalidates all sessions). Use short lifetimes, or switch tosession.storage: redisfor revocation via the Admin API (/api/sessions). - No CAS single-logout (SLO) callback. The IdP-initiated back-channel logout POST is not handled;
logout_pathperforms a simple local cookie clear + redirect only (plus a storedeletein redis mode). - No ticket/proxy-ticket refresh. There is no renewal handling in v1, in either storage mode; when the session expires the user is redirected through CAS login again.
Errors
The node returns the Context with an error, so the graph engine routes through the error port and appends the error to context.errors. The status below is the one prepared on context.response; wire error to client (or an error-handler) for the caller to see it.
| Code | Status | When |
|---|---|---|
CAS_AUTH_PROVIDER_ERROR | 502 | Ticket validation against the CAS server failed (transport error or an unusable reply). |
SESSION_STORE_ERROR | 503 | The redis session store could not be read or written (session.storage: redis). A store failure is never a silent 401: it always surfaces here. |