import argparse import base64 import json import secrets import yaml from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import padding, rsa from flask import Flask, redirect, render_template, request, session, url_for parser = argparse.ArgumentParser() parser.add_argument("config", help="Path to configuration file") args = parser.parse_args() with open(args.config) as f: config = yaml.safe_load(f) app = Flask(__name__) app.secret_key = config["secret_key"] private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) @app.route("/") def index(): return render_template("index.html", application_name=config["application_name"]) @app.route("/authorize") def authorize(): public_key = private_key.public_key().public_bytes( serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo, ).decode() nonce = secrets.token_hex(16) client_id = secrets.token_hex(48) session["nonce"] = nonce session["client_id"] = client_id params = { "auth_redirect": config["redirect_url"], "application_name": config["application_name"], "scopes": "read", "client_id": client_id, "nonce": nonce, "public_key": public_key, } query = "&".join(f"{k}={v}" for k, v in params.items()) return redirect(f"{config['forum_url']}/user-api-key/new?{query}") @app.route("/callback") def callback(): payload_b64 = request.args.get("payload") if not payload_b64: return "Missing payload", 400 try: payload = base64.b64decode(payload_b64) data = private_key.decrypt(payload, padding.PKCS1v15()) response = json.loads(data) except Exception: return "Failed to decrypt or decode payload", 400 if response.get("nonce") != session.get("nonce"): return "Invalid nonce", 400 key = response["key"] calendar_url = f"{config['forum_url']}/discourse-post-event/events.ics?order=desc&api_key={key}" return render_template( "result.html", application_name=config["application_name"], calendar_url=calendar_url, ) if __name__ == "__main__": app.run(host="0.0.0.0", port=8000)