Skip to content

Commit 378abf0

Browse files
committed
Change Dpop to use the JtiValidator + unit-tests.
1 parent 5d1461a commit 378abf0

4 files changed

Lines changed: 102 additions & 27 deletions

File tree

src/JtiStorageInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* This interface defines methods that a storage layer need to implement
77
* in order to allow the JTI Service to validate JTI tokens.
88
*
9-
* Such a token is a string between 12 and [..] characters of length.
9+
* Such a token is a string between 12 and 256 characters of length.
1010
*/
1111
interface JtiStorageInterface
1212
{

src/Utils/DPop.php

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@
1818
*/
1919
class DPop {
2020

21+
private JtiValidator $jtiValidator;
22+
23+
public function __construct(JtiValidator $jtiValidator)
24+
{
25+
$this->jtiValidator = $jtiValidator;
26+
}
27+
2128
/**
2229
* This method fetches the WebId from a request and verifies
2330
* that the request has a valid DPoP token that matches
@@ -223,13 +230,20 @@ public function validateDpop($dpop, $request) {
223230

224231
$leeway = new \DateInterval("PT60S"); // allow 60 seconds clock skew
225232
$clock = SystemClock::fromUTC();
226-
$validationsConstraints[] = new LooseValidAt($clock, $leeway); // It will use the current time to validate (iat, nbf and exp)
233+
$validationConstraints[] = new LooseValidAt($clock, $leeway); // It will use the current time to validate (iat, nbf and exp)
227234
if (!$jwtConfig->validator()->validate($dpop, ...$validationConstraints)) {
228235
$jwtConfig->validator()->assert($dpop, ...$validationConstraints); // throws an explanatory exception
229236
}
230237

231238
// 9. that, within a reasonable consideration of accuracy and resource utilization, a JWT with the same "jti" value has not been received previously (see Section 9.1).
232-
// TODO: Check if we know the jti;
239+
$jti = $dpop->claims()->get("jti");
240+
if ($jti === null) {
241+
throw new \Exception("jti is missing");
242+
}
243+
$isJtiValid = $this->jtiValidator->validate($jti, (string) $request->getUri());
244+
if (! $isJtiValid) {
245+
throw new \Exception("jti is invalid");
246+
}
233247

234248
// 10. that, if used with an access token, it also contains the 'ath' claim, with a hash of the access token
235249
// TODO: implement
@@ -245,11 +259,10 @@ private function getSubjectFromJwt($jwt) {
245259
throw new \Exception("Invalid JWT token", 409, $e);
246260
}
247261

248-
// @FIXME: What happens when "sub" is not provided?
249262
$sub = $jwt->claims()->get("sub");
250-
if ($sub === null) {
251-
throw new \Exception('Invalid token: Missing "SUB');
252-
}
263+
if ($sub === null) {
264+
throw new \Exception('Invalid token: Missing "SUB');
265+
}
253266
return $sub;
254267
}
255268
}

src/Utils/JtiValidator.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ public function validate($jti, $targetUri): bool
7373

7474
return $isValid;
7575
}
76+
7677
////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\
78+
7779
private function shouldRotate(): bool
7880
{
7981
$shouldRotate = false;

tests/unit/Utils/DPOPTest.php

Lines changed: 80 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
/**
1111
* @coversDefaultClass \Pdsinterop\Solid\Auth\Utils\DPop
1212
* @covers ::<!public>
13+
* @covers ::__construct
1314
*
1415
* @uses \Pdsinterop\Solid\Auth\Utils\Base64Url
16+
* @uses \Pdsinterop\Solid\Auth\Utils\JtiValidator
1517
*/
1618
class DPOPTest extends AbstractTestCase
1719
{
@@ -74,11 +76,22 @@ private function getWrongKey()
7476
/////////////////////////////////// TESTS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
7577

7678
/**
77-
* @testdox Dpop SHOULD be created WHEN instantiated without parameters
79+
* @testdox Dpop SHOULD complain WHEN instantiated without JtiValidator
80+
*/
81+
final public function testInstantiationWithoutJtiValidator(): void
82+
{
83+
$this->expectArgumentCountError(1);
84+
85+
new DPop();
86+
}
87+
88+
/**
89+
* @testdox Dpop SHOULD be created WHEN instantiated with JtiValidator
7890
*/
7991
final public function testInstantiation(): void
8092
{
81-
$actual = new DPop();
93+
$mockJtiValidator = $this->createMockJtiValidator();
94+
$actual = new DPop($mockJtiValidator);
8295
$expected = DPop::class;
8396

8497
$this->assertInstanceOf($expected, $actual);
@@ -93,7 +106,8 @@ final public function testValidateDpopWithoutJwt(): void
93106
{
94107
$this->expectArgumentCountError(1);
95108

96-
$dpop = new DPop();
109+
$mockJtiValidator = $this->createMockJtiValidator();
110+
$dpop = new DPop($mockJtiValidator);
97111

98112
$dpop->validateDpop();
99113
}
@@ -107,7 +121,8 @@ final public function testValidateDpopWithoutRequest(): void
107121
{
108122
$this->expectArgumentCountError(2);
109123

110-
$dpop = new DPop();
124+
$mockJtiValidator = $this->createMockJtiValidator();
125+
$dpop = new DPop($mockJtiValidator);
111126

112127
$dpop->validateDpop('mock jwt');
113128
}
@@ -122,7 +137,9 @@ public function testValidateDpopWithWrongTyp(): void
122137
$this->dpop['header']['typ'] = 'jwt';
123138
$token = $this->sign($this->dpop);
124139

125-
$dpop = new DPop();
140+
$mockJtiValidator = $this->createMockJtiValidator();
141+
$dpop = new DPop($mockJtiValidator);
142+
126143
$this->expectException(\Exception::class);
127144
$this->expectExceptionMessage('typ is not dpop+jwt');
128145

@@ -139,7 +156,9 @@ public function testValidateDpopWithAlgNone(): void
139156
$this->dpop['header']['alg'] = 'none';
140157
$token = $this->sign($this->dpop);
141158

142-
$dpop = new DPop();
159+
$mockJtiValidator = $this->createMockJtiValidator();
160+
$dpop = new DPop($mockJtiValidator);
161+
143162
$this->expectException(\Exception::class);
144163
$this->expectExceptionMessage('alg is none');
145164
$result = $dpop->validateDpop($token['token'], $this->serverRequest);
@@ -161,7 +180,9 @@ public function testValidateDpopWithWrongKey(): void
161180
];
162181
$token = $this->sign($this->dpop);
163182

164-
$dpop = new DPop();
183+
$mockJtiValidator = $this->createMockJtiValidator();
184+
$dpop = new DPop($mockJtiValidator);
185+
165186
try {
166187
$dpop->validateDpop($token['token'], $this->serverRequest);
167188
} catch(RequiredConstraintsViolated $e) {
@@ -177,10 +198,21 @@ public function testValidateDpopWithWrongKey(): void
177198
*/
178199
public function testValidateDpopWithCorrectToken(): void
179200
{
201+
$this->dpop['payload']['jti'] = 'mock jti';
202+
180203
$token = $this->sign($this->dpop);
181204

182-
$dpop = new DPop();
205+
$mockJtiValidator = $this->createMockJtiValidator();
206+
207+
$mockJtiValidator->expects($this->once())
208+
->method('validate')
209+
->willReturn(true)
210+
;
211+
212+
$dpop = new DPop($mockJtiValidator);
213+
183214
$result = $dpop->validateDpop($token['token'], $this->serverRequest);
215+
184216
$this->assertTrue($result);
185217
}
186218

@@ -191,7 +223,8 @@ public function testValidateDpopWithCorrectToken(): void
191223
*/
192224
final public function testGetWebIdWithoutRequest(): void
193225
{
194-
$dpop = new DPop();
226+
$mockJtiValidator = $this->createMockJtiValidator();
227+
$dpop = new DPop($mockJtiValidator);
195228

196229
$this->expectArgumentCountError(1);
197230

@@ -205,8 +238,8 @@ final public function testGetWebIdWithoutRequest(): void
205238
*/
206239
final public function testGetWebIdWithoutHttpAuthorizationHeader(): void
207240
{
208-
209-
$dpop = new DPop();
241+
$mockJtiValidator = $this->createMockJtiValidator();
242+
$dpop = new DPop($mockJtiValidator);
210243

211244
$request = new ServerRequest(array(),array(), $this->url);
212245

@@ -222,7 +255,8 @@ final public function testGetWebIdWithoutHttpAuthorizationHeader(): void
222255
*/
223256
final public function testGetWebIdWithIncorrectAuthHeaderFormat(): void
224257
{
225-
$dpop = new DPop();
258+
$mockJtiValidator = $this->createMockJtiValidator();
259+
$dpop = new DPop($mockJtiValidator);
226260

227261
$request = new ServerRequest(array('HTTP_AUTHORIZATION' => 'IncorrectAuthorizationFormat'),array(), $this->url);
228262

@@ -239,7 +273,8 @@ final public function testGetWebIdWithIncorrectAuthHeaderFormat(): void
239273
*/
240274
final public function testGetWebIdWithInvalidJwt(): void
241275
{
242-
$dpop = new DPop();
276+
$mockJtiValidator = $this->createMockJtiValidator();
277+
$dpop = new DPop($mockJtiValidator);
243278

244279
$this->expectException(\Exception::class);
245280
$this->expectExceptionMessage('Invalid JWT token');
@@ -256,7 +291,8 @@ final public function testGetWebIdWithInvalidJwt(): void
256291
*/
257292
final public function testGetWebIdWithoutDpop(): void
258293
{
259-
$dpop = new DPop();
294+
$mockJtiValidator = $this->createMockJtiValidator();
295+
$dpop = new DPop($mockJtiValidator);
260296

261297
$request = new ServerRequest(array('HTTP_AUTHORIZATION' => "Basic YWxhZGRpbjpvcGVuc2VzYW1l"),array(), $this->url);
262298

@@ -283,7 +319,8 @@ final public function testGetWebIdWithDpopWithoutKeyId(): void
283319

284320
$token = $this->sign($this->dpop);
285321

286-
$dpop = new DPop();
322+
$mockJtiValidator = $this->createMockJtiValidator();
323+
$dpop = new DPop($mockJtiValidator);
287324

288325
$request = new ServerRequest(array(
289326
'HTTP_AUTHORIZATION' => "dpop {$token['token']}",
@@ -311,7 +348,8 @@ final public function testGetWebIdWithDpopWithoutConfirmationClaim(): void
311348

312349
$token = $this->sign($this->dpop);
313350

314-
$dpop = new DPop();
351+
$mockJtiValidator = $this->createMockJtiValidator();
352+
$dpop = new DPop($mockJtiValidator);
315353

316354
$request = new ServerRequest(array(
317355
'HTTP_AUTHORIZATION' => "dpop {$token['token']}",
@@ -340,7 +378,8 @@ final public function testGetWebIdWithDpopWithoutThumbprint(): void
340378

341379
$token = $this->sign($this->dpop);
342380

343-
$dpop = new DPop();
381+
$mockJtiValidator = $this->createMockJtiValidator();
382+
$dpop = new DPop($mockJtiValidator);
344383

345384
$request = new ServerRequest(array(
346385
'HTTP_AUTHORIZATION' => "dpop {$token['token']}",
@@ -369,7 +408,8 @@ final public function testGetWebIdWithDpopWithMismatchingThumbprintAndKeyId(): v
369408

370409
$token = $this->sign($this->dpop);
371410

372-
$dpop = new DPop();
411+
$mockJtiValidator = $this->createMockJtiValidator();
412+
$dpop = new DPop($mockJtiValidator);
373413

374414
$request = new ServerRequest(array(
375415
'HTTP_AUTHORIZATION' => "dpop {$token['token']}",
@@ -397,7 +437,8 @@ final public function testGetWebIdWithDpopWithoutSub(): void
397437

398438
$token = $this->sign($this->dpop);
399439

400-
$dpop = new DPop();
440+
$mockJtiValidator = $this->createMockJtiValidator();
441+
$dpop = new DPop($mockJtiValidator);
401442

402443
$request = new ServerRequest(array(
403444
'HTTP_AUTHORIZATION' => "dpop {$token['token']}",
@@ -422,11 +463,19 @@ final public function testGetWebIdWithDpop(): void
422463
{
423464
$this->dpop['header']['jwk'][JwkParameter::KEY_ID] = self::MOCK_THUMBPRINT;
424465
$this->dpop['payload']['cnf'] = ['jkt' => self::MOCK_THUMBPRINT];
466+
$this->dpop['payload']['jti'] = 'mock jti';
425467
$this->dpop['payload']['sub'] = self::MOCK_SUBJECT;
426468

427469
$token = $this->sign($this->dpop);
428470

429-
$dpop = new DPop();
471+
$mockJtiValidator = $this->createMockJtiValidator();
472+
473+
$mockJtiValidator->expects($this->once())
474+
->method('validate')
475+
->willReturn(true)
476+
;
477+
478+
$dpop = new DPop($mockJtiValidator);
430479

431480
$request = new ServerRequest(array(
432481
'HTTP_AUTHORIZATION' => "dpop {$token['token']}",
@@ -438,6 +487,17 @@ final public function testGetWebIdWithDpop(): void
438487
$this->assertEquals(self::MOCK_SUBJECT, $actual);
439488
}
440489

490+
////////////////////////////// MOCKS AND STUBS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\
491+
492+
private function createMockJtiValidator()
493+
{
494+
$mockJtiValidator = $this->getMockBuilder(JtiValidator::class)
495+
->disableOriginalConstructor()
496+
->getMock();
497+
498+
return $mockJtiValidator;
499+
}
500+
441501
///////////////////////////// HELPER FUNCTIONS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\
442502

443503
protected function sign($dpop, $privateKey = null)

0 commit comments

Comments
 (0)