Skip to content

Commit cf8ac71

Browse files
committed
feat: license JWT/JWKS and verify parity updates
1 parent 3801442 commit cf8ac71

3 files changed

Lines changed: 286 additions & 200 deletions

File tree

composer.json

Lines changed: 70 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,70 @@
1-
{
2-
"name": "licensechain/sdk",
3-
"description": "Official LicenseChain PHP SDK for license management and validation",
4-
"type": "library",
5-
"license": "Elastic-2.0",
6-
"authors": [
7-
{
8-
"name": "LicenseChain Team",
9-
"email": "support@licensechain.app"
10-
}
11-
],
12-
"require": {
13-
"php": ">=7.4",
14-
"guzzlehttp/guzzle": "^7.0",
15-
"ext-json": "*",
16-
"ext-curl": "*"
17-
},
18-
"require-dev": {
19-
"phpunit/phpunit": "^9.0",
20-
"phpstan/phpstan": "^1.0",
21-
"squizlabs/php_codesniffer": "^3.0",
22-
"php-cs-fixer/shim": "^3.0"
23-
},
24-
"autoload": {
25-
"psr-4": {
26-
"LicenseChain\\": "src/"
27-
}
28-
},
29-
"autoload-dev": {
30-
"psr-4": {
31-
"LicenseChain\\Tests\\": "tests/"
32-
}
33-
},
34-
"scripts": {
35-
"test": "phpunit",
36-
"test-coverage": "phpunit --coverage-html coverage",
37-
"cs-check": "phpcs",
38-
"cs-fix": "phpcbf",
39-
"stan": "phpstan analyse",
40-
"quality": [
41-
"@cs-check",
42-
"@stan",
43-
"@test"
44-
]
45-
},
46-
"config": {
47-
"sort-packages": true,
48-
"optimize-autoloader": true
49-
},
50-
"keywords": [
51-
"license",
52-
"validation",
53-
"api",
54-
"sdk",
55-
"php",
56-
"licensechain",
57-
"license-management",
58-
"webhook",
59-
"authentication"
60-
],
61-
"homepage": "https://docs.licensechain.app/sdks/php",
62-
"support": {
63-
"email": "support@licensechain.app",
64-
"issues": "https://github.com/LicenseChain/LicenseChain-PHP-SDK/issues",
65-
"source": "https://github.com/LicenseChain/LicenseChain-PHP-SDK"
66-
},
67-
"minimum-stability": "stable",
68-
"prefer-stable": true
69-
}
1+
{
2+
"name": "licensechain/sdk",
3+
"description": "Official LicenseChain PHP SDK for license management and validation",
4+
"type": "library",
5+
"license": "Elastic-2.0",
6+
"authors": [
7+
{
8+
"name": "LicenseChain Team",
9+
"email": "support@licensechain.app"
10+
}
11+
],
12+
"require": {
13+
"php": ">=8.0",
14+
"guzzlehttp/guzzle": "^7.0",
15+
"firebase/php-jwt": "^7.0",
16+
"ext-json": "*",
17+
"ext-curl": "*"
18+
},
19+
"require-dev": {
20+
"phpunit/phpunit": "^9.0",
21+
"phpstan/phpstan": "^1.0",
22+
"squizlabs/php_codesniffer": "^3.0",
23+
"php-cs-fixer/shim": "^3.0"
24+
},
25+
"autoload": {
26+
"psr-4": {
27+
"LicenseChain\\": "src/"
28+
}
29+
},
30+
"autoload-dev": {
31+
"psr-4": {
32+
"LicenseChain\\Tests\\": "tests/"
33+
}
34+
},
35+
"scripts": {
36+
"test": "phpunit",
37+
"test-coverage": "phpunit --coverage-html coverage",
38+
"cs-check": "phpcs",
39+
"cs-fix": "phpcbf",
40+
"stan": "phpstan analyse",
41+
"quality": [
42+
"@cs-check",
43+
"@stan",
44+
"@test"
45+
]
46+
},
47+
"config": {
48+
"sort-packages": true,
49+
"optimize-autoloader": true
50+
},
51+
"keywords": [
52+
"license",
53+
"validation",
54+
"api",
55+
"sdk",
56+
"php",
57+
"licensechain",
58+
"license-management",
59+
"webhook",
60+
"authentication"
61+
],
62+
"homepage": "https://docs.licensechain.app/sdks/php",
63+
"support": {
64+
"email": "support@licensechain.app",
65+
"issues": "https://github.com/LicenseChain/LicenseChain-PHP-SDK/issues",
66+
"source": "https://github.com/LicenseChain/LicenseChain-PHP-SDK"
67+
},
68+
"minimum-stability": "stable",
69+
"prefer-stable": true
70+
}

src/LicenseAssertion.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
namespace LicenseChain;
4+
5+
use Firebase\JWT\JWT;
6+
use Firebase\JWT\JWK;
7+
use GuzzleHttp\Client;
8+
9+
/**
10+
* RS256 license_token verification via JWKS (parity with Node verifyLicenseAssertionJwt).
11+
*/
12+
final class LicenseAssertion
13+
{
14+
/** @var string Must match Core API LICENSE_TOKEN_USE_CLAIM */
15+
public const LICENSE_TOKEN_USE_CLAIM = 'licensechain_license_v1';
16+
17+
/**
18+
* @param array{expectedAppId?: string, issuer?: string} $options
19+
* @return object Decoded JWT payload (stdClass)
20+
*/
21+
public static function verifyLicenseAssertionJwt(string $token, string $jwksUrl, array $options = []): object
22+
{
23+
$token = trim($token);
24+
if ($token === '') {
25+
throw new \InvalidArgumentException('empty token');
26+
}
27+
$jwksUrl = trim($jwksUrl);
28+
if ($jwksUrl === '') {
29+
throw new \InvalidArgumentException('empty jwksUrl');
30+
}
31+
32+
$client = new Client(['timeout' => 20]);
33+
$jwksJson = (string) $client->get($jwksUrl)->getBody();
34+
$jwks = json_decode($jwksJson, true);
35+
if (!is_array($jwks)) {
36+
throw new \RuntimeException('invalid JWKS JSON');
37+
}
38+
39+
$keys = JWK::parseKeySet($jwks);
40+
$decoded = JWT::decode($token, $keys);
41+
42+
$issuer = isset($options['issuer']) ? (string) $options['issuer'] : '';
43+
if ($issuer !== '' && (!isset($decoded->iss) || (string) $decoded->iss !== $issuer)) {
44+
throw new \RuntimeException('issuer mismatch');
45+
}
46+
47+
$tu = isset($decoded->token_use) ? (string) $decoded->token_use : '';
48+
if ($tu !== self::LICENSE_TOKEN_USE_CLAIM) {
49+
throw new \RuntimeException('Invalid license token: expected token_use "' . self::LICENSE_TOKEN_USE_CLAIM . '"');
50+
}
51+
52+
$expectedAppId = isset($options['expectedAppId']) ? trim((string) $options['expectedAppId']) : '';
53+
if ($expectedAppId !== '') {
54+
$aud = $decoded->aud ?? null;
55+
$ok = false;
56+
if (is_string($aud) && $aud === $expectedAppId) {
57+
$ok = true;
58+
}
59+
if (is_array($aud) && in_array($expectedAppId, $aud, true)) {
60+
$ok = true;
61+
}
62+
if (!$ok) {
63+
throw new \RuntimeException('Invalid license token: aud does not match expected app id');
64+
}
65+
}
66+
67+
return $decoded;
68+
}
69+
}

0 commit comments

Comments
 (0)