Developers / Guides

Verified Ham with Python (Authlib)

Sign-in for a Flask app with Authlib, which reads everything from discovery.

Authlib reads everything it needs from the discovery document, including the keys to check ID tokens with. This is a complete Flask app, also in the repository as examples/python.

1. Register your app

  1. Create a Verified Ham account, if you don't have one, and sign in.
  2. Go to Register an app.
  3. Give it a name. People see it when they sign in.
  4. Set the redirect URI to:
    http://localhost:5000/callback
  5. Choose Confidential, and register.
  6. Copy the client ID and client secret. The secret is shown only once.

You can sign in to your own app straight away, to test. Other people can sign in once you've verified your own call sign.

2. The app

pip install flask authlib requests, save this as app.py, and run VH_CLIENT_ID=… VH_CLIENT_SECRET=… flask --app app run --port 5000.

"""Sign in with Verified Ham in a Flask app, with Authlib.

    pip install -r requirements.txt
    VH_CLIENT_ID=... VH_CLIENT_SECRET=... flask --app app run --port 5000

Register the app at https://verifiedham.net/developers/apps/new with the
redirect URI http://localhost:5000/callback, then open http://localhost:5000.
"""

import json
import os

from authlib.integrations.flask_client import OAuth
from flask import Flask, redirect, session, url_for
from markupsafe import escape

app = Flask(__name__)
app.secret_key = os.environ.get("SESSION_SECRET", "change me")

oauth = OAuth(app)
oauth.register(
    "verifiedham",
    client_id=os.environ["VH_CLIENT_ID"],
    client_secret=os.environ["VH_CLIENT_SECRET"],
    # Authlib reads the endpoints and signing keys from discovery.
    server_metadata_url=os.environ.get("VH_ISSUER", "https://verifiedham.net") + "/.well-known/openid-configuration",
    client_kwargs={"scope": "openid callsign", "code_challenge_method": "S256"},
)


@app.route("/login")
def login():
    return oauth.verifiedham.authorize_redirect(url_for("callback", _external=True))


@app.route("/callback")
def callback():
    token = oauth.verifiedham.authorize_access_token()
    # The ID token's claims, checked against the provider's keys.
    session["user"] = dict(token["userinfo"])
    return redirect("/")


@app.route("/logout")
def logout():
    session.clear()
    return redirect("/")


@app.route("/")
def home():
    user = session.get("user")
    if not user:
        return '<a href="/login">Sign in with Verified Ham</a>'

    # Key your users by sub, and trust the call sign only when callsign_verified is true.
    who = f"<strong>{escape(user['callsign'])}</strong>, verified" if user.get("callsign_verified") else "without a verified call sign yet"
    return f'<p>Signed in {who}. <a href="/logout">Sign out</a></p><pre>{escape(json.dumps(user, indent=2))}</pre>'

How it works

  • code_challenge_method: S256 turns on PKCE, which Verified Ham requires.
  • authorize_access_token() checks the state, exchanges the code, and validates the ID token against https://verifiedham.net/.well-known/jwks.json.
  • token["userinfo"] holds the ID token's claims: sub, callsign and callsign_verified.
  • For Django, Authlib's Django client takes the same register() arguments.