Skip to content

Commit 4933e28

Browse files
authored
Merge pull request #11 from pdsinterop/dpop-webid
Adding WAC / Dpop
2 parents a65254c + 93a4a42 commit 4933e28

2 files changed

Lines changed: 517 additions & 0 deletions

File tree

src/Utils/DPop.php

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Pdsinterop\Solid\Auth\Utils;
4+
5+
use Lcobucci\JWT\Parser;
6+
use Lcobucci\JWT\Signer\Key;
7+
use Lcobucci\JWT\ValidationData;
8+
use CoderCat\JWKToPEM\JWKConverter;
9+
10+
class DPop {
11+
public function getWebId($request) {
12+
$auth = explode(" ", $request->getServerParams()['HTTP_AUTHORIZATION']);
13+
$jwt = $auth[1];
14+
15+
if (strtolower($auth[0]) == "dpop") {
16+
$dpop = $request->getServerParams()['HTTP_DPOP'];
17+
if ($dpop) {
18+
$dpopKey = $this->getDpopKey($dpop, $request);
19+
if (!$this->validateJwtDpop($jwt, $dpopKey)) {
20+
throw new \Exception("Invalid token");
21+
}
22+
}
23+
}
24+
25+
if ($jwt) {
26+
$webId = $this->getSubjectFromJwt($jwt);
27+
} else {
28+
$webId = "public";
29+
}
30+
31+
return $webId;
32+
}
33+
34+
public function getDpopKey($dpop, $request) {
35+
//error_log("11");
36+
$this->validateDpop($dpop, $request);
37+
//error_log("22");
38+
39+
$parser = new \Lcobucci\JWT\Parser();
40+
// 1. the string value is a well-formed JWT,
41+
$dpop = $parser->parse($dpop);
42+
$jwk = $dpop->getHeader("jwk");
43+
//error_log(print_r($jwk, true));
44+
45+
return $jwk->kid;
46+
}
47+
48+
private function validateJwtDpop($jwt, $dpopKey) {
49+
$parser = new \Lcobucci\JWT\Parser();
50+
$jwt = $parser->parse($jwt);
51+
$cnf = $jwt->getClaim("cnf");
52+
53+
if ($cnf->jkt == $dpopKey) {
54+
//error_log("dpopKey matches");
55+
return true;
56+
}
57+
//error_log("dpopKey mismatch");
58+
//error_log(print_r($cnf, true));
59+
//error_log($dpopKey);
60+
61+
return false;
62+
}
63+
64+
private function validateDpop($dpop, $request) {
65+
/*
66+
4.2. Checking DPoP Proofs
67+
To check if a string that was received as part of an HTTP Request is
68+
a valid DPoP proof, the receiving server MUST ensure that
69+
1. the string value is a well-formed JWT,
70+
2. all required claims are contained in the JWT,
71+
3. the "typ" field in the header has the value "dpop+jwt",
72+
4. the algorithm in the header of the JWT indicates an asymmetric
73+
digital signature algorithm, is not "none", is supported by the
74+
application, and is deemed secure,
75+
5. that the JWT is signed using the public key contained in the
76+
"jwk" header of the JWT,
77+
6. the "htm" claim matches the HTTP method value of the HTTP request
78+
in which the JWT was received (case-insensitive),
79+
7. the "htu" claims matches the HTTP URI value for the HTTP request
80+
in which the JWT was received, ignoring any query and fragment
81+
parts,
82+
8. the token was issued within an acceptable timeframe (see
83+
Section 9.1), and
84+
9. that, within a reasonable consideration of accuracy and resource
85+
utilization, a JWT with the same "jti" value has not been
86+
received previously (see Section 9.1).
87+
*/
88+
//error_log("1");
89+
90+
$parser = new \Lcobucci\JWT\Parser();
91+
// 1. the string value is a well-formed JWT,
92+
$dpop = $parser->parse($dpop);
93+
94+
//error_log("2");
95+
// 2. all required claims are contained in the JWT,
96+
$htm = $dpop->getClaim("htm"); // http method
97+
$htu = $dpop->getClaim("htu"); // http uri
98+
$typ = $dpop->getHeader("typ");
99+
$alg = $dpop->getHeader("alg");
100+
101+
//error_log("3");
102+
// 3. the "typ" field in the header has the value "dpop+jwt",
103+
if ($typ != "dpop+jwt") {
104+
throw new \Exception("typ is not dpop+jwt");
105+
}
106+
107+
//error_log("4");
108+
// 4. the algorithm in the header of the JWT indicates an asymmetric
109+
// digital signature algorithm, is not "none", is supported by the
110+
// application, and is deemed secure,
111+
if ($alg == "none") {
112+
throw new \Exception("alg is none");
113+
}
114+
if ($alg != "RS256") {
115+
throw new \Exception("alg is not supported");
116+
}
117+
118+
//error_log("5");
119+
// 5. that the JWT is signed using the public key contained in the
120+
// "jwk" header of the JWT,
121+
$jwk = $dpop->getHeader("jwk");
122+
$jwkConverter = new JWKConverter();
123+
$pem = $jwkConverter->toPEM(json_decode(json_encode($jwk), true));
124+
$signer = new \Lcobucci\JWT\Signer\Rsa\Sha256();
125+
$key = new \Lcobucci\JWT\Signer\Key($pem);
126+
if (!$dpop->verify($signer, $key)) {
127+
throw new \Exception("invalid signature");
128+
}
129+
130+
//error_log("6");
131+
// 6. the "htm" claim matches the HTTP method value of the HTTP request
132+
// in which the JWT was received (case-insensitive),
133+
if (strtolower($htm) != strtolower($request->getMethod())) {
134+
throw new \Exception("htm http method is invalid");
135+
}
136+
137+
//error_log("7");
138+
// 7. the "htu" claims matches the HTTP URI value for the HTTP request
139+
// in which the JWT was received, ignoring any query and fragment
140+
// parts,
141+
$requestedPath = (string)$request->getUri();
142+
$requestedPath = preg_replace("/[?#].*$/", "", $requestedPath);
143+
// FIXME: Remove this; it was disabled for testing with a server running on 443 internally but accessible on :444
144+
$htu = str_replace(":444", "", $htu);
145+
$requestedPath = str_replace(":444", "", $requestedPath);
146+
147+
//error_log("REQUESTED HTU $htu");
148+
//error_log("REQUESTED PATH $requestedPath");
149+
if ($htu != $requestedPath) {
150+
throw new \Exception("htu does not match requested path");
151+
}
152+
153+
//error_log("8");
154+
// 8. the token was issued within an acceptable timeframe (see Section 9.1), and
155+
$validationData = new ValidationData(); // It will use the current time to validate (iat, nbf and exp)
156+
if (!$dpop->validate($validationData)) {
157+
throw new \Exception("token timing is invalid");
158+
}
159+
160+
// 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).
161+
// FIXME: Check if we know the jti;
162+
//error_log("9");
163+
164+
return true;
165+
}
166+
167+
private function getSubjectFromJwt($jwt) {
168+
$parser = new \Lcobucci\JWT\Parser();
169+
try {
170+
$jwt = $parser->parse($jwt);
171+
} catch(\Exception $e) {
172+
return $this->server->getResponse()->withStatus(409, "Invalid JWT token");
173+
}
174+
175+
$sub = $jwt->getClaim("sub");
176+
return $sub;
177+
}
178+
}

0 commit comments

Comments
 (0)