#!/usr/bin/env python3
"""Dummy identitetsleverandør for testing. Later som den er crew-panelet.
Crew-panelet har ikke OIDC ennå, så uten noe slikt kan ikke OIDC-veien prøves
i det hele tatt. Denne gjør det mulig: full autorisasjonskode-runde med PKCE,
og du velger selv hvilken rolle du vil logge inn med.
Den er *ikke* en IdP. Den signerer med klienthemmeligheten (HS256), lagrer
koder i minnet og spør ikke om passord. Bruk den bare bak compose.
"""
from __future__ import annotations
import base64
import hashlib
import os
import secrets
import time
import urllib.parse
import jwt
from flask import Flask, abort, jsonify, redirect, request
ISSUER = os.environ.get("IDP_ISSUER", "http://localhost:9000")
CLIENT_ID = os.environ.get("IDP_CLIENT_ID", "announcer")
CLIENT_SECRET = os.environ.get("IDP_CLIENT_SECRET", "test-client-secret-kun-for-docker")
# Folk man kan logge inn som. Rollenavnene må finnes i announcers [roles].
USERS = {
"kari": {"sub": "101", "name": "Kari Nordmann", "email": "kari@crew.test", "roles": ["crew"]},
"kent": {"sub": "1", "name": "Kent", "email": "kent@crew.test", "roles": ["admin"]},
"ola": {"sub": "102", "name": "Ola Utenfor", "email": "ola@crew.test", "roles": []},
}
app = Flask(__name__)
codes: dict[str, dict] = {}
@app.get("/.well-known/openid-configuration")
def discovery():
return jsonify({
"issuer": ISSUER,
"authorization_endpoint": f"{ISSUER}/oauth/authorize",
"token_endpoint": f"{ISSUER}/oauth/token",
"response_types_supported": ["code"],
"id_token_signing_alg_values_supported": ["HS256"],
"code_challenge_methods_supported": ["S256"],
})
@app.get("/oauth/authorize")
def authorize():
if request.args.get("client_id") != CLIENT_ID:
abort(400, "ukjent client_id")
for required in ("redirect_uri", "state", "code_challenge"):
if not request.args.get(required):
abort(400, f"mangler {required}")
if request.args.get("code_challenge_method") != "S256":
abort(400, "krever PKCE med S256")
who = request.args.get("who")
if who not in USERS:
# Ingen innloggingsside, bare en liste å trykke på.
rows = "".join(
f'
'
f'{v["name"]} – roller: {", ".join(v["roles"]) or "ingen"}'
for k, v in USERS.items()
)
return f"""Dummy-IdP
Dummy-IdP
Står for crew-panelet. Velg hvem du vil logge inn som:
Ola har ingen roller – han skal få 403, ikke 401.
"""
code = secrets.token_urlsafe(24)
codes[code] = {
"user": who,
"challenge": request.args["code_challenge"],
"redirect_uri": request.args["redirect_uri"],
"exp": time.time() + 300,
}
sep = "&" if "?" in request.args["redirect_uri"] else "?"
params = urllib.parse.urlencode({"code": code, "state": request.args["state"]})
return redirect(f"{request.args['redirect_uri']}{sep}{params}")
@app.post("/oauth/token")
def token():
code = request.form.get("code", "")
entry = codes.pop(code, None) # engangsbruk
if not entry or entry["exp"] < time.time():
return jsonify({"error": "invalid_grant"}), 400
if request.form.get("redirect_uri") != entry["redirect_uri"]:
return jsonify({"error": "invalid_grant", "error_description": "redirect_uri stemmer ikke"}), 400
# PKCE: verifier skal hashe til challenge vi fikk i authorize.
verifier = request.form.get("code_verifier", "")
expected = base64.urlsafe_b64encode(
hashlib.sha256(verifier.encode()).digest()
).rstrip(b"=").decode()
if not verifier or expected != entry["challenge"]:
return jsonify({"error": "invalid_grant", "error_description": "PKCE stemmer ikke"}), 400
user = USERS[entry["user"]]
claims = {
"iss": ISSUER,
"aud": CLIENT_ID,
"iat": int(time.time()),
"exp": int(time.time()) + 3600,
**user,
}
return jsonify({
"access_token": secrets.token_urlsafe(16),
"token_type": "Bearer",
"expires_in": 3600,
"id_token": jwt.encode(claims, CLIENT_SECRET, algorithm="HS256"),
})
@app.get("/token")
def mint():
"""Lager et JWT til token-lenka og til curl, slik panelet skal gjøre.
http://localhost:9000/token?who=kari -> rått token
http://localhost:9000/token?who=kari&go=1 -> logger deg rett inn
"""
user = USERS.get(request.args.get("who", "kari"))
if not user:
abort(404, "ukjent bruker")
raw = jwt.encode(
{"iss": ISSUER, "aud": CLIENT_ID, "exp": int(time.time()) + 300, **user},
os.environ.get("ANNOUNCER_JWT_SECRET", "test-hemmelighet-kun-for-docker-ikke-bruk-ekte"),
algorithm="HS256",
)
if request.args.get("go"):
base = os.environ.get("ANNOUNCER_PUBLIC_URL", "http://localhost:8091")
return redirect(f"{base}/auth/token?t={raw}")
return raw, 200, {"Content-Type": "text/plain"}
if __name__ == "__main__":
app.run(host="0.0.0.0", port=9000, threaded=True)