authgent 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.
- authgent-0.1.0/.gitignore +13 -0
- authgent-0.1.0/PKG-INFO +308 -0
- authgent-0.1.0/README.md +274 -0
- authgent-0.1.0/authgent/__init__.py +32 -0
- authgent-0.1.0/authgent/adapters/__init__.py +1 -0
- authgent-0.1.0/authgent/adapters/langchain.py +223 -0
- authgent-0.1.0/authgent/adapters/mcp.py +40 -0
- authgent-0.1.0/authgent/adapters/protected_resource.py +92 -0
- authgent-0.1.0/authgent/client.py +329 -0
- authgent-0.1.0/authgent/delegation.py +50 -0
- authgent-0.1.0/authgent/dpop.py +193 -0
- authgent-0.1.0/authgent/errors.py +43 -0
- authgent-0.1.0/authgent/jwks.py +63 -0
- authgent-0.1.0/authgent/middleware/__init__.py +1 -0
- authgent-0.1.0/authgent/middleware/fastapi.py +141 -0
- authgent-0.1.0/authgent/middleware/flask.py +106 -0
- authgent-0.1.0/authgent/middleware/scope_challenge.py +220 -0
- authgent-0.1.0/authgent/models.py +98 -0
- authgent-0.1.0/authgent/py.typed +0 -0
- authgent-0.1.0/authgent/verify.py +105 -0
- authgent-0.1.0/pyproject.toml +53 -0
- authgent-0.1.0/tests/__init__.py +0 -0
- authgent-0.1.0/tests/test_client.py +284 -0
- authgent-0.1.0/tests/test_delegation.py +70 -0
- authgent-0.1.0/tests/test_dpop.py +128 -0
- authgent-0.1.0/tests/test_errors.py +32 -0
- authgent-0.1.0/tests/test_models.py +110 -0
authgent-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: authgent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for authgent — token validation, delegation chains, DPoP, middleware
|
|
5
|
+
Project-URL: Homepage, https://github.com/authgent/authgent
|
|
6
|
+
Project-URL: Documentation, https://github.com/authgent/authgent/tree/main/sdks/python
|
|
7
|
+
Project-URL: Repository, https://github.com/authgent/authgent
|
|
8
|
+
Project-URL: Changelog, https://github.com/authgent/authgent/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Issues, https://github.com/authgent/authgent/issues
|
|
10
|
+
Author: Dhruv Agnihotri
|
|
11
|
+
License-Expression: Apache-2.0
|
|
12
|
+
Keywords: a2a,agent,ai-agent,auth,delegation,dpop,identity,mcp,oauth
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Security
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: cryptography>=43.0.0
|
|
22
|
+
Requires-Dist: httpx>=0.27.0
|
|
23
|
+
Requires-Dist: pydantic>=2.9.0
|
|
24
|
+
Requires-Dist: pyjwt>=2.9.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8.3.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.6.0; extra == 'dev'
|
|
29
|
+
Provides-Extra: fastapi
|
|
30
|
+
Requires-Dist: fastapi>=0.115.0; extra == 'fastapi'
|
|
31
|
+
Provides-Extra: flask
|
|
32
|
+
Requires-Dist: flask>=3.0.0; extra == 'flask'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# authgent — Python SDK
|
|
36
|
+
|
|
37
|
+
The open-source identity SDK for AI agents. Token verification, delegation chain validation, DPoP sender-constrained tokens, and middleware for FastAPI and Flask.
|
|
38
|
+
|
|
39
|
+
[](https://pypi.org/project/authgent/)
|
|
40
|
+
[](https://python.org)
|
|
41
|
+
[](../../LICENSE)
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install authgent
|
|
47
|
+
|
|
48
|
+
# With FastAPI middleware
|
|
49
|
+
pip install authgent[fastapi]
|
|
50
|
+
|
|
51
|
+
# With Flask middleware
|
|
52
|
+
pip install authgent[flask]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Quick Start
|
|
56
|
+
|
|
57
|
+
### Verify a Token
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from authgent import verify_token
|
|
61
|
+
|
|
62
|
+
identity = await verify_token(
|
|
63
|
+
token="eyJ...",
|
|
64
|
+
issuer="http://localhost:8000",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
print(identity.subject) # "client:agnt_xxx"
|
|
68
|
+
print(identity.scopes) # ["search:execute"]
|
|
69
|
+
print(identity.delegation_chain) # DelegationChain(depth=0)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Validate Delegation Chains
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from authgent import verify_token, verify_delegation_chain
|
|
76
|
+
|
|
77
|
+
identity = await verify_token(token=token, issuer="http://localhost:8000")
|
|
78
|
+
|
|
79
|
+
# Enforce: max 3 hops, must originate from a human
|
|
80
|
+
verify_delegation_chain(
|
|
81
|
+
identity.delegation_chain,
|
|
82
|
+
max_depth=3,
|
|
83
|
+
require_human_root=True,
|
|
84
|
+
allowed_actors=["client:agnt_trusted_orchestrator"],
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Server API Client
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from authgent import AgentAuthClient
|
|
92
|
+
|
|
93
|
+
client = AgentAuthClient("http://localhost:8000")
|
|
94
|
+
|
|
95
|
+
# Register an agent
|
|
96
|
+
agent = await client.register_agent(
|
|
97
|
+
name="search-bot",
|
|
98
|
+
scopes=["search:execute"],
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Get a token
|
|
102
|
+
token = await client.get_token(
|
|
103
|
+
client_id=agent.client_id,
|
|
104
|
+
client_secret=agent.client_secret,
|
|
105
|
+
scope="search:execute",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Delegate to another agent (token exchange)
|
|
109
|
+
delegated = await client.exchange_token(
|
|
110
|
+
subject_token=token.access_token,
|
|
111
|
+
audience="https://downstream-api.example.com",
|
|
112
|
+
scopes=["read"],
|
|
113
|
+
client_id=downstream_agent.client_id,
|
|
114
|
+
client_secret=downstream_agent.client_secret,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Introspect a token
|
|
118
|
+
info = await client.introspect_token(delegated.access_token)
|
|
119
|
+
print(info["active"]) # True
|
|
120
|
+
print(info["act"]) # {"sub": "client:agnt_search", "act": {"sub": "user:123"}}
|
|
121
|
+
|
|
122
|
+
# Revoke a token
|
|
123
|
+
await client.revoke_token(delegated.access_token)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### DPoP (Sender-Constrained Tokens)
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from authgent import DPoPClient
|
|
130
|
+
|
|
131
|
+
# Create an ephemeral key pair
|
|
132
|
+
dpop = DPoPClient.create()
|
|
133
|
+
print(dpop.jkt) # JWK thumbprint for cnf binding
|
|
134
|
+
|
|
135
|
+
# Generate proof for a request
|
|
136
|
+
proof = dpop.create_proof(
|
|
137
|
+
method="POST",
|
|
138
|
+
url="https://api.example.com/data",
|
|
139
|
+
access_token="eyJ...",
|
|
140
|
+
)
|
|
141
|
+
# Send as DPoP header alongside the access token
|
|
142
|
+
|
|
143
|
+
# Verify an incoming DPoP proof
|
|
144
|
+
from authgent import verify_dpop_proof
|
|
145
|
+
|
|
146
|
+
result = verify_dpop_proof(
|
|
147
|
+
proof_jwt=request.headers["DPoP"],
|
|
148
|
+
access_token=token,
|
|
149
|
+
http_method="POST",
|
|
150
|
+
http_uri="https://api.example.com/data",
|
|
151
|
+
)
|
|
152
|
+
print(result["jkt"]) # JWK thumbprint — must match token's cnf.jkt
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Middleware
|
|
156
|
+
|
|
157
|
+
### FastAPI
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from fastapi import FastAPI, Depends, Request
|
|
161
|
+
from authgent.middleware.fastapi import AgentAuthMiddleware, get_agent_identity
|
|
162
|
+
|
|
163
|
+
app = FastAPI()
|
|
164
|
+
app.add_middleware(AgentAuthMiddleware, issuer="http://localhost:8000")
|
|
165
|
+
|
|
166
|
+
@app.post("/tools/search")
|
|
167
|
+
async def search(request: Request):
|
|
168
|
+
identity = get_agent_identity(request)
|
|
169
|
+
print(f"Agent: {identity.subject}")
|
|
170
|
+
print(f"Scopes: {identity.scopes}")
|
|
171
|
+
print(f"Delegation depth: {identity.delegation_chain.depth}")
|
|
172
|
+
return {"results": [...]}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Flask
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from flask import Flask, g
|
|
179
|
+
from authgent.middleware.flask import agent_auth_required
|
|
180
|
+
|
|
181
|
+
app = Flask(__name__)
|
|
182
|
+
|
|
183
|
+
@app.route("/tools/search", methods=["POST"])
|
|
184
|
+
@agent_auth_required(issuer="http://localhost:8000", scopes=["search:execute"])
|
|
185
|
+
def search():
|
|
186
|
+
identity = g.agent_identity
|
|
187
|
+
return {"agent": identity.subject}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### MCP Scope Challenge
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from authgent.middleware.scope_challenge import ScopeChallengeHandler
|
|
194
|
+
|
|
195
|
+
handler = ScopeChallengeHandler(
|
|
196
|
+
server_url="http://localhost:8000",
|
|
197
|
+
client_id="agnt_xxx",
|
|
198
|
+
client_secret="sec_xxx",
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Automatically detect 403 scope challenges and trigger step-up
|
|
202
|
+
result = await handler.handle_scope_challenge(
|
|
203
|
+
response=http_response,
|
|
204
|
+
action="access_pii",
|
|
205
|
+
scope="data:pii:read",
|
|
206
|
+
)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Adapters
|
|
210
|
+
|
|
211
|
+
### MCP Server
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
from authgent.adapters.mcp import MCPAuthProvider
|
|
215
|
+
|
|
216
|
+
auth = MCPAuthProvider(server_url="http://localhost:8000")
|
|
217
|
+
identity = await auth.verify(token)
|
|
218
|
+
|
|
219
|
+
# Discovery URLs for MCP clients
|
|
220
|
+
auth.metadata_url # http://localhost:8000/.well-known/oauth-authorization-server
|
|
221
|
+
auth.jwks_url # http://localhost:8000/.well-known/jwks.json
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Protected Resource Metadata (RFC 9728)
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
from authgent.adapters.protected_resource import ProtectedResourceMetadata
|
|
228
|
+
|
|
229
|
+
metadata = ProtectedResourceMetadata(
|
|
230
|
+
resource="https://mcp-server.example.com",
|
|
231
|
+
authorization_servers=["http://localhost:8000"],
|
|
232
|
+
scopes_supported=["tools:execute", "db:read"],
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Serve at /.well-known/oauth-protected-resource
|
|
236
|
+
@app.get("/.well-known/oauth-protected-resource")
|
|
237
|
+
async def resource_metadata():
|
|
238
|
+
return metadata.to_dict()
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Error Handling
|
|
242
|
+
|
|
243
|
+
All SDK errors extend `AuthgentError`:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
from authgent import (
|
|
247
|
+
verify_token,
|
|
248
|
+
AuthgentError,
|
|
249
|
+
InvalidTokenError,
|
|
250
|
+
DelegationError,
|
|
251
|
+
DPoPError,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
identity = await verify_token(token=token, issuer=issuer)
|
|
256
|
+
except InvalidTokenError as e:
|
|
257
|
+
# Token expired, wrong issuer, bad signature
|
|
258
|
+
print(f"Invalid token: {e}")
|
|
259
|
+
except DelegationError as e:
|
|
260
|
+
# Chain too deep, unauthorized actor, scope escalation
|
|
261
|
+
print(f"Delegation violation: {e}")
|
|
262
|
+
except DPoPError as e:
|
|
263
|
+
# Proof mismatch, expired, wrong binding
|
|
264
|
+
print(f"DPoP error: {e}")
|
|
265
|
+
except AuthgentError as e:
|
|
266
|
+
# Any other SDK error
|
|
267
|
+
print(f"Auth error: {e}")
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## API Reference
|
|
271
|
+
|
|
272
|
+
### Core Functions
|
|
273
|
+
|
|
274
|
+
| Function | Description |
|
|
275
|
+
|:---------|:------------|
|
|
276
|
+
| `verify_token(token, issuer)` | Verify JWT against issuer's JWKS, return `AgentIdentity` |
|
|
277
|
+
| `verify_delegation_chain(chain, ...)` | Enforce depth, actors, human root policies |
|
|
278
|
+
| `verify_dpop_proof(proof_jwt, ...)` | Verify DPoP proof-of-possession |
|
|
279
|
+
| `DPoPClient.create()` | Create ephemeral DPoP proof generator |
|
|
280
|
+
| `AgentAuthClient(url)` | Full server API client |
|
|
281
|
+
|
|
282
|
+
### Models
|
|
283
|
+
|
|
284
|
+
| Class | Fields |
|
|
285
|
+
|:------|:-------|
|
|
286
|
+
| `AgentIdentity` | `subject`, `scopes`, `claims`, `delegation_chain` |
|
|
287
|
+
| `DelegationChain` | `depth`, `actors`, `human_root` |
|
|
288
|
+
| `TokenClaims` | `sub`, `scope`, `iss`, `exp`, `iat`, `jti`, `act`, `cnf` |
|
|
289
|
+
|
|
290
|
+
### Middleware
|
|
291
|
+
|
|
292
|
+
| Import | Framework |
|
|
293
|
+
|:-------|:----------|
|
|
294
|
+
| `authgent.middleware.fastapi` | FastAPI (ASGI) |
|
|
295
|
+
| `authgent.middleware.flask` | Flask (WSGI) |
|
|
296
|
+
| `authgent.middleware.scope_challenge` | MCP scope challenge handler |
|
|
297
|
+
|
|
298
|
+
### Adapters
|
|
299
|
+
|
|
300
|
+
| Import | Purpose |
|
|
301
|
+
|:-------|:--------|
|
|
302
|
+
| `authgent.adapters.mcp` | MCP server auth provider |
|
|
303
|
+
| `authgent.adapters.protected_resource` | RFC 9728 metadata |
|
|
304
|
+
| `authgent.adapters.langchain` | LangChain tool auth |
|
|
305
|
+
|
|
306
|
+
## License
|
|
307
|
+
|
|
308
|
+
[Apache 2.0](../../LICENSE)
|
authgent-0.1.0/README.md
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# authgent — Python SDK
|
|
2
|
+
|
|
3
|
+
The open-source identity SDK for AI agents. Token verification, delegation chain validation, DPoP sender-constrained tokens, and middleware for FastAPI and Flask.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/authgent/)
|
|
6
|
+
[](https://python.org)
|
|
7
|
+
[](../../LICENSE)
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install authgent
|
|
13
|
+
|
|
14
|
+
# With FastAPI middleware
|
|
15
|
+
pip install authgent[fastapi]
|
|
16
|
+
|
|
17
|
+
# With Flask middleware
|
|
18
|
+
pip install authgent[flask]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### Verify a Token
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from authgent import verify_token
|
|
27
|
+
|
|
28
|
+
identity = await verify_token(
|
|
29
|
+
token="eyJ...",
|
|
30
|
+
issuer="http://localhost:8000",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
print(identity.subject) # "client:agnt_xxx"
|
|
34
|
+
print(identity.scopes) # ["search:execute"]
|
|
35
|
+
print(identity.delegation_chain) # DelegationChain(depth=0)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Validate Delegation Chains
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from authgent import verify_token, verify_delegation_chain
|
|
42
|
+
|
|
43
|
+
identity = await verify_token(token=token, issuer="http://localhost:8000")
|
|
44
|
+
|
|
45
|
+
# Enforce: max 3 hops, must originate from a human
|
|
46
|
+
verify_delegation_chain(
|
|
47
|
+
identity.delegation_chain,
|
|
48
|
+
max_depth=3,
|
|
49
|
+
require_human_root=True,
|
|
50
|
+
allowed_actors=["client:agnt_trusted_orchestrator"],
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Server API Client
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from authgent import AgentAuthClient
|
|
58
|
+
|
|
59
|
+
client = AgentAuthClient("http://localhost:8000")
|
|
60
|
+
|
|
61
|
+
# Register an agent
|
|
62
|
+
agent = await client.register_agent(
|
|
63
|
+
name="search-bot",
|
|
64
|
+
scopes=["search:execute"],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Get a token
|
|
68
|
+
token = await client.get_token(
|
|
69
|
+
client_id=agent.client_id,
|
|
70
|
+
client_secret=agent.client_secret,
|
|
71
|
+
scope="search:execute",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Delegate to another agent (token exchange)
|
|
75
|
+
delegated = await client.exchange_token(
|
|
76
|
+
subject_token=token.access_token,
|
|
77
|
+
audience="https://downstream-api.example.com",
|
|
78
|
+
scopes=["read"],
|
|
79
|
+
client_id=downstream_agent.client_id,
|
|
80
|
+
client_secret=downstream_agent.client_secret,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Introspect a token
|
|
84
|
+
info = await client.introspect_token(delegated.access_token)
|
|
85
|
+
print(info["active"]) # True
|
|
86
|
+
print(info["act"]) # {"sub": "client:agnt_search", "act": {"sub": "user:123"}}
|
|
87
|
+
|
|
88
|
+
# Revoke a token
|
|
89
|
+
await client.revoke_token(delegated.access_token)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### DPoP (Sender-Constrained Tokens)
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from authgent import DPoPClient
|
|
96
|
+
|
|
97
|
+
# Create an ephemeral key pair
|
|
98
|
+
dpop = DPoPClient.create()
|
|
99
|
+
print(dpop.jkt) # JWK thumbprint for cnf binding
|
|
100
|
+
|
|
101
|
+
# Generate proof for a request
|
|
102
|
+
proof = dpop.create_proof(
|
|
103
|
+
method="POST",
|
|
104
|
+
url="https://api.example.com/data",
|
|
105
|
+
access_token="eyJ...",
|
|
106
|
+
)
|
|
107
|
+
# Send as DPoP header alongside the access token
|
|
108
|
+
|
|
109
|
+
# Verify an incoming DPoP proof
|
|
110
|
+
from authgent import verify_dpop_proof
|
|
111
|
+
|
|
112
|
+
result = verify_dpop_proof(
|
|
113
|
+
proof_jwt=request.headers["DPoP"],
|
|
114
|
+
access_token=token,
|
|
115
|
+
http_method="POST",
|
|
116
|
+
http_uri="https://api.example.com/data",
|
|
117
|
+
)
|
|
118
|
+
print(result["jkt"]) # JWK thumbprint — must match token's cnf.jkt
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Middleware
|
|
122
|
+
|
|
123
|
+
### FastAPI
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from fastapi import FastAPI, Depends, Request
|
|
127
|
+
from authgent.middleware.fastapi import AgentAuthMiddleware, get_agent_identity
|
|
128
|
+
|
|
129
|
+
app = FastAPI()
|
|
130
|
+
app.add_middleware(AgentAuthMiddleware, issuer="http://localhost:8000")
|
|
131
|
+
|
|
132
|
+
@app.post("/tools/search")
|
|
133
|
+
async def search(request: Request):
|
|
134
|
+
identity = get_agent_identity(request)
|
|
135
|
+
print(f"Agent: {identity.subject}")
|
|
136
|
+
print(f"Scopes: {identity.scopes}")
|
|
137
|
+
print(f"Delegation depth: {identity.delegation_chain.depth}")
|
|
138
|
+
return {"results": [...]}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Flask
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from flask import Flask, g
|
|
145
|
+
from authgent.middleware.flask import agent_auth_required
|
|
146
|
+
|
|
147
|
+
app = Flask(__name__)
|
|
148
|
+
|
|
149
|
+
@app.route("/tools/search", methods=["POST"])
|
|
150
|
+
@agent_auth_required(issuer="http://localhost:8000", scopes=["search:execute"])
|
|
151
|
+
def search():
|
|
152
|
+
identity = g.agent_identity
|
|
153
|
+
return {"agent": identity.subject}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### MCP Scope Challenge
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from authgent.middleware.scope_challenge import ScopeChallengeHandler
|
|
160
|
+
|
|
161
|
+
handler = ScopeChallengeHandler(
|
|
162
|
+
server_url="http://localhost:8000",
|
|
163
|
+
client_id="agnt_xxx",
|
|
164
|
+
client_secret="sec_xxx",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Automatically detect 403 scope challenges and trigger step-up
|
|
168
|
+
result = await handler.handle_scope_challenge(
|
|
169
|
+
response=http_response,
|
|
170
|
+
action="access_pii",
|
|
171
|
+
scope="data:pii:read",
|
|
172
|
+
)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Adapters
|
|
176
|
+
|
|
177
|
+
### MCP Server
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from authgent.adapters.mcp import MCPAuthProvider
|
|
181
|
+
|
|
182
|
+
auth = MCPAuthProvider(server_url="http://localhost:8000")
|
|
183
|
+
identity = await auth.verify(token)
|
|
184
|
+
|
|
185
|
+
# Discovery URLs for MCP clients
|
|
186
|
+
auth.metadata_url # http://localhost:8000/.well-known/oauth-authorization-server
|
|
187
|
+
auth.jwks_url # http://localhost:8000/.well-known/jwks.json
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Protected Resource Metadata (RFC 9728)
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from authgent.adapters.protected_resource import ProtectedResourceMetadata
|
|
194
|
+
|
|
195
|
+
metadata = ProtectedResourceMetadata(
|
|
196
|
+
resource="https://mcp-server.example.com",
|
|
197
|
+
authorization_servers=["http://localhost:8000"],
|
|
198
|
+
scopes_supported=["tools:execute", "db:read"],
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Serve at /.well-known/oauth-protected-resource
|
|
202
|
+
@app.get("/.well-known/oauth-protected-resource")
|
|
203
|
+
async def resource_metadata():
|
|
204
|
+
return metadata.to_dict()
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Error Handling
|
|
208
|
+
|
|
209
|
+
All SDK errors extend `AuthgentError`:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
from authgent import (
|
|
213
|
+
verify_token,
|
|
214
|
+
AuthgentError,
|
|
215
|
+
InvalidTokenError,
|
|
216
|
+
DelegationError,
|
|
217
|
+
DPoPError,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
identity = await verify_token(token=token, issuer=issuer)
|
|
222
|
+
except InvalidTokenError as e:
|
|
223
|
+
# Token expired, wrong issuer, bad signature
|
|
224
|
+
print(f"Invalid token: {e}")
|
|
225
|
+
except DelegationError as e:
|
|
226
|
+
# Chain too deep, unauthorized actor, scope escalation
|
|
227
|
+
print(f"Delegation violation: {e}")
|
|
228
|
+
except DPoPError as e:
|
|
229
|
+
# Proof mismatch, expired, wrong binding
|
|
230
|
+
print(f"DPoP error: {e}")
|
|
231
|
+
except AuthgentError as e:
|
|
232
|
+
# Any other SDK error
|
|
233
|
+
print(f"Auth error: {e}")
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## API Reference
|
|
237
|
+
|
|
238
|
+
### Core Functions
|
|
239
|
+
|
|
240
|
+
| Function | Description |
|
|
241
|
+
|:---------|:------------|
|
|
242
|
+
| `verify_token(token, issuer)` | Verify JWT against issuer's JWKS, return `AgentIdentity` |
|
|
243
|
+
| `verify_delegation_chain(chain, ...)` | Enforce depth, actors, human root policies |
|
|
244
|
+
| `verify_dpop_proof(proof_jwt, ...)` | Verify DPoP proof-of-possession |
|
|
245
|
+
| `DPoPClient.create()` | Create ephemeral DPoP proof generator |
|
|
246
|
+
| `AgentAuthClient(url)` | Full server API client |
|
|
247
|
+
|
|
248
|
+
### Models
|
|
249
|
+
|
|
250
|
+
| Class | Fields |
|
|
251
|
+
|:------|:-------|
|
|
252
|
+
| `AgentIdentity` | `subject`, `scopes`, `claims`, `delegation_chain` |
|
|
253
|
+
| `DelegationChain` | `depth`, `actors`, `human_root` |
|
|
254
|
+
| `TokenClaims` | `sub`, `scope`, `iss`, `exp`, `iat`, `jti`, `act`, `cnf` |
|
|
255
|
+
|
|
256
|
+
### Middleware
|
|
257
|
+
|
|
258
|
+
| Import | Framework |
|
|
259
|
+
|:-------|:----------|
|
|
260
|
+
| `authgent.middleware.fastapi` | FastAPI (ASGI) |
|
|
261
|
+
| `authgent.middleware.flask` | Flask (WSGI) |
|
|
262
|
+
| `authgent.middleware.scope_challenge` | MCP scope challenge handler |
|
|
263
|
+
|
|
264
|
+
### Adapters
|
|
265
|
+
|
|
266
|
+
| Import | Purpose |
|
|
267
|
+
|:-------|:--------|
|
|
268
|
+
| `authgent.adapters.mcp` | MCP server auth provider |
|
|
269
|
+
| `authgent.adapters.protected_resource` | RFC 9728 metadata |
|
|
270
|
+
| `authgent.adapters.langchain` | LangChain tool auth |
|
|
271
|
+
|
|
272
|
+
## License
|
|
273
|
+
|
|
274
|
+
[Apache 2.0](../../LICENSE)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""authgent SDK — token validation, delegation chains, DPoP for AI agents."""
|
|
2
|
+
|
|
3
|
+
from authgent.verify import verify_token
|
|
4
|
+
from authgent.delegation import verify_delegation_chain
|
|
5
|
+
from authgent.dpop import verify_dpop_proof, DPoPClient
|
|
6
|
+
from authgent.client import AgentAuthClient
|
|
7
|
+
from authgent.models import AgentIdentity, DelegationChain, TokenClaims
|
|
8
|
+
from authgent.errors import (
|
|
9
|
+
AuthgentError,
|
|
10
|
+
InvalidTokenError,
|
|
11
|
+
DelegationError,
|
|
12
|
+
DPoPError,
|
|
13
|
+
ServerError,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"verify_token",
|
|
20
|
+
"verify_delegation_chain",
|
|
21
|
+
"verify_dpop_proof",
|
|
22
|
+
"DPoPClient",
|
|
23
|
+
"AgentAuthClient",
|
|
24
|
+
"AgentIdentity",
|
|
25
|
+
"DelegationChain",
|
|
26
|
+
"TokenClaims",
|
|
27
|
+
"AuthgentError",
|
|
28
|
+
"InvalidTokenError",
|
|
29
|
+
"DelegationError",
|
|
30
|
+
"DPoPError",
|
|
31
|
+
"ServerError",
|
|
32
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""SDK adapters for MCP and other protocols."""
|