Proxy CAS3 authentication to an OAuth2/OIDC-style client such as Keycloak.
+----------------+ +---------------------+ +-------------------+
| | | | | |
| Keycloak | | cas-proxy | | CAS Server |
| | | | | |
+-------+--------+ +---------+-----------+ +-------+-----------+
| | |
| /authorize | |
+--------------------->| |
| | /cas/login?service=... |
| |-------------------------->|
| | |
| | callback ticket |
| |<--------------------------|
| | /callback?ticket=... |
| +-------------------------->|
| | |
| | serviceValidate |
| |-------------------------->|
| | CAS attributes |
| |<--------------------------|
| | |
| auth code | |
|<---------------------+ |
| | |
| /token | |
+--------------------->| |
| | |
| tokens | |
|<---------------------+ |
| | |
| /userinfo | |
+--------------------->| |
| | |
| user info | |
|<---------------------+ |
- Runtime state now uses SQLite instead of process-local dictionaries.
- Auth codes are consumed once inside a transaction.
- Session, auth code, and token records have expirations and are cleaned up.
- Redirect URI validation checks scheme, host, port, and optional path instead of using
startswith. - CAS validation uses timeouts, network error handling, and
defusedxml. - Cookies use
HttpOnly,SameSite,Secure, andmax_age. /userinfohandles missing or malformed bearer tokens cleanly.- Configuration can still come from
config.py, with environment variables able to override it.
python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
cp config.example.py config.pyEdit config.py before starting the service. Do not commit config.py; it is ignored by git.
The old config.py style is still supported. This minimal configuration matches the original deployment shape:
CAS_LOGIN_URL = "https://cas.sustech.edu.cn/cas/login"
CAS_VALIDATE_URL = "https://cas.sustech.edu.cn/cas/p3/serviceValidate"
SERVICE_URL = "https://cas-proxy.cra.moe/callback"
REDIRECT_URI_ALLOWLIST = ["https://sso.cra.ac.cn"]
CLIENT_ID = "cra-cas-proxy"
CLIENT_SECRET = "replace-with-a-new-random-secret"
SECRET_KEY = "replace-with-a-long-random-jwt-signing-secret"REDIRECT_URL_MATCH = "https://sso.cra.ac.cn" is also accepted for compatibility, but REDIRECT_URI_ALLOWLIST is preferred.
Useful optional settings:
ACCESS_TOKEN_EXPIRY = 1800
AUTH_CODE_EXPIRY = 300
SESSION_EXPIRY = 600
DATABASE_PATH = "cas_proxy.sqlite3"
ISSUER = "cas-proxy"
CAS_REQUEST_TIMEOUT = 8.0
COOKIE_SECURE = True
COOKIE_SAMESITE = "Lax"
HOST = "127.0.0.1"
PORT = 59084
REQUIRE_TOKEN_REDIRECT_URI = FalseAny setting can also be provided as an environment variable with the same name. Environment variables take precedence over config.py.
For local testing:
python casproxy.pyFor production, run behind a reverse proxy and use multiple workers if needed:
gunicorn -w 4 -b 127.0.0.1:59084 casproxy:appSQLite is opened in WAL mode, so multiple Gunicorn workers can share the same DATABASE_PATH on one host. If you later run several machines, move the state store to Redis or another shared backend.
- Rotate
CLIENT_SECRETandSECRET_KEYif they have been pasted into chat, logs, tickets, or shell history. - Keep
COOKIE_SECURE = Truein production. SECRET_KEYsigns JWTs; use a long random value rather than a short UUID.- Keep
SERVICE_URLexactly equal to the CAS callback URL registered with CAS. - Keep the redirect allowlist as narrow as possible.
curl http://127.0.0.1:59084/healthzAfter installing dependencies:
python -m unittest discover -s tests