authaction-python-sdk 0.1.0__tar.gz

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.
@@ -0,0 +1,215 @@
1
+ Metadata-Version: 2.4
2
+ Name: authaction-python-sdk
3
+ Version: 0.1.0
4
+ Summary: AuthAction JWT verification SDK for Python — Django, Flask, and FastAPI
5
+ License: MIT
6
+ Keywords: oauth2,jwt,jwks,authentication,authaction,django,flask,fastapi
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: PyJWT[crypto]>=2.8.0
10
+ Provides-Extra: django
11
+ Requires-Dist: django>=3.2; extra == "django"
12
+ Requires-Dist: djangorestframework>=3.14; extra == "django"
13
+ Provides-Extra: flask
14
+ Requires-Dist: flask>=2.3; extra == "flask"
15
+ Provides-Extra: fastapi
16
+ Requires-Dist: fastapi>=0.100; extra == "fastapi"
17
+ Requires-Dist: httpx>=0.24; extra == "fastapi"
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=8.0; extra == "dev"
20
+ Requires-Dist: pytest-mock>=3.12; extra == "dev"
21
+ Requires-Dist: django>=3.2; extra == "dev"
22
+ Requires-Dist: djangorestframework>=3.14; extra == "dev"
23
+ Requires-Dist: flask>=2.3; extra == "dev"
24
+ Requires-Dist: fastapi>=0.100; extra == "dev"
25
+ Requires-Dist: httpx>=0.24; extra == "dev"
26
+
27
+ # authaction-python-sdk
28
+
29
+ JWT verification SDK for Python backends. Validates AuthAction access tokens via JWKS — handles key fetching, caching, and rotation automatically.
30
+
31
+ Works with **Django REST Framework**, **Flask**, and **FastAPI**.
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ # Core only
37
+ pip install authaction-python-sdk
38
+
39
+ # With Django support
40
+ pip install "authaction-python-sdk[django]"
41
+
42
+ # With Flask support
43
+ pip install "authaction-python-sdk[flask]"
44
+
45
+ # With FastAPI support
46
+ pip install "authaction-python-sdk[fastapi]"
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Core
52
+
53
+ ```python
54
+ from authaction import AuthAction
55
+
56
+ aa = AuthAction(
57
+ domain=os.getenv("AUTHACTION_DOMAIN"), # e.g. myapp.eu.authaction.com
58
+ audience=os.getenv("AUTHACTION_AUDIENCE"), # e.g. https://api.myapp.com
59
+ )
60
+
61
+ # Verify a raw token — raises TokenExpiredError / TokenInvalidError on failure
62
+ payload = aa.verify_token(token)
63
+
64
+ # Verify from Authorization header — returns None on missing/invalid, never raises
65
+ payload = aa.verify_request(request.headers.get("Authorization"))
66
+
67
+ print(payload["sub"]) # user identifier
68
+ print(payload["email"]) # any JWT claim
69
+ ```
70
+
71
+ ---
72
+
73
+ ## Django REST Framework
74
+
75
+ ### 1. Configure settings
76
+
77
+ ```python
78
+ # settings.py
79
+ AUTHACTION = {
80
+ "DOMAIN": os.getenv("AUTHACTION_DOMAIN"),
81
+ "AUDIENCE": os.getenv("AUTHACTION_AUDIENCE"),
82
+ }
83
+
84
+ REST_FRAMEWORK = {
85
+ "DEFAULT_AUTHENTICATION_CLASSES": [
86
+ "authaction.django.AuthActionAuthentication",
87
+ ],
88
+ "DEFAULT_PERMISSION_CLASSES": [
89
+ "rest_framework.permissions.IsAuthenticated",
90
+ ],
91
+ }
92
+ ```
93
+
94
+ ### 2. Use in views
95
+
96
+ ```python
97
+ from rest_framework.decorators import api_view, permission_classes
98
+ from rest_framework.permissions import AllowAny, IsAuthenticated
99
+ from rest_framework.response import Response
100
+
101
+ @api_view(["GET"])
102
+ @permission_classes([AllowAny])
103
+ def public_view(request):
104
+ return Response({"message": "Public"})
105
+
106
+ @api_view(["GET"])
107
+ def protected_view(request):
108
+ return Response({"sub": request.user.sub, "email": request.user.email})
109
+ ```
110
+
111
+ `request.user` is an `AuthenticatedToken` — access any JWT claim as an attribute:
112
+
113
+ ```python
114
+ request.user.sub # str
115
+ request.user.email # any claim
116
+ request.user.payload # dict — all raw claims
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Flask
122
+
123
+ ```python
124
+ from authaction import AuthAction
125
+ from authaction.flask import make_require_auth
126
+
127
+ aa = AuthAction(
128
+ domain=os.getenv("AUTHACTION_DOMAIN"),
129
+ audience=os.getenv("AUTHACTION_AUDIENCE"),
130
+ )
131
+ require_auth = make_require_auth(aa)
132
+
133
+ @app.get("/public")
134
+ def public_route():
135
+ return {"message": "Public"}
136
+
137
+ @app.get("/protected")
138
+ @require_auth
139
+ def protected_route():
140
+ from flask import g
141
+ return {"sub": g.current_user["sub"]}
142
+ ```
143
+
144
+ The decoded payload is available as `g.current_user` (a `dict`) inside decorated routes.
145
+
146
+ ---
147
+
148
+ ## FastAPI
149
+
150
+ ```python
151
+ from fastapi import FastAPI, Depends
152
+ from authaction import AuthAction
153
+ from authaction.fastapi import make_require_auth
154
+
155
+ aa = AuthAction(
156
+ domain=os.getenv("AUTHACTION_DOMAIN"),
157
+ audience=os.getenv("AUTHACTION_AUDIENCE"),
158
+ )
159
+ require_auth = make_require_auth(aa)
160
+
161
+ app = FastAPI()
162
+
163
+ @app.get("/public")
164
+ def public_route():
165
+ return {"message": "Public"}
166
+
167
+ @app.get("/protected")
168
+ def protected_route(user: dict = Depends(require_auth)):
169
+ return {"sub": user["sub"], "email": user.get("email")}
170
+ ```
171
+
172
+ ### Scope enforcement
173
+
174
+ ```python
175
+ require_admin = make_require_auth(aa, scopes=["admin"])
176
+
177
+ @app.delete("/users/{user_id}")
178
+ def delete_user(user_id: str, user: dict = Depends(require_admin)):
179
+ ... # raises HTTP 403 if token lacks 'admin' scope
180
+ ```
181
+
182
+ ---
183
+
184
+ ## Exceptions
185
+
186
+ ```python
187
+ from authaction.exceptions import TokenExpiredError, TokenInvalidError
188
+
189
+ try:
190
+ payload = aa.verify_token(token)
191
+ except TokenExpiredError:
192
+ # token exp claim is in the past
193
+ ...
194
+ except TokenInvalidError:
195
+ # bad signature, wrong issuer/audience, malformed JWT
196
+ ...
197
+ ```
198
+
199
+ ## Environment variables
200
+
201
+ ```bash
202
+ AUTHACTION_DOMAIN=your-tenant.eu.authaction.com
203
+ AUTHACTION_AUDIENCE=https://api.your-app.com
204
+ ```
205
+
206
+ ## How JWKS caching works
207
+
208
+ Uses `PyJWT`'s `PyJWKClient` which:
209
+ - Fetches public keys from `https://<domain>/.well-known/jwks.json` on first use
210
+ - Caches up to 16 keys in-process (LRU, configurable via `jwks_cache_keys`)
211
+ - Automatically re-fetches when an unknown `kid` is encountered (key rotation)
212
+
213
+ ## License
214
+
215
+ MIT
@@ -0,0 +1,189 @@
1
+ # authaction-python-sdk
2
+
3
+ JWT verification SDK for Python backends. Validates AuthAction access tokens via JWKS — handles key fetching, caching, and rotation automatically.
4
+
5
+ Works with **Django REST Framework**, **Flask**, and **FastAPI**.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ # Core only
11
+ pip install authaction-python-sdk
12
+
13
+ # With Django support
14
+ pip install "authaction-python-sdk[django]"
15
+
16
+ # With Flask support
17
+ pip install "authaction-python-sdk[flask]"
18
+
19
+ # With FastAPI support
20
+ pip install "authaction-python-sdk[fastapi]"
21
+ ```
22
+
23
+ ---
24
+
25
+ ## Core
26
+
27
+ ```python
28
+ from authaction import AuthAction
29
+
30
+ aa = AuthAction(
31
+ domain=os.getenv("AUTHACTION_DOMAIN"), # e.g. myapp.eu.authaction.com
32
+ audience=os.getenv("AUTHACTION_AUDIENCE"), # e.g. https://api.myapp.com
33
+ )
34
+
35
+ # Verify a raw token — raises TokenExpiredError / TokenInvalidError on failure
36
+ payload = aa.verify_token(token)
37
+
38
+ # Verify from Authorization header — returns None on missing/invalid, never raises
39
+ payload = aa.verify_request(request.headers.get("Authorization"))
40
+
41
+ print(payload["sub"]) # user identifier
42
+ print(payload["email"]) # any JWT claim
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Django REST Framework
48
+
49
+ ### 1. Configure settings
50
+
51
+ ```python
52
+ # settings.py
53
+ AUTHACTION = {
54
+ "DOMAIN": os.getenv("AUTHACTION_DOMAIN"),
55
+ "AUDIENCE": os.getenv("AUTHACTION_AUDIENCE"),
56
+ }
57
+
58
+ REST_FRAMEWORK = {
59
+ "DEFAULT_AUTHENTICATION_CLASSES": [
60
+ "authaction.django.AuthActionAuthentication",
61
+ ],
62
+ "DEFAULT_PERMISSION_CLASSES": [
63
+ "rest_framework.permissions.IsAuthenticated",
64
+ ],
65
+ }
66
+ ```
67
+
68
+ ### 2. Use in views
69
+
70
+ ```python
71
+ from rest_framework.decorators import api_view, permission_classes
72
+ from rest_framework.permissions import AllowAny, IsAuthenticated
73
+ from rest_framework.response import Response
74
+
75
+ @api_view(["GET"])
76
+ @permission_classes([AllowAny])
77
+ def public_view(request):
78
+ return Response({"message": "Public"})
79
+
80
+ @api_view(["GET"])
81
+ def protected_view(request):
82
+ return Response({"sub": request.user.sub, "email": request.user.email})
83
+ ```
84
+
85
+ `request.user` is an `AuthenticatedToken` — access any JWT claim as an attribute:
86
+
87
+ ```python
88
+ request.user.sub # str
89
+ request.user.email # any claim
90
+ request.user.payload # dict — all raw claims
91
+ ```
92
+
93
+ ---
94
+
95
+ ## Flask
96
+
97
+ ```python
98
+ from authaction import AuthAction
99
+ from authaction.flask import make_require_auth
100
+
101
+ aa = AuthAction(
102
+ domain=os.getenv("AUTHACTION_DOMAIN"),
103
+ audience=os.getenv("AUTHACTION_AUDIENCE"),
104
+ )
105
+ require_auth = make_require_auth(aa)
106
+
107
+ @app.get("/public")
108
+ def public_route():
109
+ return {"message": "Public"}
110
+
111
+ @app.get("/protected")
112
+ @require_auth
113
+ def protected_route():
114
+ from flask import g
115
+ return {"sub": g.current_user["sub"]}
116
+ ```
117
+
118
+ The decoded payload is available as `g.current_user` (a `dict`) inside decorated routes.
119
+
120
+ ---
121
+
122
+ ## FastAPI
123
+
124
+ ```python
125
+ from fastapi import FastAPI, Depends
126
+ from authaction import AuthAction
127
+ from authaction.fastapi import make_require_auth
128
+
129
+ aa = AuthAction(
130
+ domain=os.getenv("AUTHACTION_DOMAIN"),
131
+ audience=os.getenv("AUTHACTION_AUDIENCE"),
132
+ )
133
+ require_auth = make_require_auth(aa)
134
+
135
+ app = FastAPI()
136
+
137
+ @app.get("/public")
138
+ def public_route():
139
+ return {"message": "Public"}
140
+
141
+ @app.get("/protected")
142
+ def protected_route(user: dict = Depends(require_auth)):
143
+ return {"sub": user["sub"], "email": user.get("email")}
144
+ ```
145
+
146
+ ### Scope enforcement
147
+
148
+ ```python
149
+ require_admin = make_require_auth(aa, scopes=["admin"])
150
+
151
+ @app.delete("/users/{user_id}")
152
+ def delete_user(user_id: str, user: dict = Depends(require_admin)):
153
+ ... # raises HTTP 403 if token lacks 'admin' scope
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Exceptions
159
+
160
+ ```python
161
+ from authaction.exceptions import TokenExpiredError, TokenInvalidError
162
+
163
+ try:
164
+ payload = aa.verify_token(token)
165
+ except TokenExpiredError:
166
+ # token exp claim is in the past
167
+ ...
168
+ except TokenInvalidError:
169
+ # bad signature, wrong issuer/audience, malformed JWT
170
+ ...
171
+ ```
172
+
173
+ ## Environment variables
174
+
175
+ ```bash
176
+ AUTHACTION_DOMAIN=your-tenant.eu.authaction.com
177
+ AUTHACTION_AUDIENCE=https://api.your-app.com
178
+ ```
179
+
180
+ ## How JWKS caching works
181
+
182
+ Uses `PyJWT`'s `PyJWKClient` which:
183
+ - Fetches public keys from `https://<domain>/.well-known/jwks.json` on first use
184
+ - Caches up to 16 keys in-process (LRU, configurable via `jwks_cache_keys`)
185
+ - Automatically re-fetches when an unknown `kid` is encountered (key rotation)
186
+
187
+ ## License
188
+
189
+ MIT
@@ -0,0 +1,70 @@
1
+ """
2
+ AuthAction Python SDK — JWT verification for Django and Flask APIs.
3
+
4
+ Quick start::
5
+
6
+ from authaction import AuthAction
7
+
8
+ aa = AuthAction(domain="myapp.eu.authaction.com", audience="https://api.myapp.com")
9
+
10
+ # Verify a raw token (raises TokenExpiredError / TokenInvalidError on failure)
11
+ payload = aa.verify_token(token)
12
+
13
+ # Verify from an Authorization header value (returns None on missing/invalid)
14
+ payload = aa.verify_request(request.headers.get("Authorization"))
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import Any
20
+
21
+ from ._verifier import JwtVerifier
22
+ from .exceptions import AuthActionError, TokenExpiredError, TokenInvalidError
23
+
24
+ __all__ = [
25
+ "AuthAction",
26
+ "JwtVerifier",
27
+ "AuthActionError",
28
+ "TokenExpiredError",
29
+ "TokenInvalidError",
30
+ ]
31
+
32
+ __version__ = "0.1.0"
33
+
34
+
35
+ class AuthAction:
36
+ """
37
+ Top-level AuthAction client.
38
+
39
+ :param domain: AuthAction tenant domain (e.g. ``myapp.eu.authaction.com``).
40
+ :param audience: API identifier registered in AuthAction.
41
+ :param jwks_cache_keys: Maximum number of public keys to cache (default 16).
42
+
43
+ Example::
44
+
45
+ aa = AuthAction(
46
+ domain=os.getenv("AUTHACTION_DOMAIN"),
47
+ audience=os.getenv("AUTHACTION_AUDIENCE"),
48
+ )
49
+ payload = aa.verify_token(token)
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ domain: str,
55
+ audience: str,
56
+ jwks_cache_keys: int = 16,
57
+ ) -> None:
58
+ self._verifier = JwtVerifier(
59
+ domain=domain,
60
+ audience=audience,
61
+ jwks_cache_keys=jwks_cache_keys,
62
+ )
63
+
64
+ def verify_token(self, token: str) -> dict[str, Any]:
65
+ """Verify a raw JWT string and return the decoded claims."""
66
+ return self._verifier.verify_token(token)
67
+
68
+ def verify_request(self, authorization_header: str | None) -> dict[str, Any] | None:
69
+ """Verify a Bearer token from an Authorization header value."""
70
+ return self._verifier.verify_request(authorization_header)
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import jwt
6
+ from jwt import PyJWKClient, PyJWKClientError
7
+
8
+ from .exceptions import TokenExpiredError, TokenInvalidError
9
+
10
+
11
+ class JwtVerifier:
12
+ """
13
+ Core JWT verifier.
14
+
15
+ Wraps PyJWT's PyJWKClient which already handles:
16
+ - JWKS fetching from /.well-known/jwks.json
17
+ - In-memory key caching (LRU, configurable size)
18
+ - Automatic key rotation (re-fetches when an unknown kid is seen)
19
+
20
+ Always validates: RS256 algorithm, issuer, audience, and expiry.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ domain: str,
26
+ audience: str,
27
+ jwks_cache_keys: int = 16,
28
+ ) -> None:
29
+ self._domain = domain
30
+ self._audience = audience
31
+ self._issuer = f"https://{domain}"
32
+ self._client = PyJWKClient(
33
+ f"https://{domain}/.well-known/jwks.json",
34
+ cache_keys=True,
35
+ max_cached_keys=jwks_cache_keys,
36
+ )
37
+
38
+ def verify_token(self, token: str) -> dict[str, Any]:
39
+ """
40
+ Verify a raw JWT string and return the decoded claims.
41
+
42
+ Raises:
43
+ TokenExpiredError: The token's ``exp`` claim is in the past.
44
+ TokenInvalidError: Signature, issuer, audience, or structure is invalid.
45
+ """
46
+ try:
47
+ signing_key = self._client.get_signing_key_from_jwt(token)
48
+ except PyJWKClientError as exc:
49
+ raise TokenInvalidError(f"JWKS key lookup failed: {exc}") from exc
50
+
51
+ try:
52
+ return jwt.decode(
53
+ token,
54
+ signing_key.key,
55
+ algorithms=["RS256"],
56
+ audience=self._audience,
57
+ issuer=self._issuer,
58
+ )
59
+ except jwt.ExpiredSignatureError as exc:
60
+ raise TokenExpiredError("Token has expired") from exc
61
+ except jwt.InvalidTokenError as exc:
62
+ raise TokenInvalidError(str(exc)) from exc
63
+
64
+ def verify_request(self, authorization_header: str | None) -> dict[str, Any] | None:
65
+ """
66
+ Extract the Bearer token from an Authorization header value and verify it.
67
+
68
+ Returns ``None`` when the header is absent or not a Bearer scheme.
69
+ Never raises — returns ``None`` on invalid tokens.
70
+ """
71
+ if not authorization_header or not authorization_header.startswith("Bearer "):
72
+ return None
73
+ token = authorization_header[7:].strip()
74
+ try:
75
+ return self.verify_token(token)
76
+ except (TokenExpiredError, TokenInvalidError):
77
+ return None
@@ -0,0 +1,29 @@
1
+ """
2
+ Django REST Framework integration for AuthAction.
3
+
4
+ Configure once in ``settings.py``::
5
+
6
+ AUTHACTION = {
7
+ "DOMAIN": "myapp.eu.authaction.com",
8
+ "AUDIENCE": "https://api.myapp.com",
9
+ }
10
+
11
+ REST_FRAMEWORK = {
12
+ "DEFAULT_AUTHENTICATION_CLASSES": [
13
+ "authaction.django.AuthActionAuthentication",
14
+ ],
15
+ "DEFAULT_PERMISSION_CLASSES": [
16
+ "rest_framework.permissions.IsAuthenticated",
17
+ ],
18
+ }
19
+
20
+ Access the decoded payload in views::
21
+
22
+ @api_view(["GET"])
23
+ def protected(request):
24
+ return Response({"sub": request.user.sub})
25
+ """
26
+
27
+ from .authentication import AuthActionAuthentication, AuthenticatedToken
28
+
29
+ __all__ = ["AuthActionAuthentication", "AuthenticatedToken"]
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from rest_framework.authentication import BaseAuthentication
6
+ from rest_framework.exceptions import AuthenticationFailed
7
+ from rest_framework.request import Request
8
+
9
+ from .._verifier import JwtVerifier
10
+ from ..exceptions import TokenExpiredError, TokenInvalidError
11
+
12
+
13
+ class AuthenticatedToken:
14
+ """
15
+ Minimal user-like object that wraps the decoded JWT claims.
16
+ Assigned to ``request.user`` after successful authentication.
17
+ """
18
+
19
+ is_authenticated = True
20
+
21
+ def __init__(self, payload: dict[str, Any]) -> None:
22
+ self.payload = payload
23
+
24
+ @property
25
+ def sub(self) -> str:
26
+ return str(self.payload.get("sub", ""))
27
+
28
+ def __getattr__(self, item: str) -> Any:
29
+ try:
30
+ return self.payload[item]
31
+ except KeyError:
32
+ raise AttributeError(item) from None
33
+
34
+
35
+ def _get_verifier() -> JwtVerifier:
36
+ """Lazily construct the verifier from Django settings."""
37
+ from django.conf import settings # imported lazily to avoid Django startup order issues
38
+
39
+ config: dict[str, Any] = getattr(settings, "AUTHACTION", {})
40
+ domain = config.get("DOMAIN") or ""
41
+ audience = config.get("AUDIENCE") or ""
42
+ if not domain or not audience:
43
+ raise ImproperlyConfigured( # noqa: F821
44
+ "AUTHACTION settings must include 'DOMAIN' and 'AUDIENCE'. "
45
+ "Add AUTHACTION = {'DOMAIN': '...', 'AUDIENCE': '...'} to settings.py."
46
+ )
47
+ return JwtVerifier(domain=domain, audience=audience)
48
+
49
+
50
+ try:
51
+ from django.core.exceptions import ImproperlyConfigured # noqa: F401
52
+ except ImportError:
53
+ ImproperlyConfigured = RuntimeError # type: ignore[misc,assignment]
54
+
55
+
56
+ class AuthActionAuthentication(BaseAuthentication):
57
+ """
58
+ Django REST Framework authentication class that validates AuthAction JWTs.
59
+
60
+ Read the token from ``Authorization: Bearer <token>`` and return
61
+ ``(AuthenticatedToken(payload), raw_token)`` on success, or ``None``
62
+ when the header is absent (to allow other authenticators to run).
63
+
64
+ Raises ``AuthenticationFailed`` (HTTP 401) when the header is present
65
+ but the token is invalid or expired.
66
+
67
+ Usage::
68
+
69
+ REST_FRAMEWORK = {
70
+ "DEFAULT_AUTHENTICATION_CLASSES": [
71
+ "authaction.django.AuthActionAuthentication",
72
+ ],
73
+ }
74
+ """
75
+
76
+ _verifier: JwtVerifier | None = None
77
+
78
+ @classmethod
79
+ def get_verifier(cls) -> JwtVerifier:
80
+ if cls._verifier is None:
81
+ cls._verifier = _get_verifier()
82
+ return cls._verifier
83
+
84
+ def authenticate(self, request: Request) -> tuple[AuthenticatedToken, str] | None:
85
+ auth_header: str = request.headers.get("Authorization", "")
86
+
87
+ if not auth_header.startswith("Bearer "):
88
+ return None # Not a Bearer request — let other authenticators try
89
+
90
+ token = auth_header[7:].strip()
91
+ if not token:
92
+ raise AuthenticationFailed("Empty Bearer token")
93
+
94
+ try:
95
+ payload = self.get_verifier().verify_token(token)
96
+ except TokenExpiredError:
97
+ raise AuthenticationFailed("Token has expired")
98
+ except TokenInvalidError as exc:
99
+ raise AuthenticationFailed(str(exc))
100
+
101
+ return AuthenticatedToken(payload), token
102
+
103
+ def authenticate_header(self, request: Request) -> str:
104
+ return "Bearer"
@@ -0,0 +1,10 @@
1
+ class AuthActionError(Exception):
2
+ """Base exception for all AuthAction SDK errors."""
3
+
4
+
5
+ class TokenExpiredError(AuthActionError):
6
+ """The JWT's ``exp`` claim is in the past."""
7
+
8
+
9
+ class TokenInvalidError(AuthActionError):
10
+ """The JWT signature, issuer, audience, or structure is invalid."""