ProvenPin Install

itsdangerous URL-safe timed tokens: working versions (tested 2026-10-16)

Sign URL-safe, expiring email-confirmation and password-reset tokens with salts and max_age, testing expiry and salt separation offline with freezegun.

passed6 passed · 0 failed2026-10-16 21:56 UTC
Status
passed
Last passed
2026-10-16 21:56 UTC
Tests
6 / 6
Language
Python
Runtime
Python 3.13.16
pinned versions3
itsdangerous@2.2.0freezegun@1.5.5pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
Python 3.13.16
linux 6.17.0-1022-azure
x64
runner: github-actions 38972656245

The code

Exactly the files that passed. Your agent can pull them with get_recipe("itsdangerous-urlsafe-timed-tokens") (install), or see them on GitHub.

requirements.txt
itsdangerous==2.2.0
freezegun==1.5.5
pytest==9.1.1
tokens.py
import os

from itsdangerous import BadSignature, URLSafeTimedSerializer

SECRET_KEY = os.environ.get("SECRET_KEY", "dev-only-secret")
EMAIL_SALT = "email-confirm-v1"
RESET_SALT = "password-reset-v1"
EMAIL_MAX_AGE = 60 * 60 * 24
RESET_MAX_AGE = 60 * 30


def _serializer(salt: str) -> URLSafeTimedSerializer:
    return URLSafeTimedSerializer(secret_key=SECRET_KEY, salt=salt)


def make_email_token(user_id: int) -> str:
    return _serializer(EMAIL_SALT).dumps({"uid": user_id})


def confirm_email(token: str, max_age: int = EMAIL_MAX_AGE) -> int:
    data = _serializer(EMAIL_SALT).loads(token, max_age=max_age)
    return data["uid"]


def make_reset_token(user_id: int, password_hash: str) -> str:
    return _serializer(RESET_SALT).dumps({"uid": user_id, "ph": password_hash})


def load_reset_token(token: str, password_hash: str, max_age: int = RESET_MAX_AGE) -> int:
    data = _serializer(RESET_SALT).loads(token, max_age=max_age)
    if data["ph"] != password_hash:
        raise BadSignature("token was issued against an older password")
    return data["uid"]
tests/test_tokens.py
import re

import pytest
from freezegun import freeze_time

from itsdangerous import BadSignature, SignatureExpired

import tokens


def test_roundtrip_within_max_age():
    with freeze_time("2026-01-01 00:00:00"):
        token = tokens.make_email_token(42)
    with freeze_time("2026-01-01 00:10:00"):
        assert tokens.confirm_email(token) == 42


def test_expiry_raises_and_is_a_bad_signature():
    with freeze_time("2026-01-01 00:00:00"):
        token = tokens.make_email_token(42)
    with freeze_time("2026-01-03 00:00:00"):
        with pytest.raises(SignatureExpired):
            tokens.confirm_email(token)
    # gotcha: SignatureExpired subclasses BadSignature, so catch it first
    assert issubclass(SignatureExpired, BadSignature)


def test_salts_keep_contexts_separate():
    token = tokens.make_email_token(42)
    with pytest.raises(BadSignature):
        tokens.load_reset_token(token, "unused-hash")


def test_token_is_url_safe():
    token = tokens.make_email_token(42)
    assert re.fullmatch(r"[A-Za-z0-9._-]+", token)
    assert not set(token) & set("+/=")
    assert "/reset?token=" + token


def test_reset_token_dies_when_password_changes():
    token = tokens.make_reset_token(7, "old-hash")
    assert tokens.load_reset_token(token, "old-hash") == 7
    with pytest.raises(BadSignature):
        tokens.load_reset_token(token, "new-hash")


def test_tampered_token_is_rejected():
    token = tokens.make_email_token(42)
    with pytest.raises(BadSignature):
        tokens.confirm_email(token[:-1] + ("A" if token[-1] != "A" else "B"))
pytest.ini
[pytest]
pythonpath = .

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.