import os import uuid from datetime import datetime import jwt from flask import Flask, redirect, render_template, request, url_for JWT_SECRET = os.environ.get("PASSPORT_JWT_SECRET") if not JWT_SECRET: raise RuntimeError("PASSPORT_JWT_SECRET environment variable is required") app = Flask(__name__) def parse_datetime(value): parsed = datetime.fromisoformat(value) if parsed.tzinfo is None: parsed = parsed.astimezone() return parsed def format_timestamp(timestamp): return datetime.fromtimestamp(timestamp).astimezone().strftime("%Y.%m.%d %H:%M") @app.get("/") def index(): return render_template("index.html", error=None, values={}) @app.post("/issue") def issue_pass(): values = { "holder": request.form.get("holder", "").strip(), "location": request.form.get("location", "").strip(), "valid_from": request.form.get("valid_from", "").strip(), "valid_until": request.form.get("valid_until", "").strip(), } if not values["holder"] or not values["location"]: return render_template( "index.html", error="Holder and location are required.", values=values ), 400 if len(values["holder"]) > 80 or len(values["location"]) > 120: return render_template( "index.html", error="Holder or location is too long.", values=values ), 400 try: valid_from = parse_datetime(values["valid_from"]) valid_until = parse_datetime(values["valid_until"]) except (TypeError, ValueError): return render_template( "index.html", error="Enter valid start and end times.", values=values ), 400 if valid_from >= valid_until: return render_template( "index.html", error="Valid From must be earlier than Valid Until.", values=values ), 400 issued_at = int(datetime.now().astimezone().timestamp()) payload = { "iss": "passport", "purpose": "access-pass", "holder": values["holder"], "location": values["location"], "iat": issued_at, "nbf": int(valid_from.timestamp()), "exp": int(valid_until.timestamp()), "jti": uuid.uuid4().hex, } token = jwt.encode(payload, JWT_SECRET, algorithm="HS256") return redirect(url_for("show_pass", token=token)) @app.get("/pass/") def show_pass(token): if len(token) > 4096: return render_template("invalid.html"), 400 try: payload = jwt.decode( token, JWT_SECRET, algorithms=["HS256"], issuer="passport", options={ "require": ["iss", "purpose", "holder", "location", "iat", "nbf", "exp"], "verify_exp": False, "verify_nbf": False, }, ) if payload.get("purpose") != "access-pass": raise jwt.InvalidTokenError("Invalid purpose") if not isinstance(payload.get("holder"), str) or not payload["holder"].strip(): raise jwt.InvalidTokenError("Invalid holder") if not isinstance(payload.get("location"), str) or not payload["location"].strip(): raise jwt.InvalidTokenError("Invalid location") if len(payload["holder"]) > 80 or len(payload["location"]) > 120: raise jwt.InvalidTokenError("Claim is too long") if any( isinstance(payload[name], bool) or not isinstance(payload[name], (int, float)) for name in ("iat", "nbf", "exp") ): raise jwt.InvalidTokenError("Invalid timestamp") nbf = int(payload["nbf"]) exp = int(payload["exp"]) if nbf >= exp: raise jwt.InvalidTokenError("Invalid validity period") valid_from_text = format_timestamp(nbf) valid_until_text = format_timestamp(exp) except (jwt.InvalidTokenError, TypeError, ValueError, OverflowError, OSError): return render_template("invalid.html"), 400 now = int(datetime.now().astimezone().timestamp()) if now < nbf: status = "NOT ACTIVE" status_class = "pending" elif now >= exp: status = "EXPIRED" status_class = "expired" else: status = "VALID" status_class = "valid" credential_id = str(payload.get("jti", "SIGNED-CREDENTIAL"))[:12].upper() return render_template( "pass.html", pass_data=payload, status=status, status_class=status_class, valid_from=valid_from_text, valid_until=valid_until_text, credential_id=credential_id, ) if __name__ == "__main__": app.run()