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,20 @@
1
+ """Agent metadata handling for AAuth."""
2
+
3
+ from typing import Dict, Any
4
+
5
+
6
+ def generate_agent_metadata(agent_id: str, jwks_uri: str) -> Dict[str, Any]:
7
+ """Generate agent metadata JSON per AAuth spec Section 8.1.
8
+
9
+ Args:
10
+ agent_id: Agent identifier (HTTPS URL)
11
+ jwks_uri: URL to agent's JSON Web Key Set
12
+
13
+ Returns:
14
+ Agent metadata dictionary with required fields
15
+ """
16
+ return {
17
+ "agent": agent_id,
18
+ "jwks_uri": jwks_uri
19
+ }
20
+
@@ -0,0 +1,114 @@
1
+ """Auth server metadata handling for AAuth."""
2
+
3
+ from typing import Dict, Any, Optional
4
+
5
+
6
+ def generate_auth_metadata(
7
+ auth_id: str,
8
+ jwks_uri: str,
9
+ token_endpoint: str,
10
+ auth_endpoint: str,
11
+ signing_algs_supported: Optional[list[str]] = None,
12
+ request_types_supported: Optional[list[str]] = None,
13
+ scopes_supported: Optional[list[str]] = None
14
+ ) -> Dict[str, Any]:
15
+ """Generate auth server metadata JSON per AAuth spec Section 8.2.
16
+
17
+ Args:
18
+ auth_id: Auth server identifier (HTTPS URL)
19
+ jwks_uri: URL to auth server's JSON Web Key Set
20
+ token_endpoint: Endpoint for auth requests, code exchange, token exchange, and refresh
21
+ auth_endpoint: Endpoint for user authentication and consent flow
22
+ signing_algs_supported: Optional list of supported HTTPSig algorithms
23
+ request_types_supported: Optional list of supported request_type values
24
+ scopes_supported: Optional list of supported scopes
25
+
26
+ Returns:
27
+ Auth server metadata dictionary with required fields
28
+ """
29
+ metadata = {
30
+ "issuer": auth_id,
31
+ "jwks_uri": jwks_uri,
32
+ "agent_token_endpoint": token_endpoint,
33
+ "agent_auth_endpoint": auth_endpoint
34
+ }
35
+
36
+ if signing_algs_supported:
37
+ metadata["agent_signing_algs_supported"] = signing_algs_supported
38
+ else:
39
+ # Default to Ed25519
40
+ metadata["agent_signing_algs_supported"] = ["ed25519"]
41
+
42
+ if request_types_supported:
43
+ metadata["request_types_supported"] = request_types_supported
44
+ else:
45
+ # Default to auth, code, exchange, refresh
46
+ metadata["request_types_supported"] = ["auth", "code", "exchange", "refresh"]
47
+
48
+ if scopes_supported:
49
+ metadata["scopes_supported"] = scopes_supported
50
+
51
+ return metadata
52
+
53
+
54
+ def fetch_metadata(url: str) -> Dict[str, Any]:
55
+ """Fetch metadata document from URL via HTTPS (sync version for backward compatibility).
56
+
57
+ Args:
58
+ url: HTTPS URL to metadata document (HTTP allowed for localhost development)
59
+
60
+ Returns:
61
+ Parsed metadata dictionary
62
+
63
+ Raises:
64
+ ValueError: If URL is not HTTPS (except localhost for development)
65
+ MetadataError: If HTTP request fails
66
+ """
67
+ import httpx
68
+ from ..errors import MetadataError
69
+
70
+ # Verify HTTPS (allow HTTP for localhost development)
71
+ if not url.startswith("https://"):
72
+ # Allow HTTP for localhost/127.0.0.1 for development
73
+ parsed = httpx.URL(url)
74
+ if parsed.host not in ("localhost", "127.0.0.1", "::1"):
75
+ raise ValueError(f"Metadata URL must use HTTPS (except localhost): {url}")
76
+
77
+ # Fetch metadata
78
+ try:
79
+ response = httpx.get(url, timeout=10.0)
80
+ response.raise_for_status()
81
+ return response.json()
82
+ except Exception as e:
83
+ raise MetadataError(
84
+ f"Failed to fetch metadata from {url}: {e}",
85
+ metadata_url=url
86
+ ) from e
87
+
88
+
89
+ async def fetch_auth_metadata(url: str, http_client=None) -> Dict[str, Any]:
90
+ """Fetch auth server metadata from URL (async version).
91
+
92
+ Args:
93
+ url: URL to auth server metadata document (e.g., https://auth.example/.well-known/aauth-issuer)
94
+ http_client: Optional HTTP client (default: uses DefaultHTTPClient from keys.jwks)
95
+
96
+ Returns:
97
+ Parsed auth server metadata dictionary
98
+
99
+ Raises:
100
+ MetadataError: If fetch fails
101
+ """
102
+ from ..keys.jwks import DefaultHTTPClient
103
+ from ..errors import MetadataError
104
+
105
+ client = http_client or DefaultHTTPClient()
106
+
107
+ try:
108
+ return await client.fetch_json(url)
109
+ except Exception as e:
110
+ raise MetadataError(
111
+ f"Failed to fetch auth server metadata from {url}: {e}",
112
+ metadata_url=url
113
+ )
114
+
@@ -0,0 +1,42 @@
1
+ """Resource metadata handling for AAuth."""
2
+
3
+ from typing import Dict, Any, Optional
4
+
5
+
6
+ def generate_resource_metadata(
7
+ resource_id: str,
8
+ jwks_uri: str,
9
+ resource_token_endpoint: str,
10
+ supported_scopes: Optional[list[str]] = None,
11
+ scope_descriptions: Optional[Dict[str, str]] = None
12
+ ) -> Dict[str, Any]:
13
+ """Generate resource metadata JSON per AAuth spec Section 8.3.
14
+
15
+ Args:
16
+ resource_id: Resource identifier (HTTPS URL)
17
+ jwks_uri: URL to resource's JSON Web Key Set (REQUIRED)
18
+ resource_token_endpoint: Endpoint where agents request resource tokens (REQUIRED per spec)
19
+ supported_scopes: Optional list of supported scope values
20
+ scope_descriptions: Optional dictionary mapping scope names to descriptions
21
+
22
+ Returns:
23
+ Resource metadata dictionary with required fields
24
+
25
+ Note:
26
+ Per SPEC.md Section 8.3, resource_token_endpoint is REQUIRED.
27
+ auth_server is NOT in resource metadata - it's only provided in Agent-Auth challenge headers.
28
+ """
29
+ metadata = {
30
+ "resource": resource_id,
31
+ "jwks_uri": jwks_uri,
32
+ "resource_token_endpoint": resource_token_endpoint
33
+ }
34
+
35
+ if supported_scopes:
36
+ metadata["supported_scopes"] = supported_scopes
37
+
38
+ if scope_descriptions:
39
+ metadata["scope_descriptions"] = scope_descriptions
40
+
41
+ return metadata
42
+
@@ -0,0 +1,2 @@
1
+ """Resource role implementation for AAuth."""
2
+
@@ -0,0 +1,87 @@
1
+ """Agent-Auth challenge building for resource role."""
2
+
3
+ from typing import Optional, List
4
+ from ..headers.agent_auth import build_agent_auth_challenge
5
+ from ..tokens.resource_token import create_resource_token
6
+ from ..keys.jwk import calculate_jwk_thumbprint, public_key_to_jwk
7
+ from ..errors import ChallengeError
8
+
9
+
10
+ class ChallengeBuilder:
11
+ """Builds Agent-Auth challenges for resources."""
12
+
13
+ def __init__(
14
+ self,
15
+ resource_id: str,
16
+ resource_private_key,
17
+ resource_kid: str,
18
+ auth_server: str
19
+ ):
20
+ """Initialize challenge builder.
21
+
22
+ Args:
23
+ resource_id: Resource identifier (HTTPS URL)
24
+ resource_private_key: Resource's private key for signing resource tokens
25
+ resource_kid: Resource's key ID
26
+ auth_server: Auth server identifier (HTTPS URL)
27
+ """
28
+ self.resource_id = resource_id
29
+ self.resource_private_key = resource_private_key
30
+ self.resource_kid = resource_kid
31
+ self.auth_server = auth_server
32
+
33
+ def build_challenge(
34
+ self,
35
+ require_signature: bool = True,
36
+ require_identity: bool = False,
37
+ require_auth_token: bool = False,
38
+ agent_id: Optional[str] = None,
39
+ agent_public_key=None,
40
+ scope: Optional[str] = None
41
+ ) -> str:
42
+ """Build Agent-Auth challenge header.
43
+
44
+ Args:
45
+ require_signature: Require HTTP signature
46
+ require_identity: Require agent identity
47
+ require_auth_token: Require authorization token
48
+ agent_id: Agent identifier (for resource token)
49
+ agent_public_key: Agent's public key (for resource token)
50
+ scope: Required scope (for resource token)
51
+
52
+ Returns:
53
+ Agent-Auth header value
54
+
55
+ Raises:
56
+ ChallengeError: If challenge cannot be built
57
+ """
58
+ resource_token = None
59
+
60
+ if require_auth_token:
61
+ if not agent_id or not agent_public_key or not scope:
62
+ raise ChallengeError(
63
+ "agent_id, agent_public_key, and scope required for auth-token challenge"
64
+ )
65
+
66
+ # Create resource token
67
+ agent_jwk = public_key_to_jwk(agent_public_key)
68
+ agent_jkt = calculate_jwk_thumbprint(agent_jwk)
69
+
70
+ resource_token = create_resource_token(
71
+ iss=self.resource_id,
72
+ aud=self.auth_server,
73
+ agent=agent_id,
74
+ agent_jkt=agent_jkt,
75
+ scope=scope,
76
+ private_key=self.resource_private_key,
77
+ kid=self.resource_kid
78
+ )
79
+
80
+ return build_agent_auth_challenge(
81
+ require_signature=require_signature,
82
+ require_identity=require_identity,
83
+ require_auth_token=require_auth_token,
84
+ resource_token=resource_token,
85
+ auth_server=self.auth_server if require_auth_token else None
86
+ )
87
+
@@ -0,0 +1,74 @@
1
+ """Resource token issuance for resource role."""
2
+
3
+ from typing import Optional
4
+ from ..tokens.resource_token import create_resource_token
5
+ from ..keys.jwk import calculate_jwk_thumbprint, public_key_to_jwk
6
+ from ..errors import TokenError
7
+
8
+
9
+ class ResourceTokenIssuer:
10
+ """Issues resource tokens for agents."""
11
+
12
+ def __init__(
13
+ self,
14
+ resource_id: str,
15
+ resource_private_key,
16
+ resource_kid: str,
17
+ auth_server: str
18
+ ):
19
+ """Initialize resource token issuer.
20
+
21
+ Args:
22
+ resource_id: Resource identifier (HTTPS URL)
23
+ resource_private_key: Resource's private key for signing
24
+ resource_kid: Resource's key ID
25
+ auth_server: Auth server identifier (HTTPS URL)
26
+ """
27
+ self.resource_id = resource_id
28
+ self.resource_private_key = resource_private_key
29
+ self.resource_kid = resource_kid
30
+ self.auth_server = auth_server
31
+
32
+ def issue_token(
33
+ self,
34
+ agent_id: str,
35
+ agent_public_key,
36
+ scope: str,
37
+ exp: Optional[int] = None
38
+ ) -> str:
39
+ """Issue a resource token.
40
+
41
+ Args:
42
+ agent_id: Agent identifier (HTTPS URL)
43
+ agent_public_key: Agent's public signing key
44
+ scope: Space-separated scope values
45
+ exp: Optional expiration timestamp
46
+
47
+ Returns:
48
+ Resource token (JWT string)
49
+
50
+ Raises:
51
+ TokenError: If token creation fails
52
+ """
53
+ try:
54
+ # Calculate agent JWK thumbprint
55
+ agent_jwk = public_key_to_jwk(agent_public_key)
56
+ agent_jkt = calculate_jwk_thumbprint(agent_jwk)
57
+
58
+ # Create resource token
59
+ return create_resource_token(
60
+ iss=self.resource_id,
61
+ aud=self.auth_server,
62
+ agent=agent_id,
63
+ agent_jkt=agent_jkt,
64
+ scope=scope,
65
+ private_key=self.resource_private_key,
66
+ kid=self.resource_kid,
67
+ exp=exp
68
+ )
69
+ except Exception as e:
70
+ raise TokenError(
71
+ f"Failed to issue resource token: {e}",
72
+ token_type="resource+jwt"
73
+ ) from e
74
+
@@ -0,0 +1,170 @@
1
+ """Request verification for resource role."""
2
+
3
+ from typing import Dict, Any, Optional, List, Callable
4
+ from ..signing.verifier import verify_signature
5
+ from ..headers.signature_key import parse_signature_key
6
+ from ..headers.signature_input import parse_signature_input
7
+ from ..errors import SignatureError
8
+
9
+
10
+ class RequestVerifier:
11
+ """Verifies incoming requests for resources."""
12
+
13
+ def __init__(
14
+ self,
15
+ canonical_authorities: List[str],
16
+ jwks_fetcher: Optional[Callable] = None,
17
+ trusted_auth_servers: Optional[List[str]] = None
18
+ ):
19
+ """Initialize request verifier.
20
+
21
+ Args:
22
+ canonical_authorities: List of canonical authorities (host:port) per SPEC 10.3.1
23
+ jwks_fetcher: Optional JWKS fetcher function
24
+ trusted_auth_servers: Optional list of trusted auth server identifiers
25
+ """
26
+ self.canonical_authorities = canonical_authorities
27
+ self.jwks_fetcher = jwks_fetcher
28
+ self.trusted_auth_servers = trusted_auth_servers or []
29
+
30
+ def verify_request(
31
+ self,
32
+ method: str,
33
+ target_uri: str,
34
+ headers: Dict[str, str],
35
+ body: Optional[bytes],
36
+ require_identity: bool = False,
37
+ require_auth_token: bool = False
38
+ ) -> Dict[str, Any]:
39
+ """Verify incoming request.
40
+
41
+ Args:
42
+ method: HTTP method
43
+ target_uri: Target URI
44
+ headers: Request headers
45
+ body: Request body bytes
46
+ require_identity: Whether agent identity is required
47
+ require_auth_token: Whether auth token is required
48
+
49
+ Returns:
50
+ Dictionary with verification result:
51
+ - valid: bool
52
+ - agent_id: Optional[str]
53
+ - agent_delegate: Optional[str]
54
+ - user_sub: Optional[str]
55
+ - scopes: Optional[List[str]]
56
+ - error: Optional[str]
57
+
58
+ Raises:
59
+ SignatureError: If verification fails due to invalid format
60
+ """
61
+ # Extract signature headers
62
+ signature_input_header = headers.get("signature-input") or headers.get("Signature-Input")
63
+ signature_header = headers.get("signature") or headers.get("Signature")
64
+ signature_key_header = headers.get("signature-key") or headers.get("Signature-Key")
65
+
66
+ if not (signature_input_header and signature_header and signature_key_header):
67
+ return {
68
+ "valid": False,
69
+ "error": "Missing signature headers"
70
+ }
71
+
72
+ # Parse Signature-Key to determine scheme
73
+ try:
74
+ parsed_key = parse_signature_key(signature_key_header)
75
+ scheme = parsed_key["scheme"]
76
+ except Exception as e:
77
+ return {
78
+ "valid": False,
79
+ "error": f"Invalid Signature-Key: {e}"
80
+ }
81
+
82
+ # Check canonical authority
83
+ from urllib.parse import urlparse
84
+ parsed_uri = urlparse(target_uri)
85
+ request_authority = parsed_uri.netloc
86
+
87
+ if request_authority not in self.canonical_authorities:
88
+ return {
89
+ "valid": False,
90
+ "error": f"Request authority {request_authority} not in canonical authorities"
91
+ }
92
+
93
+ # Verify signature
94
+ try:
95
+ is_valid = verify_signature(
96
+ method=method,
97
+ target_uri=target_uri,
98
+ headers=headers,
99
+ body=body,
100
+ signature_input_header=signature_input_header,
101
+ signature_header=signature_header,
102
+ signature_key_header=signature_key_header,
103
+ jwks_fetcher=self.jwks_fetcher
104
+ )
105
+
106
+ if not is_valid:
107
+ return {
108
+ "valid": False,
109
+ "error": "Signature verification failed"
110
+ }
111
+ except SignatureError as e:
112
+ return {
113
+ "valid": False,
114
+ "error": str(e)
115
+ }
116
+
117
+ # Extract identity/authorization info based on scheme
118
+ result = {
119
+ "valid": True,
120
+ "agent_id": None,
121
+ "agent_delegate": None,
122
+ "user_sub": None,
123
+ "scopes": None
124
+ }
125
+
126
+ if scheme == "jwks":
127
+ # Extract agent ID from Signature-Key
128
+ params = parsed_key["params"]
129
+ result["agent_id"] = params.get("id")
130
+
131
+ elif scheme == "jwt":
132
+ # Extract from JWT token
133
+ jwt_token = parsed_key["params"].get("jwt")
134
+ if jwt_token:
135
+ try:
136
+ import jwt as pyjwt
137
+ payload = pyjwt.decode(jwt_token, options={"verify_signature": False})
138
+
139
+ # Determine token type
140
+ header = pyjwt.get_unverified_header(jwt_token)
141
+ typ = header.get("typ")
142
+
143
+ if typ == "agent+jwt":
144
+ result["agent_id"] = payload.get("iss")
145
+ result["agent_delegate"] = payload.get("sub")
146
+ elif typ == "auth+jwt":
147
+ result["agent_id"] = payload.get("agent")
148
+ result["agent_delegate"] = payload.get("agent_delegate")
149
+ result["user_sub"] = payload.get("sub")
150
+ scope_str = payload.get("scope")
151
+ if scope_str:
152
+ result["scopes"] = scope_str.split()
153
+ except Exception:
154
+ pass
155
+
156
+ # Check requirements
157
+ if require_identity and not result["agent_id"]:
158
+ return {
159
+ "valid": False,
160
+ "error": "Agent identity required but not present"
161
+ }
162
+
163
+ if require_auth_token and not result.get("scopes"):
164
+ return {
165
+ "valid": False,
166
+ "error": "Auth token required but not present"
167
+ }
168
+
169
+ return result
170
+
@@ -0,0 +1,2 @@
1
+ """HTTP Message Signing (RFC 9421) for AAuth."""
2
+
@@ -0,0 +1,35 @@
1
+ """Signature algorithm support for AAuth."""
2
+
3
+ # Supported algorithms per AAuth spec Section 10.2
4
+ # MUST support ed25519, MAY support others
5
+
6
+ ED25519 = "ed25519"
7
+ RSA_PSS_SHA512 = "rsa-pss-sha512"
8
+ RSA_PSS_SHA256 = "rsa-pss-sha256"
9
+ ECDSA_P256_SHA256 = "ecdsa-p256-sha256"
10
+ ECDSA_P384_SHA384 = "ecdsa-p384-sha384"
11
+
12
+ # Required algorithm
13
+ REQUIRED_ALGORITHM = ED25519
14
+
15
+ # All supported algorithms
16
+ SUPPORTED_ALGORITHMS = [
17
+ ED25519,
18
+ RSA_PSS_SHA512,
19
+ RSA_PSS_SHA256,
20
+ ECDSA_P256_SHA256,
21
+ ECDSA_P384_SHA384,
22
+ ]
23
+
24
+
25
+ def is_supported(algorithm: str) -> bool:
26
+ """Check if algorithm is supported.
27
+
28
+ Args:
29
+ algorithm: Algorithm name
30
+
31
+ Returns:
32
+ True if supported, False otherwise
33
+ """
34
+ return algorithm.lower() in [alg.lower() for alg in SUPPORTED_ALGORITHMS]
35
+