Skip to content

Commit ba89585

Browse files
authored
Merge pull request #26 from pdsinterop/validateDPOP
added tests for validateDPOP, fixed some validations
2 parents 6517a1b + 6cbd9f7 commit ba89585

4 files changed

Lines changed: 238 additions & 40 deletions

File tree

src/Utils/DPop.php

Lines changed: 66 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,44 @@
99
use Lcobucci\JWT\Signer\Key\InMemory;
1010
use Lcobucci\JWT\Signer\Rsa\Sha256;
1111
use Lcobucci\JWT\Validation\Constraint\LooseValidAt;
12+
use Lcobucci\JWT\Validation\Constraint\SignedWith;
1213

1314
use Jose\Component\Core\JWK;
1415
use Jose\Component\Core\Util\ECKey;
1516
use Jose\Component\Core\Util\RSAKey;
1617

18+
/**
19+
* This class contains code to fetch the WebId from a request
20+
* It also verifies that the request has a valid DPoP token
21+
* that matches the access token
22+
*/
1723
class DPop {
24+
25+
/**
26+
* This method fetches the WebId from a request and verifies
27+
* that the request has a valid DPoP token that matches
28+
* the access token.
29+
* @param Psr\Http\Message\ServerRequestInterface $request Server Request
30+
* @return string the WebId, or "public" if no WebId is found
31+
* @throws \Exception "Invalid token" when the DPoP token is invalid
32+
* @throws \Exception "Missng DPoP token" when the DPoP token is missing, but the Authorisation header in the request specifies it
33+
*/
1834
public function getWebId($request) {
1935
$auth = explode(" ", $request->getServerParams()['HTTP_AUTHORIZATION']);
2036
$jwt = $auth[1] ?? false;
2137

2238
if (strtolower($auth[0]) == "dpop") {
2339
$dpop = $request->getServerParams()['HTTP_DPOP'];
40+
//@FIXME: check that there is just one DPoP token in the request
2441
if ($dpop) {
2542
$dpopKey = $this->getDpopKey($dpop, $request);
26-
if (!$this->validateJwtDpop($jwt, $dpopKey)) {
27-
throw new \Exception("Invalid token");
43+
try {
44+
$this->validateJwtDpop($jwt, $dpopKey);
45+
} catch (Lcobucci\JWT\Validation\RequiredConstraintsViolated $e) {
46+
throw new \Exception("Invalid token", $e);
2847
}
48+
} else {
49+
throw new \Exception("Missing DPoP token");
2950
}
3051
}
3152

@@ -38,16 +59,21 @@ public function getWebId($request) {
3859
return $webId;
3960
}
4061

62+
/**
63+
* Returns the "kid" from the "jwk" header in the DPoP token.
64+
* The DPoP token must be valid.
65+
* @param string $dpop The DPoP token
66+
* @param Psr\Http\Message\ServerRequestInterface $request Server Request
67+
* @return string the "kid" from the "jwk" header in the DPoP token.
68+
* @throws Lcobucci\JWT\Validation\RequiredConstraintsViolated
69+
*/
4170
public function getDpopKey($dpop, $request) {
42-
//error_log("11");
4371
$this->validateDpop($dpop, $request);
44-
//error_log("22");
4572

4673
// 1. the string value is a well-formed JWT,
4774
$jwtConfig = $configuration = Configuration::forUnsecuredSigner();
4875
$dpop = $jwtConfig->parser()->parse($dpop);
49-
$jwk = $dpop->headers()->get("jwk");
50-
//error_log(print_r($jwk, true));
76+
$jwk = $dpop->headers()->get("jwk");
5177

5278
return $jwk['kid'];
5379
}
@@ -58,17 +84,22 @@ private function validateJwtDpop($jwt, $dpopKey) {
5884
$cnf = $jwt->claims()->get("cnf");
5985

6086
if ($cnf['jkt'] == $dpopKey) {
61-
//error_log("dpopKey matches");
6287
return true;
6388
}
64-
//error_log("dpopKey mismatch");
65-
//error_log(print_r($cnf, true));
66-
//error_log($dpopKey);
6789

90+
//@FIXME: add check for "ath" claim in DPoP token, per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-dpop#section-7
6891
return false;
6992
}
7093

71-
private function validateDpop($dpop, $request) {
94+
/**
95+
* Validates that the DPOP token matches all requirements from
96+
* https://datatracker.ietf.org/doc/html/draft-ietf-oauth-dpop#section-4.2
97+
* @param string $dpop The DPOP token
98+
* @param Psr\Http\Message\ServerRequestInterface $request Server Request
99+
* @return bool True if the DPOP token is valid, false otherwise
100+
* @throws Lcobucci\JWT\Validation\RequiredConstraintsViolated
101+
*/
102+
public function validateDpop($dpop, $request) {
72103
/*
73104
4.2. Checking DPoP Proofs
74105
To check if a string that was received as part of an HTTP Request is
@@ -91,34 +122,43 @@ private function validateDpop($dpop, $request) {
91122
9. that, within a reasonable consideration of accuracy and resource
92123
utilization, a JWT with the same "jti" value has not been
93124
received previously (see Section 9.1).
125+
10. that, if used with an access token, it also contains the 'ath'
126+
claim, with a hash of the access token
94127
*/
95-
//error_log("1");
96128
// 1. the string value is a well-formed JWT,
97129
$jwtConfig = $configuration = Configuration::forUnsecuredSigner();
98130
$dpop = $jwtConfig->parser()->parse($dpop);
99131

100-
//error_log("2");
101132
// 2. all required claims are contained in the JWT,
102133
$htm = $dpop->claims()->get("htm"); // http method
134+
if (!$htm) {
135+
throw new \Exception("missing htm");
136+
}
103137
$htu = $dpop->claims()->get("htu"); // http uri
138+
if (!$htu) {
139+
throw new \Exception("missing htu");
140+
}
104141
$typ = $dpop->headers()->get("typ");
142+
if (!$typ) {
143+
throw new \Exception("missing typ");
144+
}
105145
$alg = $dpop->headers()->get("alg");
146+
if (!$alg) {
147+
throw new \Exception("missing alg");
148+
}
106149

107-
//error_log("3");
108150
// 3. the "typ" field in the header has the value "dpop+jwt",
109151
if ($typ != "dpop+jwt") {
110152
throw new \Exception("typ is not dpop+jwt");
111153
}
112154

113-
//error_log("4");
114155
// 4. the algorithm in the header of the JWT indicates an asymmetric
115156
// digital signature algorithm, is not "none", is supported by the
116157
// application, and is deemed secure,
117158
if ($alg == "none") {
118159
throw new \Exception("alg is none");
119160
}
120161

121-
//error_log("5");
122162
// 5. that the JWT is signed using the public key contained in the
123163
// "jwk" header of the JWT,
124164
$jwk = $dpop->headers()->get("jwk");
@@ -130,59 +170,48 @@ private function validateDpop($dpop, $request) {
130170
break;
131171
case "ES256":
132172
$pem = \Jose\Component\Core\Util\ECKey::convertToPEM($webTokenJwk);
133-
$signer = \Lcobucci\JWT\Signer\Ecdsa\Sha256::create();
173+
$signer = \Lcobucci\JWT\Signer\Ecdsa\Sha256::create();
134174
break;
135175
default:
136176
throw new \Exception("unsupported algorithm");
137177
break;
138178
}
139179
$key = InMemory::plainText($pem);
140-
$jwtConfig = Configuration::forSymmetricSigner($signer, InMemory::plainText($pem));
141-
142-
// FIXME: Add constraints;
143-
// $constraint = new LooseValidAt($clock, $leeway); // It will use the current time to validate (iat, nbf and exp)
144-
// $jwtConfig->setValidationConstraints($constraint);
145-
// if (!$jwtConfig->validator()->validate($dpop, ...$jwtConfig->validationConstraints())) {
146-
// throw new \Exception("invalid signature");
147-
// }
180+
$validationConstraints = [];
181+
$validationConstraints[] = new SignedWith($signer, $key);
148182

149-
//error_log("6");
150183
// 6. the "htm" claim matches the HTTP method value of the HTTP request
151184
// in which the JWT was received (case-insensitive),
152185
if (strtolower($htm) != strtolower($request->getMethod())) {
153186
throw new \Exception("htm http method is invalid");
154187
}
155188

156-
//error_log("7");
157189
// 7. the "htu" claims matches the HTTP URI value for the HTTP request
158190
// in which the JWT was received, ignoring any query and fragment
159191
// parts,
160192
$requestedPath = (string)$request->getUri();
161193
$requestedPath = preg_replace("/[?#].*$/", "", $requestedPath);
162-
// FIXME: Remove this; it was disabled for testing with a server running on 443 internally but accessible on :444
163-
$htu = str_replace(":444", "", $htu);
164-
$requestedPath = str_replace(":444", "", $requestedPath);
165194

166195
//error_log("REQUESTED HTU $htu");
167196
//error_log("REQUESTED PATH $requestedPath");
168197
if ($htu != $requestedPath) {
169198
throw new \Exception("htu does not match requested path");
170199
}
171200

172-
//error_log("8");
173201
// 8. the token was issued within an acceptable timeframe (see Section 9.1), and
174202

175203
$leeway = new \DateInterval("PT60S"); // allow 60 seconds clock skew
176204
$clock = SystemClock::fromUTC();
177-
$constraint = new LooseValidAt($clock, $leeway); // It will use the current time to validate (iat, nbf and exp)
178-
$jwtConfig->setValidationConstraints($constraint);
179-
if (!$jwtConfig->validator()->validate($dpop, ...$jwtConfig->validationConstraints())) {
180-
throw new \Exception("token timing is invalid");
205+
$validationsConstraints[] = new LooseValidAt($clock, $leeway); // It will use the current time to validate (iat, nbf and exp)
206+
if (!$jwtConfig->validator()->validate($dpop, ...$validationConstraints)) {
207+
$jwtConfig->validator()->assert($dpop, ...$validationConstraints); // throws an explanatory exception
181208
}
182209

183210
// 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).
184-
// FIXME: Check if we know the jti;
185-
//error_log("9");
211+
// TODO: Check if we know the jti;
212+
213+
// 10. that, if used with an access token, it also contains the 'ath' claim, with a hash of the access token
214+
// TODO: implement
186215

187216
return true;
188217
}

src/Utils/Jwks.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@ final public function jsonSerialize()
3939
*
4040
* @return array
4141
*/
42-
private function createKey(string $certificate, $subject) : array
42+
private function createKey(string $certificate, $subject, $exponent) : array
4343
{
4444
return [
4545
JwkParameter::ALGORITHM => 'RS256',
4646
JwkParameter::KEY_ID => md5($certificate),
4747
JwkParameter::KEY_TYPE => 'RSA',
48-
RsaParameter::PUBLIC_EXPONENT => 'AQAB', // Hard-coded as `Base64Url::encode($keyInfo['rsa']['e'])` tends to be empty...
48+
RsaParameter::PUBLIC_EXPONENT => Base64Url::encode($exponent),
4949
RsaParameter::PUBLIC_MODULUS => Base64Url::encode($subject),
5050
JwkParameter::KEY_OPERATIONS => array("verify"),
5151
];
@@ -70,7 +70,7 @@ private function create() : array
7070
$key = openssl_pkey_get_public($certificate);
7171
$keyInfo = openssl_pkey_get_details($key);
7272

73-
$jwks['keys'][] = $this->createKey($certificate, $keyInfo['rsa']['n']);
73+
$jwks['keys'][] = $this->createKey($certificate, $keyInfo['rsa']['n'], $keyInfo['rsa']['e']);
7474
});
7575

7676
return $jwks;

tests/fixtures/keys/wrong.key

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiGzr51tpH6F2HwMUSKCX
3+
zxyYfZyNJpKZWzNb3AMrNZTGlKDHHuVCkxNmHV0yFIxr3flRNmvxebLxsuYPAmCF
4+
ccV1r+1Pry244MHOIq3aq5mIRq+smVSPk350WpyO4jn8mOLOiH+CYe9LXmJSPBvO
5+
zZHwjEp+VmIGp5oDUZc5nnrf/UkQcj6jvKj0TanD8vGpDg9w3WbkQHWbFAMGPQdc
6+
YF5CZ68QPKPS86/aOdcnyoliSyIMn9BhrSXS8+Q3fCZHsYgejUjD7e0sx/+gBCrW
7+
MOuzbyD29mgbqETiSCZS1YLxgPnA34NRRKY06G0fMusXSGsXC+y7EU8JjTvTs4/L
8+
PwIDAQAB
9+
-----END PUBLIC KEY-----

0 commit comments

Comments
 (0)