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 ;
@@ -68,7 +69,15 @@ private function validateJwtDpop($jwt, $dpopKey) {
6869 return false ;
6970 }
7071
71- private function validateDpop ($ dpop , $ request ) {
72+ /**
73+ * Validates that the DPOP token matches all requirements from
74+ * https://datatracker.ietf.org/doc/html/draft-ietf-oauth-dpop#section-4.2
75+ * @param string $dpop The DPOP token
76+ * @param Psr\Http\Message\ServerRequestInterface $request Server Request
77+ * @return bool True if the DPOP token is valid, false otherwise
78+ * @throws Lcobucci\JWT\Validation\RequiredConstraintsViolated
79+ */
80+ public function validateDpop ($ dpop , $ request ) {
7281 /*
7382 4.2. Checking DPoP Proofs
7483 To check if a string that was received as part of an HTTP Request is
@@ -91,34 +100,43 @@ private function validateDpop($dpop, $request) {
91100 9. that, within a reasonable consideration of accuracy and resource
92101 utilization, a JWT with the same "jti" value has not been
93102 received previously (see Section 9.1).
103+ 10. that, if used with an access token, it also contains the 'ath'
104+ claim, with a hash of the access token
94105 */
95- //error_log("1");
96106 // 1. the string value is a well-formed JWT,
97107 $ jwtConfig = $ configuration = Configuration::forUnsecuredSigner ();
98108 $ dpop = $ jwtConfig ->parser ()->parse ($ dpop );
99109
100- //error_log("2");
101110 // 2. all required claims are contained in the JWT,
102111 $ htm = $ dpop ->claims ()->get ("htm " ); // http method
112+ if (!$ htm ) {
113+ throw new \Exception ("missing htm " );
114+ }
103115 $ htu = $ dpop ->claims ()->get ("htu " ); // http uri
116+ if (!$ htu ) {
117+ throw new \Exception ("missing htu " );
118+ }
104119 $ typ = $ dpop ->headers ()->get ("typ " );
120+ if (!$ typ ) {
121+ throw new \Exception ("missing typ " );
122+ }
105123 $ alg = $ dpop ->headers ()->get ("alg " );
124+ if (!$ alg ) {
125+ throw new \Exception ("missing alg " );
126+ }
106127
107- //error_log("3");
108128 // 3. the "typ" field in the header has the value "dpop+jwt",
109129 if ($ typ != "dpop+jwt " ) {
110130 throw new \Exception ("typ is not dpop+jwt " );
111131 }
112132
113- //error_log("4");
114133 // 4. the algorithm in the header of the JWT indicates an asymmetric
115134 // digital signature algorithm, is not "none", is supported by the
116135 // application, and is deemed secure,
117136 if ($ alg == "none " ) {
118137 throw new \Exception ("alg is none " );
119138 }
120139
121- //error_log("5");
122140 // 5. that the JWT is signed using the public key contained in the
123141 // "jwk" header of the JWT,
124142 $ jwk = $ dpop ->headers ()->get ("jwk " );
@@ -130,59 +148,48 @@ private function validateDpop($dpop, $request) {
130148 break ;
131149 case "ES256 " :
132150 $ pem = \Jose \Component \Core \Util \ECKey::convertToPEM ($ webTokenJwk );
133- $ signer = \Lcobucci \JWT \Signer \Ecdsa \Sha256::create ();
151+ $ signer = \Lcobucci \JWT \Signer \Ecdsa \Sha256::create ();
134152 break ;
135153 default :
136154 throw new \Exception ("unsupported algorithm " );
137155 break ;
138156 }
139157 $ 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- // }
158+ $ validationConstraints = [];
159+ $ validationConstraints [] = new SignedWith ($ signer , $ key );
148160
149- //error_log("6");
150161 // 6. the "htm" claim matches the HTTP method value of the HTTP request
151162 // in which the JWT was received (case-insensitive),
152163 if (strtolower ($ htm ) != strtolower ($ request ->getMethod ())) {
153164 throw new \Exception ("htm http method is invalid " );
154165 }
155166
156- //error_log("7");
157167 // 7. the "htu" claims matches the HTTP URI value for the HTTP request
158168 // in which the JWT was received, ignoring any query and fragment
159169 // parts,
160170 $ requestedPath = (string )$ request ->getUri ();
161171 $ 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 );
165172
166173 //error_log("REQUESTED HTU $htu");
167174 //error_log("REQUESTED PATH $requestedPath");
168175 if ($ htu != $ requestedPath ) {
169176 throw new \Exception ("htu does not match requested path " );
170177 }
171178
172- //error_log("8");
173179 // 8. the token was issued within an acceptable timeframe (see Section 9.1), and
174180
175181 $ leeway = new \DateInterval ("PT60S " ); // allow 60 seconds clock skew
176182 $ 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 " );
183+ $ validationsConstraints [] = new LooseValidAt ($ clock , $ leeway ); // It will use the current time to validate (iat, nbf and exp)
184+ if (!$ jwtConfig ->validator ()->validate ($ dpop , ...$ validationConstraints )) {
185+ $ jwtConfig ->validator ()->assert ($ dpop , ...$ validationConstraints ); // throws an explanatory exception
181186 }
182187
183188 // 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");
189+ // TODO: Check if we know the jti;
190+
191+ // 10. that, if used with an access token, it also contains the 'ath' claim, with a hash of the access token
192+ // TODO: implement
186193
187194 return true ;
188195 }
0 commit comments