tba-protocol-jwt 0.4.0.dev2__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.
- tba_protocol_jwt/__init__.py +59 -0
- tba_protocol_jwt/bindings.py +37 -0
- tba_protocol_jwt/claims.py +35 -0
- tba_protocol_jwt/compatibility.py +42 -0
- tba_protocol_jwt/errors.py +20 -0
- tba_protocol_jwt/features.py +24 -0
- tba_protocol_jwt/migrations.py +14 -0
- tba_protocol_jwt/profile.py +23 -0
- tba_protocol_jwt/protocol.py +34 -0
- tba_protocol_jwt/py.typed +0 -0
- tba_protocol_jwt/schemas.py +25 -0
- tba_protocol_jwt/versions.py +25 -0
- tba_protocol_jwt-0.4.0.dev2.dist-info/METADATA +21 -0
- tba_protocol_jwt-0.4.0.dev2.dist-info/RECORD +15 -0
- tba_protocol_jwt-0.4.0.dev2.dist-info/WHEEL +4 -0
|
@@ -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
|
|
File without changes
|
|
@@ -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"]
|
|
@@ -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,15 @@
|
|
|
1
|
+
tba_protocol_jwt/__init__.py,sha256=vFpAqdtxc3-WKSlKIZN3iyE-7PSWMFwpPTmFiLcmypM,1733
|
|
2
|
+
tba_protocol_jwt/bindings.py,sha256=_H75m7F6jgtvKqisDqPRpcdEvrMAcs8KZxX3nrMp3zs,956
|
|
3
|
+
tba_protocol_jwt/claims.py,sha256=S3F2z8YTDGaomKKx3stzyAj-2ZLTpo8V_6zKKpODXwg,1107
|
|
4
|
+
tba_protocol_jwt/compatibility.py,sha256=xTsjdZRaFFhZiZ58xWoW3L22_TWFHkPCFcul1vN-rwg,924
|
|
5
|
+
tba_protocol_jwt/errors.py,sha256=C9ZJVUJ9Fcmq0kGwCx5xyCzpkDbhYV82OEHTIvg-wXY,501
|
|
6
|
+
tba_protocol_jwt/features.py,sha256=A9bUAGpiwPR8OH75TEz7QSjhHlZTzi-Xc7ih2eyqMgw,639
|
|
7
|
+
tba_protocol_jwt/migrations.py,sha256=5F0E3WFz6Z875don7DIrXGfASgX8v1PAp6ultEwVC7U,406
|
|
8
|
+
tba_protocol_jwt/profile.py,sha256=qA11EjDOlVHH-AlGp63Cl-McmoxoBXnIuLI3mXa7414,882
|
|
9
|
+
tba_protocol_jwt/protocol.py,sha256=cSEEl5G2S2z1VPxYCCTrn4fHHhj7la28t-3Ci1ayYnk,1130
|
|
10
|
+
tba_protocol_jwt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
tba_protocol_jwt/schemas.py,sha256=vtOD69MWr0HVn_p7eybuHRbCTYVDWdaUc7hdFheyhZ8,653
|
|
12
|
+
tba_protocol_jwt/versions.py,sha256=L8LcvrByJxOc5EFSsBwNN_GkaGUc_xFMfgMQk-pVLL8,668
|
|
13
|
+
tba_protocol_jwt-0.4.0.dev2.dist-info/METADATA,sha256=aNgd1B35t8c486otlNP6L4XatrAdSwwbr6Bp5QyZZZ0,977
|
|
14
|
+
tba_protocol_jwt-0.4.0.dev2.dist-info/WHEEL,sha256=L-WvLdSBvJWHXv5zrQxDvNRnIgoBzLUpcbuiw19KCuQ,88
|
|
15
|
+
tba_protocol_jwt-0.4.0.dev2.dist-info/RECORD,,
|