-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
315 lines (250 loc) · 10.7 KB
/
Copy pathmain.py
File metadata and controls
315 lines (250 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
from fastapi import FastAPI, Request, Depends, HTTPException, status
from fastapi.responses import RedirectResponse, HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.sessions import SessionMiddleware
from pydantic import BaseModel
import urllib.parse
import httpx
import xmltodict
from typing import Optional
import os
from dotenv import load_dotenv
load_dotenv()
app = FastAPI(title="OpenCAS - FastAPI CAS Authentication")
# Add CORS middleware to allow requests from Next.js app
app.add_middleware(
CORSMiddleware,
allow_origins=os.getenv(
"ALLOWED_ORIGINS", "http://localhost:3000").split(","),
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Add session middleware for storing user session
app.add_middleware(
SessionMiddleware,
secret_key=os.getenv(
"SECRET_KEY", "your-secret-key-change-this-in-production")
)
# Setup templates
templates = Jinja2Templates(directory="templates")
# CAS Configuration
CAS_SERVER_URL = "https://login.iiit.ac.in/cas"
CAS_LOGIN_URL = f"{CAS_SERVER_URL}/login"
CAS_VALIDATE_URL = f"{CAS_SERVER_URL}/serviceValidate"
# Pydantic models for API
class AuthValidateRequest(BaseModel):
ticket: str
service: str
class UserData(BaseModel):
id: str
email: str
name: str
roll_number: str
first_name: str
last_name: str
class AuthValidateResponse(BaseModel):
success: bool
user: Optional[UserData] = None
error: Optional[str] = None
def get_current_user(request: Request):
"""Dependency to get the current authenticated user from session"""
user = request.session.get("user")
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated"
)
return user
def get_service_url(request: Request) -> str:
"""Generate the service URL for CAS"""
# Use the base URL of the application
return str(request.base_url).rstrip("/")
@app.get("/", response_class=HTMLResponse)
async def home(request: Request, ticket: Optional[str] = None):
"""Home page - shows login button or user info
NOTE: This endpoint does NOT handle CAS callbacks.
Tickets should be handled by the /auth/callback endpoint.
This prevents OpenCAS from intercepting tickets meant for external applications.
"""
# Normal home page rendering - no ticket processing
user = request.session.get("user")
return templates.TemplateResponse(
"index.html",
{"request": request, "user": user}
)
@app.get("/login")
async def login(request: Request, service: Optional[str] = None):
"""Redirect to CAS login page.
If an external application provides a `service` query parameter (for example
the Next.js app's URL), forward that to the CAS server so CAS will redirect
back to the external application instead of OpenCAS. This enables a proxy
login flow where apps redirect users to OpenCAS which then sends them to CAS
with the desired service URL.
"""
# Prefer an explicit `service` query parameter sent by the calling app.
# Fall back to OpenCAS's own auth callback URL if none is provided.
if service:
service_url = service
else:
# For OpenCAS's own login, use the auth callback endpoint
service_url = f"{get_service_url(request)}/auth/callback"
cas_login_url = f"{CAS_LOGIN_URL}?service={urllib.parse.quote(service_url)}"
return RedirectResponse(url=cas_login_url)
@app.get("/auth/callback")
async def auth_callback(request: Request, ticket: Optional[str] = None):
"""Handle CAS callback for OpenCAS's own authentication.
This endpoint receives tickets that were issued specifically for OpenCAS.
External applications should handle their own tickets at their callback URLs.
"""
if not ticket:
# No ticket provided, redirect to home
return RedirectResponse(url="/", status_code=303)
try:
# Build the service URL that was used for this callback
service_url = f"{get_service_url(request)}/auth/callback"
# Validate the ticket with CAS server
validation_url = f"{CAS_VALIDATE_URL}?ticket={ticket}&service={urllib.parse.quote(service_url)}"
async with httpx.AsyncClient() as client:
response = await client.get(validation_url)
xml_response = response.text
# Parse XML response
result = xmltodict.parse(xml_response)
# Check for authentication failure
service_response = result.get("cas:serviceResponse", {})
if "cas:authenticationFailure" in service_response:
# Authentication failed
return RedirectResponse(url="/?error=auth_failed", status_code=303)
# Extract authentication success data
auth_success = service_response.get("cas:authenticationSuccess")
if not auth_success:
return RedirectResponse(url="/?error=auth_failed", status_code=303)
# Extract user information
username = auth_success.get("cas:user")
attributes = auth_success.get("cas:attributes", {})
# Extract user details from attributes
email = attributes.get("cas:E-Mail", f"{username}@iiit.ac.in")
first_name = attributes.get("cas:FirstName", username)
last_name = attributes.get("cas:LastName", "")
roll_no = attributes.get("cas:RollNo", username)
# Store user in session
user_data = {
"id": username,
"email": email,
"name": f"{first_name} {last_name}".strip() if first_name and last_name else username,
"roll_number": roll_no,
"first_name": first_name,
"last_name": last_name
}
request.session["user"] = user_data
# Redirect to dashboard
return RedirectResponse(url="/dashboard", status_code=303)
except Exception as e:
print(f"CAS validation error: {e}")
return RedirectResponse(url="/?error=validation_error", status_code=303)
@app.get("/logout")
async def logout(request: Request, service: Optional[str] = None):
"""Logout the user from OpenCAS and CAS.
If a service URL is provided, redirect to CAS logout with that service URL
so the user is redirected back to the external application after logout.
Otherwise, redirect to CAS logout and return to OpenCAS home.
"""
# Clear the local session
request.session.clear()
# Determine where to redirect after CAS logout
if service:
# External application wants user redirected back to it after logout
redirect_url = service
else:
# No service specified, redirect back to OpenCAS home
redirect_url = str(request.base_url).rstrip("/")
# CAS logout URL - must use full URL path
# The service parameter tells CAS where to redirect the user after logout
cas_logout_url = f"{CAS_SERVER_URL}/logout?service={urllib.parse.quote(redirect_url)}"
return RedirectResponse(url=cas_logout_url)
@app.get("/dashboard", response_class=HTMLResponse)
async def dashboard(request: Request, user: dict = Depends(get_current_user)):
"""Protected dashboard page"""
return templates.TemplateResponse(
"dashboard.html",
{"request": request, "user": user}
)
@app.get("/api/me")
async def get_user_info(user: dict = Depends(get_current_user)):
"""API endpoint to get current user information"""
return user
@app.post("/api/auth/validate", response_model=AuthValidateResponse)
async def validate_cas_ticket(request: AuthValidateRequest):
"""
API endpoint to validate CAS ticket and return user information.
This is used by external applications to authenticate users.
"""
try:
ticket = request.ticket
service = request.service
if not ticket:
return AuthValidateResponse(
success=False,
error="No CAS ticket provided"
)
if not service:
return AuthValidateResponse(
success=False,
error="No service URL provided"
)
# Validate the ticket with the CAS server
validation_url = f"{CAS_VALIDATE_URL}?ticket={ticket}&service={urllib.parse.quote(service)}"
async with httpx.AsyncClient() as client:
response = await client.get(validation_url)
xml_response = response.text
# Parse XML response
result = xmltodict.parse(xml_response)
# Check for authentication failure
service_response = result.get("cas:serviceResponse", {})
if "cas:authenticationFailure" in service_response:
failure = service_response["cas:authenticationFailure"]
error_msg = failure.get("#text", "Unknown error")
error_code = failure.get("@code", "UNKNOWN")
return AuthValidateResponse(
success=False,
error=f"CAS authentication failed: {error_msg} (Code: {error_code})"
)
# Extract authentication success data
auth_success = service_response.get("cas:authenticationSuccess")
if not auth_success:
return AuthValidateResponse(
success=False,
error="CAS authentication failed: No success response"
)
# Extract user information
username = auth_success.get("cas:user")
attributes = auth_success.get("cas:attributes", {})
# Extract user details from attributes
email = attributes.get("cas:E-Mail", f"{username}@iiit.ac.in")
first_name = attributes.get("cas:FirstName", username)
last_name = attributes.get("cas:LastName", "")
roll_no = attributes.get("cas:RollNo", username)
# Create user data object
user_data = UserData(
id=username,
email=email,
name=f"{first_name} {last_name}".strip(
) if first_name and last_name else username,
roll_number=roll_no,
first_name=first_name,
last_name=last_name
)
return AuthValidateResponse(
success=True,
user=user_data
)
except Exception as e:
return AuthValidateResponse(
success=False,
error=f"Error validating ticket: {str(e)}"
)
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)