authwarden 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- authwarden/__init__.py +43 -0
- authwarden/authentication/__init__.py +0 -0
- authwarden/authentication/encryption.py +47 -0
- authwarden/authentication/jwt.py +345 -0
- authwarden/authentication/oauth.py +516 -0
- authwarden/authentication/oauth_state.py +94 -0
- authwarden/authentication/password.py +133 -0
- authwarden/core/__init__.py +0 -0
- authwarden/core/config.py +143 -0
- authwarden/core/context.py +44 -0
- authwarden/core/manager.py +207 -0
- authwarden/dependencies/__init__.py +0 -0
- authwarden/dependencies/current_user.py +85 -0
- authwarden/dependencies/permissions.py +81 -0
- authwarden/email/__init__.py +0 -0
- authwarden/email/base.py +77 -0
- authwarden/email/console.py +53 -0
- authwarden/email/mailgun.py +97 -0
- authwarden/email/sendgrid.py +96 -0
- authwarden/email/smtp.py +103 -0
- authwarden/email/templates.py +139 -0
- authwarden/exceptions.py +241 -0
- authwarden/flows/__init__.py +0 -0
- authwarden/flows/change_password.py +49 -0
- authwarden/flows/forgot_password.py +63 -0
- authwarden/flows/login.py +134 -0
- authwarden/flows/logout.py +32 -0
- authwarden/flows/oauth_accounts.py +24 -0
- authwarden/flows/oauth_authorize.py +53 -0
- authwarden/flows/oauth_callback.py +147 -0
- authwarden/flows/oauth_connect.py +85 -0
- authwarden/flows/oauth_disconnect.py +35 -0
- authwarden/flows/refresh.py +36 -0
- authwarden/flows/register.py +85 -0
- authwarden/flows/resend_verification.py +58 -0
- authwarden/flows/reset_password.py +55 -0
- authwarden/flows/reset_password_otp.py +73 -0
- authwarden/flows/set_password.py +38 -0
- authwarden/flows/verify_email.py +50 -0
- authwarden/flows/verify_otp.py +85 -0
- authwarden/mfa/__init__.py +0 -0
- authwarden/mfa/backup_codes.py +37 -0
- authwarden/mfa/totp.py +127 -0
- authwarden/models/__init__.py +0 -0
- authwarden/models/requests.py +119 -0
- authwarden/models/token.py +49 -0
- authwarden/models/user.py +184 -0
- authwarden/notifications/__init__.py +0 -0
- authwarden/notifications/service.py +165 -0
- authwarden/permissions/__init__.py +0 -0
- authwarden/permissions/policies.py +46 -0
- authwarden/permissions/roles.py +75 -0
- authwarden/py.typed +0 -0
- authwarden/routers/__init__.py +0 -0
- authwarden/routers/_errors.py +33 -0
- authwarden/routers/auth.py +201 -0
- authwarden/routers/mfa.py +65 -0
- authwarden/routers/oauth.py +129 -0
- authwarden/session/__init__.py +0 -0
- authwarden/session/base.py +98 -0
- authwarden/session/memory.py +116 -0
- authwarden/session/redis.py +149 -0
- authwarden/sms/__init__.py +0 -0
- authwarden/sms/base.py +42 -0
- authwarden/sms/console.py +20 -0
- authwarden/sms/sns.py +47 -0
- authwarden/sms/templates.py +36 -0
- authwarden/sms/twilio.py +55 -0
- authwarden/storage/__init__.py +0 -0
- authwarden/storage/base.py +162 -0
- authwarden/storage/memory.py +208 -0
- authwarden/utils.py +125 -0
- authwarden-0.7.0.dist-info/METADATA +431 -0
- authwarden-0.7.0.dist-info/RECORD +76 -0
- authwarden-0.7.0.dist-info/WHEEL +4 -0
- authwarden-0.7.0.dist-info/licenses/LICENSE +21 -0
authwarden/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""authwarden — production-grade FastAPI authentication library.
|
|
2
|
+
|
|
3
|
+
from authwarden import AuthWarden, WardenConfig
|
|
4
|
+
from authwarden.storage.memory import MemoryUserStore
|
|
5
|
+
|
|
6
|
+
warden = AuthWarden(config=WardenConfig(secret_key="..."), user_store=MemoryUserStore())
|
|
7
|
+
app.include_router(warden.router, prefix="/auth", tags=["auth"])
|
|
8
|
+
"""
|
|
9
|
+
from authwarden.core.config import OAuthProviderConfig, WardenConfig
|
|
10
|
+
from authwarden.core.manager import AuthWarden
|
|
11
|
+
from authwarden.exceptions import AuthError
|
|
12
|
+
from authwarden.models.token import LogoutRequest, RefreshTokenRequest, TokenPair, TokenPayload
|
|
13
|
+
from authwarden.models.user import (
|
|
14
|
+
OAuthAccount,
|
|
15
|
+
OAuthAccountRead,
|
|
16
|
+
OAuthUserInfo,
|
|
17
|
+
UserCreate,
|
|
18
|
+
UserInDB,
|
|
19
|
+
UserRead,
|
|
20
|
+
)
|
|
21
|
+
from authwarden.storage.base import AbstractUserStore
|
|
22
|
+
from authwarden.storage.memory import MemoryUserStore
|
|
23
|
+
|
|
24
|
+
__version__ = "0.7.0"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"AuthWarden",
|
|
28
|
+
"WardenConfig",
|
|
29
|
+
"OAuthProviderConfig",
|
|
30
|
+
"AuthError",
|
|
31
|
+
"UserCreate",
|
|
32
|
+
"UserRead",
|
|
33
|
+
"UserInDB",
|
|
34
|
+
"OAuthAccount",
|
|
35
|
+
"OAuthAccountRead",
|
|
36
|
+
"OAuthUserInfo",
|
|
37
|
+
"TokenPair",
|
|
38
|
+
"TokenPayload",
|
|
39
|
+
"RefreshTokenRequest",
|
|
40
|
+
"LogoutRequest",
|
|
41
|
+
"AbstractUserStore",
|
|
42
|
+
"MemoryUserStore",
|
|
43
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Symmetric encryption for OAuth tokens at rest.
|
|
2
|
+
|
|
3
|
+
Uses Fernet (AES128-CBC + HMAC) from the cryptography package.
|
|
4
|
+
The Fernet key is deterministically derived from WardenConfig.secret_key
|
|
5
|
+
via SHA-256, so no separate key management is required.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import base64
|
|
9
|
+
import hashlib
|
|
10
|
+
from cryptography.fernet import Fernet, InvalidToken as FernetInvalidToken
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _derive_fernet_key(secret_key: str) -> bytes:
|
|
14
|
+
"""Derive a valid 32-byte url-safe base64 Fernet key from the app secret."""
|
|
15
|
+
digest = hashlib.sha256(secret_key.encode()).digest()
|
|
16
|
+
return base64.urlsafe_b64encode(digest)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def encrypt_token(value: str, secret_key: str) -> str:
|
|
20
|
+
"""Encrypt a token string for at-rest storage.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
value: Plain-text token (e.g. OAuth access_token).
|
|
24
|
+
secret_key: WardenConfig.secret_key — used to derive the encryption key.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Encrypted ciphertext as a string, safe to store in the database.
|
|
28
|
+
"""
|
|
29
|
+
f = Fernet(_derive_fernet_key(secret_key))
|
|
30
|
+
return f.encrypt(value.encode()).decode()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def decrypt_token(ciphertext: str, secret_key: str) -> str:
|
|
34
|
+
"""Decrypt a previously encrypted token.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
ciphertext: The encrypted string returned by encrypt_token().
|
|
38
|
+
secret_key: The same secret_key used to encrypt.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The original plain-text token.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
cryptography.fernet.InvalidToken: If decryption fails (wrong key or tampered data).
|
|
45
|
+
"""
|
|
46
|
+
f = Fernet(_derive_fernet_key(secret_key))
|
|
47
|
+
return f.decrypt(ciphertext.encode()).decode()
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""JWT access and refresh token management for authwarden.
|
|
2
|
+
|
|
3
|
+
Wraps PyJWT — never implements custom signing or verification.
|
|
4
|
+
Provides a pluggable token blacklist with in-memory and Redis backends.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from typing import Protocol, runtime_checkable
|
|
10
|
+
|
|
11
|
+
import jwt as pyjwt
|
|
12
|
+
from jwt import ExpiredSignatureError, InvalidTokenError
|
|
13
|
+
|
|
14
|
+
from authwarden.core.config import WardenConfig
|
|
15
|
+
from authwarden.exceptions import InvalidToken, TokenExpired, TokenRevoked
|
|
16
|
+
from authwarden.models.token import TokenPair, TokenPayload
|
|
17
|
+
from authwarden.utils import generate_jti, to_timestamp, utcnow
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ---- Token Blacklist ----------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
@runtime_checkable
|
|
23
|
+
class AbstractTokenBlacklist(Protocol): # Protocol defines a contract that other classes must satisfy
|
|
24
|
+
"""Protocol for token blacklisting.
|
|
25
|
+
Implementations must be async-safe and handle their own TTL expiry.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
async def add(self, jti: str, ttl_seconds: int) -> None:
|
|
29
|
+
"""Add a jti to the blacklist.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
jti: The JWT ID to blacklist.
|
|
33
|
+
ttl_seconds: How long to retain the entry. Should match
|
|
34
|
+
the remaining lifetime of the token.
|
|
35
|
+
"""
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
async def contains(self, jti: str) -> bool:
|
|
39
|
+
"""Return True if the jti is currently blacklisted.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
jti: The JWT ID to check.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
True if blacklisted and not yet expired, False otherwise.
|
|
46
|
+
"""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class MemoryTokenBlacklist:
|
|
52
|
+
"""In-memory token blacklist backed by a plain dict.
|
|
53
|
+
|
|
54
|
+
Entries are lazily cleaned up when they are checked past their TTL.
|
|
55
|
+
Not suitable for multi-process deployments — use RedisTokenBlacklist instead.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self) -> None:
|
|
59
|
+
self._store: dict[str, float] = {} # jti -> unix expiry timestamp
|
|
60
|
+
|
|
61
|
+
async def add(self, jti: str, ttl_seconds: int) -> None:
|
|
62
|
+
"""Blacklist a jti for the given number of seconds."""
|
|
63
|
+
self._store[jti] = time.time() + ttl_seconds
|
|
64
|
+
|
|
65
|
+
async def contains(self, jti: str) -> bool:
|
|
66
|
+
"""Return True if the jti is blacklisted and has not expired"""
|
|
67
|
+
expiry = self._store.get(jti)
|
|
68
|
+
if expiry is None:
|
|
69
|
+
return False
|
|
70
|
+
if time.time() > expiry:
|
|
71
|
+
del self._store[jti]
|
|
72
|
+
return False
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
def clear(self) -> None:
|
|
76
|
+
"""Remove all entries - useful between test cases."""
|
|
77
|
+
self._store.clear()
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def size(self) -> int:
|
|
81
|
+
"""Return the number of active blacklist entries."""
|
|
82
|
+
return len(self._store)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class RedisTokenBlacklist:
|
|
86
|
+
"""Redis-backed token blacklist using native key expiry.
|
|
87
|
+
|
|
88
|
+
Requires the ``redis`` optional dependency::
|
|
89
|
+
|
|
90
|
+
pip install authwarden[redis]
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
redis_client: An initialised ``redis.asyncio`` client instance.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
_KEY_PREFIX = "authwarden:blacklist:"
|
|
97
|
+
|
|
98
|
+
def __init__(self, redis_client) -> None: # noqa: ANN001
|
|
99
|
+
self._redis = redis_client
|
|
100
|
+
|
|
101
|
+
async def add(self, jti: str, ttl_seconds: int) -> None:
|
|
102
|
+
"""Store a blacklisted jti in Redis with automatic expiry."""
|
|
103
|
+
await self._redis.setex(
|
|
104
|
+
f"{self._KEY_PREFIX}{jti}",
|
|
105
|
+
max(1, ttl_seconds),
|
|
106
|
+
"1",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
async def contains(self, jti: str) -> bool:
|
|
110
|
+
"""Return True if the Redis key exists (not yet expired)."""
|
|
111
|
+
result = await self._redis.exists(f"{self._KEY_PREFIX}{jti}")
|
|
112
|
+
return bool(result)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ── JWT Handler ───────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
class JWTHandler:
|
|
118
|
+
"""Issues, decodes, and revokes JWT access and refresh tokens.
|
|
119
|
+
|
|
120
|
+
All token signing/verification is delegated to PyJWT. The handler
|
|
121
|
+
manages token creation, type enforcement, and jti-based revocation.
|
|
122
|
+
|
|
123
|
+
Usage::
|
|
124
|
+
|
|
125
|
+
handler = JWTHandler(config)
|
|
126
|
+
pair = handler.create_token_pair("user-uuid", roles=["admin"])
|
|
127
|
+
payload = await handler.verify_token(pair.access_token)
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def __init__(
|
|
131
|
+
self,
|
|
132
|
+
config: WardenConfig,
|
|
133
|
+
blacklist: AbstractTokenBlacklist | None = None,
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Initialise the JWT handler.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
config: WardenConfig — provides secret_key, algorithm, TTLs.
|
|
139
|
+
blacklist: Blacklist backend. Defaults to MemoryTokenBlacklist.
|
|
140
|
+
"""
|
|
141
|
+
self._config = config
|
|
142
|
+
self._blacklist: AbstractTokenBlacklist = blacklist or MemoryTokenBlacklist()
|
|
143
|
+
|
|
144
|
+
def _build_payload(
|
|
145
|
+
self,
|
|
146
|
+
user_id: str,
|
|
147
|
+
token_type: str,
|
|
148
|
+
roles: list[str],
|
|
149
|
+
scopes: list[str],
|
|
150
|
+
ttl: int,
|
|
151
|
+
) -> dict:
|
|
152
|
+
"""Build a raw JWT payload dict."""
|
|
153
|
+
now = to_timestamp(utcnow())
|
|
154
|
+
return {
|
|
155
|
+
"sub": user_id,
|
|
156
|
+
"jti": generate_jti(),
|
|
157
|
+
"type": token_type,
|
|
158
|
+
"roles": roles,
|
|
159
|
+
"scopes": scopes,
|
|
160
|
+
"iat": now,
|
|
161
|
+
"exp": now + ttl,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
def create_access_token(
|
|
165
|
+
self,
|
|
166
|
+
user_id: str,
|
|
167
|
+
roles: list[str] | None = None,
|
|
168
|
+
scopes: list[str] | None = None,
|
|
169
|
+
) -> str:
|
|
170
|
+
"""Issue a signed JWT access token.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
user_id: The user's UUID — stored as the ``sub`` claim.
|
|
174
|
+
roles: Roles to embed in the payload.
|
|
175
|
+
scopes: Scopes to embed in the payload.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
A signed JWT string (type ``"access"``).
|
|
179
|
+
"""
|
|
180
|
+
payload = self._build_payload(
|
|
181
|
+
user_id=user_id,
|
|
182
|
+
token_type="access",
|
|
183
|
+
roles=roles or [],
|
|
184
|
+
scopes=scopes or [],
|
|
185
|
+
ttl=self._config.access_token_ttl,
|
|
186
|
+
)
|
|
187
|
+
return pyjwt.encode(
|
|
188
|
+
payload,
|
|
189
|
+
self._config.secret_key,
|
|
190
|
+
algorithm=self._config.algorithm,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def create_refresh_token(
|
|
194
|
+
self,
|
|
195
|
+
user_id: str,
|
|
196
|
+
roles: list[str] | None = None,
|
|
197
|
+
scopes: list[str] | None = None,
|
|
198
|
+
) -> str:
|
|
199
|
+
"""Issue a signed JWT refresh token.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
user_id: The user's UUID.
|
|
203
|
+
roles: Roles to embed in the payload.
|
|
204
|
+
scopes: Scopes to embed in the payload.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
A signed JWT string (type ``"refresh"``).
|
|
208
|
+
"""
|
|
209
|
+
payload = self._build_payload(
|
|
210
|
+
user_id=user_id,
|
|
211
|
+
token_type="refresh",
|
|
212
|
+
roles=roles or [],
|
|
213
|
+
scopes=scopes or [],
|
|
214
|
+
ttl=self._config.refresh_token_ttl,
|
|
215
|
+
)
|
|
216
|
+
return pyjwt.encode(
|
|
217
|
+
payload,
|
|
218
|
+
self._config.secret_key,
|
|
219
|
+
algorithm=self._config.algorithm,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def create_token_pair(
|
|
223
|
+
self,
|
|
224
|
+
user_id: str,
|
|
225
|
+
roles: list[str] | None = None,
|
|
226
|
+
scopes: list[str] | None = None,
|
|
227
|
+
) -> TokenPair:
|
|
228
|
+
"""Issue an access + refresh token pair for a user.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
user_id: The user's UUID.
|
|
232
|
+
roles: Roles to embed in both tokens.
|
|
233
|
+
scopes: Scopes to embed in both tokens.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
A TokenPair containing both signed tokens.
|
|
237
|
+
"""
|
|
238
|
+
return TokenPair(
|
|
239
|
+
access_token=self.create_access_token(user_id, roles, scopes),
|
|
240
|
+
refresh_token=self.create_refresh_token(user_id, roles, scopes),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def decode_token(self, token: str, expected_type: str = "access") -> TokenPayload:
|
|
244
|
+
"""Decode and validate a JWT token's signature and claims.
|
|
245
|
+
|
|
246
|
+
Does NOT check the blacklist — use ``verify_token()`` for full
|
|
247
|
+
validation including revocation checks.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
token: The raw JWT string.
|
|
251
|
+
expected_type: ``"access"`` or ``"refresh"`` — enforced against
|
|
252
|
+
the ``type`` claim in the payload.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
A TokenPayload with all decoded claims.
|
|
256
|
+
|
|
257
|
+
Raises:
|
|
258
|
+
TokenExpired: If the ``exp`` claim is in the past.
|
|
259
|
+
InvalidToken: If the signature is invalid, claims are malformed,
|
|
260
|
+
or the token type does not match ``expected_type``.
|
|
261
|
+
"""
|
|
262
|
+
try:
|
|
263
|
+
raw = pyjwt.decode(
|
|
264
|
+
token,
|
|
265
|
+
self._config.secret_key,
|
|
266
|
+
algorithms=[self._config.algorithm],
|
|
267
|
+
)
|
|
268
|
+
except ExpiredSignatureError:
|
|
269
|
+
raise TokenExpired()
|
|
270
|
+
except InvalidTokenError:
|
|
271
|
+
raise InvalidToken()
|
|
272
|
+
|
|
273
|
+
if raw.get("type") != expected_type:
|
|
274
|
+
raise InvalidToken(
|
|
275
|
+
f"Expected token type '{expected_type}', got '{raw.get('type')}'"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
return TokenPayload(**raw)
|
|
280
|
+
except Exception:
|
|
281
|
+
raise InvalidToken("Token payload is missing required claims")
|
|
282
|
+
|
|
283
|
+
async def blacklist_jti(self, jti: str, ttl_seconds: int) -> None:
|
|
284
|
+
"""Directly add a jti to the blacklist.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
jti: The JWT ID to blacklist.
|
|
288
|
+
ttl_seconds: Seconds until the blacklist entry expires.
|
|
289
|
+
"""
|
|
290
|
+
await self._blacklist.add(jti, ttl_seconds)
|
|
291
|
+
|
|
292
|
+
async def is_blacklisted(self, jti: str) -> bool:
|
|
293
|
+
"""Check whether a jti is currently blacklisted.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
jti: The JWT ID to check.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
True if blacklisted, False otherwise.
|
|
300
|
+
"""
|
|
301
|
+
return await self._blacklist.contains(jti)
|
|
302
|
+
|
|
303
|
+
async def blacklist_token(
|
|
304
|
+
self, token: str, expected_type: str = "access"
|
|
305
|
+
) -> None:
|
|
306
|
+
"""Decode a token and blacklist its jti for the remaining lifetime.
|
|
307
|
+
|
|
308
|
+
Used on logout to revoke the presented token.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
token: Raw JWT string to revoke.
|
|
312
|
+
expected_type: Expected type claim value.
|
|
313
|
+
|
|
314
|
+
Raises:
|
|
315
|
+
TokenExpired: If the token is already expired (nothing to revoke).
|
|
316
|
+
InvalidToken: If the token is malformed.
|
|
317
|
+
"""
|
|
318
|
+
payload = self.decode_token(token, expected_type=expected_type)
|
|
319
|
+
remaining = max(0, payload.exp - to_timestamp(utcnow()))
|
|
320
|
+
await self.blacklist_jti(payload.jti, remaining)
|
|
321
|
+
|
|
322
|
+
async def verify_token(
|
|
323
|
+
self, token: str, expected_type: str = "access"
|
|
324
|
+
) -> TokenPayload:
|
|
325
|
+
"""Decode a token and verify it has not been revoked.
|
|
326
|
+
|
|
327
|
+
This is the primary method auth middleware and dependencies should call.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
token: Raw JWT string.
|
|
331
|
+
expected_type: Expected token type — ``"access"`` or ``"refresh"``.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
A validated TokenPayload.
|
|
335
|
+
|
|
336
|
+
Raises:
|
|
337
|
+
TokenExpired: Token has expired.
|
|
338
|
+
InvalidToken: Token is malformed or wrong type.
|
|
339
|
+
TokenRevoked: Token's jti is in the blacklist.
|
|
340
|
+
"""
|
|
341
|
+
payload = self.decode_token(token, expected_type=expected_type)
|
|
342
|
+
if await self.is_blacklisted(payload.jti):
|
|
343
|
+
raise TokenRevoked()
|
|
344
|
+
return payload
|
|
345
|
+
|