cas-oauth-oidc
Skill11k starsChanged yesterdayApache-2.0View on GitHub
Review and change CAS OAuth2/OIDC protocol behavior: token endpoint error mapping, RFC 8707 resource indicators, discovery metadata, DPoP (RFC 9449) and certificate-bound tokens (RFC 8705), client ID metadata documents, device user code approval. Use when touching support/cas-server-support-oauth* or support/cas-server-support-oidc* outside verifiable credentials; for client authentication, access-token-protected endpoints, the authenticated profile and the login handshake, use cas-oauth-oidc-authentication.
- Reads credentials
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 1 is distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 40
Every compactor of an OAuth ticket that carries resources (code, access token, refresh token, device code, pushed authorization request) …
- Very specific: line 51
Do not assert that encoding a JWT twice gives the same string.
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
OAuth and OpenID Connect
OAuth/OIDC token endpoint error mapping
OAuth20AccessTokenEndpointControllerturns failures into OAuth errors in two places. A validator returningfalseisinvalid_grant, but a validator that throws lands in the generic catch asinvalid_request. So a validator must catch its own lookups:TicketRegistry.getTicket(id, type)throwsInvalidTicketExceptionfor a missing ticket rather than returningnull. Exceptions from extractors and generators go throughACCESS_TOKEN_RESPONSE_EXCEPTIONS, which is keyed by the exact class name (subclasses do not match) and defaults toinvalid_grant.- Grant-neutral exceptions in
org.apereo.cas.support.oauth.validator.tokenmap to the RFC 8628 / CIBA Core 1.0 polling errors:OAuth20AuthorizationPendingException→authorization_pending,OAuth20ExpiredTokenException→expired_token. Use these for any new polling grant rather than the device-code ones (UnapprovedOAuth20DeviceUserCodeException,ThrottledOAuth20DeviceUserCodeApprovalException,InvalidOAuth20DeviceTokenException→invalid_grant). The CIBA validator therefore lets expired and unapproved requests through; the extractor reports them. TicketRegistry.getTicket(id)andgetTicket(id, type)delete an expired ticket and return nothing, so expired and unknown look the same. To reportexpired_token, look the ticket up withgetTicket(id, _ -> true), checkisExpired(), and delete it yourself. That works only while the registry still holds the ticket: the registry cleaner, or a store with native TTL such as Redis, removes it, and from then on it is reported as unknown. Scenarios that assertexpired_tokensetcas.ticket.registry.cleaner.schedule.enabled=false.OidcConsentApprovalViewResolverandprompt=consent: the consent view's Allow link is the original authorize URL plusbypass_approval_promptand the approval key, so it still carriesprompt=consent. Under that prompt, onlyisConsentApprovalSubmitted(a valid approval key) may let the request through; the service-level or global bypass may not. Returning a plainfalsethere loops the consent screen forever.
OAuth resource indicators (RFC 8707)
- Resource indicators live on the token (
OAuth20Token#getResources/setResources;BaseOAuth20Token, the device code and the CIBA request keep them). The audience of a token has one rule,OAuth20Utils.determineAudience, used by the JWT access token and by introspection (whose generators receive the client's registered service): the token's fixed audience (OAuth20Token#getAudience, set by a token exchange to its audience or resource) or else the serviceaudience(client id when undefined) plus the resources. The exchanged token never inherits the subject token's resources;oidc-token-exchangeasserts this with a subject token issued for a resource. - Both
allowedResources(service and token exchange policy) are regular expressions matched byRegisteredServiceOAuthResourceMatcher: every requested resource must match one in full, case-sensitively. A token exchange checks the policy (which also requires a resource when it defines patterns) and, when defined, the service's ownallowedResources. Rejected resources are logged at DEBUG only. oauth2-login-devicecodeandoidc-login-ciba(poll) cover resources,invalid_targetand the introspected audience for the device and CIBA flows.- A rejected resource is
invalid_targeteverywhere (RFC 8707): validators and extractors throwOAuth20InvalidTargetException(useOAuth20Utils.verifyRequestedResources), which the token endpoint maps in both its validation and extraction phases; the authorization endpoint and CIBA write the error themselves. - Resources are given to the code, access token, refresh token and device code factories when the ticket is created
(the
createoverloads that takeresources; the older signatures delegate with none). Do not set them afterwards. - Consent: the approval key covers the requested resources (appended only when there are some, so earlier remembered
decisions stay valid), and the OAuth and OIDC consent views list them (
screen.oauth.confirm.resources*, every bundle).oauth2-login-authzcodecovers consent, token endpoint narrowing, refresh andinvalid_targetend to end. - At the token endpoint, a code or refresh token grant may narrow the access token to some of its resources
(
OAuth20Utils.narrowRequestedResources; the service'sallowedResourceswhen the grant carries none); the refresh token keeps all the resources of its grant (determineRefreshTokenResources). - Every compactor of an OAuth ticket that carries resources (code, access token, refresh token, device code, pushed authorization request) must write and restore them, and the access token compactor its fixed audience; a new token field is a compact layout change (see the stateless registry section).
- Request objects may carry list-valued claims such as
resourceas a single string; the parameter resolver readsString[]and collection claims throughgetJwtRequestClaimValues, so do not callgetStringArrayClaimdirectly. - The login round-trip does not lose request parameters: after
callbackAuthorize, the callback restores the saved original/authorizeURL with every value; the callback URL's own parameters only feed the login flow.
OpenID Connect discovery metadata
- Discovery metadata is a contract, not a description: RFC 8414 makes some entries mandatory because of what others advertise.
token_endpoint_auth_signing_alg_values_supportedMUST be present whenprivate_key_jwtorclient_secret_jwtis intoken_endpoint_auth_methods_supported(and MUST NOT containnone). When adding an entry to one list, check whether the specification makes a second entry mandatory as a result. - Nothing in CAS's own test suite reads this metadata as a client would, so missing entries stay invisible until a real relying party parses it. Treat a third-party client's complaint about metadata as a CAS defect until the specification says otherwise.
- Do not assert that encoding a JWT twice gives the same string. The OIDC test
keystore.jwksholds an RSA key marked"use": "enc"and an EC key; only the EC key signs, and ECDSA (like RSA-PSS and any JWE) is randomized. Compare decoded values or headers, and hash the token actually issued (IdTokenGenerationContext.encodedAccessToken) when checkingat_hash.
Sender-constrained tokens (DPoP)
- CAS mints DPoP-bound tokens but the protected-resource half of RFC 9449 is thin.
BaseOAuth20Controller.getAccessTokenFromRequestis the single place every protected resource resolves a token from, and it parses the authorization scheme by name; a token type CAS is willing to issue has to be listed there or the token is silently unusable, reported as[access_token]: [null]and thenINVALID_TICKET. - The two Nimbus verifiers are not interchangeable.
DPoPTokenRequestVerifierbelongs to the token endpoint and checkshtm,htuandjti;DPoPProtectedResourceRequestVerifierbelongs to every other endpoint and additionally binds the proof to the token throughathand to the confirmation recorded at issuance.OAuth20ProofOfPossessionValidatornow has one method for each:validateandvalidateProtectedResourceRequest. A resource endpoint that calls the wrong one appears to work, becausehtustill matches, while checking nothing that matters. athhashes the access token as the client presented it, not its decoded identifier. PassgetAccessTokenFromRequest(...).getKey(), nevergetValue().- Before reporting a gap here, read the endpoint. The userinfo endpoint was already doing the resource verification correctly in an overridden
validateAccessToken, and the defect worth reporting was the redundant second call beside it -- the opposite shape from the one first assumed. Grepping for the validator bean found the wrong call and missed the right one. - Identifiers that arrive on an unauthenticated request parameter are not identities. Under grants that carry their own proof of authorization -- the OpenID4VCI pre-authorized code above all --
client_idis whatever the wallet felt like sending. Resolve from the authenticated profile, then the token, then the parameter. - DPoP nonces (RFC 9449 sections 8 and 9) are transient session tickets, like attestation challenges, through
BaseTransientSessionNonceService(one marker property per kind, so a challenge is never accepted as a nonce). Nimbus treats an empty set of accepted nonces as "anonceclaim is prohibited", so a verifier must be handed the proof's own nonce (OAuth20DPoPNonceService.getAcceptedNonces) and CAS checks it afterwards (isAccepted); the protected resource verifier's single-Nonceoverload withnullprohibits the claim too. On failure the fresh nonce goes on the servlet response beforeInvalidDPoPNonceExceptionis thrown, and each endpoint maps it:400 use_dpop_nonceat the token endpoint and in combined mode,OAuth20Utils.useDPoPNonceResponse(algs)(401+WWW-Authenticate: DPoP error="use_dpop_nonce") at resources and Heimdall. The validator runs before the authorization code is consumed, so the client retries with the same code. dpop_jkt(RFC 9449 section 10) is an authentication attribute of the authorization request, likestateandnonce:OAuth20AuthorizeEndpointController.bindProofOfPossessionKeyadds it, the pushed authorization request keeps all authentication attributes and merges them into the authorization at/authorize, and the stateless code compactor retains it explicitly. ADPoPheader at PAR goes throughOAuth20ProofOfPossessionValidator.validateKeyBinding, which verifies without touching the profile (validatecopies the proof's claims onto it), and its thumbprint is passed on as a request attribute. The token endpoint compares the code'sdpop_jktwith theDPoPConfirmationthe validator put on the profile.dpop_bound_access_tokens(RFC 9449 section 5.2) isdpopBoundAccessTokenson the service. With noDPoPheader,validateresolves the client as it would for a proof (profile, then token, thenclient_id) and throwsInvalidDPoPProofException, which the token endpoint answers with400 invalid_dpop_proofbefore any grant is redeemed. Grants with no authenticated client profile (device code polling, JWT bearer) callvalidateTokenRequest(request, response, service)instead, which hands the proof and its thumbprint to the extractor as request attributes;BaseAccessTokenGrantRequestExtractorlets them win over the profile's, which may be a stale one kept in the session.DPoPTokenRequestVerifierrequireshtmto bePOST, so a device poll byGETcannot be bound. The device token generator rebuilds its request context, so a new context field must be copied there too. Do not wrap a call that throwsInvalidDPoP*Exceptionin jOOλUnchecked: it comes out asUncheckedException, the endpoint's catch misses it, and the client getsinvalid_request.OidcJwtAuthenticatorverifies aclient_secret_jwtassertion with the application's unexpired client secrets (SpEL, thenoauthRegisteredServiceCipherExecutor, UTF-8 bytes as the HMAC key per OIDC Core 10.1) whentokenEndpointAuthenticationMethodisclient_secret_jwtor blank, and still adds thejwkskeys (a raw secret there is the deprecated fallback). An HMAC key shorter than the algorithm's hash output is skipped inisVerificationKeyUsablewith a warning naming the application; jose4j would reject it anyway, only with a generic warning. Puppeteer:oidc-login-jwt-hmac-metadata-corscoversclientSecretwithclient_secret_jwt,oidc-login-jwt-rsacoversprivate_key_jwt; keep each service's method matching how its script signs.- A DPoP-bound access token is accepted at a protected resource only as
Authorization: DPoP <token>(RFC 9449 section 7.2).validateProtectedResourceRequestchecks the scheme itself, before the proof, and throwsDPoPBoundAccessTokenDowngradeException; the profile and VC endpoints answer401 invalid_tokenthroughBaseOAuth20Controller.unauthorized, whose challenge uses the scheme the client sent.getAccessTokenFromRequestprefers theaccess_token/tokenparameters over the header, so the check compares the presented token with the header's. Tests and puppeteer scenarios that present a bound token at a resource must send it in that header. - Refresh tokens of public clients (
OAuth20Utils.isPublicClient: no secret, auth method unset ornone) carryDPoPConfirmationin their authentication (OAuth20DefaultTokenGenerator.bindRefreshTokenToProofOfPossessionKey). The refresh grant's authentication is the refresh token's, so its access tokens inherit the confirmation;BaseOAuth20TokenRequestValidator.verifyBoundProofOfPossessionKey(token, attribute, manager)makes the proof match first, for codes (dpop_jkt) and refresh tokens alike. A refresh token built from a ticket-granting ticket alone has no authentication, so read it null-safely. A public client redeems a code only with PKCE. - Stateless compactors keep only the authentication attributes they are told to retain.
DPoPConfirmationis retained for access and refresh tokens (dpop_jktfor codes); without it a DPoP-bound token expands as a plain bearer token. A new binding attribute must be added there too. - Certificate-bound tokens (RFC 8705):
OAuth20X509Authenticatorleaves the certificate thumbprint (x509_digest) on the client's profile; the extractor copies it toAccessTokenRequestContext.x509CertificateDigestand the token generator puts it on the access token's authentication, so every grant is bound, not onlyclient_credentials(whose principal is the client profile).OAuth20Utils.getBoundCertificateThumbprintandisBoundCertificatePresented(thejakarta.servlet.request.X509Certificaterequest attribute, as pac4j uses at the token endpoint) are shared byOAuth20Utils.verifyProofOfPossessionand Heimdall. A test or puppeteer call that presents a certificate-bound token at a resource must also present the client certificate (requestAttrin MockMvc, thehttpsAgentargument ofcas.doPost). tls_client_certificate_bound_access_tokens(RFC 8705 section 3.4) istlsClientCertificateBoundAccessTokenson the service, enforced next todpopBoundAccessTokensinDefaultOAuth20ProofOfPossessionValidator(validatefor profile-authenticated grants,validateTokenRequestfor device code polling and the JWT bearer grant): the request's authenticated profile must be this client's and carryx509_digest, orCertificateBoundAccessTokenRequiredExceptionbecomes400 invalid_requestbefore any grant is redeemed. A new service flag also belongs in dynamic registration (request, translator, response utils), the Palantir switch list inservicestab.htmland the MCPCasOidcTools.OidcClientsummary.- The validator owns the header checks of RFC 9449 section 4.3.
getProofOfPossessionHeaderrefuses a secondDPoPheader (HttpServletRequest.getHeader, like pac4j'sgetRequestHeaderbefore it, silently returns the first), andgetSignedProofOfPosessionJwtturns a parse failure intoInvalidDPoPProofException, so a value that is not a JWS isinvalid_dpop_proofrather thaninvalid_request. Read the header through the former, never withgetHeader(DPOP); PAR has no check of its own any more. - Protected resources map proof failures in one place,
OAuth20Utils.verifyProofOfPossession(called byBaseOAuth20Controller.verifyProofOfPossessionfor profile/userinfo and VC, and byOAuth20AccessTokenAuthenticationInterceptor):use_dpop_nonce,invalid_tokenfor a downgrade,invalid_dpop_prooffor anything else, each a401with a challenge. ADPoPchallenge carriesalgs(RFC 9449 section 7.1) fromOAuth20ProofOfPossessionValidator.getAcceptedSigningAlgorithms(), which is the discoverydpop_signing_alg_values_supportedin order:unauthorizedadds it whenever the scheme isDPoP, anduseDPoPNonceResponseand Heimdall'sunauthenticatedtake it as an argument.
Client ID metadata documents
OidcClientIdMetadataDocumentResolver(DefaultOidcClientIdMetadataDocumentResolver) runs at the top ofOidcHandlerInterceptorAdapter.preHandlefor every OIDC endpoint with aclient_idparameter: anhttps://client id is fetched, checked and saved as anOidcRegisteredService(propertyoidcClientIdMetadataDocument, refresh instant inoidcClientIdMetadataDocumentRefresh, expiration = last fetch + retention with delete-when-expired, evaluation orderInteger.MAX_VALUE) so every other client lookup works unchanged. Off by default (cas.authn.oidc.registration.client-id-metadata-document.enabled).- It reuses the DCR translator and then undoes what does not fit: no client secret, exact
\Q...\Eredirect URIs instead of the translator's|-joined regex, empty post-logout URIs (the translator maps them tologoutUrl, which CAS calls for back-channel logout). Metadata CAS would fetch server-side (jwks_uri,sector_identifier_uri, back-channel endpoints) is refused, because only the document fetch is SSRF-checked. DefaultOidcClientIdMetadataDocumentFetcherresolves the host, requires every address to be public (isPublicAddress) and pins them withHttpExecutionRequest.resolvedAddresses, with redirects off, one attempt,maximumResponseSize5 KB and the CAShttpClientfor TLS. Tests inject a permissive address policy and host resolver and serve the document over plain HTTP fromMockWebServer(false, ...): with pinned addresses the request gets a dedicated client, and the self-signedMockWebServer(true, ...)certificate fails the handshake there. Never add a setting that relaxes the address check. Expiration dates on services are written as aZonedDateTimetruncated to seconds; aLocalDateTimewith fractions makesDateTimeUtils.zonedDateTimeOfwarn on every expiration check.- Consent for these clients is never bypassed (
OidcConsentApprovalViewResolver);confirm.htmlshowsclientIdHost,redirectHostand a loopback warning (screen.oidc.confirm.cimd.*, every bundle).
Device user code approval
- The approval form at
/oauth2.0/devicecarriesapprovaltoken, an HMAC of the ticket-granting ticket id keyed by the TGC signing and encryption keys (the TGT id itself when they are blank, so it holds on every node), andOAuth20DeviceUserCodeApprovalEndpointControllerchecks it before it looks the user code up. Spring Security CSRF covers only the endpoints that configurers declare, and the authorization endpoint sets no CSRF cookie, so nothing else protects that POST: keep the check, and keep the hidden input indeviceCodeApproval.html. Error keys live undercas.oauth.device.error.*. - CSRF protection for
CasWebSecurityConfigurers (how to declare protected paths): see thecas-web-securityskill.
Trending
Quiet since August.about the whole repository
Who's talking about apereo/cas
- Stack Overflow“What is the relation between CAS vs SAML?”Stack Overflow · Apr 28, 2017 · 23 votes
- strapi/strapi“Add support for login via cas - github.com/apereo/cas”GitHub · Sep 26, 2020 · 8 reactions
- Stack Overflow“Opensaml error receiving correct unmarshaller”Stack Overflow · Apr 10, 2017 · 6 votes
- Stack Overflow“How to enable file-based authentication in CAS 5”Stack Overflow · Apr 14, 2017 · 5 votes
- google/error-prone“ErrorProne 2.37: StringConcatToTextBlock causes java.util.NoSuchElementException”GitHub · Mar 19, 2025 · 3 reactions
The biggest 5 of 27 mentions linking to the repository that our crawl found. Each line is their own headline.
See all 27
Talked about on Stack Overflow, GitHub, Hacker News and the web (27 times).
On Stack Overflow · 14
- Apr 28, 2017What is the relation between CAS vs SAML?23 votes
- Apr 10, 2017Opensaml error receiving correct unmarshaller6 votes
- Apr 14, 2017How to enable file-based authentication in CAS 55 votes
- Aug 2, 2021How to create a SAML IDP that authenticates with an API from scratch2 votes
- Feb 3, 2020CAS 6.1 - State Parameter null with Pac4J2 votes
9 more on Stack Overflow
- Jul 4, 2017CAS Spnego - KrbException: Checksum failed2 votes · 3 answers
- Dec 11, 2023What happened "org.apereo.cas.web.security" in 7.0.0-RC9 of org.apereo.cas?1 vote
- Jul 23, 2020Cas - configure Principal transformation1 vote · 1 answer
- Nov 25, 2019CAS Disable Service X-Frame-Options not work1 vote
- Apr 24, 2019inject model data into spring webflow in cas1 vote
- Mar 4, 2019Prevent CAS authentication if X.509 user isn't registered1 vote
- Nov 10, 2021CAS Apereo 6.1.x importing PasswordEncoderUtils0 votes · 1 answer
- Aug 2, 2021How to SSO to a directory server from a python application0 votes
- Nov 7, 2016using CAS with with custom login page0 votes
On GitHub · 9
- Sep 26, 2020Add support for login via cas - github.com/apereo/cas — strapi/strapi8 reactions · 21 comments
- Mar 19, 2025ErrorProne 2.37: StringConcatToTextBlock causes java.util.NoSuchElementException — google/error-prone3 reactions · 3 comments
- Mar 26, 2020Renovate crash: JavaScript heap out of memory (many Gradle dependencies) — renovatebot/renovate1 reaction · 8 comments
- Aug 10, 2026Add Apereo CAS project to regression testing project lists — checkstyle/test-configssays it tested it0 reactions · 1 comment
- Jul 17, 2026[Argus] CVE-2026-59099: Apereo CAS 7.3.0 < 8.0.0-RC6 - AES-GCM 논스(Nonce) 재사용으로 인한 정보 노출 — LEEKIYOON-SEC/Argus-DEV0 reactions
4 more on GitHub
- Dec 5, 2024Extract Attr within moved file — pouryafard75/RM-ASTDiff0 reactions · 14 comments
- Jan 18, 2023Authentication/Authorization: We need a API-friendly strategy — wolpert/svarm0 reactions · 1 comment
- Apr 19, 2020COMPARAISON KEYCLOAK / CAS — pegasus-io/belenios-containers0 reactions
- Mar 20, 2018apereo/cas OAuth endpoints need @Controller annotations — easy-software-ufal/annotations_repos0 reactions
On Hacker News · 3
- Feb 20, 2017Enterprise Single Sign on for all3 points
- Aug 8, 2023The Central Authentication Service Project2 points
- Oct 16, 2016Apereo CAS: Single Sign-On for the Web1 point
On the web · 1
- Apr 11, 2025apereo/cas - Where software is built — google.com
In this repository30 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- cas-actuatorsSkill
- cas-attribute-consentSkill
- cas-build-ciSkill
- cas-docs-siteSkill
- cas-google-authenticatorSkill
- cas-groovy-scriptingSkill
- cas-handoffSkill
- cas-heimdall-authzenSkill
- cas-import-prSkill
- cas-interrupt-notificationsSkill
- cas-ldapSkill
- cas-mcp-serverSkill
- cas-oauth-oidc-authenticationSkill
- cas-oid4vcSkill
- cas-passwordless-webauthnSkill
- cas-protocolSkill
- cas-puppeteer-scenariosSkill
- cas-samlSkill
- cas-sandbox-verificationSkill
- cas-service-registriesSkill
- cas-session-storesSkill
- cas-startup-performanceSkill
- cas-storage-backendsSkill
- cas-testingSkill
- cas-ticket-registriesSkill
- cas-uiSkill
- cas-web-securitySkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

