Skip to content

Commit ab6fec7

Browse files
committed
feat: verify_license_assertion_jwt + verify_with_details (PyJWT)
1 parent c868440 commit ab6fec7

4 files changed

Lines changed: 53 additions & 0 deletions

File tree

licensechain/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
SerializationError, DeserializationError, ConfigurationError, UnknownError
1212
)
1313
from .webhook_handler import WebhookHandler, WebhookEvents
14+
from .license_assertion import LICENSE_TOKEN_USE_CLAIM, verify_license_assertion_jwt
1415
from .utils import (
1516
validate_email, validate_license_key, validate_uuid, validate_amount,
1617
validate_currency, validate_status, sanitize_input, sanitize_metadata,
@@ -28,6 +29,8 @@
2829
__email__ = 'support@licensechain.app'
2930

3031
__all__ = [
32+
'LICENSE_TOKEN_USE_CLAIM',
33+
'verify_license_assertion_jwt',
3134
'Client',
3235
'WebhookHandler',
3336
'WebhookEvents',

licensechain/license_assertion.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Verify LicenseChain license_token (RS256) via JWKS."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Any, Dict, Optional
6+
7+
LICENSE_TOKEN_USE_CLAIM = "licensechain_license_v1"
8+
9+
10+
def verify_license_assertion_jwt(
11+
token: str,
12+
jwks_url: str,
13+
*,
14+
expected_app_id: Optional[str] = None,
15+
issuer: Optional[str] = None,
16+
) -> Dict[str, Any]:
17+
"""
18+
Verify license_token from POST /v1/licenses/verify using license_jwks_uri or GET /v1/licenses/jwks.
19+
20+
Requires: PyJWT and cryptography (see requirements.txt).
21+
"""
22+
import jwt
23+
from jwt import PyJWKClient
24+
25+
jwks_client = PyJWKClient(jwks_url)
26+
signing_key = jwks_client.get_signing_key_from_jwt(token)
27+
decode_kw: Dict[str, Any] = {
28+
"algorithms": ["RS256"],
29+
"options": {"verify_aud": False},
30+
}
31+
if issuer:
32+
decode_kw["issuer"] = issuer
33+
payload = jwt.decode(token, signing_key.key, **decode_kw)
34+
if payload.get("token_use") != LICENSE_TOKEN_USE_CLAIM:
35+
raise ValueError(f'Invalid license token: expected token_use "{LICENSE_TOKEN_USE_CLAIM}"')
36+
if expected_app_id is not None and expected_app_id != "" and payload.get("aud") != expected_app_id:
37+
raise ValueError("Invalid license token: aud does not match expected app id")
38+
return payload

licensechain/services/license_service.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,17 @@ def validate(self, license_key: str, hwuid: Optional[str] = None) -> bool:
6464
payload['hwuid'] = hashlib.sha256(raw.encode("utf-8")).hexdigest()
6565
response = self.api_client.post('/licenses/verify', payload)
6666
return response.get('valid', False)
67+
68+
def verify_with_details(self, license_key: str, hwuid: Optional[str] = None) -> Dict[str, Any]:
69+
"""Full POST /licenses/verify response (optional license_token, license_jwks_uri)."""
70+
validate_not_empty(license_key, 'license_key')
71+
payload: Dict[str, Any] = {'key': license_key}
72+
if hwuid and hwuid.strip():
73+
payload['hwuid'] = hwuid.strip()
74+
else:
75+
raw = f"licensechain|python|{socket.gethostname()}|{platform.system()}|{platform.machine()}"
76+
payload['hwuid'] = hashlib.sha256(raw.encode("utf-8")).hexdigest()
77+
return self.api_client.post('/licenses/verify', payload)
6778

6879
def list_user_licenses(self, user_id: str, page: Optional[int] = None, limit: Optional[int] = None) -> Dict[str, Any]:
6980
"""List licenses for a user."""

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ typing-extensions>=4.5.0
77
# Optional dependencies for enhanced functionality
88
cryptography>=41.0.0
99
python-dateutil>=2.8.0
10+
PyJWT>=2.8.0

0 commit comments

Comments
 (0)