Skip to content

Signing

Signer turns a JSON-serialisable value into a signed, timestamped, URL-safe token and verifies it later.

Use it when the server hands a value to the client, the client returns it, and the server needs to know the value was not tampered with — password-reset links, email-confirmation tokens, signed cookies, and similar.

Signing is built on HMAC-SHA256 (RFC 2104) from the standard library.

from veloce.signing import Signer

signer = Signer(secret="server-secret", salt="reset-token")

token = signer.dumps({"user_id": 42})
data = signer.loads(token)
assert data == {"user_id": 42}

Import path

Signer, BadSignature, BadTimeSignature, and BadData are part of the top-level public API and can also be imported directly with from veloce import Signer. This guide imports from veloce.signing to keep the source module explicit.

Warning

A token is not encryption — the payload is signed, not hidden. Anyone can read the value inside a token; they just cannot change it without the secret. Do not put data in a token that the client must not see.

Signing and reading values

Signer.dumps serialises the value to JSON, appends a timestamp, and signs both. Signer.loads verifies the signature and returns the original value. The signature is checked before the payload is decoded, so a tampered token is rejected before its JSON ever reaches the parser.

from veloce.signing import Signer

signer = Signer(secret="server-secret")

token = signer.dumps({"email": "alice@example.com", "action": "confirm"})
claims = signer.loads(token)
assert claims["email"] == "alice@example.com"

The value passed to dumps may be any JSON-serialisable object — a dict, a list, a string, a number. loads returns exactly what you put in.

Per-purpose salts

The salt argument (default "veloce.signing") derives a distinct signing key from the same secret. Tokens signed with one salt do not verify under another, so you can share one application secret across purposes — sessions, CSRF, password reset — without those tokens cross-validating.

from veloce.signing import BadSignature, Signer

reset_signer = Signer(secret="app-secret", salt="password-reset")
confirm_signer = Signer(secret="app-secret", salt="email-confirm")

token = reset_signer.dumps({"user_id": 7})

# The same secret with a different salt cannot read the token.
try:
    confirm_signer.loads(token)
except BadSignature:
    print("reset token rejected by the confirm signer")

Use a different, descriptive salt per token purpose. The secret stays the same; the salt keeps the namespaces apart.

Timed signatures

Pass max_age (in seconds) to Signer.loads to reject tokens older than a window. This is what makes a password-reset link expire.

from veloce.signing import BadTimeSignature, Signer

signer = Signer(secret="server-secret", salt="password-reset")
token = signer.dumps({"user_id": 42})

# Within the window the token reads back normally.
assert signer.loads(token, max_age=3600) == {"user_id": 42}

# A token whose age exceeds max_age raises BadTimeSignature. A negative
# window forces that path for a freshly minted token: its age (0 seconds)
# already exceeds a -1 second window.
try:
    signer.loads(token, max_age=-1)
except BadTimeSignature:
    print("token expired")

The timestamp is embedded in the token at dumps time, so the same token can be checked against different max_age windows by different callers. A token whose timestamp is in the future (a clock skew or a forged date) also raises BadTimeSignature.

Handling failures

Verification raises one of three exceptions, all subclasses of BadSignature:

  • BadSignature — the signature did not match the configured secret (tampering, wrong secret, or wrong salt).
  • BadTimeSignature — the signature verified but the token is older than max_age (or future-dated).
  • BadData — the token was structurally malformed (bad base64, invalid JSON, or not a string).

Because BadTimeSignature and BadData both inherit from BadSignature, a single except BadSignature catches all three when you do not need to distinguish them.

from veloce import HTTPException, Veloce
from veloce.signing import BadSignature, Signer

app = Veloce()
signer = Signer(secret="server-secret", salt="password-reset")


@app.get("/reset/{token}")
async def reset(token: str):
    try:
        claims = signer.loads(token, max_age=3600)
    except BadSignature:
        raise HTTPException(400, "Invalid or expired reset link")
    return {"user_id": claims["user_id"]}

Catching BadSignature here covers expired links, tampered tokens, and malformed input with one branch — the user sees the same generic error for all of them, which avoids leaking why a token was rejected.

Rotating the secret

To change the signing secret without invalidating tokens already in the wild, configure the new secret as primary and register the old one as a fallback with add_fallback_secret. New tokens are signed with the primary secret; tokens signed with the fallback still verify during the rotation window.

from veloce.signing import Signer

signer = Signer(secret="new-secret", salt="password-reset")
signer.add_fallback_secret("old-secret", salt="password-reset")

# Tokens minted under the old secret still verify; new tokens use the new one.

The fallback secret is accepted for verification only — it is never used to sign new tokens. Remove the fallback once the rotation window has passed and old tokens have expired.

An empty fallback is refused, the same way an empty primary secret is. A fallback is a verification key, so an empty one does not merely sign weakly: it accepts anything signed with the key derived from b"", which anyone can compute. Watch for the shape a rotation list grows from an environment:

# Raises: the second entry installs a key anyone can derive.
SessionMiddleware(secret_key=[os.environ["SECRET_KEY"],
                              os.environ.get("OLD_SECRET_KEY", "")])

Changed in version 0.20.0

An empty fallback secret was previously accepted, and every token signed with the key derived from it verified.

Next steps

  • Security schemes — extract Bearer tokens and API keys from incoming requests.
  • Passwords — hash and verify passwords for the login endpoint that issues your signed tokens.
  • Sessions — Veloce's session support builds on the same signing primitives.
  • The API reference has full signatures for Signer and the exception hierarchy.