Skip to content

Commit 9cbce61

Browse files
committed
Add login page
1 parent 21bb70a commit 9cbce61

File tree

3 files changed

+154
-11
lines changed

3 files changed

+154
-11
lines changed

examples/mock_oidc_server/app.py

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,17 @@
1414

1515
from cryptography.hazmat.primitives import serialization
1616
from cryptography.hazmat.primitives.asymmetric import rsa
17-
from fastapi import FastAPI, Form, HTTPException
17+
from fastapi import FastAPI, Form, HTTPException, Request
1818
from fastapi.middleware.cors import CORSMiddleware
1919
from fastapi.responses import JSONResponse, RedirectResponse
20+
from fastapi.templating import Jinja2Templates
2021
from jose import jwt
2122

2223
app = FastAPI()
2324

25+
# Configure templates
26+
templates = Jinja2Templates(directory=str(Path(__file__).parent / "templates"))
27+
2428
# Configure CORS
2529
app.add_middleware(
2630
CORSMiddleware,
@@ -39,7 +43,7 @@
3943
"REDIRECT_URI", "http://localhost:8000/docs/oauth2-redirect"
4044
)
4145
ISSUER = os.environ.get("ISSUER", "http://localhost:3000")
42-
SCOPES = os.environ.get("SCOPES", "")
46+
AVAILABLE_SCOPES = os.environ.get("SCOPES", "")
4347
KEY_ID = "1"
4448

4549

@@ -104,6 +108,7 @@ def int_to_base64url(value):
104108
authorization_codes = {}
105109
pkce_challenges = {}
106110
access_tokens = {}
111+
auth_requests = {}
107112

108113
# Mock client registry
109114
CLIENT_REGISTRY = {
@@ -125,7 +130,7 @@ async def root():
125130
@app.get("/.well-known/openid-configuration")
126131
async def openid_configuration():
127132
"""Return OpenID Connect configuration."""
128-
scopes_set = set(["openid", "profile", *SCOPES.split(",")])
133+
scopes_set = set(["openid", "profile", *AVAILABLE_SCOPES.split(",")])
129134
return {
130135
"issuer": ISSUER,
131136
"authorization_endpoint": f"{ISSUER}/authorize",
@@ -149,6 +154,7 @@ async def jwks():
149154

150155
@app.get("/authorize")
151156
async def authorize(
157+
request: Request,
152158
response_type: str,
153159
client_id: str,
154160
redirect_uri: str,
@@ -174,23 +180,60 @@ async def authorize(
174180
if code_challenge_method != "S256":
175181
raise HTTPException(status_code=400, detail="Only S256 PKCE is supported")
176182

183+
# Store the auth request details
184+
request_id = os.urandom(16).hex()
185+
auth_requests[request_id] = {
186+
"client_id": client_id,
187+
"redirect_uri": redirect_uri,
188+
"state": state,
189+
"scope": scope,
190+
"code_challenge": code_challenge,
191+
"code_challenge_method": code_challenge_method,
192+
}
193+
194+
# Show login page
195+
scopes = sorted(set(("openid profile " + scope).split()))
196+
return templates.TemplateResponse(
197+
"login.html",
198+
{
199+
"request": request,
200+
"request_id": request_id,
201+
"client_id": client_id,
202+
"scopes": scopes,
203+
},
204+
)
205+
206+
207+
@app.post("/login")
208+
async def login(request_id: str = Form(...)):
209+
"""Handle login form submission."""
210+
# Retrieve the stored auth request
211+
if request_id not in auth_requests:
212+
raise HTTPException(status_code=400, detail="Invalid request")
213+
214+
auth_request = auth_requests.pop(request_id)
215+
177216
# Generate authorization code
178217
code = os.urandom(32).hex()
179218

180219
# Store authorization details
181220
authorization_codes[code] = {
182-
"client_id": client_id,
183-
"redirect_uri": redirect_uri,
184-
"scope": " ".join(sorted(set(("openid profile " + scope).split(" ")))),
221+
"client_id": auth_request["client_id"],
222+
"redirect_uri": auth_request["redirect_uri"],
223+
"scope": " ".join(
224+
sorted(set(("openid profile " + auth_request["scope"]).split(" ")))
225+
),
185226
}
186227

187228
# Store PKCE challenge if provided
188-
if code_challenge:
189-
pkce_challenges[code] = code_challenge
229+
if auth_request["code_challenge"]:
230+
pkce_challenges[code] = auth_request["code_challenge"]
190231

191232
# Redirect back to client with the code
192-
params = {"code": code, "state": state}
193-
return RedirectResponse(url=f"{redirect_uri}?{urlencode(params)}")
233+
params = {"code": code, "state": auth_request["state"]}
234+
return RedirectResponse(
235+
url=f"{auth_request['redirect_uri']}?{urlencode(params)}", status_code=303
236+
)
194237

195238

196239
@app.post("/token")

examples/mock_oidc_server/requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ fastapi==0.109.2
22
uvicorn==0.27.1
33
python-jose==3.3.0
44
python-multipart==0.0.9
5-
cryptography==42.0.2
5+
cryptography==42.0.2
6+
Jinja2==3.1.6
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>Mock OIDC Login</title>
5+
<style>
6+
body {
7+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
8+
display: flex;
9+
justify-content: center;
10+
align-items: center;
11+
height: 100vh;
12+
margin: 0;
13+
background-color: #f5f5f5;
14+
}
15+
.login-container {
16+
background: white;
17+
padding: 2rem;
18+
border-radius: 8px;
19+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
20+
text-align: center;
21+
max-width: 400px;
22+
width: 100%;
23+
}
24+
h1 {
25+
color: #333;
26+
margin-bottom: 1.5rem;
27+
}
28+
.info {
29+
color: #666;
30+
margin-bottom: 1rem;
31+
}
32+
.scopes {
33+
background: #f8f9fa;
34+
border-radius: 4px;
35+
padding: 1rem;
36+
margin-bottom: 2rem;
37+
text-align: left;
38+
}
39+
.scopes h2 {
40+
color: #495057;
41+
font-size: 1rem;
42+
margin: 0 0 0.5rem 0;
43+
}
44+
.scope-list {
45+
list-style: none;
46+
margin: 0;
47+
padding: 0;
48+
}
49+
.scope-list li {
50+
color: #666;
51+
padding: 0.25rem 0;
52+
font-size: 0.9rem;
53+
font-family: monospace;
54+
}
55+
.scope-list li::before {
56+
content: "✓";
57+
color: #28a745;
58+
margin-right: 0.5rem;
59+
}
60+
form {
61+
display: flex;
62+
justify-content: center;
63+
}
64+
button {
65+
background-color: #007bff;
66+
color: white;
67+
border: none;
68+
padding: 0.75rem 2rem;
69+
border-radius: 4px;
70+
cursor: pointer;
71+
font-size: 1rem;
72+
transition: background-color 0.2s;
73+
}
74+
button:hover {
75+
background-color: #0056b3;
76+
}
77+
</style>
78+
</head>
79+
<body>
80+
<div class="login-container">
81+
<h1>Mock OIDC Login</h1>
82+
<p class="info">
83+
Application <strong>{{ client_id }}</strong> is requesting access to your account.
84+
</p>
85+
<div class="scopes">
86+
<h2>Requested Permissions:</h2>
87+
<ul class="scope-list">
88+
{% for scope in scopes %}
89+
<li>{{ scope }}</li>
90+
{% endfor %}
91+
</ul>
92+
</div>
93+
<form method="POST" action="/login">
94+
<input type="hidden" name="request_id" value="{{ request_id }}">
95+
<button type="submit">Continue with Login</button>
96+
</form>
97+
</div>
98+
</body>
99+
</html>

0 commit comments

Comments
 (0)