chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
Weather API OAuth2 Test Server
|
||||
|
||||
A simple FastAPI server that implements OAuth2 flows for weather API testing:
|
||||
- Client Credentials Flow
|
||||
- Authorization Code Flow
|
||||
|
||||
Usage:
|
||||
python oauth2_test_server.py
|
||||
|
||||
Endpoints:
|
||||
GET /auth - Authorization endpoint (auth code flow)
|
||||
POST /token - Token endpoint (both flows)
|
||||
GET /.well-known/openid_configuration - OpenID Connect discovery
|
||||
GET /api/weather - Weather API (requires Bearer token)
|
||||
"""
|
||||
|
||||
import secrets
|
||||
import time
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi import Form
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Query
|
||||
from fastapi import Request
|
||||
from fastapi import status
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.responses import RedirectResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI(title="Weather API OAuth2 Server", version="1.0.0")
|
||||
|
||||
# In-memory storage (for testing only)
|
||||
clients = {
|
||||
"test_client": {
|
||||
"client_secret": "test_secret",
|
||||
"redirect_uris": [
|
||||
"http://localhost:8080/callback",
|
||||
"urn:ietf:wg:oauth:2.0:oob",
|
||||
],
|
||||
"scopes": ["read", "write", "admin"],
|
||||
}
|
||||
}
|
||||
|
||||
authorization_codes = {} # code -> {client_id, redirect_uri, scope, expires_at}
|
||||
access_tokens = {} # token -> {client_id, scope, expires_at, token_type}
|
||||
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
access_token: str
|
||||
token_type: str = "Bearer"
|
||||
expires_in: int = 3600
|
||||
refresh_token: Optional[str] = None
|
||||
scope: Optional[str] = None
|
||||
|
||||
|
||||
@app.get("/.well-known/openid_configuration")
|
||||
async def openid_configuration():
|
||||
"""OpenID Connect Discovery endpoint."""
|
||||
return {
|
||||
"issuer": "http://localhost:8080",
|
||||
"authorization_endpoint": "http://localhost:8080/auth",
|
||||
"token_endpoint": "http://localhost:8080/token",
|
||||
"userinfo_endpoint": "http://localhost:8080/userinfo",
|
||||
"revocation_endpoint": "http://localhost:8080/revoke",
|
||||
"scopes_supported": ["openid", "read", "write", "admin"],
|
||||
"response_types_supported": ["code"],
|
||||
"grant_types_supported": ["authorization_code", "client_credentials"],
|
||||
"token_endpoint_auth_methods_supported": [
|
||||
"client_secret_basic",
|
||||
"client_secret_post",
|
||||
],
|
||||
"subject_types_supported": ["public"],
|
||||
}
|
||||
|
||||
|
||||
@app.get("/auth")
|
||||
async def authorize(
|
||||
response_type: str = Query(...),
|
||||
client_id: str = Query(...),
|
||||
redirect_uri: str = Query(...),
|
||||
scope: str = Query(default="read"),
|
||||
state: str = Query(default=""),
|
||||
):
|
||||
"""Authorization endpoint for OAuth2 authorization code flow."""
|
||||
|
||||
# Validate client
|
||||
if client_id not in clients:
|
||||
raise HTTPException(status_code=400, detail="Invalid client_id")
|
||||
|
||||
client = clients[client_id]
|
||||
if redirect_uri not in client["redirect_uris"]:
|
||||
raise HTTPException(status_code=400, detail="Invalid redirect_uri")
|
||||
|
||||
if response_type != "code":
|
||||
raise HTTPException(status_code=400, detail="Unsupported response_type")
|
||||
|
||||
# Generate authorization code
|
||||
auth_code = secrets.token_urlsafe(32)
|
||||
authorization_codes[auth_code] = {
|
||||
"client_id": client_id,
|
||||
"redirect_uri": redirect_uri,
|
||||
"scope": scope,
|
||||
"expires_at": time.time() + 600, # 10 minutes
|
||||
}
|
||||
|
||||
# Simulate user consent - in real implementation, this would show a consent form
|
||||
params = f"code={auth_code}"
|
||||
if state:
|
||||
params += f"&state={state}"
|
||||
|
||||
return RedirectResponse(url=f"{redirect_uri}?{params}")
|
||||
|
||||
|
||||
@app.post("/token")
|
||||
async def token_endpoint(
|
||||
request: Request,
|
||||
grant_type: str = Form(...),
|
||||
client_id: str = Form(default=None),
|
||||
client_secret: str = Form(default=None),
|
||||
code: str = Form(default=None),
|
||||
redirect_uri: str = Form(default=None),
|
||||
scope: str = Form(default="read"),
|
||||
):
|
||||
"""Token endpoint for both client credentials and authorization code flows."""
|
||||
|
||||
# Support both HTTP Basic auth and form-based client authentication
|
||||
auth_header = request.headers.get("Authorization")
|
||||
|
||||
if auth_header and auth_header.startswith("Basic "):
|
||||
# HTTP Basic authentication
|
||||
import base64
|
||||
|
||||
try:
|
||||
encoded_credentials = auth_header[6:] # Remove "Basic " prefix
|
||||
decoded = base64.b64decode(encoded_credentials).decode("utf-8")
|
||||
basic_client_id, basic_client_secret = decoded.split(":", 1)
|
||||
client_id = client_id or basic_client_id
|
||||
client_secret = client_secret or basic_client_secret
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=401, detail="Invalid authorization header"
|
||||
)
|
||||
|
||||
if not client_id or not client_secret:
|
||||
raise HTTPException(status_code=400, detail="Client credentials required")
|
||||
|
||||
# Validate client credentials
|
||||
if client_id not in clients:
|
||||
raise HTTPException(status_code=401, detail="Invalid client")
|
||||
|
||||
client = clients[client_id]
|
||||
if client["client_secret"] != client_secret:
|
||||
raise HTTPException(status_code=401, detail="Invalid client credentials")
|
||||
|
||||
if grant_type == "client_credentials":
|
||||
return await handle_client_credentials(client_id, scope)
|
||||
elif grant_type == "authorization_code":
|
||||
return await handle_authorization_code(client_id, code, redirect_uri, scope)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Unsupported grant_type")
|
||||
|
||||
|
||||
async def handle_client_credentials(
|
||||
client_id: str, scope: str
|
||||
) -> TokenResponse:
|
||||
"""Handle client credentials flow."""
|
||||
|
||||
# Generate access token
|
||||
access_token = secrets.token_urlsafe(32)
|
||||
expires_at = time.time() + 3600 # 1 hour
|
||||
|
||||
# Store token
|
||||
access_tokens[access_token] = {
|
||||
"client_id": client_id,
|
||||
"scope": scope,
|
||||
"expires_at": expires_at,
|
||||
"token_type": "Bearer",
|
||||
}
|
||||
|
||||
return TokenResponse(
|
||||
access_token=access_token,
|
||||
token_type="Bearer",
|
||||
expires_in=3600,
|
||||
scope=scope,
|
||||
)
|
||||
|
||||
|
||||
async def handle_authorization_code(
|
||||
client_id: str, code: str, redirect_uri: str, scope: str
|
||||
) -> TokenResponse:
|
||||
"""Handle authorization code flow."""
|
||||
|
||||
if not code:
|
||||
raise HTTPException(status_code=400, detail="Missing authorization code")
|
||||
|
||||
if code not in authorization_codes:
|
||||
raise HTTPException(status_code=400, detail="Invalid authorization code")
|
||||
|
||||
auth_data = authorization_codes[code]
|
||||
|
||||
# Validate authorization code
|
||||
if time.time() > auth_data["expires_at"]:
|
||||
del authorization_codes[code]
|
||||
raise HTTPException(status_code=400, detail="Authorization code expired")
|
||||
|
||||
if auth_data["client_id"] != client_id:
|
||||
raise HTTPException(status_code=400, detail="Client mismatch")
|
||||
|
||||
if redirect_uri and auth_data["redirect_uri"] != redirect_uri:
|
||||
raise HTTPException(status_code=400, detail="Redirect URI mismatch")
|
||||
|
||||
# Generate tokens
|
||||
access_token = secrets.token_urlsafe(32)
|
||||
refresh_token = secrets.token_urlsafe(32)
|
||||
expires_at = time.time() + 3600 # 1 hour
|
||||
|
||||
# Store token
|
||||
access_tokens[access_token] = {
|
||||
"client_id": client_id,
|
||||
"scope": auth_data["scope"],
|
||||
"expires_at": expires_at,
|
||||
"token_type": "Bearer",
|
||||
}
|
||||
|
||||
# Clean up authorization code (one-time use)
|
||||
del authorization_codes[code]
|
||||
|
||||
return TokenResponse(
|
||||
access_token=access_token,
|
||||
token_type="Bearer",
|
||||
expires_in=3600,
|
||||
refresh_token=refresh_token,
|
||||
scope=auth_data["scope"],
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/weather")
|
||||
async def get_weather(
|
||||
request: Request, city: str = "San Francisco", units: str = "metric"
|
||||
):
|
||||
"""Weather API endpoint that returns weather data for a city."""
|
||||
|
||||
# Check authentication
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if not auth_header or not auth_header.startswith("Bearer "):
|
||||
raise HTTPException(
|
||||
status_code=401, detail="Missing or invalid authorization header"
|
||||
)
|
||||
|
||||
token = auth_header[7:] # Remove "Bearer " prefix
|
||||
|
||||
if token not in access_tokens:
|
||||
raise HTTPException(status_code=401, detail="Invalid access token")
|
||||
|
||||
token_data = access_tokens[token]
|
||||
|
||||
if time.time() > token_data["expires_at"]:
|
||||
del access_tokens[token]
|
||||
raise HTTPException(status_code=401, detail="Access token expired")
|
||||
|
||||
# Return weather data (simulated)
|
||||
from datetime import datetime
|
||||
import random
|
||||
|
||||
conditions = ["Sunny", "Partly Cloudy", "Cloudy", "Light Rain", "Clear"]
|
||||
|
||||
weather_data = {
|
||||
"city": city,
|
||||
"temperature": random.randint(15, 30),
|
||||
"condition": random.choice(conditions),
|
||||
"humidity": random.randint(40, 80),
|
||||
"wind_speed": random.randint(5, 25),
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"units": units,
|
||||
"api_client": token_data["client_id"],
|
||||
}
|
||||
|
||||
return weather_data
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""Root endpoint with server information."""
|
||||
return HTMLResponse("""
|
||||
<html>
|
||||
<head><title>Weather API OAuth2 Server</title></head>
|
||||
<body>
|
||||
<h1>Weather API OAuth2 Server</h1>
|
||||
<h2>Available Endpoints:</h2>
|
||||
<ul>
|
||||
<li><strong>GET /auth</strong> - Authorization endpoint</li>
|
||||
<li><strong>POST /token</strong> - Token endpoint</li>
|
||||
<li><strong>GET /.well-known/openid_configuration</strong> - Discovery</li>
|
||||
<li><strong>GET /api/weather</strong> - Weather API (requires Bearer token)</li>
|
||||
</ul>
|
||||
|
||||
<h2>Test Client Credentials:</h2>
|
||||
<ul>
|
||||
<li><strong>Client ID:</strong> test_client</li>
|
||||
<li><strong>Client Secret:</strong> test_secret</li>
|
||||
<li><strong>Scopes:</strong> read, write, admin</li>
|
||||
</ul>
|
||||
|
||||
<h2>Example cURL Commands:</h2>
|
||||
<h3>Client Credentials Flow:</h3>
|
||||
<pre>
|
||||
curl -X POST http://localhost:8080/token \\
|
||||
-d "grant_type=client_credentials" \\
|
||||
-d "client_id=test_client" \\
|
||||
-d "client_secret=test_secret" \\
|
||||
-d "scope=read write"
|
||||
</pre>
|
||||
|
||||
<h3>Test Weather API:</h3>
|
||||
<pre>
|
||||
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \\
|
||||
"http://localhost:8080/api/weather?city=Tokyo"
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
""")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
print("🌤️ Starting Weather API OAuth2 Server...")
|
||||
print("📖 Documentation: http://localhost:8080/docs")
|
||||
print("🏠 Server Info: http://localhost:8080")
|
||||
print(
|
||||
'🔧 Test with: curl -H "Authorization: Bearer TOKEN"'
|
||||
' "http://localhost:8080/api/weather?city=Tokyo"'
|
||||
)
|
||||
uvicorn.run(app, host="0.0.0.0", port=8080, log_level="info")
|
||||
Reference in New Issue
Block a user