Skip to content

Commit 1125bda

Browse files
authored
Merge pull request #22 from Staffbase/NFS-000-jwt-upgrade
Nfs 000 jwt upgrade
2 parents 6e1f2d6 + 4d2fad4 commit 1125bda

File tree

11 files changed

+296
-276
lines changed

11 files changed

+296
-276
lines changed

.github/workflows/php.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
strategy:
1313
matrix:
1414
operating-system: ['ubuntu-latest']
15-
php-versions: ['7.3', '7.4']
15+
php-versions: ['7.4']
1616
phpunit-versions: ['latest']
1717
steps:
1818
- name: Checkout
@@ -24,12 +24,12 @@ jobs:
2424
php-version: ${{ matrix.php-versions }}
2525
extensions: mbstring, intl
2626
ini-values: post_max_size=256M, max_execution_time=180
27-
coverage: none
27+
coverage: none
2828
tools: php-cs-fixer, phpunit:${{ matrix.phpunit-versions }}
29-
29+
3030
- name: Install dependencies
3131
if: steps.composer-cache.outputs.cache-hit != 'true'
3232
run: composer update --prefer-dist --no-progress
3333

3434
- name: Run test suite
35-
run: composer test
35+
run: composer test

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ composer require staffbase/plugins-sdk-php
1717

1818
Dependencies are also managed by Composer. When using this repository keep the following dependencies in mind (cf. [composer.json](composer.json)):
1919

20-
* php: >=7.3
21-
* lcobucci/jwt: ^3.2
20+
* php: >=7.4.0
21+
* lcobucci/jwt: ^4.1
2222

2323
## API Reference
2424

composer.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "staffbase/plugins-sdk-php",
3-
"version": "1.5.4",
3+
"version": "2.0.0",
44
"type": "library",
55
"description": "Staffbase PHP SDK library for plugins.",
66
"keywords": ["staffbase", "plugins", "library", "php", "sdk"],
@@ -13,8 +13,8 @@
1313
}
1414
],
1515
"require": {
16-
"php": "^7.3",
17-
"lcobucci/jwt": "^3.2"
16+
"php": "^7.4",
17+
"lcobucci/jwt": "^4.1"
1818

1919
},
2020
"require-dev": {
@@ -29,6 +29,8 @@
2929
}
3030
},
3131
"scripts": {
32-
"test": "phpunit"
33-
}
32+
"test": "phpunit --colors='always' --debug $PHPUNIT_ARGS",
33+
"lint": "phpcs --standard=PSR2 --extensions=php --ignore=*/vendor/* src test",
34+
"fix": "phpcbf --standard=PSR2 --extensions=php --ignore=*/vendor/* src test"
35+
}
3436
}

src/PluginSession.php

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -195,20 +195,6 @@ protected function closeSession() {
195195
session_write_close();
196196
}
197197

198-
/**
199-
* (DEPRECATED) Translate a base64 string to PEM encoded public key.
200-
*
201-
* @param string $data base64 encoded key
202-
* @deprecated
203-
* @return string PEM encoded key
204-
*/
205-
public static function base64ToPEMPublicKey($data) {
206-
207-
error_log("Warning: PluginSession::base64ToPEMPublicKey() is deprecated. Please switch over to SSOToken::base64ToPEMPublicKey().");
208-
209-
return SSOToken::base64ToPEMPublicKey($data);
210-
}
211-
212198
/**
213199
* Test if a claim is set.
214200
*

src/SSOData.php

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
namespace Staffbase\plugins\sdk;
1616

17+
use DateTimeImmutable;
18+
1719
/**
1820
* A container for the data transmitted from Staffbase app to a plugin
1921
* using the Staffbase single-sign-on.
@@ -91,19 +93,22 @@ protected function getClaimSafe($name) {
9193
}
9294

9395
/**
94-
* Get targeted audience of the token.
96+
* Get targeted audience of the token. Currently only
97+
* one audience is supported.
9598
*
9699
* @return null|string
97100
*/
98101
public function getAudience() {
99102

100-
return $this->getClaimSafe(self::CLAIM_AUDIENCE);
103+
$audience = $this->getClaimSafe(self::CLAIM_AUDIENCE);
104+
105+
return !is_array($audience) ? $audience : $audience[0] ?? null;
101106
}
102107

103108
/**
104109
* Get the time when the token expires.
105110
*
106-
* @return int
111+
* @return DateTimeImmutable
107112
*/
108113
public function getExpireAtTime() {
109114

@@ -113,7 +118,7 @@ public function getExpireAtTime() {
113118
/**
114119
* Get the time when the token starts to be valid.
115120
*
116-
* @return int
121+
* @return DateTimeImmutable
117122
*/
118123
public function getNotBeforeTime() {
119124

@@ -123,7 +128,7 @@ public function getNotBeforeTime() {
123128
/**
124129
* Get the time when the token was issued.
125130
*
126-
* @return int
131+
* @return DateTimeImmutable
127132
*/
128133
public function getIssuedAtTime() {
129134

src/SSOToken.php

Lines changed: 91 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,19 @@
1414

1515
namespace Staffbase\plugins\sdk;
1616

17-
use Lcobucci\JWT\Token;
18-
use Lcobucci\JWT\Parser;
17+
use DateInterval;
18+
use Lcobucci\Clock\SystemClock;
19+
use Lcobucci\JWT\Configuration;
1920
use Lcobucci\JWT\Signer\Key;
20-
use Lcobucci\JWT\ValidationData;
21-
use Lcobucci\JWT\Claim\Validatable;
21+
use Lcobucci\JWT\Token;
22+
use Lcobucci\JWT\Signer\Key\InMemory;
23+
use Lcobucci\JWT\Validation\Constraint\SignedWith;
24+
use Lcobucci\JWT\Validation\Constraint\StrictValidAt;
25+
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
2226
use Lcobucci\JWT\Signer\Rsa\Sha256;
2327
use Staffbase\plugins\sdk\Exceptions\SSOException;
2428
use Staffbase\plugins\sdk\Exceptions\SSOAuthenticationException;
29+
use Staffbase\plugins\sdk\Validation\HasInstanceId;
2530

2631
/**
2732
* A container which is able to decrypt and store the data transmitted
@@ -32,8 +37,17 @@ class SSOToken extends SSOData
3237
/**
3338
* @var Token $token
3439
*/
35-
private $token = null;
40+
private ?Token $token = null;
41+
42+
/**
43+
* @var Key $key
44+
*/
45+
private Key $key;
3646

47+
/**
48+
* @var Configuration $config
49+
*/
50+
private Configuration $config;
3751
/**
3852
* Constructor
3953
*
@@ -43,55 +57,75 @@ class SSOToken extends SSOData
4357
*
4458
* @throws SSOException on invalid parameters.
4559
*/
46-
public function __construct($appSecret, $tokenData, $leeway = 0) {
60+
public function __construct(string $appSecret, string $tokenData, ?int $leeway = 0) {
4761

4862
if (!trim($appSecret))
4963
throw new SSOException('Parameter appSecret for SSOToken is empty.');
5064

5165
if (!trim($tokenData))
5266
throw new SSOException('Parameter tokenData for SSOToken is empty.');
5367

54-
if (!is_numeric($leeway))
55-
throw new SSOException('Parameter leeway has to be numeric.');
56-
57-
// convert secret to PEM if its a plain base64 string and does not yield an url
58-
if(strpos(trim($appSecret),'-----') !== 0 && strpos(trim($appSecret), 'file://') !==0 )
59-
$appSecret = self::base64ToPEMPublicKey($appSecret);
68+
$this->key = $this->getKey(trim($appSecret));
69+
$this->config = Configuration::forSymmetricSigner(new Sha256(), $this->key);
6070

61-
$this->parseToken($appSecret, $tokenData, $leeway);
71+
$this->parseToken($tokenData, $leeway);
6272
}
6373

6474
/**
6575
* Creates and validates an SSO token.
6676
*
67-
* @param string $appSecret Either a PEM formatted key or a file:// URL of the same.
6877
* @param string $tokenData The token text.
6978
* @param int $leeway count of seconds added to current timestamp
7079
*
7180
* @throws SSOAuthenticationException if the parsing/verification/validation of the token fails.
7281
*/
73-
protected function parseToken($appSecret, $tokenData, $leeway) {
74-
82+
protected function parseToken(string $tokenData, int $leeway) {
7583
// parse text
76-
$this->token = (new Parser())->parse((string) $tokenData);
77-
78-
// verify signature
79-
$signer = new Sha256();
80-
$key = new Key($appSecret);
84+
$this->token = $this->config->parser()->parse($tokenData);
85+
86+
$constrains = [
87+
new StrictValidAt(SystemClock::fromUTC(), $this->getLeewayInterval($leeway)),
88+
new SignedWith(new Sha256(),$this->key),
89+
new HasInstanceId()
90+
];
91+
92+
try {
93+
$this->config->validator()->assert($this->token, ...$constrains);
94+
} catch (RequiredConstraintsViolated $violation) {
95+
throw new SSOAuthenticationException($violation->getMessage());
96+
}
97+
}
8198

82-
if (!$this->token->verify($signer, $key))
83-
throw new SSOAuthenticationException('Token verification failed.');
99+
/**
100+
* Test if a claim is set.
101+
*
102+
* @param string $claim name.
103+
*
104+
* @return boolean
105+
*/
106+
protected function hasClaim($claim) {
107+
return $this->token->claims()->has($claim);
108+
}
84109

85-
// validate claims
86-
$data = new ValidationData(time(), $leeway); // iat, nbf and exp are validated by default
110+
/**
111+
* Get a claim without checking for existence.
112+
*
113+
* @param string $claim name.
114+
*
115+
* @return mixed
116+
*/
117+
protected function getClaim($claim) {
118+
return $this->token->claims()->get($claim);
119+
}
87120

88-
if (!$this->token->validate($data)) {
89-
$this->throwVerboseException($data);
90-
}
121+
/**
122+
* Get an array of all available claims and their values.
123+
*
124+
* @return array
125+
*/
126+
protected function getAllClaims() {
91127

92-
// its a security risk to work with tokens lacking instance id
93-
if (!trim($this->getInstanceId()))
94-
throw new SSOAuthenticationException('Token lacks instance id.');
128+
return $this->token->claims()->all();
95129
}
96130

97131
/**
@@ -101,7 +135,7 @@ protected function parseToken($appSecret, $tokenData, $leeway) {
101135
*
102136
* @return string PEM encoded key
103137
*/
104-
public static function base64ToPEMPublicKey($data) {
138+
public static function base64ToPEMPublicKey(string $data): string {
105139

106140
$data = strtr($data, array(
107141
"\r" => "",
@@ -115,80 +149,39 @@ public static function base64ToPEMPublicKey($data) {
115149
}
116150

117151
/**
118-
* Validate the token with more verbose exceptions
152+
* Decides between the new key methods, the JWT library offers
119153
*
120-
* Due to minor shortcomings of the library we have to redo the validation
121-
* manually to get the reason for the failure and propagate it.
122-
* We emulate the validation process for the v3.x of the library.
123-
*
124-
* This will most likely have to change on library upgrade either
125-
* by using then supported verbosity or reimplementing validation
126-
* as done in the new flow.
127-
*
128-
* @param ValidationData $data to validate against
129-
*
130-
* @throws SSOAuthenticationException always.
154+
* @param string $appSecret
155+
* @return Key
131156
*/
132-
protected function throwVerboseException(ValidationData $data) {
133-
134-
foreach ($this->token->getClaims() as $claim) {
135-
if ($claim instanceof Validatable) {
136-
if (!$claim->validate($data)) {
137-
138-
$claimName = $claim->getName();
139-
$claimValue = $claim->getValue();
140-
141-
// get the short class-name of the validatable claim
142-
$segments = explode('\\', get_class($claim));
143-
$operator = array_pop($segments);
144-
$operand = $data->get($claimName);
145-
146-
throw new SSOAuthenticationException("Token Validation failed on claim '$claimName' $claimValue $operator $operand.");
147-
}
148-
}
157+
private function getKey(string $appSecret): Key {
158+
if(strpos($appSecret,'-----') === 0 ) {
159+
$key = InMemory::plainText($appSecret);
160+
} else if (strpos($appSecret, 'file://') === 0 ) {
161+
$key = InMemory::file($appSecret);
162+
} else {
163+
$key = InMemory::plainText($this->base64ToPEMPublicKey($appSecret));
149164
}
150-
151-
// unknown reason, probably an addition to used library
152-
throw new SSOAuthenticationException('Token Validation failed.');
165+
return $key;
153166
}
154167

155168
/**
156-
* Test if a claim is set.
169+
* Formats the leeway integer value into a DateInterval as this is
170+
* needed by the JWT library
157171
*
158-
* @param string $claim name.
159-
*
160-
* @return boolean
161-
*/
162-
protected function hasClaim($claim) {
163-
164-
return $this->token->hasClaim($claim);
165-
}
166-
167-
/**
168-
* Get a claim without checking for existence.
169-
*
170-
* @param string $claim name.
171-
*
172-
* @return mixed
173-
*/
174-
protected function getClaim($claim) {
175-
176-
return $this->token->getClaim($claim);
177-
}
178-
179-
/**
180-
* Get an array of all available claims and their values.
181-
*
182-
* @return array
172+
* @param int $leeway count of seconds added to current timestamp
173+
* @return DateInterval DateInterval
183174
*/
184-
protected function getAllClaims() {
185-
186-
$res = [];
187-
$claims = $this->token->getClaims();
188-
189-
foreach($claims as $claim)
190-
$res[$claim->getName()] = $claim->getValue();
175+
private function getLeewayInterval (int $leeway): DateInterval {
176+
$leewayInterval = "PT{$leeway}S";
177+
178+
try {
179+
$interval = new DateInterval($leewayInterval);
180+
} catch (\Exception $e) {
181+
error_log("Wrong date interval $leewayInterval");
182+
$interval = new DateInterval('PT0S');
183+
}
191184

192-
return $res;
185+
return $interval;
193186
}
194187
}

0 commit comments

Comments
 (0)