aauth 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.
- aauth/__init__.py +137 -0
- aauth/agent/__init__.py +2 -0
- aauth/agent/challenge_handler.py +61 -0
- aauth/agent/signer.py +78 -0
- aauth/errors.py +51 -0
- aauth/headers/__init__.py +2 -0
- aauth/headers/agent_auth.py +146 -0
- aauth/headers/signature.py +53 -0
- aauth/headers/signature_input.py +65 -0
- aauth/headers/signature_key.py +99 -0
- aauth/http/__init__.py +2 -0
- aauth/http/request.py +140 -0
- aauth/http/response.py +106 -0
- aauth/keys/__init__.py +2 -0
- aauth/keys/jwk.py +123 -0
- aauth/keys/jwks.py +191 -0
- aauth/keys/keypair.py +16 -0
- aauth/metadata/__init__.py +2 -0
- aauth/metadata/agent.py +20 -0
- aauth/metadata/auth_server.py +114 -0
- aauth/metadata/resource.py +42 -0
- aauth/resource/__init__.py +2 -0
- aauth/resource/challenge_builder.py +87 -0
- aauth/resource/token_issuer.py +74 -0
- aauth/resource/verifier.py +170 -0
- aauth/signing/__init__.py +2 -0
- aauth/signing/algorithms.py +35 -0
- aauth/signing/signature_base.py +193 -0
- aauth/signing/signer.py +138 -0
- aauth/signing/verifier.py +323 -0
- aauth/tokens/__init__.py +2 -0
- aauth/tokens/agent_token.py +202 -0
- aauth/tokens/auth_token.py +227 -0
- aauth/tokens/resource_token.py +68 -0
- aauth-0.1.0.dist-info/METADATA +584 -0
- aauth-0.1.0.dist-info/RECORD +39 -0
- aauth-0.1.0.dist-info/WHEEL +5 -0
- aauth-0.1.0.dist-info/licenses/LICENSE +21 -0
- aauth-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Agent token creation and validation for AAuth."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from typing import Dict, Any, Optional, Callable
|
|
6
|
+
import jwt
|
|
7
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
8
|
+
from ..keys.jwk import jwk_to_public_key
|
|
9
|
+
from ..errors import TokenError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_agent_token(
|
|
13
|
+
iss: str,
|
|
14
|
+
sub: str,
|
|
15
|
+
cnf_jwk: Dict[str, Any],
|
|
16
|
+
private_key: Ed25519PrivateKey,
|
|
17
|
+
kid: str,
|
|
18
|
+
exp: Optional[int] = None,
|
|
19
|
+
aud: Optional[str] = None
|
|
20
|
+
) -> str:
|
|
21
|
+
"""Create an agent token (agent+jwt) per AAuth spec Section 5.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
iss: Agent server identifier (HTTPS URL) - also the agent identifier
|
|
25
|
+
sub: Agent delegate identifier (persists across key rotations)
|
|
26
|
+
cnf_jwk: Agent delegate's public signing key (JWK format)
|
|
27
|
+
private_key: Agent server's Ed25519 private key for signing
|
|
28
|
+
kid: Key ID for agent server's signing key
|
|
29
|
+
exp: Expiration timestamp (Unix time). If None, defaults to 1 hour from now.
|
|
30
|
+
aud: Optional audience restriction (string or array of strings)
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Signed JWT string (agent+jwt)
|
|
34
|
+
"""
|
|
35
|
+
# Set expiration (default 1 hour)
|
|
36
|
+
if exp is None:
|
|
37
|
+
exp = int(time.time()) + 3600 # 1 hour
|
|
38
|
+
|
|
39
|
+
# Build header
|
|
40
|
+
header = {
|
|
41
|
+
"typ": "agent+jwt",
|
|
42
|
+
"alg": "EdDSA",
|
|
43
|
+
"kid": kid
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Build payload
|
|
47
|
+
payload = {
|
|
48
|
+
"iss": iss,
|
|
49
|
+
"sub": sub,
|
|
50
|
+
"exp": exp,
|
|
51
|
+
"cnf": {
|
|
52
|
+
"jwk": cnf_jwk
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if aud:
|
|
57
|
+
payload["aud"] = aud
|
|
58
|
+
|
|
59
|
+
# Sign token
|
|
60
|
+
# PyJWT supports EdDSA with cryptography Ed25519PrivateKey objects directly
|
|
61
|
+
token = jwt.encode(
|
|
62
|
+
payload,
|
|
63
|
+
private_key,
|
|
64
|
+
algorithm="EdDSA",
|
|
65
|
+
headers=header
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return token
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def verify_agent_token(
|
|
72
|
+
token: str,
|
|
73
|
+
jwks_fetcher: Callable[[str], Optional[Dict[str, Any]]],
|
|
74
|
+
expected_aud: Optional[str] = None
|
|
75
|
+
) -> Dict[str, Any]:
|
|
76
|
+
"""Verify an agent token per AAuth spec Section 5.7.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
token: Agent token JWT string
|
|
80
|
+
jwks_fetcher: Function that takes agent server URL (iss) and returns JWKS dict
|
|
81
|
+
expected_aud: Optional expected audience (for recipient validation)
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Dictionary with verified claims including 'cnf' with 'jwk'
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
TokenError: If token is invalid
|
|
88
|
+
jwt.InvalidTokenError: If token is invalid
|
|
89
|
+
ValueError: If claims don't match expectations
|
|
90
|
+
"""
|
|
91
|
+
# Parse header and payload (unverified first)
|
|
92
|
+
try:
|
|
93
|
+
header = jwt.get_unverified_header(token)
|
|
94
|
+
payload = jwt.decode(token, options={"verify_signature": False})
|
|
95
|
+
except Exception as e:
|
|
96
|
+
raise TokenError(f"Failed to parse agent token: {e}", token_type="agent+jwt")
|
|
97
|
+
|
|
98
|
+
# Step 1-2: Check typ claim
|
|
99
|
+
typ = header.get("typ")
|
|
100
|
+
if typ != "agent+jwt":
|
|
101
|
+
raise TokenError(
|
|
102
|
+
f"Invalid token type: expected agent+jwt, got {typ}",
|
|
103
|
+
token_type="agent+jwt"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Step 3-4: Extract kid and iss
|
|
107
|
+
kid = header.get("kid")
|
|
108
|
+
if not kid:
|
|
109
|
+
raise TokenError("Token header missing 'kid'", token_type="agent+jwt")
|
|
110
|
+
|
|
111
|
+
iss = payload.get("iss")
|
|
112
|
+
if not iss:
|
|
113
|
+
raise TokenError("Token payload missing 'iss'", token_type="agent+jwt")
|
|
114
|
+
|
|
115
|
+
# Step 5-6: Fetch agent server's JWKS and match key
|
|
116
|
+
jwks = jwks_fetcher(iss)
|
|
117
|
+
if not jwks:
|
|
118
|
+
raise TokenError(
|
|
119
|
+
f"Failed to fetch JWKS from {iss}",
|
|
120
|
+
token_type="agent+jwt",
|
|
121
|
+
details={"iss": iss}
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Find key by kid
|
|
125
|
+
keys = jwks.get("keys", [])
|
|
126
|
+
signing_key = None
|
|
127
|
+
for key in keys:
|
|
128
|
+
if key.get("kid") == kid:
|
|
129
|
+
signing_key = key
|
|
130
|
+
break
|
|
131
|
+
|
|
132
|
+
if not signing_key:
|
|
133
|
+
raise TokenError(
|
|
134
|
+
f"Signing key with kid={kid} not found in JWKS",
|
|
135
|
+
token_type="agent+jwt",
|
|
136
|
+
details={"kid": kid, "iss": iss}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Step 7: Verify JWT signature
|
|
140
|
+
try:
|
|
141
|
+
public_key = jwk_to_public_key(signing_key)
|
|
142
|
+
jwt.decode(
|
|
143
|
+
token,
|
|
144
|
+
public_key,
|
|
145
|
+
algorithms=["EdDSA"],
|
|
146
|
+
options={"verify_signature": True, "verify_exp": False, "verify_aud": False}
|
|
147
|
+
)
|
|
148
|
+
except jwt.InvalidSignatureError as e:
|
|
149
|
+
raise TokenError(
|
|
150
|
+
f"JWT signature verification failed: {e}",
|
|
151
|
+
token_type="agent+jwt"
|
|
152
|
+
)
|
|
153
|
+
except Exception as e:
|
|
154
|
+
raise TokenError(
|
|
155
|
+
f"Failed to verify JWT signature: {e}",
|
|
156
|
+
token_type="agent+jwt"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Step 8: Verify exp claim
|
|
160
|
+
exp = payload.get("exp")
|
|
161
|
+
if exp:
|
|
162
|
+
now = int(time.time())
|
|
163
|
+
if now >= exp:
|
|
164
|
+
raise jwt.ExpiredSignatureError("Token has expired")
|
|
165
|
+
else:
|
|
166
|
+
raise TokenError("Token missing 'exp' claim", token_type="agent+jwt")
|
|
167
|
+
|
|
168
|
+
# Step 9: Verify sub claim is present
|
|
169
|
+
sub = payload.get("sub")
|
|
170
|
+
if not sub:
|
|
171
|
+
raise TokenError(
|
|
172
|
+
"Token missing 'sub' claim (agent delegate identifier)",
|
|
173
|
+
token_type="agent+jwt"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Step 10: Verify aud claim if present
|
|
177
|
+
aud = payload.get("aud")
|
|
178
|
+
if aud and expected_aud:
|
|
179
|
+
# Handle both string and array audience (per JWT spec)
|
|
180
|
+
if isinstance(aud, list):
|
|
181
|
+
aud_matches = expected_aud in aud
|
|
182
|
+
else:
|
|
183
|
+
aud_matches = aud == expected_aud
|
|
184
|
+
|
|
185
|
+
if not aud_matches:
|
|
186
|
+
raise TokenError(
|
|
187
|
+
f"Invalid audience: expected {expected_aud}, got {aud}",
|
|
188
|
+
token_type="agent+jwt"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Step 11: Extract cnf.jwk (already in payload)
|
|
192
|
+
cnf = payload.get("cnf")
|
|
193
|
+
if not cnf:
|
|
194
|
+
raise TokenError("Token missing 'cnf' claim", token_type="agent+jwt")
|
|
195
|
+
|
|
196
|
+
cnf_jwk = cnf.get("jwk")
|
|
197
|
+
if not cnf_jwk:
|
|
198
|
+
raise TokenError("Token missing 'cnf.jwk' claim", token_type="agent+jwt")
|
|
199
|
+
|
|
200
|
+
# Return verified claims (including cnf.jwk for HTTPSig verification)
|
|
201
|
+
return payload
|
|
202
|
+
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Auth token creation and validation for AAuth."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from typing import Dict, Any, Optional, Callable
|
|
6
|
+
import jwt
|
|
7
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
8
|
+
from ..keys.jwk import jwk_to_public_key
|
|
9
|
+
from ..errors import TokenError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_auth_token(
|
|
13
|
+
iss: str,
|
|
14
|
+
aud: str,
|
|
15
|
+
agent: str,
|
|
16
|
+
cnf_jwk: Dict[str, Any],
|
|
17
|
+
scope: str,
|
|
18
|
+
private_key: Ed25519PrivateKey,
|
|
19
|
+
kid: str,
|
|
20
|
+
exp: Optional[int] = None,
|
|
21
|
+
sub: Optional[str] = None,
|
|
22
|
+
agent_delegate: Optional[str] = None,
|
|
23
|
+
agent_is_resource: bool = False,
|
|
24
|
+
act: Optional[Dict[str, Any]] = None
|
|
25
|
+
) -> str:
|
|
26
|
+
"""Create an auth token (auth+jwt) per AAuth spec Section 7.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
iss: Auth server identifier (HTTPS URL)
|
|
30
|
+
aud: Resource identifier (HTTPS URL). When agent_is_resource=True, this should be the agent identifier.
|
|
31
|
+
agent: Agent identifier (HTTPS URL). Omitted from payload when agent_is_resource=True.
|
|
32
|
+
cnf_jwk: Agent's public signing key (JWK format)
|
|
33
|
+
scope: Space-separated scope values
|
|
34
|
+
private_key: Auth server's Ed25519 private key for signing
|
|
35
|
+
kid: Key ID for signing key
|
|
36
|
+
exp: Expiration timestamp (Unix time). If None, defaults to 1 hour from now.
|
|
37
|
+
sub: Optional user identifier
|
|
38
|
+
agent_delegate: Optional agent delegate identifier
|
|
39
|
+
agent_is_resource: If True, omit 'agent' claim and set aud to agent identifier (Phase 5: agent is resource)
|
|
40
|
+
act: Optional actor claim for token exchange delegation chain (Phase 7).
|
|
41
|
+
Contains: agent (REQUIRED), agent_delegate (OPTIONAL), sub (OPTIONAL), act (OPTIONAL for nested chains)
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Signed JWT string (auth+jwt)
|
|
45
|
+
"""
|
|
46
|
+
# Set expiration (default 1 hour)
|
|
47
|
+
if exp is None:
|
|
48
|
+
exp = int(time.time()) + 3600 # 1 hour
|
|
49
|
+
|
|
50
|
+
# Build header
|
|
51
|
+
header = {
|
|
52
|
+
"typ": "auth+jwt",
|
|
53
|
+
"alg": "EdDSA",
|
|
54
|
+
"kid": kid
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Build payload
|
|
58
|
+
payload = {
|
|
59
|
+
"iss": iss,
|
|
60
|
+
"aud": aud,
|
|
61
|
+
"cnf": {
|
|
62
|
+
"jwk": cnf_jwk
|
|
63
|
+
},
|
|
64
|
+
"scope": scope,
|
|
65
|
+
"exp": exp
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Phase 5: When agent is resource, omit 'agent' claim per SPEC.md Section 7.3
|
|
69
|
+
if not agent_is_resource:
|
|
70
|
+
payload["agent"] = agent
|
|
71
|
+
|
|
72
|
+
if sub:
|
|
73
|
+
payload["sub"] = sub
|
|
74
|
+
|
|
75
|
+
if agent_delegate:
|
|
76
|
+
payload["agent_delegate"] = agent_delegate
|
|
77
|
+
|
|
78
|
+
# Phase 7: Token exchange - add actor claim for delegation chain
|
|
79
|
+
if act:
|
|
80
|
+
payload["act"] = act
|
|
81
|
+
|
|
82
|
+
# Sign token
|
|
83
|
+
# PyJWT supports EdDSA with cryptography Ed25519PrivateKey objects directly
|
|
84
|
+
token = jwt.encode(
|
|
85
|
+
payload,
|
|
86
|
+
private_key,
|
|
87
|
+
algorithm="EdDSA",
|
|
88
|
+
headers=header
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return token
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def parse_token_claims(token: str) -> Dict[str, Any]:
|
|
95
|
+
"""Parse token claims without verification (for inspection).
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
token: JWT token string
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Dictionary with header and payload claims
|
|
102
|
+
"""
|
|
103
|
+
# Decode without verification
|
|
104
|
+
header = jwt.get_unverified_header(token)
|
|
105
|
+
payload = jwt.decode(token, options={"verify_signature": False})
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
"header": header,
|
|
109
|
+
"payload": payload
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def verify_token(
|
|
114
|
+
token: str,
|
|
115
|
+
jwks_fetcher: Callable[[str], Optional[Dict[str, Any]]],
|
|
116
|
+
expected_typ: Optional[str] = None,
|
|
117
|
+
expected_iss: Optional[str] = None,
|
|
118
|
+
expected_aud: Optional[str] = None
|
|
119
|
+
) -> Dict[str, Any]:
|
|
120
|
+
"""Verify a JWT token signature and claims.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
token: JWT token string
|
|
124
|
+
jwks_fetcher: Function that takes issuer URL and returns JWKS dict
|
|
125
|
+
expected_typ: Expected typ claim (e.g., "resource+jwt", "auth+jwt")
|
|
126
|
+
expected_iss: Expected issuer (optional)
|
|
127
|
+
expected_aud: Expected audience (optional)
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Dictionary with verified claims
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
TokenError: If token is invalid
|
|
134
|
+
jwt.InvalidTokenError: If token is invalid
|
|
135
|
+
ValueError: If claims don't match expectations
|
|
136
|
+
"""
|
|
137
|
+
# Parse header and payload (unverified first)
|
|
138
|
+
try:
|
|
139
|
+
header = jwt.get_unverified_header(token)
|
|
140
|
+
payload = jwt.decode(token, options={"verify_signature": False})
|
|
141
|
+
except Exception as e:
|
|
142
|
+
raise TokenError(f"Failed to parse token: {e}", token_type=expected_typ or "jwt")
|
|
143
|
+
|
|
144
|
+
# Check typ claim
|
|
145
|
+
if expected_typ:
|
|
146
|
+
typ = header.get("typ")
|
|
147
|
+
if typ != expected_typ:
|
|
148
|
+
raise TokenError(
|
|
149
|
+
f"Invalid token type: expected {expected_typ}, got {typ}",
|
|
150
|
+
token_type=expected_typ
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Check exp claim
|
|
154
|
+
exp = payload.get("exp")
|
|
155
|
+
if exp:
|
|
156
|
+
now = int(time.time())
|
|
157
|
+
if now >= exp:
|
|
158
|
+
raise jwt.ExpiredSignatureError("Token has expired")
|
|
159
|
+
|
|
160
|
+
# Check iss claim
|
|
161
|
+
iss = payload.get("iss")
|
|
162
|
+
if expected_iss and iss != expected_iss:
|
|
163
|
+
raise TokenError(
|
|
164
|
+
f"Invalid issuer: expected {expected_iss}, got {iss}",
|
|
165
|
+
token_type=expected_typ or "jwt"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Check aud claim
|
|
169
|
+
aud = payload.get("aud")
|
|
170
|
+
if expected_aud:
|
|
171
|
+
# Handle both string and array audience (per JWT spec)
|
|
172
|
+
if isinstance(aud, list):
|
|
173
|
+
aud_matches = expected_aud in aud
|
|
174
|
+
else:
|
|
175
|
+
aud_matches = aud == expected_aud
|
|
176
|
+
|
|
177
|
+
if not aud_matches:
|
|
178
|
+
raise TokenError(
|
|
179
|
+
f"Invalid audience: expected {expected_aud}, got {aud}",
|
|
180
|
+
token_type=expected_typ or "jwt"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Get signing key from JWKS
|
|
184
|
+
kid = header.get("kid")
|
|
185
|
+
if not kid:
|
|
186
|
+
raise TokenError("Token header missing 'kid'", token_type=expected_typ or "jwt")
|
|
187
|
+
|
|
188
|
+
jwks = jwks_fetcher(iss)
|
|
189
|
+
if not jwks:
|
|
190
|
+
raise TokenError(
|
|
191
|
+
f"Failed to fetch JWKS from {iss}",
|
|
192
|
+
token_type=expected_typ or "jwt"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Find key by kid
|
|
196
|
+
keys = jwks.get("keys", [])
|
|
197
|
+
signing_key = None
|
|
198
|
+
for key in keys:
|
|
199
|
+
if key.get("kid") == kid:
|
|
200
|
+
signing_key = key
|
|
201
|
+
break
|
|
202
|
+
|
|
203
|
+
if not signing_key:
|
|
204
|
+
raise TokenError(
|
|
205
|
+
f"Signing key with kid={kid} not found in JWKS",
|
|
206
|
+
token_type=expected_typ or "jwt"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Convert JWK to public key
|
|
210
|
+
public_key = jwk_to_public_key(signing_key)
|
|
211
|
+
|
|
212
|
+
# Verify signature
|
|
213
|
+
try:
|
|
214
|
+
jwt.decode(
|
|
215
|
+
token,
|
|
216
|
+
public_key,
|
|
217
|
+
algorithms=["EdDSA"],
|
|
218
|
+
options={"verify_signature": True, "verify_exp": False, "verify_aud": False}
|
|
219
|
+
)
|
|
220
|
+
except jwt.InvalidSignatureError as e:
|
|
221
|
+
raise TokenError(
|
|
222
|
+
f"JWT signature verification failed: {e}",
|
|
223
|
+
token_type=expected_typ or "jwt"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return payload
|
|
227
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Resource token creation and validation for AAuth."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from typing import Dict, Any, Optional
|
|
6
|
+
import jwt
|
|
7
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
8
|
+
from ..keys.jwk import calculate_jwk_thumbprint
|
|
9
|
+
from ..errors import TokenError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_resource_token(
|
|
13
|
+
iss: str,
|
|
14
|
+
aud: str,
|
|
15
|
+
agent: str,
|
|
16
|
+
agent_jkt: str,
|
|
17
|
+
scope: str,
|
|
18
|
+
private_key: Ed25519PrivateKey,
|
|
19
|
+
kid: str,
|
|
20
|
+
exp: Optional[int] = None
|
|
21
|
+
) -> str:
|
|
22
|
+
"""Create a resource token (resource+jwt) per AAuth spec Section 6.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
iss: Resource identifier (HTTPS URL)
|
|
26
|
+
aud: Auth server identifier (HTTPS URL)
|
|
27
|
+
agent: Agent identifier (HTTPS URL)
|
|
28
|
+
agent_jkt: JWK Thumbprint of agent's signing key
|
|
29
|
+
scope: Space-separated scope values
|
|
30
|
+
private_key: Resource's Ed25519 private key for signing
|
|
31
|
+
kid: Key ID for signing key
|
|
32
|
+
exp: Expiration timestamp (Unix time). If None, defaults to 10 minutes from now.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Signed JWT string (resource+jwt)
|
|
36
|
+
"""
|
|
37
|
+
# Set expiration (default 10 minutes)
|
|
38
|
+
if exp is None:
|
|
39
|
+
exp = int(time.time()) + 600 # 10 minutes
|
|
40
|
+
|
|
41
|
+
# Build header
|
|
42
|
+
header = {
|
|
43
|
+
"typ": "resource+jwt",
|
|
44
|
+
"alg": "EdDSA",
|
|
45
|
+
"kid": kid
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Build payload
|
|
49
|
+
payload = {
|
|
50
|
+
"iss": iss,
|
|
51
|
+
"aud": aud,
|
|
52
|
+
"agent": agent,
|
|
53
|
+
"agent_jkt": agent_jkt,
|
|
54
|
+
"scope": scope,
|
|
55
|
+
"exp": exp
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Sign token
|
|
59
|
+
# PyJWT supports EdDSA with cryptography Ed25519PrivateKey objects directly
|
|
60
|
+
token = jwt.encode(
|
|
61
|
+
payload,
|
|
62
|
+
private_key,
|
|
63
|
+
algorithm="EdDSA",
|
|
64
|
+
headers=header
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return token
|
|
68
|
+
|