Skip to content

Commit c02994b

Browse files
committed
working!
1 parent b432958 commit c02994b

File tree

4 files changed

+289
-14
lines changed

4 files changed

+289
-14
lines changed

docker-compose.yaml

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ services:
4747
context: .
4848
environment:
4949
UPSTREAM_URL: ${UPSTREAM_URL:-http://stac:8001}
50-
OIDC_DISCOVERY_URL: ${OIDC_DISCOVERY_URL:-http://127.0.0.1:3000/.well-known/openid-configuration}
51-
OIDC_DISCOVERY_INTERNAL_URL: ${OIDC_DISCOVERY_INTERNAL_URL:-http://auth0:3000/.well-known/openid-configuration}
50+
OIDC_DISCOVERY_URL: ${OIDC_DISCOVERY_URL:-http://localhost:3000/.well-known/openid-configuration}
51+
OIDC_DISCOVERY_INTERNAL_URL: ${OIDC_DISCOVERY_INTERNAL_URL:-http://mock-oidc:3000/.well-known/openid-configuration}
5252
env_file:
5353
- path: .env
5454
required: false
@@ -57,6 +57,14 @@ services:
5757
volumes:
5858
- ./src:/app/src
5959

60+
mock-oidc:
61+
build:
62+
context: ./mock_oidc_server
63+
ports:
64+
- "3000:3000"
65+
volumes:
66+
- ./mock_oidc_server:/app
67+
6068
# dex:
6169
# image: ghcr.io/dexidp/dex:v2.42.0-alpine
6270
# ports:
@@ -122,18 +130,18 @@ services:
122130
# } EOF
123131
# '
124132

125-
auth0:
126-
image: public.ecr.aws/primaassicurazioni/localauth0:0.8.2
127-
healthcheck:
128-
test: ["CMD", "/localauth0", "healthcheck"]
129-
ports:
130-
- "3000:3000"
131-
- "3001:3001"
132-
environment:
133-
LOCALAUTH0_CONFIG: |
134-
issuer = "https://prima.localauth0.com/"
135-
[user_info]
136-
given_name = "Locie"
133+
# auth0:
134+
# image: public.ecr.aws/primaassicurazioni/localauth0:0.8.2
135+
# healthcheck:
136+
# test: ["CMD", "/localauth0", "healthcheck"]
137+
# ports:
138+
# - "3000:3000"
139+
# - "3001:3001"
140+
# environment:
141+
# LOCALAUTH0_CONFIG: |
142+
# issuer = "https://prima.localauth0.com/"
143+
# [user_info]
144+
# given_name = "Locie"
137145

138146
networks:
139147
default:

mock_oidc_server/Dockerfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
FROM python:3.11-slim
2+
3+
WORKDIR /app
4+
5+
COPY requirements.txt .
6+
RUN pip install -r requirements.txt
7+
8+
COPY . .
9+
10+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "3000"]

mock_oidc_server/app.py

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
# ruff: noqa
2+
# type: ignore
3+
4+
import base64
5+
import hashlib
6+
import os
7+
from datetime import datetime, timedelta
8+
from typing import Optional
9+
from urllib.parse import urlencode
10+
11+
from cryptography.hazmat.primitives import serialization
12+
from cryptography.hazmat.primitives.asymmetric import rsa
13+
from fastapi import FastAPI, Form, HTTPException
14+
from fastapi.middleware.cors import CORSMiddleware
15+
from fastapi.responses import JSONResponse, RedirectResponse
16+
from jose import jwt
17+
18+
app = FastAPI()
19+
20+
# Configure CORS
21+
app.add_middleware(
22+
CORSMiddleware,
23+
allow_origins=["*"], # In production, replace with specific origins
24+
allow_credentials=True,
25+
allow_methods=["*"],
26+
allow_headers=["*"],
27+
expose_headers=["Content-Type"],
28+
max_age=86400, # 24 hours
29+
)
30+
31+
# Configuration
32+
ISSUER = "http://localhost:3000"
33+
34+
35+
# Generate RSA key pair
36+
def generate_key_pair():
37+
"""Generate RSA key pair and return private and public keys."""
38+
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
39+
private_pem = private_key.private_bytes(
40+
encoding=serialization.Encoding.PEM,
41+
format=serialization.PrivateFormat.PKCS8,
42+
encryption_algorithm=serialization.NoEncryption(),
43+
)
44+
public_key = private_key.public_key()
45+
public_numbers = public_key.public_numbers()
46+
47+
# Convert public key components to base64url format
48+
def int_to_base64url(value):
49+
"""Convert an integer to base64url format."""
50+
value_hex = format(value, "x")
51+
# Ensure even length
52+
if len(value_hex) % 2 == 1:
53+
value_hex = "0" + value_hex
54+
value_bytes = bytes.fromhex(value_hex)
55+
return base64.urlsafe_b64encode(value_bytes).rstrip(b"=").decode("ascii")
56+
57+
return (
58+
private_pem.decode("utf-8"),
59+
{
60+
"keys": [
61+
{
62+
"jwk": {
63+
"kty": "RSA",
64+
"use": "sig",
65+
"kid": "1", # Key ID
66+
"alg": "RS256",
67+
"n": int_to_base64url(public_numbers.n),
68+
"e": int_to_base64url(public_numbers.e),
69+
},
70+
}
71+
]
72+
},
73+
)
74+
75+
76+
# Generate key pair on startup
77+
PRIVATE_KEY, JWKS = generate_key_pair()
78+
79+
# In-memory storage
80+
authorization_codes = {}
81+
pkce_challenges = {}
82+
access_tokens = {}
83+
84+
# Mock client registry
85+
clients = {
86+
"stac": {
87+
"client_secret": "secret",
88+
"redirect_uris": ["http://localhost:8000/docs/oauth2-redirect"],
89+
"grant_types": ["authorization_code"],
90+
}
91+
}
92+
93+
94+
def generate_token(
95+
subject: str, expires_delta: timedelta = timedelta(minutes=15)
96+
) -> str:
97+
"""Generate a JWT token."""
98+
now = datetime.now(datetime.UTC)
99+
claims = {
100+
"iss": ISSUER,
101+
"sub": subject,
102+
"iat": now,
103+
"exp": now + expires_delta,
104+
"scope": "openid profile",
105+
"kid": "1", # Match the key ID from JWKS
106+
}
107+
return jwt.encode(claims, PRIVATE_KEY, algorithm="RS256", headers={"kid": "1"})
108+
109+
110+
@app.get("/.well-known/openid-configuration")
111+
async def openid_configuration():
112+
"""Return OpenID Connect configuration."""
113+
return {
114+
"issuer": ISSUER,
115+
"authorization_endpoint": f"{ISSUER}/authorize",
116+
"token_endpoint": f"{ISSUER}/token",
117+
"jwks_uri": f"{ISSUER}/.well-known/jwks.json",
118+
"response_types_supported": ["code"],
119+
"subject_types_supported": ["public"],
120+
"id_token_signing_alg_values_supported": ["RS256"],
121+
"scopes_supported": ["openid", "profile"],
122+
"token_endpoint_auth_methods_supported": ["client_secret_post", "none"],
123+
"claims_supported": ["sub", "iss", "iat", "exp"],
124+
"code_challenge_methods_supported": ["S256"],
125+
}
126+
127+
128+
@app.get("/.well-known/jwks.json")
129+
async def jwks():
130+
"""Return JWKS (JSON Web Key Set)."""
131+
return JWKS
132+
133+
134+
@app.get("/authorize")
135+
async def authorize(
136+
response_type: str,
137+
client_id: str,
138+
redirect_uri: str,
139+
state: str,
140+
scope: str = "",
141+
code_challenge: Optional[str] = None,
142+
code_challenge_method: Optional[str] = None,
143+
):
144+
"""Handle authorization request."""
145+
if response_type != "code":
146+
raise HTTPException(status_code=400, detail="Invalid response type")
147+
148+
# Validate client
149+
if client_id not in clients:
150+
raise HTTPException(status_code=400, detail="Invalid client_id")
151+
152+
# Validate redirect URI
153+
if redirect_uri not in clients[client_id]["redirect_uris"]:
154+
raise HTTPException(status_code=400, detail="Invalid redirect_uri")
155+
156+
# Validate PKCE if provided
157+
if code_challenge is not None:
158+
if code_challenge_method != "S256":
159+
raise HTTPException(status_code=400, detail="Only S256 PKCE is supported")
160+
161+
# Generate authorization code
162+
code = os.urandom(32).hex()
163+
164+
# Store authorization details
165+
authorization_codes[code] = {
166+
"client_id": client_id,
167+
"redirect_uri": redirect_uri,
168+
"scope": scope,
169+
}
170+
171+
# Store PKCE challenge if provided
172+
if code_challenge:
173+
pkce_challenges[code] = code_challenge
174+
175+
# Redirect back to client with the code
176+
params = {"code": code, "state": state}
177+
return RedirectResponse(url=f"{redirect_uri}?{urlencode(params)}")
178+
179+
180+
@app.post("/token")
181+
async def token(
182+
grant_type: str = Form(...),
183+
code: str = Form(...),
184+
redirect_uri: str = Form(...),
185+
client_id: str = Form(...),
186+
client_secret: Optional[str] = Form(None),
187+
code_verifier: Optional[str] = Form(None),
188+
):
189+
"""Handle token request."""
190+
if grant_type != "authorization_code":
191+
raise HTTPException(status_code=400, detail="Invalid grant type")
192+
193+
# Verify the authorization code exists
194+
if code not in authorization_codes:
195+
raise HTTPException(status_code=400, detail="Invalid authorization code")
196+
197+
auth_details = authorization_codes[code]
198+
199+
# Verify client_id matches the stored one
200+
if client_id != auth_details["client_id"]:
201+
raise HTTPException(status_code=400, detail="Client ID mismatch")
202+
203+
# Verify redirect_uri matches the stored one
204+
if redirect_uri != auth_details["redirect_uri"]:
205+
raise HTTPException(status_code=400, detail="Redirect URI mismatch")
206+
207+
# Check if PKCE was used in the authorization request
208+
if code in pkce_challenges:
209+
if not code_verifier:
210+
raise HTTPException(status_code=400, detail="Code verifier required")
211+
212+
# Verify the code verifier
213+
code_challenge = pkce_challenges[code]
214+
computed_challenge = hashlib.sha256(code_verifier.encode()).digest()
215+
computed_challenge = (
216+
base64.urlsafe_b64encode(computed_challenge).decode().rstrip("=")
217+
)
218+
219+
if computed_challenge != code_challenge:
220+
raise HTTPException(status_code=400, detail="Invalid code verifier")
221+
else:
222+
# If not PKCE, verify client secret
223+
if not client_secret:
224+
raise HTTPException(status_code=400, detail="Client secret required")
225+
226+
if client_secret != clients[client_id]["client_secret"]:
227+
raise HTTPException(status_code=400, detail="Invalid client secret")
228+
229+
# Clean up the used code and PKCE challenge
230+
del authorization_codes[code]
231+
if code in pkce_challenges:
232+
del pkce_challenges[code]
233+
234+
# Generate access token
235+
access_token = generate_token("user123")
236+
237+
response = JSONResponse(
238+
content={
239+
"access_token": access_token,
240+
"token_type": "Bearer",
241+
"expires_in": 900, # 15 minutes
242+
"scope": auth_details["scope"],
243+
}
244+
)
245+
246+
return response
247+
248+
249+
if __name__ == "__main__":
250+
import uvicorn
251+
252+
uvicorn.run(app, host="0.0.0.0", port=3000)

mock_oidc_server/requirements.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
fastapi==0.109.2
2+
uvicorn==0.27.1
3+
python-jose==3.3.0
4+
python-multipart==0.0.9
5+
cryptography==42.0.2

0 commit comments

Comments
 (0)