authaction-python-sdk 0.1.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.
authaction/__init__.py ADDED
@@ -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."""
@@ -0,0 +1,30 @@
1
+ """
2
+ FastAPI integration for AuthAction.
3
+
4
+ Usage::
5
+
6
+ from authaction import AuthAction
7
+ from authaction.fastapi import make_require_auth
8
+
9
+ aa = AuthAction(
10
+ domain=os.getenv("AUTHACTION_DOMAIN"),
11
+ audience=os.getenv("AUTHACTION_AUDIENCE"),
12
+ )
13
+ require_auth = make_require_auth(aa)
14
+
15
+ @app.get("/protected")
16
+ def protected(user: dict = Depends(require_auth)):
17
+ return {"sub": user["sub"]}
18
+
19
+ Optional: require specific scopes::
20
+
21
+ require_read = make_require_auth(aa, scopes=["read:data"])
22
+
23
+ @app.get("/data")
24
+ def get_data(user: dict = Depends(require_read)):
25
+ return {"data": "..."}
26
+ """
27
+
28
+ from .dependencies import make_require_auth
29
+
30
+ __all__ = ["make_require_auth"]
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, List, Optional
4
+
5
+ from fastapi import HTTPException, Security
6
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
7
+
8
+ from ..exceptions import TokenExpiredError, TokenInvalidError
9
+
10
+ try:
11
+ from .. import AuthAction
12
+ except ImportError: # pragma: no cover
13
+ AuthAction = None # type: ignore[assignment,misc]
14
+
15
+ _security = HTTPBearer(auto_error=False)
16
+
17
+
18
+ def make_require_auth(
19
+ aa: AuthAction, # type: ignore[valid-type]
20
+ *,
21
+ scopes: Optional[List[str]] = None,
22
+ ) -> Any:
23
+ """
24
+ Factory that returns a FastAPI dependency callable for JWT authentication.
25
+
26
+ The dependency:
27
+ - Extracts the Bearer token from the ``Authorization`` header.
28
+ - Verifies the token via :class:`~authaction.AuthAction`.
29
+ - Optionally checks that every scope in *scopes* is present in the token's
30
+ ``scope`` claim (space-separated string).
31
+ - Returns the decoded JWT payload ``dict`` on success.
32
+ - Raises ``HTTPException(401)`` on missing / expired / invalid tokens.
33
+ - Raises ``HTTPException(403)`` on insufficient scopes.
34
+
35
+ Example::
36
+
37
+ aa = AuthAction(domain=..., audience=...)
38
+ require_auth = make_require_auth(aa)
39
+
40
+ @app.get("/protected")
41
+ def protected(user: dict = Depends(require_auth)):
42
+ return {"sub": user["sub"], "email": user.get("email")}
43
+
44
+ With scope enforcement::
45
+
46
+ require_admin = make_require_auth(aa, scopes=["admin"])
47
+
48
+ @app.delete("/users/{id}")
49
+ def delete_user(id: str, user: dict = Depends(require_admin)):
50
+ ...
51
+ """
52
+
53
+ async def require_auth(
54
+ credentials: Optional[HTTPAuthorizationCredentials] = Security(_security),
55
+ ) -> dict[str, Any]:
56
+ if credentials is None:
57
+ raise HTTPException(
58
+ status_code=401,
59
+ detail="Missing Bearer token",
60
+ headers={"WWW-Authenticate": "Bearer"},
61
+ )
62
+
63
+ try:
64
+ payload = aa.verify_token(credentials.credentials)
65
+ except TokenExpiredError:
66
+ raise HTTPException(
67
+ status_code=401,
68
+ detail="Token has expired",
69
+ headers={"WWW-Authenticate": "Bearer error=\"invalid_token\""},
70
+ )
71
+ except TokenInvalidError as exc:
72
+ raise HTTPException(
73
+ status_code=401,
74
+ detail=str(exc),
75
+ headers={"WWW-Authenticate": "Bearer error=\"invalid_token\""},
76
+ )
77
+
78
+ if scopes:
79
+ token_scopes = set(str(payload.get("scope", "")).split())
80
+ missing = [s for s in scopes if s not in token_scopes]
81
+ if missing:
82
+ raise HTTPException(
83
+ status_code=403,
84
+ detail=f"Insufficient scope. Required: {', '.join(missing)}",
85
+ headers={"WWW-Authenticate": f'Bearer error="insufficient_scope"'},
86
+ )
87
+
88
+ return payload
89
+
90
+ return require_auth
@@ -0,0 +1,24 @@
1
+ """
2
+ Flask integration for AuthAction.
3
+
4
+ Usage::
5
+
6
+ from authaction import AuthAction
7
+ from authaction.flask import make_require_auth
8
+
9
+ aa = AuthAction(
10
+ domain=os.getenv("AUTHACTION_DOMAIN"),
11
+ audience=os.getenv("AUTHACTION_AUDIENCE"),
12
+ )
13
+ require_auth = make_require_auth(aa)
14
+
15
+ @app.get("/protected")
16
+ @require_auth
17
+ def protected():
18
+ from flask import g
19
+ return {"sub": g.current_user["sub"]}
20
+ """
21
+
22
+ from .decorators import make_require_auth
23
+
24
+ __all__ = ["make_require_auth"]
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import wraps
4
+ from typing import Any, Callable
5
+
6
+ from flask import g, jsonify, request
7
+
8
+ from ..exceptions import TokenExpiredError, TokenInvalidError
9
+
10
+ try:
11
+ from .. import AuthAction
12
+ except ImportError: # pragma: no cover
13
+ AuthAction = None # type: ignore[assignment,misc]
14
+
15
+
16
+ def make_require_auth(aa: AuthAction) -> Callable: # type: ignore[valid-type]
17
+ """
18
+ Factory that returns a ``@require_auth`` decorator bound to *aa*.
19
+
20
+ The decoded JWT payload is stored on ``flask.g.current_user`` so it is
21
+ accessible anywhere within the request context.
22
+
23
+ Example::
24
+
25
+ aa = AuthAction(domain=..., audience=...)
26
+ require_auth = make_require_auth(aa)
27
+
28
+ @app.get("/protected")
29
+ @require_auth
30
+ def protected():
31
+ return {"sub": g.current_user["sub"]}
32
+
33
+ To access custom claims::
34
+
35
+ @app.get("/me")
36
+ @require_auth
37
+ def me():
38
+ user = g.current_user
39
+ return {"sub": user["sub"], "email": user.get("email")}
40
+ """
41
+
42
+ def require_auth(f: Callable) -> Callable:
43
+ @wraps(f)
44
+ def decorated(*args: Any, **kwargs: Any) -> Any:
45
+ auth_header: str = request.headers.get("Authorization", "")
46
+
47
+ if not auth_header.startswith("Bearer "):
48
+ return jsonify({"error": "Unauthorized", "message": "Missing Bearer token"}), 401
49
+
50
+ token = auth_header[7:].strip()
51
+ try:
52
+ g.current_user = aa.verify_token(token)
53
+ except TokenExpiredError:
54
+ return jsonify({"error": "Unauthorized", "message": "Token has expired"}), 401
55
+ except TokenInvalidError as exc:
56
+ return jsonify({"error": "Unauthorized", "message": str(exc)}), 401
57
+
58
+ return f(*args, **kwargs)
59
+
60
+ return decorated
61
+
62
+ return require_auth
@@ -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,13 @@
1
+ authaction/__init__.py,sha256=sfM8KfpIpGR8iFPUItPDdS3OM2k8KFmskafnSJmVND0,1993
2
+ authaction/_verifier.py,sha256=n3uzTB8ZoHq5Q4efD-QexX9PabY3MtHkjYPZ5TgnL8I,2543
3
+ authaction/exceptions.py,sha256=jkyjbSigr6rmyT4INbGk-OaitS2ZO6C6Yq8Kg1FtJes,300
4
+ authaction/django/__init__.py,sha256=H-SncL_A2bJwxxROYFLpQznnGd7On0zxmZlc0_oXE-4,741
5
+ authaction/django/authentication.py,sha256=ZukHBZ22g4TE3SyIQnW1WUAFRfXs2byRqG4GqUewTAk,3371
6
+ authaction/fastapi/__init__.py,sha256=8RBrN6IU_k1U6BrTuXzUlX5-wfQ8v2WG3B_jdmle0fM,718
7
+ authaction/fastapi/dependencies.py,sha256=EQzjRjmLe5bR2StBUpiJXlbuuVHpmX6U887uwXF13CI,3003
8
+ authaction/flask/__init__.py,sha256=nPgxYGOodpGsltjC6VVLmcwWdoO7FNuhZkR8TM6Y-Bs,525
9
+ authaction/flask/decorators.py,sha256=fcZaGvrbv8fDpSD7FDH603Y1Jq9wBH1BR8z39qfKiGM,1881
10
+ authaction_python_sdk-0.1.0.dist-info/METADATA,sha256=6UEEjRQQZC0ndiIlQ8vak8NNTju1uO7chA_5X4ToQsE,5286
11
+ authaction_python_sdk-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
+ authaction_python_sdk-0.1.0.dist-info/top_level.txt,sha256=zelPBMLsly6YMYgzyO6U1PFaSZ-ElSBrVKmSqRA1MWg,11
13
+ authaction_python_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ authaction