Skip to content

Commit 3d3cf84

Browse files
feat(auth): align paseto support with footer and assertion features
1 parent d5a0705 commit 3d3cf84

23 files changed

Lines changed: 815 additions & 96 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ turn was inspired by `flask-jwt-extended`.
2525
- Support for adding custom claims to Tokens
2626
- Built-in Base64 Encoding of Tokens
2727
- Custom token types
28+
- PASETO footers and implicit assertions
2829

2930
## Installation
3031

docs/advanced-usage/additional-claims.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,19 @@ read it back with `get_token_payload()`.
88
Storing data in tokens can reduce repeated database lookups, but you still need
99
to choose those claims carefully.
1010

11+
`user_claims` is only for custom application data. Reserved top-level claims
12+
such as `sub`, `type`, `jti`, `exp`, `nbf`, `iat`, `iss`, `aud`, and `fresh`
13+
cannot be overridden there.
14+
15+
Use the dedicated parameters instead:
16+
17+
- `subject=` for `sub`
18+
- `fresh=` for access-token freshness
19+
- `audience=` for `aud`
20+
- `issuer=` for `iss`
21+
- `expires_time=` for `exp`
22+
- `create_token(type="...")` for custom token types
23+
1124
When using the `public` purpose, the token payload is signed but not encrypted.
1225
Anyone with the token can read its contents, so do not store sensitive
1326
information there. Whether `local` tokens are appropriate for sensitive data is
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
FastAPI PASETO can carry optional PASETO footers and verify implicit
2+
assertions when you need stronger token binding than payload claims alone.
3+
4+
```python hl_lines="10 30-36 42-44 49"
5+
{!../examples/footer_assertion.py!}
6+
```
7+
8+
## What They Are
9+
10+
**Footer**
11+
: Extra authenticated data attached to the token outside the payload.
12+
A footer travels with the token and is protected by PASETO integrity checks,
13+
but it is not meant for confidential application data.
14+
15+
**Implicit assertion**
16+
: Extra authenticated data that does **not** travel inside the token at all.
17+
The same value must be supplied again at verification time or validation
18+
fails.
19+
20+
In practice:
21+
22+
- Put application claims you want to read later in the payload.
23+
- Put non-secret token metadata you want attached to the token in the footer.
24+
- Put request-bound context that should never be embedded in the token in the
25+
implicit assertion.
26+
27+
## Footers
28+
29+
PASETO footers are useful for metadata such as:
30+
31+
- `kid` values for key selection
32+
- routing or tenant hints
33+
- integration metadata that should stay outside the payload
34+
35+
- Pass `footer=` to `create_access_token()`, `create_refresh_token()`, or
36+
`create_token()` to include an optional PASETO footer.
37+
- After `paseto_required()` succeeds, call `get_token_footer()` to read the
38+
decoded footer.
39+
- Footer data is authenticated by PASETO, but for `public` tokens it remains
40+
visible to anyone holding the token.
41+
42+
Example:
43+
44+
```python
45+
token = Authorize.create_access_token(
46+
subject="alice",
47+
footer={"kid": "k4.lid.primary"},
48+
)
49+
50+
Authorize.paseto_required()
51+
footer = Authorize.get_token_footer()
52+
```
53+
54+
Use a footer when the metadata belongs to the token itself and can safely
55+
travel with it.
56+
57+
## Implicit Assertions
58+
59+
Implicit assertions are useful when the token must be bound to external context
60+
such as:
61+
62+
- a tenant identifier chosen by the server
63+
- a channel or transport binding
64+
- a deployment- or service-specific contract
65+
66+
- Pass `implicit_assertion=` when creating a token.
67+
- Pass the same `implicit_assertion=` value to `paseto_required()` when
68+
validating it.
69+
- If the assertion is missing or mismatched, token validation fails.
70+
71+
Example:
72+
73+
```python
74+
token = Authorize.create_access_token(
75+
subject="alice",
76+
implicit_assertion="tenant-alpha",
77+
)
78+
79+
Authorize.paseto_required(implicit_assertion="tenant-alpha")
80+
```
81+
82+
Use an implicit assertion when the value should influence verification but
83+
should not be readable from the token or transported inside it.
84+
85+
## Choosing Between Them
86+
87+
Choose a **footer** when:
88+
89+
- the verifier needs to read the metadata from the token itself
90+
- the value can safely travel with the token
91+
92+
Choose an **implicit assertion** when:
93+
94+
- the verifier already knows the value from surrounding context
95+
- the value should not be embedded into or exposed by the token
96+
97+
Do not use either one for ordinary application claims such as user roles,
98+
permissions, or profile data. Those belong in the token payload.

docs/advanced-usage/validation.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
FastAPI PASETO exposes validation controls for issuer, audience, custom token
2-
types, and base64-encoded tokens.
2+
types, base64-encoded tokens, and token-binding options such as implicit
3+
assertions.
34

45
```python hl_lines="18-22 35-45 49 56 63"
56
{!../examples/validation.py!}
@@ -14,11 +15,16 @@ Use these options when you need stronger contracts than "any valid access token"
1415
for custom token flows such as email verification.
1516
- Use `base64_encode=True` when creating a token and
1617
`paseto_required(base64_encoded=True)` when validating it.
18+
- Use `implicit_assertion=` on both creation and validation when a token must
19+
be bound to external request context.
1720

1821
Important issuer detail:
1922

2023
- `authpaseto_encode_issuer` automatically adds `iss` only to
2124
`create_access_token()`.
2225
- `authpaseto_decode_issuer` applies to every decoded token.
23-
- If you enable issuer validation for refresh or custom tokens, those tokens
24-
must still include `iss`, typically through `user_claims`.
26+
- If you enable issuer validation for refresh or custom tokens, pass
27+
`issuer=` when creating those tokens.
28+
29+
For footer handling and `get_token_footer()`, see
30+
[Footers and Assertions](footer-assertion.md).

docs/api-doc.md

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Registers the callback used to decide whether a decoded token has been revoked.
2121

2222
## Request Validation
2323

24-
### `paseto_required(optional=False, fresh=False, refresh_token=False, type=None, base64_encoded=False, location=None, token_key=None, token_prefix=None, token=None)`
24+
### `paseto_required(optional=False, fresh=False, refresh_token=False, type=None, base64_encoded=False, location=None, token_key=None, token_prefix=None, token=None, implicit_assertion=b"")`
2525

2626
Validates the current request or websocket connection against the supplied token
2727
requirements.
@@ -41,6 +41,8 @@ Parameters:
4141
- `token_prefix`: override the configured token prefix with another non-empty
4242
string for this check.
4343
- `token`: provide a raw token directly and bypass request or websocket lookup.
44+
- `implicit_assertion`: require the same implicit assertion that was used when
45+
the token was created.
4446

4547
Notes:
4648

@@ -52,7 +54,7 @@ Notes:
5254

5355
## Token Creation
5456

55-
### `create_access_token(subject, fresh=False, purpose=None, expires_time=None, audience=None, user_claims=None, base64_encode=False)`
57+
### `create_access_token(subject, fresh=False, purpose=None, expires_time=None, audience=None, issuer=None, user_claims=None, footer=None, implicit_assertion=b"", base64_encode=False)`
5658

5759
Creates a new access token.
5860

@@ -62,21 +64,25 @@ Creates a new access token.
6264
- `expires_time`: override the configured expiration with integer seconds,
6365
`datetime`, `timedelta`, or `False`.
6466
- `audience`: string or sequence of audience values added to `aud`.
65-
- `user_claims`: additional claims merged into the payload.
67+
- `issuer`: override the `iss` claim for this token. If omitted, access tokens
68+
fall back to `authpaseto_encode_issuer`.
69+
- `user_claims`: additional non-reserved claims merged into the payload.
70+
- `footer`: optional PASETO footer as bytes, string, or dictionary.
71+
- `implicit_assertion`: optional implicit assertion bound to the token.
6672
- `base64_encode`: base64-encode the generated token string before returning it.
6773

6874
Returns a token string.
6975

70-
### `create_refresh_token(subject, purpose=None, expires_time=None, audience=None, user_claims=None, base64_encode=False)`
76+
### `create_refresh_token(subject, purpose=None, expires_time=None, audience=None, issuer=None, user_claims=None, footer=None, implicit_assertion=b"", base64_encode=False)`
7177

7278
Creates a new refresh token.
7379

7480
Parameters are the same as `create_access_token()`, except refresh tokens do not
75-
accept a `fresh` flag.
81+
accept a `fresh` flag and do not inherit `authpaseto_encode_issuer`.
7682

7783
Returns a token string.
7884

79-
### `create_token(subject, type, purpose=None, expires_time=None, audience=None, user_claims=None, base64_encode=False)`
85+
### `create_token(subject, type, purpose=None, expires_time=None, audience=None, issuer=None, user_claims=None, footer=None, implicit_assertion=b"", base64_encode=False)`
8086

8187
Creates a custom token with the caller-provided `type` claim.
8288

@@ -92,6 +98,11 @@ Returns a token string.
9298
Returns the decoded token payload for the current request or websocket
9399
connection, or `None` if no token has been validated successfully.
94100

101+
### `get_token_footer()`
102+
103+
Returns the decoded footer for the current request or websocket connection, or
104+
`None` if no token has been validated successfully or the token has no footer.
105+
95106
### `get_jti()`
96107

97108
Returns the current token identifier from the `jti` claim, or `None` if no

docs/configuration/denylist.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
`authpaseto_denylist_token_checks`
55
: Token types that should be checked against the denylist callback. Valid
66
values are `access` and `refresh`. Pass a sequence to check both. Defaults
7-
to `("access", "refresh")`.
7+
to `("access", "refresh")`. Tokens whose `type` claim is not listed skip
8+
the denylist callback.
89

910
When denylist support is enabled, register a callback with
1011
`@AuthPASETO.token_in_denylist_loader`. That callback receives the decoded token

docs/configuration/general.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ defaults, and websocket transport settings used by `AuthPASETO`.
3939
seconds or a `datetime.timedelta`. Defaults to `0`.
4040

4141
`authpaseto_encode_issuer`
42-
: Issuer value automatically added by `create_access_token()`. Defaults to
43-
`None`.
42+
: Default issuer value automatically added by `create_access_token()`.
43+
Refresh and custom tokens can set `iss` explicitly with `issuer=`.
44+
Defaults to `None`.
4445

4546
`authpaseto_decode_issuer`
4647
: Expected `iss` claim value when decoding a token. If this is set, every

docs/examples.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ from example file to the relevant guide.
1616
| `examples/denylist_redis.py` | Redis-backed denylist storage | [Revoking Tokens](usage/revoking.md) |
1717
| `examples/additional_claims.py` | Custom claims in token payloads | [Additional claims](advanced-usage/additional-claims.md) |
1818
| `examples/purpose.py` | Local vs public token purpose | [Token Purpose](advanced-usage/purpose.md) |
19+
| `examples/footer_assertion.py` | Footers and implicit assertions | [Footers and Assertions](advanced-usage/footer-assertion.md) |
1920
| `examples/overrides.py` | Route-level transport overrides | [Per-route Overrides](advanced-usage/overrides.md) |
2021
| `examples/validation.py` | Issuer, audience, base64, and custom token types | [Validation and Custom Types](advanced-usage/validation.md) |
2122
| `examples/generate_doc.py` | Manual OpenAPI customization | [Generate Documentation](advanced-usage/generate-docs.md) |

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ token-creation flow should feel familiar.
1717
- JSON body token transport for HTTP routes
1818
- Custom claims and custom token types
1919
- Base64-encoded token support
20+
- PASETO footers and implicit assertions
2021

2122
## Installation
2223

docs/usage/revoking.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ Choose which token types should be checked with
55
`token_in_denylist_loader()`. That callback receives the decoded token payload
66
and should return `True` when the token has been revoked.
77

8+
Only the configured token types are checked. For example, if you set
9+
`authpaseto_denylist_token_checks=["refresh"]`, access tokens skip denylist
10+
lookups while refresh tokens still enforce them.
11+
812
This can be utilized to invalidate token in multiple cases, e.g.:
913
- A user logs out and their currently active tokens need to be invalidated
1014
- You detect a replay attack and the leaked tokens need to be blocked

0 commit comments

Comments
 (0)