#!/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)