tba-protocol-jwt 0.4.0.dev2__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.
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: tba-protocol-jwt
3
+ Version: 0.4.0.dev2
4
+ Summary:
5
+ Requires-Python: >=3.10,<3.15
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Programming Language :: Python :: 3.10
8
+ Classifier: Programming Language :: Python :: 3.11
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Programming Language :: Python :: 3.14
12
+ Requires-Dist: tba-concrete-claim-audience (==0.4.0.dev2)
13
+ Requires-Dist: tba-concrete-claim-expiration (==0.4.0.dev2)
14
+ Requires-Dist: tba-concrete-claim-issued-at (==0.4.0.dev2)
15
+ Requires-Dist: tba-concrete-claim-issuer (==0.4.0.dev2)
16
+ Requires-Dist: tba-concrete-claim-jwt-id (==0.4.0.dev2)
17
+ Requires-Dist: tba-concrete-claim-not-before (==0.4.0.dev2)
18
+ Requires-Dist: tba-concrete-claim-subject (==0.4.0.dev2)
19
+ Requires-Dist: tba-concrete-jwt (==0.4.0.dev2)
20
+ Requires-Dist: tba-contracts-identity (==0.4.0.dev2)
21
+ Requires-Dist: tba-contracts-protected-envelope (==0.4.0.dev2)
@@ -0,0 +1,10 @@
1
+ [project]
2
+ name="tba-protocol-jwt"
3
+ version="0.4.0.dev2"
4
+ requires-python=">=3.10,<3.15"
5
+ dependencies=["tba-contracts-protected-envelope==0.4.0.dev2","tba-concrete-jwt==0.4.0.dev2","tba-contracts-identity==0.4.0.dev2","tba-concrete-claim-issuer==0.4.0.dev2","tba-concrete-claim-subject==0.4.0.dev2","tba-concrete-claim-audience==0.4.0.dev2","tba-concrete-claim-expiration==0.4.0.dev2","tba-concrete-claim-not-before==0.4.0.dev2","tba-concrete-claim-issued-at==0.4.0.dev2","tba-concrete-claim-jwt-id==0.4.0.dev2"]
6
+ [build-system]
7
+ requires=["poetry-core>=1.0.0"]
8
+ build-backend="poetry.core.masonry.api"
9
+ [tool.poetry]
10
+ packages=[{include="tba_protocol_jwt",from="src"}]
@@ -0,0 +1,59 @@
1
+ """Versioned RFC 7519 JSON Web Token ownership."""
2
+
3
+ from tba_contracts_identity.protocol_processing import (
4
+ build_protocol_capability_report,
5
+ )
6
+
7
+ from .protocol import JwtProtocol
8
+
9
+ from .bindings import CAPABILITY_REQUIREMENTS
10
+ from .claims import JWT_CLAIM_CLASSES, JWT_VERSION, compose_jwt_claim_set
11
+ from .compatibility import COMPATIBILITY_PATHS, JwtCompatibility, compatibility
12
+ from .errors import (
13
+ JwtProfileRequiredError,
14
+ JwtProtocolError,
15
+ UnsupportedJwtMediaTypeError,
16
+ )
17
+ from .features import FEATURES_BY_VERSION, supports
18
+ from .migrations import migrate_claims
19
+ from .profile import JwtProfile
20
+ from .schemas import JWT_CARRIER, JwtCarrier, select_carrier
21
+ from .versions import CURRENT_VERSION, VERSION_HISTORY, JwtVersion, select_version
22
+
23
+
24
+ def capability_report() -> dict[str, object]:
25
+ return build_protocol_capability_report(
26
+ protocol="jwt",
27
+ revision=CURRENT_VERSION.identifier,
28
+ features=tuple(FEATURES_BY_VERSION[CURRENT_VERSION.identifier]),
29
+ evidence_links=("tests/unit/test_versioned_jwt_protocol.py",),
30
+ extra_requirements=CAPABILITY_REQUIREMENTS,
31
+ include_default_artifact_requirements=False,
32
+ )
33
+
34
+
35
+ __all__ = [
36
+ "JwtProtocol",
37
+ "CAPABILITY_REQUIREMENTS",
38
+ "COMPATIBILITY_PATHS",
39
+ "CURRENT_VERSION",
40
+ "FEATURES_BY_VERSION",
41
+ "JWT_CARRIER",
42
+ "JWT_CLAIM_CLASSES",
43
+ "JWT_VERSION",
44
+ "VERSION_HISTORY",
45
+ "JwtCarrier",
46
+ "JwtCompatibility",
47
+ "JwtProfile",
48
+ "JwtProfileRequiredError",
49
+ "JwtProtocolError",
50
+ "JwtVersion",
51
+ "UnsupportedJwtMediaTypeError",
52
+ "capability_report",
53
+ "compatibility",
54
+ "compose_jwt_claim_set",
55
+ "migrate_claims",
56
+ "select_carrier",
57
+ "select_version",
58
+ "supports",
59
+ ]
@@ -0,0 +1,37 @@
1
+ """RFC 7519 structural operations mapped to semantic capabilities."""
2
+
3
+ from tba_contracts_identity.capabilities import ProtocolCapabilityRequirement
4
+
5
+ from .versions import CURRENT_VERSION
6
+
7
+ CAPABILITY_REQUIREMENTS = (
8
+ ProtocolCapabilityRequirement(
9
+ "jwt",
10
+ CURRENT_VERSION.identifier,
11
+ "jwt-compact-decode",
12
+ "compact JWT",
13
+ "artifact.processing",
14
+ "decode",
15
+ "jwt-claims",
16
+ ),
17
+ ProtocolCapabilityRequirement(
18
+ "jwt",
19
+ CURRENT_VERSION.identifier,
20
+ "jwt-structural-validation",
21
+ "registered JWT claims and JOSE structure",
22
+ "artifact.processing",
23
+ "validate",
24
+ "structurally-valid-jwt",
25
+ ),
26
+ ProtocolCapabilityRequirement(
27
+ "jwt",
28
+ CURRENT_VERSION.identifier,
29
+ "jwt-compact-encode",
30
+ "application/jwt",
31
+ "artifact.processing",
32
+ "encode",
33
+ "jwt",
34
+ ),
35
+ )
36
+
37
+ __all__ = ["CAPABILITY_REQUIREMENTS"]
@@ -0,0 +1,35 @@
1
+ """RFC 7519 registered-claim composition."""
2
+
3
+ from tba_concrete_claim_audience import AudienceClaim
4
+ from tba_concrete_claim_expiration import ExpirationClaim
5
+ from tba_concrete_claim_issued_at import IssuedAtClaim
6
+ from tba_concrete_claim_issuer import IssuerClaim
7
+ from tba_concrete_claim_jwt_id import JwtIdClaim
8
+ from tba_concrete_claim_not_before import NotBeforeClaim
9
+ from tba_concrete_claim_subject import SubjectClaim
10
+ from tba_contracts_identity.claims import ClaimSet
11
+
12
+ from .versions import CURRENT_VERSION
13
+
14
+ JWT_VERSION = CURRENT_VERSION.identifier
15
+ JWT_CLAIM_CLASSES = (
16
+ IssuerClaim,
17
+ SubjectClaim,
18
+ AudienceClaim,
19
+ ExpirationClaim,
20
+ NotBeforeClaim,
21
+ IssuedAtClaim,
22
+ JwtIdClaim,
23
+ )
24
+
25
+
26
+ def compose_jwt_claim_set(*claims: object) -> ClaimSet:
27
+ unexpected = [
28
+ claim.name for claim in claims if not isinstance(claim, JWT_CLAIM_CLASSES)
29
+ ]
30
+ if unexpected:
31
+ raise ValueError(f"claims are not registered by {JWT_VERSION}: {unexpected}")
32
+ return ClaimSet(tuple(claims), "jwt", JWT_VERSION)
33
+
34
+
35
+ __all__ = ["JWT_CLAIM_CLASSES", "JWT_VERSION", "compose_jwt_claim_set"]
@@ -0,0 +1,42 @@
1
+ """JWT revision compatibility paths."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from .versions import CURRENT_VERSION, VERSION_HISTORY
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class JwtCompatibility:
10
+ source: str
11
+ target: str
12
+ compatible: bool
13
+ migration_required: bool
14
+
15
+
16
+ COMPATIBILITY_PATHS = (
17
+ JwtCompatibility(
18
+ VERSION_HISTORY[0].identifier,
19
+ CURRENT_VERSION.identifier,
20
+ True,
21
+ True,
22
+ ),
23
+ JwtCompatibility(
24
+ CURRENT_VERSION.identifier,
25
+ CURRENT_VERSION.identifier,
26
+ True,
27
+ False,
28
+ ),
29
+ )
30
+
31
+
32
+ def compatibility(
33
+ source: str,
34
+ target: str = CURRENT_VERSION.identifier,
35
+ ) -> JwtCompatibility:
36
+ for path in COMPATIBILITY_PATHS:
37
+ if path.source == source and path.target == target:
38
+ return path
39
+ return JwtCompatibility(source, target, False, False)
40
+
41
+
42
+ __all__ = ["COMPATIBILITY_PATHS", "JwtCompatibility", "compatibility"]
@@ -0,0 +1,20 @@
1
+ """JWT protocol mapping errors."""
2
+
3
+
4
+ class JwtProtocolError(ValueError):
5
+ """Base error for an RFC 7519 mapping failure."""
6
+
7
+
8
+ class UnsupportedJwtMediaTypeError(JwtProtocolError):
9
+ """Raised when the selected artifact is not an application/JWT carrier."""
10
+
11
+
12
+ class JwtProfileRequiredError(JwtProtocolError):
13
+ """Raised when a trust/use decision is attempted without a token profile."""
14
+
15
+
16
+ __all__ = [
17
+ "JwtProfileRequiredError",
18
+ "JwtProtocolError",
19
+ "UnsupportedJwtMediaTypeError",
20
+ ]
@@ -0,0 +1,24 @@
1
+ FEATURES_BY_VERSION = {
2
+ "draft-ietf-oauth-json-web-token-32": frozenset(
3
+ {"registered-claims", "public-claims", "private-claims"}
4
+ ),
5
+ "RFC7519": frozenset(
6
+ {
7
+ "registered-claims",
8
+ "public-claims",
9
+ "private-claims",
10
+ "nested-jwt",
11
+ "string-or-array-audience",
12
+ }
13
+ ),
14
+ }
15
+
16
+
17
+ def supports(feature: str, version: str = "RFC7519") -> bool:
18
+ try:
19
+ return feature in FEATURES_BY_VERSION[version]
20
+ except KeyError as exc:
21
+ raise ValueError(f"unsupported JWT version: {version}") from exc
22
+
23
+
24
+ __all__ = ["FEATURES_BY_VERSION", "supports"]
@@ -0,0 +1,14 @@
1
+ from typing import Any, Mapping
2
+
3
+
4
+ def migrate_claims(
5
+ value: Mapping[str, Any], *, source: str, target: str = "RFC7519"
6
+ ) -> dict[str, Any]:
7
+ if source == target:
8
+ return dict(value)
9
+ if source != "draft-ietf-oauth-json-web-token-32" or target != "RFC7519":
10
+ raise ValueError(f"unsupported JWT migration: {source} -> {target}")
11
+ return dict(value)
12
+
13
+
14
+ __all__ = ["migrate_claims"]
@@ -0,0 +1,23 @@
1
+ from dataclasses import dataclass
2
+ from typing import Mapping
3
+
4
+
5
+ @dataclass(frozen=True, slots=True)
6
+ class JwtProfile:
7
+ name: str
8
+ allowed_types: frozenset[str]
9
+ required_claims: frozenset[str]
10
+ allowed_algorithms: frozenset[str]
11
+
12
+ def validate(
13
+ self, headers: Mapping[str, object], claims: Mapping[str, object]
14
+ ) -> None:
15
+ token_type = headers.get("typ")
16
+ if token_type not in self.allowed_types:
17
+ raise ValueError(f"unexpected token type for {self.name}: {token_type}")
18
+ algorithm = headers.get("alg")
19
+ if algorithm in {None, "none"} or algorithm not in self.allowed_algorithms:
20
+ raise ValueError(f"algorithm not allowed by {self.name}: {algorithm}")
21
+ missing = self.required_claims.difference(claims)
22
+ if missing:
23
+ raise ValueError(f"missing required claims: {sorted(missing)}")
@@ -0,0 +1,34 @@
1
+ import json
2
+ from tba_concrete_jwt import validate_registered_claims
3
+ from tba_contracts_protected_envelope import (
4
+ EnvelopeVerificationRequest,
5
+ ProtectedEnvelope,
6
+ )
7
+
8
+
9
+ class JwtProtocol:
10
+ def __init__(self, capability, profile):
11
+ self.capability, self.profile = capability, profile
12
+
13
+ def verify(
14
+ self,
15
+ envelope: ProtectedEnvelope,
16
+ *,
17
+ issuer: str | None = None,
18
+ audience: str | None = None,
19
+ now: int | None = None,
20
+ ):
21
+ result = self.capability.verify_envelope(
22
+ EnvelopeVerificationRequest(
23
+ envelope,
24
+ expected_profile=self.profile.name,
25
+ expected_issuer=issuer,
26
+ expected_audience=audience,
27
+ )
28
+ )
29
+ if not result.valid or result.payload is None:
30
+ raise ValueError(result.reason or "JWT signature verification failed")
31
+ claims = json.loads(result.payload)
32
+ self.profile.validate(envelope.protected_headers, claims)
33
+ validate_registered_claims(claims, issuer=issuer, audience=audience, now=now)
34
+ return claims, result
@@ -0,0 +1,25 @@
1
+ """RFC 7519 compact JWT carrier descriptors."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from tba_contracts_identity.tokens import TokenEnvelopeFormat
6
+
7
+ from .errors import UnsupportedJwtMediaTypeError
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class JwtCarrier:
12
+ media_type: str
13
+ envelope_format: TokenEnvelopeFormat
14
+
15
+
16
+ JWT_CARRIER = JwtCarrier("application/jwt", TokenEnvelopeFormat.JWT)
17
+
18
+
19
+ def select_carrier(media_type: str) -> JwtCarrier:
20
+ if media_type != JWT_CARRIER.media_type:
21
+ raise UnsupportedJwtMediaTypeError(f"unsupported JWT media type: {media_type}")
22
+ return JWT_CARRIER
23
+
24
+
25
+ __all__ = ["JWT_CARRIER", "JwtCarrier", "select_carrier"]
@@ -0,0 +1,25 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(frozen=True, slots=True)
5
+ class JwtVersion:
6
+ identifier: str
7
+ status: str
8
+ published: str
9
+
10
+
11
+ VERSION_HISTORY = (
12
+ JwtVersion("draft-ietf-oauth-json-web-token-32", "superseded-draft", "2015-02"),
13
+ JwtVersion("RFC7519", "standards-track", "2015-05"),
14
+ )
15
+ CURRENT_VERSION = VERSION_HISTORY[-1]
16
+
17
+
18
+ def select_version(identifier: str = "RFC7519") -> JwtVersion:
19
+ for version in VERSION_HISTORY:
20
+ if version.identifier == identifier:
21
+ return version
22
+ raise ValueError(f"unsupported JWT version: {identifier}")
23
+
24
+
25
+ __all__ = ["CURRENT_VERSION", "VERSION_HISTORY", "JwtVersion", "select_version"]