Skip to content

Commit 5d1461a

Browse files
committed
Add first draft JTI Validator.
1 parent 4a8178b commit 5d1461a

1 file changed

Lines changed: 86 additions & 0 deletions

File tree

src/Utils/JtiValidator.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
namespace Pdsinterop\Solid\Auth\Utils;
4+
5+
use DateInterval;
6+
use Lcobucci\JWT\Token\InvalidTokenStructure;
7+
use Pdsinterop\Solid\Auth\JtiStorageInterface;
8+
9+
/**
10+
* Validates whether a provided JTI (JWT ID) is valid.
11+
*
12+
* @see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-dpop
13+
*/
14+
class JtiValidator
15+
{
16+
////////////////////////////// CLASS PROPERTIES \\\\\\\\\\\\\\\\\\\\\\\\\\\\
17+
18+
/**
19+
* Maximum allowed amount of seconds a JTI is valid
20+
*/
21+
private int $maxIntervalSeconds = 600; // @TODO: Time::MINUTES_10
22+
23+
//////////////////////////////// PUBLIC API \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
24+
25+
/**
26+
* @param JtiStorageInterface $jtiStorage
27+
* @param DateInterval $interval
28+
*
29+
* @throw \InvalidArgumentException When the provided Interval is not valid
30+
*/
31+
final public function __construct(private JtiStorageInterface $jtiStorage, private DateInterval $interval)
32+
{
33+
$intervalSeconds = $this->interval->format('s');
34+
35+
// @CHECKME: Is there a maximum validity period? Does the spec say anything about this?
36+
// Or do we not need to check and should we just trust the user?
37+
// @FIXME: Use DateTime / DateInterval objects rather than math to compare times
38+
if ($intervalSeconds > $this->maxIntervalSeconds) {
39+
$message = vsprintf(
40+
'Given time interval (%s) is larger than the allowed maximum (%s)',
41+
[
42+
'interval' => $intervalSeconds,
43+
'maximum' => $this->maxIntervalSeconds,
44+
]
45+
);
46+
47+
throw new \InvalidArgumentException($message);
48+
}
49+
}
50+
51+
public function validate($jti, $targetUri): bool
52+
{
53+
$isValid = false;
54+
55+
$strlen = mb_strlen($jti);
56+
/* At least 96 bits of pseudorandom data are required,
57+
* which is 12 characters (or 24 hexadecimal characters)
58+
* The upper limit is chosen based on maximum field length in common database storage types (varchar)
59+
*/
60+
if ($strlen > 12 && $strlen < 256) {
61+
$isValid = $this->jtiStorage->retrieve($jti, $targetUri) === false;
62+
63+
if ($isValid === true) {
64+
// @CHECKME: Should we catch exceptions here? Catch them in the DPOP calll? Allow them to bubble up?
65+
$this->jtiStorage->store($jti, $targetUri);
66+
}
67+
68+
// @CHECKME: Should rotation be checked before or after storing the JTI?
69+
if ($this->shouldRotate()) {
70+
$this->jtiStorage->rotateBuckets();
71+
}
72+
}
73+
74+
return $isValid;
75+
}
76+
////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\
77+
private function shouldRotate(): bool
78+
{
79+
$shouldRotate = false;
80+
81+
// @CHECKME: How to round of the interval? Count up from X:00 and add increments?
82+
// This would basically be modulo?
83+
84+
return $shouldRotate;
85+
}
86+
}

0 commit comments

Comments
 (0)