cas-system-client 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cas_system_client-2.0.0/LICENSE +21 -0
- cas_system_client-2.0.0/PKG-INFO +185 -0
- cas_system_client-2.0.0/README.md +141 -0
- cas_system_client-2.0.0/cas_client/__init__.py +5 -0
- cas_system_client-2.0.0/cas_client/client.py +264 -0
- cas_system_client-2.0.0/cas_client/middleware.py +172 -0
- cas_system_client-2.0.0/cas_system_client.egg-info/PKG-INFO +185 -0
- cas_system_client-2.0.0/cas_system_client.egg-info/SOURCES.txt +11 -0
- cas_system_client-2.0.0/cas_system_client.egg-info/dependency_links.txt +1 -0
- cas_system_client-2.0.0/cas_system_client.egg-info/requires.txt +7 -0
- cas_system_client-2.0.0/cas_system_client.egg-info/top_level.txt +1 -0
- cas_system_client-2.0.0/setup.cfg +4 -0
- cas_system_client-2.0.0/setup.py +41 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 InSol-2021
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cas-system-client
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Python client for CAS (Central Authentication Service) SSO integration
|
|
5
|
+
Home-page: https://github.com/InSol-2021/one-system
|
|
6
|
+
Author: CAS System
|
|
7
|
+
Author-email: support@innovativesolution.com.np
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Source, https://github.com/InSol-2021/one-system/tree/master/packages/python-cas-client
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/InSol-2021/one-system/issues
|
|
11
|
+
Keywords: cas sso authentication jwt single-sign-on
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Classifier: Topic :: System :: Systems Administration :: Authentication/Directory
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: requests>=2.32.0
|
|
26
|
+
Provides-Extra: django
|
|
27
|
+
Requires-Dist: django>=4.2; extra == "django"
|
|
28
|
+
Provides-Extra: flask
|
|
29
|
+
Requires-Dist: flask>=3.0; extra == "flask"
|
|
30
|
+
Dynamic: author
|
|
31
|
+
Dynamic: author-email
|
|
32
|
+
Dynamic: classifier
|
|
33
|
+
Dynamic: description
|
|
34
|
+
Dynamic: description-content-type
|
|
35
|
+
Dynamic: home-page
|
|
36
|
+
Dynamic: keywords
|
|
37
|
+
Dynamic: license
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
Dynamic: project-url
|
|
40
|
+
Dynamic: provides-extra
|
|
41
|
+
Dynamic: requires-dist
|
|
42
|
+
Dynamic: requires-python
|
|
43
|
+
Dynamic: summary
|
|
44
|
+
|
|
45
|
+
# Python CAS Client
|
|
46
|
+
|
|
47
|
+
A Python package for seamless integration with **One System** CAS (Central Authentication Service) SSO servers. Works with Django, Flask, FastAPI, and any Python framework.
|
|
48
|
+
|
|
49
|
+
**Requirements:** Python 3.9+ and `requests >= 2.32`. The optional `[django]` extra requires Django 4.2+, and the `[flask]` extra requires Flask 3.0+.
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
- 🔐 **Secure SSO Authentication** — JWT token-based authentication
|
|
54
|
+
- 🛡️ **HMAC Signature Validation** — Request signing with SHA-256
|
|
55
|
+
- 👥 **Role-Based Access Control** — Decorators for role protection
|
|
56
|
+
- 🐍 **Django & Flask Support** — Built-in middleware and decorators
|
|
57
|
+
- 📦 **Type Hints** — Full typing support for IDE autocomplete
|
|
58
|
+
- 🔄 **Token Caching** — In-memory cache for validated tokens
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install cas-system-client
|
|
64
|
+
|
|
65
|
+
# With Django support
|
|
66
|
+
pip install cas-system-client[django]
|
|
67
|
+
|
|
68
|
+
# With Flask support
|
|
69
|
+
pip install cas-system-client[flask]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Quick Start
|
|
73
|
+
|
|
74
|
+
### 1. Initialize the Client
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from cas_client import CasClient
|
|
78
|
+
|
|
79
|
+
cas = CasClient(
|
|
80
|
+
server_url='https://your-cas-server.com',
|
|
81
|
+
client_id='your_client_id',
|
|
82
|
+
client_secret='your_client_secret',
|
|
83
|
+
callback_url='https://your-app.com/cas/callback',
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### How the One System SSO flow works
|
|
88
|
+
|
|
89
|
+
1. Send the browser to `cas.get_login_url()` — this redirects to
|
|
90
|
+
`GET {server_url}/sso/login?client_id=...`.
|
|
91
|
+
2. One System authenticates the user and redirects back to your registered
|
|
92
|
+
`callback_url` with a single-use JWT appended as `?token=<JWT>`.
|
|
93
|
+
3. In your callback, call `cas.validate_token(token)`. The client validates the
|
|
94
|
+
token **server-to-server** (sending `client_id` + `client_secret`) and returns
|
|
95
|
+
the user dict `{id, username, email, ...}` on success, or `None`. Tokens are
|
|
96
|
+
single-use — validate once, then rely on your framework session.
|
|
97
|
+
|
|
98
|
+
### 2. Django Integration
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
# settings.py
|
|
102
|
+
import os
|
|
103
|
+
from cas_client import CasClient
|
|
104
|
+
|
|
105
|
+
CAS_CLIENT = CasClient(
|
|
106
|
+
server_url=os.environ['CAS_SERVER_URL'],
|
|
107
|
+
client_id=os.environ['CAS_CLIENT_ID'],
|
|
108
|
+
client_secret=os.environ['CAS_CLIENT_SECRET'],
|
|
109
|
+
callback_url=os.environ['CAS_CALLBACK_URL'],
|
|
110
|
+
)
|
|
111
|
+
CAS_PROTECTED_PATHS = ['/dashboard', '/admin']
|
|
112
|
+
CAS_LOGIN_URL = '/auth/login'
|
|
113
|
+
|
|
114
|
+
MIDDLEWARE = [
|
|
115
|
+
...
|
|
116
|
+
'cas_client.middleware.DjangoCasMiddleware',
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
# views.py
|
|
120
|
+
from cas_client.middleware import django_role_required
|
|
121
|
+
|
|
122
|
+
@django_role_required('admin')
|
|
123
|
+
def admin_view(request):
|
|
124
|
+
user = request.cas_user
|
|
125
|
+
return JsonResponse({'user': user['username']})
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 3. Flask Integration
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from cas_client import CasClient
|
|
132
|
+
from cas_client.middleware import flask_cas_required, flask_role_required
|
|
133
|
+
|
|
134
|
+
cas = CasClient(
|
|
135
|
+
server_url='https://your-cas-server.com',
|
|
136
|
+
client_id='your_client_id',
|
|
137
|
+
client_secret='your_client_secret',
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@app.route('/dashboard')
|
|
141
|
+
@flask_cas_required(cas)
|
|
142
|
+
def dashboard():
|
|
143
|
+
user = flask.g.cas_user
|
|
144
|
+
return f'Hello {user["username"]}'
|
|
145
|
+
|
|
146
|
+
@app.route('/admin')
|
|
147
|
+
@flask_cas_required(cas)
|
|
148
|
+
@flask_role_required(cas, 'admin')
|
|
149
|
+
def admin():
|
|
150
|
+
return 'Admin area'
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 4. Manual Authentication
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
# Generate SSO token
|
|
157
|
+
token_data = cas.generate_sso_token('john_doe')
|
|
158
|
+
|
|
159
|
+
# Validate token
|
|
160
|
+
user = cas.validate_token(token)
|
|
161
|
+
|
|
162
|
+
# Role checks
|
|
163
|
+
if cas.user_has_role(user, 'admin'):
|
|
164
|
+
print('User is admin')
|
|
165
|
+
|
|
166
|
+
# Logout
|
|
167
|
+
cas.logout(token)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## API Reference
|
|
171
|
+
|
|
172
|
+
| Method | Description |
|
|
173
|
+
|--------|-------------|
|
|
174
|
+
| `get_login_url(return_url)` | Generate CAS login URL |
|
|
175
|
+
| `generate_sso_token(username)` | Generate SSO token |
|
|
176
|
+
| `validate_token(token)` | Validate token, returns user dict |
|
|
177
|
+
| `get_user_from_token(token)` | Get cached user data |
|
|
178
|
+
| `logout(token)` | Logout from CAS server |
|
|
179
|
+
| `user_has_role(user, role)` | Check single role |
|
|
180
|
+
| `user_has_any_role(user, roles)` | Check any of roles |
|
|
181
|
+
| `user_has_all_roles(user, roles)` | Check all roles |
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Python CAS Client
|
|
2
|
+
|
|
3
|
+
A Python package for seamless integration with **One System** CAS (Central Authentication Service) SSO servers. Works with Django, Flask, FastAPI, and any Python framework.
|
|
4
|
+
|
|
5
|
+
**Requirements:** Python 3.9+ and `requests >= 2.32`. The optional `[django]` extra requires Django 4.2+, and the `[flask]` extra requires Flask 3.0+.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- 🔐 **Secure SSO Authentication** — JWT token-based authentication
|
|
10
|
+
- 🛡️ **HMAC Signature Validation** — Request signing with SHA-256
|
|
11
|
+
- 👥 **Role-Based Access Control** — Decorators for role protection
|
|
12
|
+
- 🐍 **Django & Flask Support** — Built-in middleware and decorators
|
|
13
|
+
- 📦 **Type Hints** — Full typing support for IDE autocomplete
|
|
14
|
+
- 🔄 **Token Caching** — In-memory cache for validated tokens
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install cas-system-client
|
|
20
|
+
|
|
21
|
+
# With Django support
|
|
22
|
+
pip install cas-system-client[django]
|
|
23
|
+
|
|
24
|
+
# With Flask support
|
|
25
|
+
pip install cas-system-client[flask]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
### 1. Initialize the Client
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from cas_client import CasClient
|
|
34
|
+
|
|
35
|
+
cas = CasClient(
|
|
36
|
+
server_url='https://your-cas-server.com',
|
|
37
|
+
client_id='your_client_id',
|
|
38
|
+
client_secret='your_client_secret',
|
|
39
|
+
callback_url='https://your-app.com/cas/callback',
|
|
40
|
+
)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### How the One System SSO flow works
|
|
44
|
+
|
|
45
|
+
1. Send the browser to `cas.get_login_url()` — this redirects to
|
|
46
|
+
`GET {server_url}/sso/login?client_id=...`.
|
|
47
|
+
2. One System authenticates the user and redirects back to your registered
|
|
48
|
+
`callback_url` with a single-use JWT appended as `?token=<JWT>`.
|
|
49
|
+
3. In your callback, call `cas.validate_token(token)`. The client validates the
|
|
50
|
+
token **server-to-server** (sending `client_id` + `client_secret`) and returns
|
|
51
|
+
the user dict `{id, username, email, ...}` on success, or `None`. Tokens are
|
|
52
|
+
single-use — validate once, then rely on your framework session.
|
|
53
|
+
|
|
54
|
+
### 2. Django Integration
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# settings.py
|
|
58
|
+
import os
|
|
59
|
+
from cas_client import CasClient
|
|
60
|
+
|
|
61
|
+
CAS_CLIENT = CasClient(
|
|
62
|
+
server_url=os.environ['CAS_SERVER_URL'],
|
|
63
|
+
client_id=os.environ['CAS_CLIENT_ID'],
|
|
64
|
+
client_secret=os.environ['CAS_CLIENT_SECRET'],
|
|
65
|
+
callback_url=os.environ['CAS_CALLBACK_URL'],
|
|
66
|
+
)
|
|
67
|
+
CAS_PROTECTED_PATHS = ['/dashboard', '/admin']
|
|
68
|
+
CAS_LOGIN_URL = '/auth/login'
|
|
69
|
+
|
|
70
|
+
MIDDLEWARE = [
|
|
71
|
+
...
|
|
72
|
+
'cas_client.middleware.DjangoCasMiddleware',
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
# views.py
|
|
76
|
+
from cas_client.middleware import django_role_required
|
|
77
|
+
|
|
78
|
+
@django_role_required('admin')
|
|
79
|
+
def admin_view(request):
|
|
80
|
+
user = request.cas_user
|
|
81
|
+
return JsonResponse({'user': user['username']})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 3. Flask Integration
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from cas_client import CasClient
|
|
88
|
+
from cas_client.middleware import flask_cas_required, flask_role_required
|
|
89
|
+
|
|
90
|
+
cas = CasClient(
|
|
91
|
+
server_url='https://your-cas-server.com',
|
|
92
|
+
client_id='your_client_id',
|
|
93
|
+
client_secret='your_client_secret',
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
@app.route('/dashboard')
|
|
97
|
+
@flask_cas_required(cas)
|
|
98
|
+
def dashboard():
|
|
99
|
+
user = flask.g.cas_user
|
|
100
|
+
return f'Hello {user["username"]}'
|
|
101
|
+
|
|
102
|
+
@app.route('/admin')
|
|
103
|
+
@flask_cas_required(cas)
|
|
104
|
+
@flask_role_required(cas, 'admin')
|
|
105
|
+
def admin():
|
|
106
|
+
return 'Admin area'
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 4. Manual Authentication
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
# Generate SSO token
|
|
113
|
+
token_data = cas.generate_sso_token('john_doe')
|
|
114
|
+
|
|
115
|
+
# Validate token
|
|
116
|
+
user = cas.validate_token(token)
|
|
117
|
+
|
|
118
|
+
# Role checks
|
|
119
|
+
if cas.user_has_role(user, 'admin'):
|
|
120
|
+
print('User is admin')
|
|
121
|
+
|
|
122
|
+
# Logout
|
|
123
|
+
cas.logout(token)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## API Reference
|
|
127
|
+
|
|
128
|
+
| Method | Description |
|
|
129
|
+
|--------|-------------|
|
|
130
|
+
| `get_login_url(return_url)` | Generate CAS login URL |
|
|
131
|
+
| `generate_sso_token(username)` | Generate SSO token |
|
|
132
|
+
| `validate_token(token)` | Validate token, returns user dict |
|
|
133
|
+
| `get_user_from_token(token)` | Get cached user data |
|
|
134
|
+
| `logout(token)` | Logout from CAS server |
|
|
135
|
+
| `user_has_role(user, role)` | Check single role |
|
|
136
|
+
| `user_has_any_role(user, roles)` | Check any of roles |
|
|
137
|
+
| `user_has_all_roles(user, roles)` | Check all roles |
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAS SSO Client for Python
|
|
3
|
+
Provides SSO authentication, token validation, HMAC signing, and role management.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import hmac
|
|
8
|
+
import json
|
|
9
|
+
import time
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Optional, Dict, List, Any
|
|
12
|
+
from urllib.parse import urlencode
|
|
13
|
+
|
|
14
|
+
import requests
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger('cas_client')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CasClient:
|
|
20
|
+
"""
|
|
21
|
+
CAS SSO Client for Python applications.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
server_url: CAS server URL (e.g., https://your-cas-server.com). Used as
|
|
25
|
+
the internal/back-channel base for server-to-server calls
|
|
26
|
+
(token validation, logout, token generation).
|
|
27
|
+
client_id: Registered client ID
|
|
28
|
+
client_secret: Registered client secret
|
|
29
|
+
callback_url: OAuth callback URL
|
|
30
|
+
public_url: Public, browser-facing CAS base URL used ONLY when building
|
|
31
|
+
the browser login redirect (``get_login_url``). In split-horizon
|
|
32
|
+
deployments the browser must reach CAS at a public host while the
|
|
33
|
+
server reaches it at a different internal host. Optional; when empty
|
|
34
|
+
the login URL falls back to ``server_url`` (single-url dev).
|
|
35
|
+
signature_secret: HMAC signature secret (optional)
|
|
36
|
+
enable_signature_validation: Enable HMAC request signing (default: False)
|
|
37
|
+
timeout: Request timeout in seconds (default: 30)
|
|
38
|
+
verify_ssl: Verify SSL certificates (default: True)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
server_url: str,
|
|
44
|
+
client_id: str,
|
|
45
|
+
client_secret: str,
|
|
46
|
+
callback_url: str = '',
|
|
47
|
+
public_url: str = '',
|
|
48
|
+
signature_secret: str = '',
|
|
49
|
+
enable_signature_validation: bool = False,
|
|
50
|
+
timeout: int = 30,
|
|
51
|
+
verify_ssl: bool = True,
|
|
52
|
+
):
|
|
53
|
+
if not server_url:
|
|
54
|
+
raise ValueError('server_url is required')
|
|
55
|
+
if not client_id:
|
|
56
|
+
raise ValueError('client_id is required')
|
|
57
|
+
if not client_secret:
|
|
58
|
+
raise ValueError('client_secret is required')
|
|
59
|
+
|
|
60
|
+
self.config = {
|
|
61
|
+
'server_url': server_url.rstrip('/'),
|
|
62
|
+
# Public/browser-facing base for the login redirect only. Falls back
|
|
63
|
+
# to server_url when not provided so single-url setups are unchanged.
|
|
64
|
+
'public_url': (public_url or '').rstrip('/'),
|
|
65
|
+
'client_id': client_id,
|
|
66
|
+
'client_secret': client_secret,
|
|
67
|
+
'callback_url': callback_url,
|
|
68
|
+
'signature_secret': signature_secret or 'default-signature-secret',
|
|
69
|
+
'enable_signature_validation': enable_signature_validation,
|
|
70
|
+
'timeout': timeout,
|
|
71
|
+
'verify_ssl': verify_ssl,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
self._session = requests.Session()
|
|
75
|
+
self._session.headers.update({'Content-Type': 'application/json'})
|
|
76
|
+
self._session.verify = verify_ssl
|
|
77
|
+
self._session.timeout = timeout
|
|
78
|
+
|
|
79
|
+
# In-memory token cache
|
|
80
|
+
self._cache: Dict[str, Dict[str, Any]] = {}
|
|
81
|
+
|
|
82
|
+
def get_login_url(self, return_url: Optional[str] = None) -> str:
|
|
83
|
+
"""Generate CAS SSO login URL.
|
|
84
|
+
|
|
85
|
+
The CAS server expects only ``client_id`` on the login redirect; it
|
|
86
|
+
looks up the client's registered callback URL server-side and appends
|
|
87
|
+
the token to it (``{callback_url}?token={JWT}``).
|
|
88
|
+
|
|
89
|
+
This is the URL the user's BROWSER is redirected to, so it is built from
|
|
90
|
+
``public_url`` when configured (the public, browser-facing CAS host),
|
|
91
|
+
falling back to ``server_url`` otherwise. Server-to-server calls still
|
|
92
|
+
use ``server_url`` (the internal back-channel host).
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
return_url: Unused by the CAS server (the registered callback URL
|
|
96
|
+
is always used). Accepted for backwards compatibility.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Full CAS login URL string
|
|
100
|
+
"""
|
|
101
|
+
params = {
|
|
102
|
+
'client_id': self.config['client_id'],
|
|
103
|
+
}
|
|
104
|
+
base = self.config['public_url'] or self.config['server_url']
|
|
105
|
+
return f"{base}/sso/login?{urlencode(params)}"
|
|
106
|
+
|
|
107
|
+
def generate_sso_token(self, username: str) -> Optional[Dict[str, Any]]:
|
|
108
|
+
"""Generate SSO token for a user via client credentials.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
username: Username to generate token for
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Token data dict with 'token' and 'redirect_url', or None
|
|
115
|
+
"""
|
|
116
|
+
try:
|
|
117
|
+
timestamp = int(time.time())
|
|
118
|
+
request_data = {
|
|
119
|
+
'client_id': self.config['client_id'],
|
|
120
|
+
'client_secret': self.config['client_secret'],
|
|
121
|
+
'username': username,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
headers = {
|
|
125
|
+
'X-Client-ID': self.config['client_id'],
|
|
126
|
+
'X-Timestamp': str(timestamp),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if self.config['enable_signature_validation']:
|
|
130
|
+
headers['X-Signature'] = self._generate_signature(
|
|
131
|
+
'POST', '/api/sso/token', request_data, timestamp
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
response = self._session.post(
|
|
135
|
+
f"{self.config['server_url']}/api/sso/token",
|
|
136
|
+
json=request_data,
|
|
137
|
+
headers=headers,
|
|
138
|
+
timeout=self.config['timeout'],
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if response.status_code == 200:
|
|
142
|
+
data = response.json()
|
|
143
|
+
if 'token' in data:
|
|
144
|
+
return data
|
|
145
|
+
|
|
146
|
+
return None
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.error(f'CAS SSO token generation failed: {e}')
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
def validate_token(self, token: str) -> Optional[Dict[str, Any]]:
|
|
152
|
+
"""Validate SSO token with CAS server.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
token: JWT token to validate
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
User data dict or None
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
timestamp = int(time.time())
|
|
162
|
+
request_data = {
|
|
163
|
+
'token': token,
|
|
164
|
+
'client_id': self.config['client_id'],
|
|
165
|
+
'client_secret': self.config['client_secret'],
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
headers = {
|
|
169
|
+
'X-Client-ID': self.config['client_id'],
|
|
170
|
+
'X-Timestamp': str(timestamp),
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if self.config['enable_signature_validation']:
|
|
174
|
+
headers['X-Signature'] = self._generate_signature(
|
|
175
|
+
'POST', '/api/sso/validate', request_data, timestamp
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
response = self._session.post(
|
|
179
|
+
f"{self.config['server_url']}/api/sso/validate",
|
|
180
|
+
json=request_data,
|
|
181
|
+
headers=headers,
|
|
182
|
+
timeout=self.config['timeout'],
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if response.status_code == 200:
|
|
186
|
+
data = response.json()
|
|
187
|
+
# CAS server signals success with {"valid": true, "user": {...},
|
|
188
|
+
# "expires_at": "<datetime>"}. Honor the explicit "valid" flag.
|
|
189
|
+
if data.get('valid') and 'user' in data:
|
|
190
|
+
cache_key = hashlib.md5(token.encode()).hexdigest()
|
|
191
|
+
self._cache[cache_key] = {
|
|
192
|
+
'data': data['user'],
|
|
193
|
+
'expires': time.time() + 3600,
|
|
194
|
+
}
|
|
195
|
+
return data['user']
|
|
196
|
+
|
|
197
|
+
return None
|
|
198
|
+
except Exception as e:
|
|
199
|
+
logger.error(f'CAS token validation failed: {e}')
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
def get_user_from_token(self, token: str) -> Optional[Dict[str, Any]]:
|
|
203
|
+
"""Get cached user data from token.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
token: JWT token
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Cached user data or None
|
|
210
|
+
"""
|
|
211
|
+
cache_key = hashlib.md5(token.encode()).hexdigest()
|
|
212
|
+
cached = self._cache.get(cache_key)
|
|
213
|
+
if cached and cached['expires'] > time.time():
|
|
214
|
+
return cached['data']
|
|
215
|
+
self._cache.pop(cache_key, None)
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
def logout(self, token: Optional[str] = None) -> bool:
|
|
219
|
+
"""Logout from CAS server.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
token: JWT token to invalidate cache for
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
True if successful
|
|
226
|
+
"""
|
|
227
|
+
try:
|
|
228
|
+
if token:
|
|
229
|
+
cache_key = hashlib.md5(token.encode()).hexdigest()
|
|
230
|
+
self._cache.pop(cache_key, None)
|
|
231
|
+
|
|
232
|
+
response = self._session.post(
|
|
233
|
+
f"{self.config['server_url']}/api/logout",
|
|
234
|
+
timeout=self.config['timeout'],
|
|
235
|
+
)
|
|
236
|
+
return response.status_code == 200
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.error(f'CAS logout failed: {e}')
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
@staticmethod
|
|
242
|
+
def user_has_role(user: Dict[str, Any], role: str) -> bool:
|
|
243
|
+
"""Check if user has a specific role."""
|
|
244
|
+
return role in (user.get('roles') or [])
|
|
245
|
+
|
|
246
|
+
@staticmethod
|
|
247
|
+
def user_has_any_role(user: Dict[str, Any], roles: List[str]) -> bool:
|
|
248
|
+
"""Check if user has any of the specified roles."""
|
|
249
|
+
user_roles = set(user.get('roles') or [])
|
|
250
|
+
return bool(user_roles & set(roles))
|
|
251
|
+
|
|
252
|
+
@staticmethod
|
|
253
|
+
def user_has_all_roles(user: Dict[str, Any], roles: List[str]) -> bool:
|
|
254
|
+
"""Check if user has all specified roles."""
|
|
255
|
+
user_roles = set(user.get('roles') or [])
|
|
256
|
+
return set(roles).issubset(user_roles)
|
|
257
|
+
|
|
258
|
+
def _generate_signature(self, method: str, uri: str, data: dict, timestamp: int) -> str:
|
|
259
|
+
"""Generate HMAC SHA-256 signature for request."""
|
|
260
|
+
body = json.dumps(data)
|
|
261
|
+
payload = '|'.join([method, uri, body, str(timestamp), self.config['client_id']])
|
|
262
|
+
secret = self.config['signature_secret']
|
|
263
|
+
signature = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
|
|
264
|
+
return f'sha256={signature}'
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django and Flask middleware for CAS SSO authentication.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Optional, List
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger('cas_client')
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# =============================================================================
|
|
12
|
+
# Django Middleware
|
|
13
|
+
# =============================================================================
|
|
14
|
+
|
|
15
|
+
class DjangoCasMiddleware:
|
|
16
|
+
"""
|
|
17
|
+
Django middleware for CAS authentication.
|
|
18
|
+
|
|
19
|
+
Add to MIDDLEWARE in settings.py:
|
|
20
|
+
'cas_client.middleware.DjangoCasMiddleware',
|
|
21
|
+
|
|
22
|
+
Configure in settings.py:
|
|
23
|
+
CAS_CLIENT = CasClient(...)
|
|
24
|
+
CAS_PROTECTED_PATHS = ['/dashboard', '/admin']
|
|
25
|
+
CAS_LOGIN_URL = '/auth/login'
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, get_response):
|
|
29
|
+
self.get_response = get_response
|
|
30
|
+
|
|
31
|
+
def __call__(self, request):
|
|
32
|
+
from django.conf import settings
|
|
33
|
+
from django.shortcuts import redirect
|
|
34
|
+
from django.http import JsonResponse
|
|
35
|
+
|
|
36
|
+
protected_paths = getattr(settings, 'CAS_PROTECTED_PATHS', [])
|
|
37
|
+
login_url = getattr(settings, 'CAS_LOGIN_URL', '/auth/login')
|
|
38
|
+
cas_client = getattr(settings, 'CAS_CLIENT', None)
|
|
39
|
+
|
|
40
|
+
is_protected = any(request.path.startswith(p) for p in protected_paths)
|
|
41
|
+
|
|
42
|
+
if is_protected:
|
|
43
|
+
cas_user = request.session.get('cas_user')
|
|
44
|
+
|
|
45
|
+
# Check Bearer token if no session user
|
|
46
|
+
if not cas_user and cas_client:
|
|
47
|
+
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
|
|
48
|
+
if auth_header.startswith('Bearer '):
|
|
49
|
+
token = auth_header[7:]
|
|
50
|
+
cas_user = cas_client.get_user_from_token(token)
|
|
51
|
+
if not cas_user:
|
|
52
|
+
cas_user = cas_client.validate_token(token)
|
|
53
|
+
if cas_user:
|
|
54
|
+
request.session['cas_user'] = cas_user
|
|
55
|
+
|
|
56
|
+
if cas_user:
|
|
57
|
+
request.cas_user = cas_user
|
|
58
|
+
else:
|
|
59
|
+
if request.content_type == 'application/json':
|
|
60
|
+
return JsonResponse({'error': 'Authentication required'}, status=401)
|
|
61
|
+
return redirect(f'{login_url}?return_url={request.path}')
|
|
62
|
+
|
|
63
|
+
response = self.get_response(request)
|
|
64
|
+
return response
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def django_role_required(*roles: str):
|
|
68
|
+
"""
|
|
69
|
+
Django decorator to require specific roles.
|
|
70
|
+
|
|
71
|
+
Usage:
|
|
72
|
+
@django_role_required('admin', 'manager')
|
|
73
|
+
def admin_view(request):
|
|
74
|
+
...
|
|
75
|
+
"""
|
|
76
|
+
from functools import wraps
|
|
77
|
+
|
|
78
|
+
def decorator(view_func):
|
|
79
|
+
@wraps(view_func)
|
|
80
|
+
def wrapper(request, *args, **kwargs):
|
|
81
|
+
from django.http import JsonResponse
|
|
82
|
+
|
|
83
|
+
cas_user = getattr(request, 'cas_user', None) or request.session.get('cas_user')
|
|
84
|
+
if not cas_user:
|
|
85
|
+
return JsonResponse({'error': 'Authentication required'}, status=401)
|
|
86
|
+
|
|
87
|
+
user_roles = set(cas_user.get('roles', []))
|
|
88
|
+
if not user_roles & set(roles):
|
|
89
|
+
return JsonResponse({'error': 'Insufficient permissions'}, status=403)
|
|
90
|
+
|
|
91
|
+
return view_func(request, *args, **kwargs)
|
|
92
|
+
return wrapper
|
|
93
|
+
return decorator
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# =============================================================================
|
|
97
|
+
# Flask Middleware / Decorators
|
|
98
|
+
# =============================================================================
|
|
99
|
+
|
|
100
|
+
def flask_cas_required(cas_client):
|
|
101
|
+
"""
|
|
102
|
+
Flask decorator for CAS authentication.
|
|
103
|
+
|
|
104
|
+
Usage:
|
|
105
|
+
from cas_client.middleware import flask_cas_required
|
|
106
|
+
|
|
107
|
+
@app.route('/dashboard')
|
|
108
|
+
@flask_cas_required(cas)
|
|
109
|
+
def dashboard():
|
|
110
|
+
user = flask.session['cas_user']
|
|
111
|
+
return f'Hello {user["username"]}'
|
|
112
|
+
"""
|
|
113
|
+
from functools import wraps
|
|
114
|
+
|
|
115
|
+
def decorator(f):
|
|
116
|
+
@wraps(f)
|
|
117
|
+
def wrapper(*args, **kwargs):
|
|
118
|
+
import flask
|
|
119
|
+
|
|
120
|
+
cas_user = flask.session.get('cas_user')
|
|
121
|
+
if cas_user:
|
|
122
|
+
flask.g.cas_user = cas_user
|
|
123
|
+
return f(*args, **kwargs)
|
|
124
|
+
|
|
125
|
+
# Check Bearer token
|
|
126
|
+
auth = flask.request.headers.get('Authorization', '')
|
|
127
|
+
if auth.startswith('Bearer '):
|
|
128
|
+
token = auth[7:]
|
|
129
|
+
user = cas_client.get_user_from_token(token)
|
|
130
|
+
if not user:
|
|
131
|
+
user = cas_client.validate_token(token)
|
|
132
|
+
if user:
|
|
133
|
+
flask.g.cas_user = user
|
|
134
|
+
flask.session['cas_user'] = user
|
|
135
|
+
return f(*args, **kwargs)
|
|
136
|
+
|
|
137
|
+
if flask.request.is_json:
|
|
138
|
+
return flask.jsonify({'error': 'Authentication required'}), 401
|
|
139
|
+
login_url = flask.current_app.config.get('CAS_LOGIN_URL', '/auth/login')
|
|
140
|
+
return flask.redirect(f'{login_url}?return_url={flask.request.path}')
|
|
141
|
+
return wrapper
|
|
142
|
+
return decorator
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def flask_role_required(cas_client, *roles: str):
|
|
146
|
+
"""
|
|
147
|
+
Flask decorator for role-based access control.
|
|
148
|
+
|
|
149
|
+
Usage:
|
|
150
|
+
@app.route('/admin')
|
|
151
|
+
@flask_cas_required(cas)
|
|
152
|
+
@flask_role_required(cas, 'admin')
|
|
153
|
+
def admin():
|
|
154
|
+
...
|
|
155
|
+
"""
|
|
156
|
+
from functools import wraps
|
|
157
|
+
|
|
158
|
+
def decorator(f):
|
|
159
|
+
@wraps(f)
|
|
160
|
+
def wrapper(*args, **kwargs):
|
|
161
|
+
import flask
|
|
162
|
+
|
|
163
|
+
cas_user = getattr(flask.g, 'cas_user', None) or flask.session.get('cas_user')
|
|
164
|
+
if not cas_user:
|
|
165
|
+
return flask.jsonify({'error': 'Authentication required'}), 401
|
|
166
|
+
|
|
167
|
+
if not cas_client.user_has_any_role(cas_user, list(roles)):
|
|
168
|
+
return flask.jsonify({'error': 'Insufficient permissions'}), 403
|
|
169
|
+
|
|
170
|
+
return f(*args, **kwargs)
|
|
171
|
+
return wrapper
|
|
172
|
+
return decorator
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cas-system-client
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Python client for CAS (Central Authentication Service) SSO integration
|
|
5
|
+
Home-page: https://github.com/InSol-2021/one-system
|
|
6
|
+
Author: CAS System
|
|
7
|
+
Author-email: support@innovativesolution.com.np
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Source, https://github.com/InSol-2021/one-system/tree/master/packages/python-cas-client
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/InSol-2021/one-system/issues
|
|
11
|
+
Keywords: cas sso authentication jwt single-sign-on
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Classifier: Topic :: System :: Systems Administration :: Authentication/Directory
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: requests>=2.32.0
|
|
26
|
+
Provides-Extra: django
|
|
27
|
+
Requires-Dist: django>=4.2; extra == "django"
|
|
28
|
+
Provides-Extra: flask
|
|
29
|
+
Requires-Dist: flask>=3.0; extra == "flask"
|
|
30
|
+
Dynamic: author
|
|
31
|
+
Dynamic: author-email
|
|
32
|
+
Dynamic: classifier
|
|
33
|
+
Dynamic: description
|
|
34
|
+
Dynamic: description-content-type
|
|
35
|
+
Dynamic: home-page
|
|
36
|
+
Dynamic: keywords
|
|
37
|
+
Dynamic: license
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
Dynamic: project-url
|
|
40
|
+
Dynamic: provides-extra
|
|
41
|
+
Dynamic: requires-dist
|
|
42
|
+
Dynamic: requires-python
|
|
43
|
+
Dynamic: summary
|
|
44
|
+
|
|
45
|
+
# Python CAS Client
|
|
46
|
+
|
|
47
|
+
A Python package for seamless integration with **One System** CAS (Central Authentication Service) SSO servers. Works with Django, Flask, FastAPI, and any Python framework.
|
|
48
|
+
|
|
49
|
+
**Requirements:** Python 3.9+ and `requests >= 2.32`. The optional `[django]` extra requires Django 4.2+, and the `[flask]` extra requires Flask 3.0+.
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
- 🔐 **Secure SSO Authentication** — JWT token-based authentication
|
|
54
|
+
- 🛡️ **HMAC Signature Validation** — Request signing with SHA-256
|
|
55
|
+
- 👥 **Role-Based Access Control** — Decorators for role protection
|
|
56
|
+
- 🐍 **Django & Flask Support** — Built-in middleware and decorators
|
|
57
|
+
- 📦 **Type Hints** — Full typing support for IDE autocomplete
|
|
58
|
+
- 🔄 **Token Caching** — In-memory cache for validated tokens
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install cas-system-client
|
|
64
|
+
|
|
65
|
+
# With Django support
|
|
66
|
+
pip install cas-system-client[django]
|
|
67
|
+
|
|
68
|
+
# With Flask support
|
|
69
|
+
pip install cas-system-client[flask]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Quick Start
|
|
73
|
+
|
|
74
|
+
### 1. Initialize the Client
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from cas_client import CasClient
|
|
78
|
+
|
|
79
|
+
cas = CasClient(
|
|
80
|
+
server_url='https://your-cas-server.com',
|
|
81
|
+
client_id='your_client_id',
|
|
82
|
+
client_secret='your_client_secret',
|
|
83
|
+
callback_url='https://your-app.com/cas/callback',
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### How the One System SSO flow works
|
|
88
|
+
|
|
89
|
+
1. Send the browser to `cas.get_login_url()` — this redirects to
|
|
90
|
+
`GET {server_url}/sso/login?client_id=...`.
|
|
91
|
+
2. One System authenticates the user and redirects back to your registered
|
|
92
|
+
`callback_url` with a single-use JWT appended as `?token=<JWT>`.
|
|
93
|
+
3. In your callback, call `cas.validate_token(token)`. The client validates the
|
|
94
|
+
token **server-to-server** (sending `client_id` + `client_secret`) and returns
|
|
95
|
+
the user dict `{id, username, email, ...}` on success, or `None`. Tokens are
|
|
96
|
+
single-use — validate once, then rely on your framework session.
|
|
97
|
+
|
|
98
|
+
### 2. Django Integration
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
# settings.py
|
|
102
|
+
import os
|
|
103
|
+
from cas_client import CasClient
|
|
104
|
+
|
|
105
|
+
CAS_CLIENT = CasClient(
|
|
106
|
+
server_url=os.environ['CAS_SERVER_URL'],
|
|
107
|
+
client_id=os.environ['CAS_CLIENT_ID'],
|
|
108
|
+
client_secret=os.environ['CAS_CLIENT_SECRET'],
|
|
109
|
+
callback_url=os.environ['CAS_CALLBACK_URL'],
|
|
110
|
+
)
|
|
111
|
+
CAS_PROTECTED_PATHS = ['/dashboard', '/admin']
|
|
112
|
+
CAS_LOGIN_URL = '/auth/login'
|
|
113
|
+
|
|
114
|
+
MIDDLEWARE = [
|
|
115
|
+
...
|
|
116
|
+
'cas_client.middleware.DjangoCasMiddleware',
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
# views.py
|
|
120
|
+
from cas_client.middleware import django_role_required
|
|
121
|
+
|
|
122
|
+
@django_role_required('admin')
|
|
123
|
+
def admin_view(request):
|
|
124
|
+
user = request.cas_user
|
|
125
|
+
return JsonResponse({'user': user['username']})
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 3. Flask Integration
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from cas_client import CasClient
|
|
132
|
+
from cas_client.middleware import flask_cas_required, flask_role_required
|
|
133
|
+
|
|
134
|
+
cas = CasClient(
|
|
135
|
+
server_url='https://your-cas-server.com',
|
|
136
|
+
client_id='your_client_id',
|
|
137
|
+
client_secret='your_client_secret',
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@app.route('/dashboard')
|
|
141
|
+
@flask_cas_required(cas)
|
|
142
|
+
def dashboard():
|
|
143
|
+
user = flask.g.cas_user
|
|
144
|
+
return f'Hello {user["username"]}'
|
|
145
|
+
|
|
146
|
+
@app.route('/admin')
|
|
147
|
+
@flask_cas_required(cas)
|
|
148
|
+
@flask_role_required(cas, 'admin')
|
|
149
|
+
def admin():
|
|
150
|
+
return 'Admin area'
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 4. Manual Authentication
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
# Generate SSO token
|
|
157
|
+
token_data = cas.generate_sso_token('john_doe')
|
|
158
|
+
|
|
159
|
+
# Validate token
|
|
160
|
+
user = cas.validate_token(token)
|
|
161
|
+
|
|
162
|
+
# Role checks
|
|
163
|
+
if cas.user_has_role(user, 'admin'):
|
|
164
|
+
print('User is admin')
|
|
165
|
+
|
|
166
|
+
# Logout
|
|
167
|
+
cas.logout(token)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## API Reference
|
|
171
|
+
|
|
172
|
+
| Method | Description |
|
|
173
|
+
|--------|-------------|
|
|
174
|
+
| `get_login_url(return_url)` | Generate CAS login URL |
|
|
175
|
+
| `generate_sso_token(username)` | Generate SSO token |
|
|
176
|
+
| `validate_token(token)` | Validate token, returns user dict |
|
|
177
|
+
| `get_user_from_token(token)` | Get cached user data |
|
|
178
|
+
| `logout(token)` | Logout from CAS server |
|
|
179
|
+
| `user_has_role(user, role)` | Check single role |
|
|
180
|
+
| `user_has_any_role(user, roles)` | Check any of roles |
|
|
181
|
+
| `user_has_all_roles(user, roles)` | Check all roles |
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
cas_client/__init__.py
|
|
5
|
+
cas_client/client.py
|
|
6
|
+
cas_client/middleware.py
|
|
7
|
+
cas_system_client.egg-info/PKG-INFO
|
|
8
|
+
cas_system_client.egg-info/SOURCES.txt
|
|
9
|
+
cas_system_client.egg-info/dependency_links.txt
|
|
10
|
+
cas_system_client.egg-info/requires.txt
|
|
11
|
+
cas_system_client.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cas_client
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name='cas-system-client',
|
|
5
|
+
version='2.0.0',
|
|
6
|
+
description='Python client for CAS (Central Authentication Service) SSO integration',
|
|
7
|
+
long_description=open('README.md').read(),
|
|
8
|
+
long_description_content_type='text/markdown',
|
|
9
|
+
author='CAS System',
|
|
10
|
+
author_email='support@innovativesolution.com.np',
|
|
11
|
+
url='https://github.com/InSol-2021/one-system',
|
|
12
|
+
project_urls={
|
|
13
|
+
'Source': 'https://github.com/InSol-2021/one-system/tree/master/packages/python-cas-client',
|
|
14
|
+
'Bug Tracker': 'https://github.com/InSol-2021/one-system/issues',
|
|
15
|
+
},
|
|
16
|
+
packages=find_packages(),
|
|
17
|
+
python_requires='>=3.9',
|
|
18
|
+
install_requires=[
|
|
19
|
+
# >=2.32.0 includes fixes for CVE-2023-32681 (.netrc/proxy credential leak)
|
|
20
|
+
# and CVE-2024-35195 (cert-verify bypass).
|
|
21
|
+
'requests>=2.32.0',
|
|
22
|
+
],
|
|
23
|
+
extras_require={
|
|
24
|
+
'django': ['django>=4.2'],
|
|
25
|
+
'flask': ['flask>=3.0'],
|
|
26
|
+
},
|
|
27
|
+
classifiers=[
|
|
28
|
+
'Development Status :: 5 - Production/Stable',
|
|
29
|
+
'Intended Audience :: Developers',
|
|
30
|
+
'License :: OSI Approved :: MIT License',
|
|
31
|
+
'Programming Language :: Python :: 3',
|
|
32
|
+
'Programming Language :: Python :: 3.9',
|
|
33
|
+
'Programming Language :: Python :: 3.10',
|
|
34
|
+
'Programming Language :: Python :: 3.11',
|
|
35
|
+
'Programming Language :: Python :: 3.12',
|
|
36
|
+
'Topic :: Software Development :: Libraries',
|
|
37
|
+
'Topic :: System :: Systems Administration :: Authentication/Directory',
|
|
38
|
+
],
|
|
39
|
+
keywords='cas sso authentication jwt single-sign-on',
|
|
40
|
+
license='MIT',
|
|
41
|
+
)
|