Skip to content

Commit d2da8b2

Browse files
committed
adding DPOP and WAC handling for solid
1 parent 10a6a86 commit d2da8b2

2 files changed

Lines changed: 482 additions & 0 deletions

File tree

src/Utils/DPop.php

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

0 commit comments

Comments
 (0)