noesis-auth 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,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: noesis-auth
3
+ Version: 0.1.0
4
+ Summary: Python Auth SDK for AI tool integration with the Noesis AIToolCenter platform
5
+ Author: Noesis AI Technologies
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Noesis-AI-Technologies/AIToolCenter
8
+ Project-URL: Documentation, https://github.com/Noesis-AI-Technologies/AIToolCenter/blob/main/docs/auth-sdk-guide.md
9
+ Project-URL: Repository, https://github.com/Noesis-AI-Technologies/AIToolCenter
10
+ Project-URL: Issues, https://github.com/Noesis-AI-Technologies/AIToolCenter/issues
11
+ Keywords: auth,oauth2,jwt,sdk,ai-tools
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Security
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: httpx>=0.24.0
23
+ Requires-Dist: python-jose[cryptography]>=3.3.0
24
+ Provides-Extra: fastapi
25
+ Requires-Dist: fastapi>=0.100.0; extra == "fastapi"
26
+
27
+ # noesis-auth (Python)
28
+
29
+ Python Auth SDK for AI tool integration with the [Noesis AIToolCenter](https://github.com/Noesis-AI-Technologies/AIToolCenter) platform.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install noesis-auth
35
+
36
+ # With FastAPI middleware support:
37
+ pip install noesis-auth[fastapi]
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ### JWT Validation (Tool-side)
43
+
44
+ ```python
45
+ from auth_sdk import JWTValidator
46
+
47
+ validator = JWTValidator(
48
+ jwks_url="https://your-platform.com/.well-known/jwks.json"
49
+ )
50
+
51
+ payload = await validator.validate(token)
52
+ print(payload["sub"]) # user ID
53
+ ```
54
+
55
+ ### FastAPI Middleware
56
+
57
+ ```python
58
+ from fastapi import Depends, FastAPI
59
+ from auth_sdk import AuthMiddleware, TokenPayload
60
+
61
+ app = FastAPI()
62
+ auth = AuthMiddleware(jwks_url="https://your-platform.com/.well-known/jwks.json")
63
+
64
+ @app.get("/api/generate")
65
+ async def generate(payload: TokenPayload = Depends(auth.require_tool("your-tool-id"))):
66
+ user_id = payload.sub
67
+ # ... your tool logic
68
+ ```
69
+
70
+ ### OAuth2 Client (PKCE)
71
+
72
+ ```python
73
+ from auth_sdk import AuthClient
74
+
75
+ client = AuthClient(base_url="https://your-platform.com")
76
+
77
+ # Generate PKCE pair
78
+ pkce = AuthClient.generate_pkce()
79
+
80
+ # Build authorization URL
81
+ url = client.build_authorize_url(
82
+ client_id="your-client-id",
83
+ redirect_uri="http://localhost:3000/callback",
84
+ code_challenge=pkce.code_challenge,
85
+ )
86
+
87
+ # Exchange code for tokens
88
+ tokens = await client.exchange_code(
89
+ code=auth_code,
90
+ redirect_uri="http://localhost:3000/callback",
91
+ client_id="your-client-id",
92
+ code_verifier=pkce.code_verifier,
93
+ )
94
+ ```
95
+
96
+ ### Activation Code Redemption
97
+
98
+ ```python
99
+ result = await client.redeem_code(user_token="...", code="AXKF-M3PQ-7RBN-W2YT")
100
+ print(result.tool_id, result.expires_at)
101
+ ```
102
+
103
+ ## Features
104
+
105
+ - **JWT Validation** — RS256 (JWKS) and HS256 (shared secret) with auto-detection
106
+ - **FastAPI Middleware** — Drop-in authentication and tool access verification
107
+ - **OAuth2 Client** — Authorization URL builder, code exchange, token refresh
108
+ - **PKCE Support** — S256 code challenge generation for public clients
109
+ - **Token Introspection** — Remote token validation endpoint
110
+ - **Activation Codes** — Redeem activation codes for tool entitlements
111
+ - **JWKS Caching** — 6-hour cache with stale-while-revalidate and retry on failure
112
+
113
+ ## Requirements
114
+
115
+ - Python >= 3.11
116
+ - `httpx` >= 0.24
117
+ - `python-jose[cryptography]` >= 3.3
118
+ - `fastapi` >= 0.100 (optional, for middleware)
119
+
120
+ ## License
121
+
122
+ MIT
@@ -0,0 +1,96 @@
1
+ # noesis-auth (Python)
2
+
3
+ Python Auth SDK for AI tool integration with the [Noesis AIToolCenter](https://github.com/Noesis-AI-Technologies/AIToolCenter) platform.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install noesis-auth
9
+
10
+ # With FastAPI middleware support:
11
+ pip install noesis-auth[fastapi]
12
+ ```
13
+
14
+ ## Quick Start
15
+
16
+ ### JWT Validation (Tool-side)
17
+
18
+ ```python
19
+ from auth_sdk import JWTValidator
20
+
21
+ validator = JWTValidator(
22
+ jwks_url="https://your-platform.com/.well-known/jwks.json"
23
+ )
24
+
25
+ payload = await validator.validate(token)
26
+ print(payload["sub"]) # user ID
27
+ ```
28
+
29
+ ### FastAPI Middleware
30
+
31
+ ```python
32
+ from fastapi import Depends, FastAPI
33
+ from auth_sdk import AuthMiddleware, TokenPayload
34
+
35
+ app = FastAPI()
36
+ auth = AuthMiddleware(jwks_url="https://your-platform.com/.well-known/jwks.json")
37
+
38
+ @app.get("/api/generate")
39
+ async def generate(payload: TokenPayload = Depends(auth.require_tool("your-tool-id"))):
40
+ user_id = payload.sub
41
+ # ... your tool logic
42
+ ```
43
+
44
+ ### OAuth2 Client (PKCE)
45
+
46
+ ```python
47
+ from auth_sdk import AuthClient
48
+
49
+ client = AuthClient(base_url="https://your-platform.com")
50
+
51
+ # Generate PKCE pair
52
+ pkce = AuthClient.generate_pkce()
53
+
54
+ # Build authorization URL
55
+ url = client.build_authorize_url(
56
+ client_id="your-client-id",
57
+ redirect_uri="http://localhost:3000/callback",
58
+ code_challenge=pkce.code_challenge,
59
+ )
60
+
61
+ # Exchange code for tokens
62
+ tokens = await client.exchange_code(
63
+ code=auth_code,
64
+ redirect_uri="http://localhost:3000/callback",
65
+ client_id="your-client-id",
66
+ code_verifier=pkce.code_verifier,
67
+ )
68
+ ```
69
+
70
+ ### Activation Code Redemption
71
+
72
+ ```python
73
+ result = await client.redeem_code(user_token="...", code="AXKF-M3PQ-7RBN-W2YT")
74
+ print(result.tool_id, result.expires_at)
75
+ ```
76
+
77
+ ## Features
78
+
79
+ - **JWT Validation** — RS256 (JWKS) and HS256 (shared secret) with auto-detection
80
+ - **FastAPI Middleware** — Drop-in authentication and tool access verification
81
+ - **OAuth2 Client** — Authorization URL builder, code exchange, token refresh
82
+ - **PKCE Support** — S256 code challenge generation for public clients
83
+ - **Token Introspection** — Remote token validation endpoint
84
+ - **Activation Codes** — Redeem activation codes for tool entitlements
85
+ - **JWKS Caching** — 6-hour cache with stale-while-revalidate and retry on failure
86
+
87
+ ## Requirements
88
+
89
+ - Python >= 3.11
90
+ - `httpx` >= 0.24
91
+ - `python-jose[cryptography]` >= 3.3
92
+ - `fastapi` >= 0.100 (optional, for middleware)
93
+
94
+ ## License
95
+
96
+ MIT
@@ -0,0 +1,27 @@
1
+ from auth_sdk.client import AuthClient, AuthError, TokenResponse, RedeemResult, IntrospectResult, PKCEPair
2
+ from auth_sdk.jwt_validator import JWTValidator
3
+ from auth_sdk.models import Entitlement, TokenPayload
4
+
5
+ try:
6
+ from auth_sdk.fastapi_middleware import AuthMiddleware, require_tool_access
7
+ except ImportError:
8
+ # FastAPI is an optional dependency
9
+ AuthMiddleware = None # type: ignore[assignment, misc]
10
+ require_tool_access = None # type: ignore[assignment]
11
+
12
+ __version__ = "0.1.0"
13
+
14
+ __all__ = [
15
+ "__version__",
16
+ "AuthClient",
17
+ "AuthError",
18
+ "AuthMiddleware",
19
+ "IntrospectResult",
20
+ "JWTValidator",
21
+ "PKCEPair",
22
+ "RedeemResult",
23
+ "TokenPayload",
24
+ "TokenResponse",
25
+ "Entitlement",
26
+ "require_tool_access",
27
+ ]
@@ -0,0 +1,248 @@
1
+ """AuthClient — HTTP client for interacting with the ERP platform APIs.
2
+
3
+ Implements TODO item B1 — Python SDK AuthClient (mirrors TypeScript SDK).
4
+
5
+ Provides:
6
+ - build_authorize_url() — OAuth2 authorization URL builder
7
+ - exchange_code() — exchange auth code for tokens
8
+ - refresh_token() — refresh access token
9
+ - redeem_code() — redeem activation code
10
+ - introspect() — introspect token
11
+ - generate_pkce() — generate PKCE code verifier + challenge
12
+ """
13
+
14
+ import base64
15
+ import hashlib
16
+ import os
17
+ from dataclasses import dataclass
18
+ from urllib.parse import urlencode
19
+
20
+ import httpx
21
+
22
+ from auth_sdk.models import TokenPayload
23
+
24
+
25
+ class AuthError(Exception):
26
+ def __init__(self, message: str, code: str = "UNKNOWN", status: int = 0):
27
+ super().__init__(message)
28
+ self.code = code
29
+ self.status = status
30
+
31
+
32
+ @dataclass
33
+ class TokenResponse:
34
+ access_token: str
35
+ token_type: str
36
+ expires_in: int
37
+ refresh_token: str | None = None
38
+
39
+
40
+ @dataclass
41
+ class RedeemResult:
42
+ tool_id: str
43
+ expires_at: str
44
+ message: str = ""
45
+
46
+
47
+ @dataclass
48
+ class IntrospectResult:
49
+ active: bool
50
+ sub: str | None = None
51
+ exp: int | None = None
52
+ iat: int | None = None
53
+ jti: str | None = None
54
+ entitlements: list[dict] | None = None
55
+
56
+
57
+ @dataclass
58
+ class PKCEPair:
59
+ code_verifier: str
60
+ code_challenge: str
61
+
62
+
63
+ def _base64url_encode(data: bytes) -> str:
64
+ return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
65
+
66
+
67
+ class AuthClient:
68
+ """HTTP client for AIToolCenter platform APIs."""
69
+
70
+ def __init__(self, base_url: str, timeout: float = 10.0):
71
+ self.base_url = base_url.rstrip("/")
72
+ self.timeout = timeout
73
+
74
+ @staticmethod
75
+ def generate_pkce() -> PKCEPair:
76
+ """Generate a PKCE code verifier and S256 challenge."""
77
+ verifier_bytes = os.urandom(32)
78
+ code_verifier = _base64url_encode(verifier_bytes)
79
+ digest = hashlib.sha256(code_verifier.encode("ascii")).digest()
80
+ code_challenge = _base64url_encode(digest)
81
+ return PKCEPair(code_verifier=code_verifier, code_challenge=code_challenge)
82
+
83
+ def build_authorize_url(
84
+ self,
85
+ client_id: str,
86
+ redirect_uri: str,
87
+ state: str | None = None,
88
+ code_challenge: str | None = None,
89
+ ) -> str:
90
+ """Build the OAuth2 authorization URL."""
91
+ params = {
92
+ "response_type": "code",
93
+ "client_id": client_id,
94
+ "redirect_uri": redirect_uri,
95
+ }
96
+ if state:
97
+ params["state"] = state
98
+ if code_challenge:
99
+ params["code_challenge"] = code_challenge
100
+ params["code_challenge_method"] = "S256"
101
+ return f"{self.base_url}/oauth/authorize?{urlencode(params)}"
102
+
103
+ async def exchange_code(
104
+ self,
105
+ code: str,
106
+ redirect_uri: str,
107
+ client_id: str,
108
+ code_verifier: str | None = None,
109
+ client_secret: str | None = None,
110
+ ) -> TokenResponse:
111
+ """Exchange an authorization code for tokens."""
112
+ data = {
113
+ "grant_type": "authorization_code",
114
+ "code": code,
115
+ "redirect_uri": redirect_uri,
116
+ "client_id": client_id,
117
+ }
118
+ if code_verifier:
119
+ data["code_verifier"] = code_verifier
120
+ if client_secret:
121
+ data["client_secret"] = client_secret
122
+
123
+ async with httpx.AsyncClient(timeout=self.timeout) as client:
124
+ resp = await client.post(
125
+ f"{self.base_url}/oauth/token",
126
+ data=data,
127
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
128
+ )
129
+
130
+ if resp.status_code != 200:
131
+ body = resp.json() if resp.headers.get("content-type", "").startswith("application/json") else {}
132
+ raise AuthError(
133
+ body.get("detail", f"Code exchange failed: {resp.status_code}"),
134
+ "TOKEN_INVALID",
135
+ resp.status_code,
136
+ )
137
+
138
+ d = resp.json()
139
+ return TokenResponse(
140
+ access_token=d["access_token"],
141
+ token_type=d.get("token_type", "bearer"),
142
+ expires_in=d.get("expires_in", 0),
143
+ refresh_token=d.get("refresh_token"),
144
+ )
145
+
146
+ async def refresh_token(
147
+ self,
148
+ refresh_token: str,
149
+ client_id: str | None = None,
150
+ client_secret: str | None = None,
151
+ ) -> TokenResponse:
152
+ """Refresh an access token using a refresh token."""
153
+ data = {
154
+ "grant_type": "refresh_token",
155
+ "refresh_token": refresh_token,
156
+ }
157
+ if client_id:
158
+ data["client_id"] = client_id
159
+ if client_secret:
160
+ data["client_secret"] = client_secret
161
+
162
+ async with httpx.AsyncClient(timeout=self.timeout) as client:
163
+ resp = await client.post(
164
+ f"{self.base_url}/oauth/token",
165
+ data=data,
166
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
167
+ )
168
+
169
+ if resp.status_code != 200:
170
+ body = resp.json() if resp.headers.get("content-type", "").startswith("application/json") else {}
171
+ raise AuthError(
172
+ body.get("detail", f"Token refresh failed: {resp.status_code}"),
173
+ "TOKEN_INVALID",
174
+ resp.status_code,
175
+ )
176
+
177
+ d = resp.json()
178
+ return TokenResponse(
179
+ access_token=d["access_token"],
180
+ token_type=d.get("token_type", "bearer"),
181
+ expires_in=d.get("expires_in", 0),
182
+ refresh_token=d.get("refresh_token"),
183
+ )
184
+
185
+ async def redeem_code(self, user_token: str, code: str) -> RedeemResult:
186
+ """Redeem an activation code for the authenticated user."""
187
+ async with httpx.AsyncClient(timeout=self.timeout) as client:
188
+ resp = await client.post(
189
+ f"{self.base_url}/api/v1/activation/redeem",
190
+ json={"code": code},
191
+ headers={
192
+ "Authorization": f"Bearer {user_token}",
193
+ "Content-Type": "application/json",
194
+ },
195
+ )
196
+
197
+ if resp.status_code != 200:
198
+ body = resp.json() if resp.headers.get("content-type", "").startswith("application/json") else {}
199
+ raise AuthError(
200
+ body.get("detail", f"Redemption failed: {resp.status_code}"),
201
+ "TOKEN_INVALID",
202
+ resp.status_code,
203
+ )
204
+
205
+ d = resp.json()
206
+ return RedeemResult(
207
+ tool_id=d["tool_id"],
208
+ expires_at=d["expires_at"],
209
+ message=d.get("message", ""),
210
+ )
211
+
212
+ async def introspect(
213
+ self,
214
+ token: str,
215
+ client_id: str | None = None,
216
+ client_secret: str | None = None,
217
+ ) -> IntrospectResult:
218
+ """Introspect a token to check if it's active."""
219
+ data = {"token": token}
220
+ if client_id:
221
+ data["client_id"] = client_id
222
+ if client_secret:
223
+ data["client_secret"] = client_secret
224
+
225
+ async with httpx.AsyncClient(timeout=self.timeout) as client:
226
+ resp = await client.post(
227
+ f"{self.base_url}/oauth/introspect",
228
+ data=data,
229
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
230
+ )
231
+
232
+ if resp.status_code != 200:
233
+ body = resp.json() if resp.headers.get("content-type", "").startswith("application/json") else {}
234
+ raise AuthError(
235
+ body.get("detail", f"Introspection failed: {resp.status_code}"),
236
+ "NETWORK_ERROR",
237
+ resp.status_code,
238
+ )
239
+
240
+ d = resp.json()
241
+ return IntrospectResult(
242
+ active=d.get("active", False),
243
+ sub=d.get("sub"),
244
+ exp=d.get("exp"),
245
+ iat=d.get("iat"),
246
+ jti=d.get("jti"),
247
+ entitlements=d.get("entitlements"),
248
+ )
@@ -0,0 +1,80 @@
1
+ import functools
2
+ import time
3
+ from typing import Callable
4
+
5
+ from fastapi import HTTPException, Request, status
6
+
7
+ from auth_sdk.jwt_validator import JWTValidator
8
+ from auth_sdk.models import Entitlement, TokenPayload
9
+
10
+
11
+ class AuthMiddleware:
12
+ """FastAPI dependency for tool-side authentication and authorization."""
13
+
14
+ def __init__(self, jwks_url: str, algorithm: str = "RS256", hs256_secret: str | None = None):
15
+ self.validator = JWTValidator(
16
+ jwks_url=jwks_url,
17
+ algorithm=algorithm,
18
+ hs256_secret=hs256_secret,
19
+ )
20
+
21
+ async def authenticate(self, request: Request) -> TokenPayload:
22
+ """Extract and validate the bearer token, return parsed payload."""
23
+ auth_header = request.headers.get("Authorization", "")
24
+ if not auth_header.startswith("Bearer "):
25
+ raise HTTPException(
26
+ status_code=status.HTTP_401_UNAUTHORIZED,
27
+ detail="Missing or invalid Authorization header",
28
+ )
29
+ token = auth_header[7:]
30
+ try:
31
+ raw = await self.validator.validate(token)
32
+ except ValueError as e:
33
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(e))
34
+
35
+ entitlements = [
36
+ Entitlement(tool_id=e["tool_id"], expires_at=e["expires_at"]) for e in raw.get("entitlements", [])
37
+ ]
38
+ return TokenPayload(
39
+ sub=raw["sub"],
40
+ exp=raw["exp"],
41
+ iat=raw["iat"],
42
+ jti=raw.get("jti", ""),
43
+ entitlements=entitlements,
44
+ )
45
+
46
+ def require_tool(self, tool_id: str | int) -> Callable:
47
+ """FastAPI dependency that checks the user has access to a specific tool."""
48
+ middleware = self
49
+
50
+ async def dependency(request: Request) -> TokenPayload:
51
+ payload = await middleware.authenticate(request)
52
+ if not payload.has_tool_access(tool_id):
53
+ raise HTTPException(
54
+ status_code=status.HTTP_403_FORBIDDEN,
55
+ detail=f"No access to tool {tool_id}",
56
+ )
57
+ return payload
58
+
59
+ return dependency
60
+
61
+
62
+ def require_tool_access(tool_id: str | int, *, jwks_url: str = "", _middleware_cache: dict = {}):
63
+ """Convenience function that returns a FastAPI dependency for tool access verification.
64
+
65
+ Usage:
66
+ from auth_sdk import require_tool_access
67
+
68
+ @app.get("/api/generate")
69
+ async def generate(
70
+ payload: TokenPayload = Depends(require_tool_access(1, jwks_url="https://auth.example.com/.well-known/jwks.json"))
71
+ ):
72
+ user_id = payload.sub
73
+ ...
74
+ """
75
+ if not jwks_url:
76
+ raise ValueError("jwks_url is required for require_tool_access()")
77
+ if jwks_url not in _middleware_cache:
78
+ _middleware_cache[jwks_url] = AuthMiddleware(jwks_url=jwks_url)
79
+ middleware = _middleware_cache[jwks_url]
80
+ return middleware.require_tool(tool_id)
@@ -0,0 +1,123 @@
1
+ import asyncio
2
+ import time
3
+ import logging
4
+ from typing import Any
5
+
6
+ import httpx
7
+ from jose import jwt, JWTError, ExpiredSignatureError
8
+ from jose.constants import ALGORITHMS
9
+
10
+ logger = logging.getLogger("auth_sdk")
11
+
12
+
13
+ class JWTValidator:
14
+ """Validates JWTs using JWKS fetched from the Auth Center.
15
+
16
+ Supports RS256 (via JWKS) and HS256 (via shared secret).
17
+ When using HS256, pass the shared secret via ``hs256_secret``.
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ jwks_url: str,
23
+ algorithm: str = "RS256",
24
+ cache_ttl: int = 21600,
25
+ hs256_secret: str | None = None,
26
+ ):
27
+ self.jwks_url = jwks_url
28
+ self.algorithm = algorithm
29
+ self.cache_ttl = cache_ttl # default 6 hours
30
+ self.hs256_secret = hs256_secret
31
+ self._jwks: dict[str, Any] | None = None
32
+ self._jwks_fetched_at: float = 0
33
+
34
+ async def _fetch_jwks(self) -> dict:
35
+ """Fetch JWKS from the auth server with one retry on failure."""
36
+ last_error: Exception | None = None
37
+ for attempt in range(2):
38
+ try:
39
+ async with httpx.AsyncClient() as client:
40
+ resp = await client.get(self.jwks_url, timeout=10)
41
+ resp.raise_for_status()
42
+ return resp.json()
43
+ except Exception as e:
44
+ last_error = e
45
+ if attempt == 0:
46
+ logger.warning("JWKS fetch attempt %d failed: %s, retrying...", attempt + 1, e)
47
+ await asyncio.sleep(1)
48
+ raise RuntimeError(f"Failed to fetch JWKS from {self.jwks_url}: {last_error}") from last_error
49
+
50
+ async def get_jwks(self) -> dict:
51
+ now = time.time()
52
+ if self._jwks is None or (now - self._jwks_fetched_at) > self.cache_ttl:
53
+ try:
54
+ self._jwks = await self._fetch_jwks()
55
+ self._jwks_fetched_at = now
56
+ logger.info("JWKS refreshed from %s", self.jwks_url)
57
+ except Exception as e:
58
+ if self._jwks is not None:
59
+ logger.warning("Failed to refresh JWKS, using cached: %s", e)
60
+ else:
61
+ raise RuntimeError(f"Cannot validate tokens: JWKS unavailable from {self.jwks_url}") from e
62
+ return self._jwks
63
+
64
+ async def validate(self, token: str) -> dict:
65
+ """Validate JWT signature and expiry. Returns decoded payload.
66
+
67
+ Auto-detects the algorithm from the JWT header:
68
+ - If alg=HS256 and hs256_secret is configured → symmetric validation
69
+ - If alg=RS256 → asymmetric validation via JWKS
70
+ - Falls back to configured self.algorithm if header detection fails
71
+ """
72
+ # Auto-detect algorithm from token header
73
+ try:
74
+ unverified_header = jwt.get_unverified_header(token)
75
+ token_alg = unverified_header.get("alg", self.algorithm)
76
+ except JWTError:
77
+ token_alg = self.algorithm
78
+
79
+ if token_alg == "HS256" and self.hs256_secret:
80
+ # Symmetric validation — no JWKS needed
81
+ try:
82
+ payload = jwt.decode(
83
+ token,
84
+ self.hs256_secret,
85
+ algorithms=["HS256"],
86
+ options={"verify_aud": False},
87
+ )
88
+ return payload
89
+ except ExpiredSignatureError:
90
+ raise ValueError("Token has expired")
91
+ except JWTError as e:
92
+ raise ValueError(f"Token validation failed: {e}") from e
93
+
94
+ # Asymmetric validation via JWKS
95
+ jwks = await self.get_jwks()
96
+
97
+ # If JWKS returns empty keys and we have an hs256_secret, fall back
98
+ if not jwks.get("keys") and self.hs256_secret:
99
+ try:
100
+ payload = jwt.decode(
101
+ token,
102
+ self.hs256_secret,
103
+ algorithms=["HS256"],
104
+ options={"verify_aud": False},
105
+ )
106
+ return payload
107
+ except ExpiredSignatureError:
108
+ raise ValueError("Token has expired")
109
+ except JWTError as e:
110
+ raise ValueError(f"Token validation failed: {e}") from e
111
+
112
+ try:
113
+ payload = jwt.decode(
114
+ token,
115
+ jwks,
116
+ algorithms=[self.algorithm],
117
+ options={"verify_aud": False},
118
+ )
119
+ return payload
120
+ except ExpiredSignatureError:
121
+ raise ValueError("Token has expired")
122
+ except JWTError as e:
123
+ raise ValueError(f"Token validation failed: {e}") from e
@@ -0,0 +1,22 @@
1
+ from dataclasses import dataclass, field
2
+
3
+
4
+ @dataclass
5
+ class Entitlement:
6
+ tool_id: str
7
+ expires_at: int # unix timestamp
8
+
9
+
10
+ @dataclass
11
+ class TokenPayload:
12
+ sub: str
13
+ exp: int
14
+ iat: int
15
+ jti: str
16
+ entitlements: list[Entitlement] = field(default_factory=list)
17
+
18
+ def has_tool_access(self, tool_id: str) -> bool:
19
+ import time
20
+
21
+ now = int(time.time())
22
+ return any((e.tool_id == str(tool_id) and e.expires_at > now) for e in self.entitlements)
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: noesis-auth
3
+ Version: 0.1.0
4
+ Summary: Python Auth SDK for AI tool integration with the Noesis AIToolCenter platform
5
+ Author: Noesis AI Technologies
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Noesis-AI-Technologies/AIToolCenter
8
+ Project-URL: Documentation, https://github.com/Noesis-AI-Technologies/AIToolCenter/blob/main/docs/auth-sdk-guide.md
9
+ Project-URL: Repository, https://github.com/Noesis-AI-Technologies/AIToolCenter
10
+ Project-URL: Issues, https://github.com/Noesis-AI-Technologies/AIToolCenter/issues
11
+ Keywords: auth,oauth2,jwt,sdk,ai-tools
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Security
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: httpx>=0.24.0
23
+ Requires-Dist: python-jose[cryptography]>=3.3.0
24
+ Provides-Extra: fastapi
25
+ Requires-Dist: fastapi>=0.100.0; extra == "fastapi"
26
+
27
+ # noesis-auth (Python)
28
+
29
+ Python Auth SDK for AI tool integration with the [Noesis AIToolCenter](https://github.com/Noesis-AI-Technologies/AIToolCenter) platform.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install noesis-auth
35
+
36
+ # With FastAPI middleware support:
37
+ pip install noesis-auth[fastapi]
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ### JWT Validation (Tool-side)
43
+
44
+ ```python
45
+ from auth_sdk import JWTValidator
46
+
47
+ validator = JWTValidator(
48
+ jwks_url="https://your-platform.com/.well-known/jwks.json"
49
+ )
50
+
51
+ payload = await validator.validate(token)
52
+ print(payload["sub"]) # user ID
53
+ ```
54
+
55
+ ### FastAPI Middleware
56
+
57
+ ```python
58
+ from fastapi import Depends, FastAPI
59
+ from auth_sdk import AuthMiddleware, TokenPayload
60
+
61
+ app = FastAPI()
62
+ auth = AuthMiddleware(jwks_url="https://your-platform.com/.well-known/jwks.json")
63
+
64
+ @app.get("/api/generate")
65
+ async def generate(payload: TokenPayload = Depends(auth.require_tool("your-tool-id"))):
66
+ user_id = payload.sub
67
+ # ... your tool logic
68
+ ```
69
+
70
+ ### OAuth2 Client (PKCE)
71
+
72
+ ```python
73
+ from auth_sdk import AuthClient
74
+
75
+ client = AuthClient(base_url="https://your-platform.com")
76
+
77
+ # Generate PKCE pair
78
+ pkce = AuthClient.generate_pkce()
79
+
80
+ # Build authorization URL
81
+ url = client.build_authorize_url(
82
+ client_id="your-client-id",
83
+ redirect_uri="http://localhost:3000/callback",
84
+ code_challenge=pkce.code_challenge,
85
+ )
86
+
87
+ # Exchange code for tokens
88
+ tokens = await client.exchange_code(
89
+ code=auth_code,
90
+ redirect_uri="http://localhost:3000/callback",
91
+ client_id="your-client-id",
92
+ code_verifier=pkce.code_verifier,
93
+ )
94
+ ```
95
+
96
+ ### Activation Code Redemption
97
+
98
+ ```python
99
+ result = await client.redeem_code(user_token="...", code="AXKF-M3PQ-7RBN-W2YT")
100
+ print(result.tool_id, result.expires_at)
101
+ ```
102
+
103
+ ## Features
104
+
105
+ - **JWT Validation** — RS256 (JWKS) and HS256 (shared secret) with auto-detection
106
+ - **FastAPI Middleware** — Drop-in authentication and tool access verification
107
+ - **OAuth2 Client** — Authorization URL builder, code exchange, token refresh
108
+ - **PKCE Support** — S256 code challenge generation for public clients
109
+ - **Token Introspection** — Remote token validation endpoint
110
+ - **Activation Codes** — Redeem activation codes for tool entitlements
111
+ - **JWKS Caching** — 6-hour cache with stale-while-revalidate and retry on failure
112
+
113
+ ## Requirements
114
+
115
+ - Python >= 3.11
116
+ - `httpx` >= 0.24
117
+ - `python-jose[cryptography]` >= 3.3
118
+ - `fastapi` >= 0.100 (optional, for middleware)
119
+
120
+ ## License
121
+
122
+ MIT
@@ -0,0 +1,19 @@
1
+ README.md
2
+ __init__.py
3
+ client.py
4
+ fastapi_middleware.py
5
+ jwt_validator.py
6
+ models.py
7
+ py.typed
8
+ pyproject.toml
9
+ ./__init__.py
10
+ ./client.py
11
+ ./fastapi_middleware.py
12
+ ./jwt_validator.py
13
+ ./models.py
14
+ ./py.typed
15
+ noesis_auth.egg-info/PKG-INFO
16
+ noesis_auth.egg-info/SOURCES.txt
17
+ noesis_auth.egg-info/dependency_links.txt
18
+ noesis_auth.egg-info/requires.txt
19
+ noesis_auth.egg-info/top_level.txt
@@ -0,0 +1,5 @@
1
+ httpx>=0.24.0
2
+ python-jose[cryptography]>=3.3.0
3
+
4
+ [fastapi]
5
+ fastapi>=0.100.0
@@ -0,0 +1 @@
1
+ auth_sdk
File without changes
@@ -0,0 +1,46 @@
1
+ [project]
2
+ name = "noesis-auth"
3
+ version = "0.1.0"
4
+ description = "Python Auth SDK for AI tool integration with the Noesis AIToolCenter platform"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.11"
8
+ authors = [
9
+ { name = "Noesis AI Technologies" },
10
+ ]
11
+ keywords = ["auth", "oauth2", "jwt", "sdk", "ai-tools"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Developers",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Topic :: Security",
19
+ "Topic :: Software Development :: Libraries :: Python Modules",
20
+ "Typing :: Typed",
21
+ ]
22
+ dependencies = [
23
+ "httpx>=0.24.0",
24
+ "python-jose[cryptography]>=3.3.0",
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ fastapi = [
29
+ "fastapi>=0.100.0",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/Noesis-AI-Technologies/AIToolCenter"
34
+ Documentation = "https://github.com/Noesis-AI-Technologies/AIToolCenter/blob/main/docs/auth-sdk-guide.md"
35
+ Repository = "https://github.com/Noesis-AI-Technologies/AIToolCenter"
36
+ Issues = "https://github.com/Noesis-AI-Technologies/AIToolCenter/issues"
37
+
38
+ [build-system]
39
+ requires = ["setuptools>=68.0"]
40
+ build-backend = "setuptools.build_meta"
41
+
42
+ [tool.setuptools]
43
+ packages = ["auth_sdk"]
44
+
45
+ [tool.setuptools.package-dir]
46
+ auth_sdk = "."
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+