aauth 0.3.2__tar.gz → 0.3.3__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.
- {aauth-0.3.2 → aauth-0.3.3}/PKG-INFO +1 -1
- {aauth-0.3.2 → aauth-0.3.3}/aauth/__init__.py +3 -0
- aauth-0.3.3/aauth/agent/__init__.py +13 -0
- aauth-0.3.3/aauth/agent/token_exchange.py +147 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth.egg-info/PKG-INFO +1 -1
- {aauth-0.3.2 → aauth-0.3.3}/aauth.egg-info/SOURCES.txt +3 -1
- {aauth-0.3.2 → aauth-0.3.3}/pyproject.toml +1 -1
- aauth-0.3.3/tests/test_token_exchange.py +200 -0
- aauth-0.3.2/aauth/agent/__init__.py +0 -2
- {aauth-0.3.2 → aauth-0.3.3}/LICENSE +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/README.md +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/agent/challenge_handler.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/agent/poller.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/agent/signer.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/debug.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/errors.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/headers/__init__.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/headers/aauth_header.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/headers/agent_auth.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/http/__init__.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/http/deferred.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/http/request.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/http/response.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/identifiers.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/keys/__init__.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/keys/jwk.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/keys/jwks.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/keys/keypair.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/metadata/__init__.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/metadata/agent.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/metadata/auth_server.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/metadata/mission_manager.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/metadata/resource.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/resource/__init__.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/resource/challenge_builder.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/resource/token_issuer.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/resource/verifier.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/signing/__init__.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/signing/algorithms.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/signing/signature.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/signing/signature_base.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/signing/signature_input.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/signing/signature_key.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/signing/signer.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/signing/verifier.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/tokens/__init__.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/tokens/agent_token.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/tokens/auth_token.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth/tokens/resource_token.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth.egg-info/dependency_links.txt +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth.egg-info/requires.txt +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/aauth.egg-info/top_level.txt +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/setup.cfg +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/tests/test_phase1.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/tests/test_phase10.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/tests/test_phase11.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/tests/test_phase12.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/tests/test_phase2.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/tests/test_phase3.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/tests/test_phase4.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/tests/test_phase5.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/tests/test_phase6.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/tests/test_phase7.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/tests/test_phase8.py +0 -0
- {aauth-0.3.2 → aauth-0.3.3}/tests/test_phase9.py +0 -0
|
@@ -147,6 +147,7 @@ from .metadata.mission_manager import (
|
|
|
147
147
|
# Agent role
|
|
148
148
|
from .agent.signer import AgentRequestSigner
|
|
149
149
|
from .agent.challenge_handler import ChallengeHandler
|
|
150
|
+
from .agent.token_exchange import exchange_resource_token, extract_resource_token
|
|
150
151
|
|
|
151
152
|
# Resource role
|
|
152
153
|
from .resource.verifier import RequestVerifier
|
|
@@ -278,6 +279,8 @@ __all__ = [
|
|
|
278
279
|
# Agent role
|
|
279
280
|
"AgentRequestSigner",
|
|
280
281
|
"ChallengeHandler",
|
|
282
|
+
"exchange_resource_token",
|
|
283
|
+
"extract_resource_token",
|
|
281
284
|
|
|
282
285
|
# Resource role
|
|
283
286
|
"RequestVerifier",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Agent role implementation for AAuth."""
|
|
2
|
+
|
|
3
|
+
from .signer import AgentRequestSigner
|
|
4
|
+
from .challenge_handler import ChallengeHandler
|
|
5
|
+
from .token_exchange import exchange_resource_token, extract_resource_token
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AgentRequestSigner",
|
|
9
|
+
"ChallengeHandler",
|
|
10
|
+
"exchange_resource_token",
|
|
11
|
+
"extract_resource_token",
|
|
12
|
+
]
|
|
13
|
+
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""AAuth PS token exchange for three-party mode (SPEC §4.1.3)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import httpx
|
|
5
|
+
import jwt as _jwt
|
|
6
|
+
from typing import Optional, Mapping
|
|
7
|
+
|
|
8
|
+
from ..errors import TokenError, MetadataError
|
|
9
|
+
from ..signing.signer import sign_request
|
|
10
|
+
from ..metadata.mission_manager import fetch_ps_metadata_async
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def extract_resource_token(headers: Mapping[str, str]) -> Optional[str]:
|
|
14
|
+
"""Extract resource_token from an AAuth 401 challenge response.
|
|
15
|
+
|
|
16
|
+
Parses the AAuth-Requirement header to find the resource-token parameter
|
|
17
|
+
per SPEC §6. If not found or header is missing, returns None.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
headers: HTTP response headers (dict-like, case-insensitive access).
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
resource_token JWT string if present, else None.
|
|
24
|
+
"""
|
|
25
|
+
from ..headers.aauth_header import get_challenge_header_value, parse_aauth_header
|
|
26
|
+
|
|
27
|
+
raw = get_challenge_header_value(headers)
|
|
28
|
+
if not raw:
|
|
29
|
+
return None
|
|
30
|
+
try:
|
|
31
|
+
parsed = parse_aauth_header(raw)
|
|
32
|
+
return parsed.get("resource_token")
|
|
33
|
+
except Exception:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def exchange_resource_token(
|
|
38
|
+
resource_token: str,
|
|
39
|
+
private_key,
|
|
40
|
+
agent_jwt: str,
|
|
41
|
+
*,
|
|
42
|
+
ps_discovery_timeout: float = 10.0,
|
|
43
|
+
exchange_timeout: float = 30.0,
|
|
44
|
+
) -> str:
|
|
45
|
+
"""Exchange a resource_token for an auth_token via the PS (SPEC §4.1.3).
|
|
46
|
+
|
|
47
|
+
Three-party token exchange flow:
|
|
48
|
+
1. Decode resource_token to get PS URL from ``aud`` claim.
|
|
49
|
+
2. Discover PS token_endpoint via /.well-known/aauth-person.json
|
|
50
|
+
(falls back to {aud}/token if metadata fetch fails).
|
|
51
|
+
3. POST {resource_token} to PS, signed with aa-agent+jwt.
|
|
52
|
+
4. Return auth_token (aa-auth+jwt) from PS response.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
resource_token: The resource token JWT from the 401 AAuth challenge.
|
|
56
|
+
private_key: Agent's Ed25519 private key for request signing.
|
|
57
|
+
agent_jwt: Agent token (aa-agent+jwt) for Signature-Key header.
|
|
58
|
+
ps_discovery_timeout: Timeout in seconds for PS metadata fetch.
|
|
59
|
+
exchange_timeout: Timeout in seconds for PS token endpoint POST.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
auth_token string (aa-auth+jwt) returned by the PS.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
TokenError: resource_token is malformed, PS returns non-200,
|
|
66
|
+
or response missing auth_token.
|
|
67
|
+
MetadataError: PS metadata fetch encounters a hard error
|
|
68
|
+
(non-timeout, non-404 failures).
|
|
69
|
+
"""
|
|
70
|
+
# Step 1: Decode resource_token to get PS URL from aud claim
|
|
71
|
+
try:
|
|
72
|
+
claims = _jwt.decode(resource_token, options={"verify_signature": False})
|
|
73
|
+
except Exception as exc:
|
|
74
|
+
raise TokenError(
|
|
75
|
+
f"Cannot decode resource_token JWT: {exc}",
|
|
76
|
+
token_type="aa-resource+jwt",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
aud = claims.get("aud")
|
|
80
|
+
if not aud:
|
|
81
|
+
raise TokenError(
|
|
82
|
+
"resource_token missing 'aud' claim — cannot locate PS",
|
|
83
|
+
token_type="aa-resource+jwt",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Step 2: Discover PS token_endpoint from metadata
|
|
87
|
+
ps_base = aud.rstrip("/")
|
|
88
|
+
token_endpoint = f"{ps_base}/token" # fallback
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
meta = await fetch_ps_metadata_async(ps_base, timeout=ps_discovery_timeout)
|
|
92
|
+
discovered = meta.get("token_endpoint")
|
|
93
|
+
if discovered:
|
|
94
|
+
token_endpoint = discovered
|
|
95
|
+
except Exception:
|
|
96
|
+
# Metadata fetch failed — use default fallback endpoint
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
# Step 3: Sign and POST resource_token to PS
|
|
100
|
+
body = json.dumps({"resource_token": resource_token}, separators=(",", ":")).encode()
|
|
101
|
+
headers = {"Content-Type": "application/json"}
|
|
102
|
+
|
|
103
|
+
sig_headers = sign_request(
|
|
104
|
+
method="POST",
|
|
105
|
+
target_uri=token_endpoint,
|
|
106
|
+
headers=headers,
|
|
107
|
+
body=None,
|
|
108
|
+
private_key=private_key,
|
|
109
|
+
sig_scheme="jwt",
|
|
110
|
+
jwt=agent_jwt,
|
|
111
|
+
)
|
|
112
|
+
headers.update(sig_headers)
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
async with httpx.AsyncClient(
|
|
116
|
+
timeout=httpx.Timeout(exchange_timeout)
|
|
117
|
+
) as http_client:
|
|
118
|
+
resp = await http_client.post(token_endpoint, headers=headers, content=body)
|
|
119
|
+
except Exception as exc:
|
|
120
|
+
raise TokenError(
|
|
121
|
+
f"PS token_endpoint request failed: {exc}",
|
|
122
|
+
token_type="aa-auth+jwt",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if resp.status_code != 200:
|
|
126
|
+
raise TokenError(
|
|
127
|
+
f"PS token_endpoint returned HTTP {resp.status_code}: {resp.text[:500]}",
|
|
128
|
+
token_type="aa-auth+jwt",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Step 4: Extract and return auth_token
|
|
132
|
+
try:
|
|
133
|
+
data = resp.json()
|
|
134
|
+
except Exception as exc:
|
|
135
|
+
raise TokenError(
|
|
136
|
+
f"PS response is not valid JSON: {exc}",
|
|
137
|
+
token_type="aa-auth+jwt",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
auth_token = data.get("auth_token")
|
|
141
|
+
if not auth_token:
|
|
142
|
+
raise TokenError(
|
|
143
|
+
f"PS response missing 'auth_token'; response keys: {list(data.keys())}",
|
|
144
|
+
token_type="aa-auth+jwt",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return auth_token
|
|
@@ -14,6 +14,7 @@ aauth/agent/__init__.py
|
|
|
14
14
|
aauth/agent/challenge_handler.py
|
|
15
15
|
aauth/agent/poller.py
|
|
16
16
|
aauth/agent/signer.py
|
|
17
|
+
aauth/agent/token_exchange.py
|
|
17
18
|
aauth/headers/__init__.py
|
|
18
19
|
aauth/headers/aauth_header.py
|
|
19
20
|
aauth/headers/agent_auth.py
|
|
@@ -57,4 +58,5 @@ tests/test_phase5.py
|
|
|
57
58
|
tests/test_phase6.py
|
|
58
59
|
tests/test_phase7.py
|
|
59
60
|
tests/test_phase8.py
|
|
60
|
-
tests/test_phase9.py
|
|
61
|
+
tests/test_phase9.py
|
|
62
|
+
tests/test_token_exchange.py
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Tests for PS token exchange (three-party mode, SPEC §4.1.3)."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from typing import Dict, Any
|
|
8
|
+
from urllib.parse import urlparse
|
|
9
|
+
|
|
10
|
+
from aauth.agent.token_exchange import extract_resource_token, exchange_resource_token
|
|
11
|
+
from aauth.tokens.resource_token import create_resource_token
|
|
12
|
+
from aauth.tokens.agent_token import create_agent_token
|
|
13
|
+
from aauth.keys.keypair import generate_ed25519_keypair
|
|
14
|
+
from aauth.keys.jwk import public_key_to_jwk, calculate_jwk_thumbprint
|
|
15
|
+
from aauth.errors import TokenError, MetadataError
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestExtractResourceToken:
|
|
20
|
+
"""Tests for extract_resource_token helper."""
|
|
21
|
+
|
|
22
|
+
def test_extract_from_valid_challenge_header(self):
|
|
23
|
+
"""Extract resource_token from a valid AAuth challenge header."""
|
|
24
|
+
test_token = "eyJhbGciOiJFZERTQSJ9.eyJhdWQiOiJodHRwczovL2V4YW1wbGUifQ.sig"
|
|
25
|
+
headers = {
|
|
26
|
+
"AAuth-Requirement": f'requirement=auth-token; resource-token="{test_token}"'
|
|
27
|
+
}
|
|
28
|
+
result = extract_resource_token(headers)
|
|
29
|
+
assert result == test_token
|
|
30
|
+
|
|
31
|
+
def test_extract_from_case_insensitive_header(self):
|
|
32
|
+
"""extract_resource_token is case-insensitive for header names."""
|
|
33
|
+
test_token = "eyJhbGciOiJFZERTQSJ9.eyJhdWQiOiJodHRwczovL2V4YW1wbGUifQ.sig"
|
|
34
|
+
headers = {
|
|
35
|
+
"aauth-requirement": f'requirement=auth-token; resource-token="{test_token}"'
|
|
36
|
+
}
|
|
37
|
+
result = extract_resource_token(headers)
|
|
38
|
+
assert result == test_token
|
|
39
|
+
|
|
40
|
+
def test_extract_from_missing_header(self):
|
|
41
|
+
"""extract_resource_token returns None when no AAuth header present."""
|
|
42
|
+
headers = {"Content-Type": "application/json"}
|
|
43
|
+
result = extract_resource_token(headers)
|
|
44
|
+
assert result is None
|
|
45
|
+
|
|
46
|
+
def test_extract_from_header_without_resource_token_param(self):
|
|
47
|
+
"""extract_resource_token returns None when resource-token param missing."""
|
|
48
|
+
headers = {"AAuth-Requirement": "requirement=identity"}
|
|
49
|
+
result = extract_resource_token(headers)
|
|
50
|
+
assert result is None
|
|
51
|
+
|
|
52
|
+
def test_extract_handles_malformed_header_gracefully(self):
|
|
53
|
+
"""extract_resource_token returns None on parse errors."""
|
|
54
|
+
headers = {"AAuth-Requirement": "garbage_header_content"}
|
|
55
|
+
result = extract_resource_token(headers)
|
|
56
|
+
assert result is None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TestExchangeResourceToken:
|
|
60
|
+
"""Tests for exchange_resource_token function."""
|
|
61
|
+
|
|
62
|
+
@pytest.fixture
|
|
63
|
+
def agent_keys(self):
|
|
64
|
+
"""Generate test agent keys."""
|
|
65
|
+
agent_priv, agent_pub = generate_ed25519_keypair()
|
|
66
|
+
agent_jwk = public_key_to_jwk(agent_pub)
|
|
67
|
+
agent_jkt = calculate_jwk_thumbprint(agent_jwk)
|
|
68
|
+
return agent_priv, agent_pub, agent_jkt
|
|
69
|
+
|
|
70
|
+
def test_exchange_resource_token_missing_aud(self, agent_keys):
|
|
71
|
+
"""Raises TokenError if resource_token missing aud claim."""
|
|
72
|
+
import jwt
|
|
73
|
+
|
|
74
|
+
agent_priv, _, _ = agent_keys
|
|
75
|
+
|
|
76
|
+
# Create JWT without aud claim
|
|
77
|
+
bad_token = jwt.encode(
|
|
78
|
+
{"iss": "https://ps.example", "agent": "https://agent.example"},
|
|
79
|
+
agent_priv,
|
|
80
|
+
algorithm="EdDSA",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
async def run_test():
|
|
84
|
+
with pytest.raises(TokenError, match="missing 'aud' claim"):
|
|
85
|
+
await exchange_resource_token(
|
|
86
|
+
resource_token=bad_token,
|
|
87
|
+
private_key=agent_priv,
|
|
88
|
+
agent_jwt="dummy-jwt",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
asyncio.run(run_test())
|
|
92
|
+
|
|
93
|
+
def test_exchange_resource_token_malformed(self, agent_keys):
|
|
94
|
+
"""Raises TokenError if resource_token is not valid JWT."""
|
|
95
|
+
agent_priv, _, _ = agent_keys
|
|
96
|
+
|
|
97
|
+
async def run_test():
|
|
98
|
+
with pytest.raises(TokenError, match="Cannot decode resource_token"):
|
|
99
|
+
await exchange_resource_token(
|
|
100
|
+
resource_token="not-a-jwt",
|
|
101
|
+
private_key=agent_priv,
|
|
102
|
+
agent_jwt="dummy-jwt",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
asyncio.run(run_test())
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@pytest.mark.integration
|
|
109
|
+
class TestTokenExchangeIntegration:
|
|
110
|
+
"""Integration tests using a real PS-like endpoint."""
|
|
111
|
+
|
|
112
|
+
def test_exchange_against_real_ps_server(self):
|
|
113
|
+
"""End-to-end test with a real PS-like server."""
|
|
114
|
+
# Import here to avoid hard dependency unless running integration tests
|
|
115
|
+
from starlette.applications import Starlette
|
|
116
|
+
from starlette.responses import JSONResponse
|
|
117
|
+
import threading
|
|
118
|
+
import uvicorn
|
|
119
|
+
import time
|
|
120
|
+
|
|
121
|
+
# Generate test keys
|
|
122
|
+
agent_priv, agent_pub = generate_ed25519_keypair()
|
|
123
|
+
agent_jwk = public_key_to_jwk(agent_pub)
|
|
124
|
+
agent_jkt = calculate_jwk_thumbprint(agent_jwk)
|
|
125
|
+
|
|
126
|
+
ps_priv, ps_pub = generate_ed25519_keypair()
|
|
127
|
+
|
|
128
|
+
# Create a simple Starlette app that mimics PS behavior
|
|
129
|
+
app = Starlette()
|
|
130
|
+
|
|
131
|
+
@app.route("/.well-known/aauth-person.json", methods=["GET"])
|
|
132
|
+
async def ps_metadata(request):
|
|
133
|
+
return JSONResponse(
|
|
134
|
+
{
|
|
135
|
+
"issuer": "http://127.0.0.1:8766",
|
|
136
|
+
"token_endpoint": "http://127.0.0.1:8766/token",
|
|
137
|
+
"jwks_uri": "http://127.0.0.1:8766/.well-known/jwks.json",
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
@app.route("/token", methods=["POST"])
|
|
142
|
+
async def token_endpoint(request):
|
|
143
|
+
body = await request.json()
|
|
144
|
+
# Just echo back a fake auth token
|
|
145
|
+
auth_token = "eyJhbGciOiJFZERTQSIsInR5cCI6ImFhLWF1dGhydHQifQ.eyJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjg3NjYiLCJhdWQiOiJodHRwczovL3Jlc291cmNlLmV4YW1wbGUiLCJhZ2VudCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZSJ9.sig"
|
|
146
|
+
return JSONResponse({"auth_token": auth_token})
|
|
147
|
+
|
|
148
|
+
# Run server in background thread
|
|
149
|
+
server_thread = threading.Thread(
|
|
150
|
+
target=lambda: uvicorn.run(
|
|
151
|
+
app,
|
|
152
|
+
host="127.0.0.1",
|
|
153
|
+
port=8766,
|
|
154
|
+
log_level="critical",
|
|
155
|
+
),
|
|
156
|
+
daemon=True,
|
|
157
|
+
)
|
|
158
|
+
server_thread.start()
|
|
159
|
+
time.sleep(0.5) # Give server time to start
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
# Create resource token pointing to our test PS
|
|
163
|
+
resource_token = create_resource_token(
|
|
164
|
+
iss="http://127.0.0.1:8766",
|
|
165
|
+
aud="http://127.0.0.1:8766",
|
|
166
|
+
agent="https://agent.example",
|
|
167
|
+
agent_jkt=agent_jkt,
|
|
168
|
+
scope="data.read",
|
|
169
|
+
private_key=ps_priv,
|
|
170
|
+
kid="ps-key-1",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Create agent JWT using correct signature
|
|
174
|
+
agent_jwt = create_agent_token(
|
|
175
|
+
iss="https://agent.example",
|
|
176
|
+
sub="https://agent.example",
|
|
177
|
+
cnf_jwk=agent_jwk,
|
|
178
|
+
private_key=agent_priv,
|
|
179
|
+
kid="agent-key-1",
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Perform the exchange
|
|
183
|
+
async def run_exchange():
|
|
184
|
+
return await exchange_resource_token(
|
|
185
|
+
resource_token=resource_token,
|
|
186
|
+
private_key=agent_priv,
|
|
187
|
+
agent_jwt=agent_jwt,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
auth_token = asyncio.run(run_exchange())
|
|
191
|
+
|
|
192
|
+
# Verify we got back a token
|
|
193
|
+
assert auth_token is not None
|
|
194
|
+
assert isinstance(auth_token, str)
|
|
195
|
+
assert len(auth_token) > 0
|
|
196
|
+
# Should have 3 JWT parts
|
|
197
|
+
assert len(auth_token.split(".")) == 3
|
|
198
|
+
finally:
|
|
199
|
+
# Note: conftest will clean up the server via _server_lifecycle
|
|
200
|
+
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|