layer7-web-passport/app.py
2026-09-01 23:59:23 +09:00

146 lines
4.5 KiB
Python

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/<token>")
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()