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.
@@ -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
+