230 lines
7.8 KiB
Python
Executable File
230 lines
7.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
OIDC Test Client
|
|
Demonstriert den vollständigen Authorization Code Flow
|
|
"""
|
|
|
|
from flask import Flask, request, redirect, session, render_template_string
|
|
import requests
|
|
from urllib.parse import urlencode
|
|
import secrets
|
|
|
|
app = Flask(__name__)
|
|
app.secret_key = secrets.token_hex(32)
|
|
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
|
app.config['SESSION_COOKIE_SECURE'] = False
|
|
|
|
# OIDC Server Konfiguration
|
|
OIDC_ISSUER = "http://localhost:5000"
|
|
CLIENT_ID = "test-client"
|
|
CLIENT_SECRET = "test-secret"
|
|
REDIRECT_URI = "http://localhost:8080/callback"
|
|
|
|
# In-Memory Store für OAuth States (überlebt Debug-Reloads nicht, aber reicht für Tests)
|
|
oauth_states = {}
|
|
|
|
# HTML Template
|
|
HOME_TEMPLATE = """
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>OIDC Test Client</title>
|
|
<link rel="stylesheet" href="http://localhost:5000/static/styles.css">
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header>
|
|
<h1>🧪 OIDC Test Client</h1>
|
|
<p>Test-Anwendung für den Authorization Code Flow</p>
|
|
</header>
|
|
|
|
{% if user_info %}
|
|
<div class="modal-content" style="max-width: 600px; margin: 0 auto;">
|
|
<h2 style="color: var(--text-main); margin-bottom: 24px;">✅ Login erfolgreich!</h2>
|
|
|
|
<div class="stat-card" style="margin-bottom: 20px;">
|
|
<h3>User Information</h3>
|
|
<div style="margin-top: 16px;">
|
|
<p><strong>Subject ID:</strong> {{ user_info.sub }}</p>
|
|
<p><strong>Name:</strong> {{ user_info.name }}</p>
|
|
<p><strong>Email:</strong> {{ user_info.email }}</p>
|
|
<p><strong>Username:</strong> {{ user_info.preferred_username }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card" style="margin-bottom: 20px;">
|
|
<h3>Access Token</h3>
|
|
<div style="margin-top: 16px; word-break: break-all; font-family: monospace; font-size: 0.85rem;">
|
|
{{ tokens.access_token }}
|
|
</div>
|
|
<p style="margin-top: 12px; color: var(--text-secondary); font-size: 0.9rem;">
|
|
Expires in: {{ tokens.expires_in }} seconds
|
|
</p>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<h3>ID Token (JWT)</h3>
|
|
<details>
|
|
<summary style="cursor: pointer; color: var(--primary); margin-bottom: 12px;">Token anzeigen</summary>
|
|
<div style="word-break: break-all; font-family: monospace; font-size: 0.85rem; margin-top: 12px;">
|
|
{{ tokens.id_token }}
|
|
</div>
|
|
</details>
|
|
</div>
|
|
|
|
<div style="text-align: center; margin-top: 24px;">
|
|
<a href="/logout">
|
|
<button class="danger">Logout</button>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="modal-content" style="max-width: 500px; margin: 0 auto; text-align: center;">
|
|
<div style="margin: 40px 0;">
|
|
<svg style="width: 80px; height: 80px; stroke: var(--primary); margin-bottom: 24px;" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
|
|
</svg>
|
|
|
|
<h2 style="color: var(--text-main); margin-bottom: 16px;">Nicht eingeloggt</h2>
|
|
<p style="color: var(--text-secondary); margin-bottom: 32px;">
|
|
Teste den OIDC Authorization Code Flow
|
|
</p>
|
|
|
|
<a href="/login">
|
|
<button style="font-size: 1.1rem; padding: 14px 32px;">
|
|
Mit OIDC einloggen →
|
|
</button>
|
|
</a>
|
|
</div>
|
|
|
|
<div style="margin-top: 40px; padding-top: 24px; border-top: 2px solid var(--border-main); text-align: left;">
|
|
<h3 style="color: var(--text-main); margin-bottom: 16px;">Flow-Ablauf:</h3>
|
|
<ol style="color: var(--text-secondary); line-height: 1.8;">
|
|
<li>Klick auf "Mit OIDC einloggen"</li>
|
|
<li>Weiterleitung zum OIDC Server (localhost:5000)</li>
|
|
<li>Login mit deinen Credentials</li>
|
|
<li>Authorization Code wird zurückgegeben</li>
|
|
<li>Client tauscht Code gegen Tokens</li>
|
|
<li>UserInfo wird abgerufen</li>
|
|
</ol>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
|
|
@app.route('/')
|
|
def index():
|
|
"""Startseite"""
|
|
user_info = session.get('user_info')
|
|
tokens = session.get('tokens')
|
|
|
|
return render_template_string(
|
|
HOME_TEMPLATE,
|
|
user_info=user_info,
|
|
tokens=tokens
|
|
)
|
|
|
|
|
|
@app.route('/login')
|
|
def login():
|
|
"""Startet den Authorization Flow"""
|
|
# State für CSRF-Protection generieren
|
|
state = secrets.token_urlsafe(32)
|
|
|
|
# State in In-Memory Store speichern
|
|
oauth_states[state] = True
|
|
|
|
# Authorization URL bauen
|
|
auth_params = {
|
|
'client_id': CLIENT_ID,
|
|
'redirect_uri': REDIRECT_URI,
|
|
'response_type': 'code',
|
|
'scope': 'openid profile email',
|
|
'state': state
|
|
}
|
|
|
|
auth_url = f"{OIDC_ISSUER}/authorize?{urlencode(auth_params)}"
|
|
|
|
return redirect(auth_url)
|
|
|
|
|
|
@app.route('/callback')
|
|
def callback():
|
|
"""Callback Endpoint - empfängt Authorization Code"""
|
|
# Authorization Code und State aus Query Params
|
|
code = request.args.get('code')
|
|
state = request.args.get('state')
|
|
|
|
# State validieren (CSRF-Protection)
|
|
if not state or state not in oauth_states:
|
|
return f"Invalid state parameter. State: {state}, Valid states: {list(oauth_states.keys())}", 400
|
|
|
|
# State verbrauchen (einmalige Verwendung)
|
|
del oauth_states[state]
|
|
|
|
if not code:
|
|
error = request.args.get('error')
|
|
error_description = request.args.get('error_description', '')
|
|
return f"Authorization failed: {error} - {error_description}", 400
|
|
|
|
# Authorization Code gegen Tokens tauschen
|
|
token_data = {
|
|
'grant_type': 'authorization_code',
|
|
'code': code,
|
|
'redirect_uri': REDIRECT_URI,
|
|
'client_id': CLIENT_ID,
|
|
'client_secret': CLIENT_SECRET
|
|
}
|
|
|
|
try:
|
|
token_response = requests.post(f"{OIDC_ISSUER}/token", data=token_data)
|
|
token_response.raise_for_status()
|
|
tokens = token_response.json()
|
|
except requests.RequestException as e:
|
|
return f"Token exchange failed: {str(e)}", 500
|
|
|
|
# UserInfo mit Access Token abrufen
|
|
try:
|
|
userinfo_response = requests.get(
|
|
f"{OIDC_ISSUER}/userinfo",
|
|
headers={'Authorization': f"Bearer {tokens['access_token']}"}
|
|
)
|
|
userinfo_response.raise_for_status()
|
|
user_info = userinfo_response.json()
|
|
except requests.RequestException as e:
|
|
return f"UserInfo request failed: {str(e)}", 500
|
|
|
|
# In Session speichern
|
|
session['tokens'] = tokens
|
|
session['user_info'] = user_info
|
|
|
|
# Zurück zur Startseite
|
|
return redirect('/')
|
|
|
|
|
|
@app.route('/logout')
|
|
def logout():
|
|
"""Logout - löscht Session"""
|
|
session.clear()
|
|
return redirect('/')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
print("=" * 60)
|
|
print("🧪 OIDC Test Client gestartet")
|
|
print("=" * 60)
|
|
print(f"Client URL: http://localhost:8080")
|
|
print(f"OIDC Server: {OIDC_ISSUER}")
|
|
print(f"Callback URI: {REDIRECT_URI}")
|
|
print("=" * 60)
|
|
print("\nÖffne im Browser: http://localhost:8080")
|
|
print("=" * 60)
|
|
|
|
app.run(host='0.0.0.0', port=8080, debug=True)
|