99use Lcobucci \JWT \Signer \Key \InMemory ;
1010use Lcobucci \JWT \Signer \Rsa \Sha256 ;
1111use Lcobucci \JWT \Validation \Constraint \LooseValidAt ;
12+ use Lcobucci \JWT \Validation \Constraint \SignedWith ;
1213
1314use Jose \Component \Core \JWK ;
1415use Jose \Component \Core \Util \ECKey ;
1516use 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+ */
1723class 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 }
0 commit comments