@@ -59,13 +59,7 @@ public function getWebId($request) {
5959
6060 //@FIXME: check that there is just one DPoP token in the request
6161 try {
62- $ dpopKey = $ this ->getDpopKey ($ dpop , $ request );
63- } catch (InvalidTokenStructure $ e ) {
64- throw new InvalidTokenException ("Invalid JWT token: {$ e ->getMessage ()}" , 0 , $ e );
65- }
66-
67- try {
68- $ this ->validateJwtDpop ($ jwt , $ dpopKey );
62+ $ this ->validateJwtDpop ($ jwt , $ dpop , $ request );
6963 } catch (RequiredConstraintsViolated $ e ) {
7064 throw new InvalidTokenException ($ e ->getMessage (), 0 , $ e );
7165 }
@@ -76,109 +70,146 @@ public function getWebId($request) {
7670 }
7771
7872 /**
79- * Returns the "kid" from the "jwk" header in the DPoP token.
80- * The DPoP token must be valid.
81- *
82- * @param string $dpop The DPoP token
83- * @param ServerRequestInterface $request Server Request
84- *
85- * @return string the "kid" from the "jwk" header in the DPoP token.
86- *
87- * @throws RequiredConstraintsViolated
73+ * kept for backwards compatability
74+ * note: the "kid" value is not guaranteed to be a hash of the jwk
75+ * so to compare a jkt, calculate the jwk thumbprint instead
76+ * @param string $dpop The DPoP token, raw
77+ * @param ServerRequestInterface $request Server Request
78+ * @return string The "kid" from the "jwk" header
8879 */
8980 public function getDpopKey ($ dpop , $ request ) {
90- $ kid = '' ;
91-
9281 $ this ->validateDpop ($ dpop , $ request );
9382
94- // 1. the string value is a well-formed JWT,
9583 $ jwtConfig = Configuration::forUnsecuredSigner ();
9684 $ dpop = $ jwtConfig ->parser ()->parse ($ dpop );
9785 $ jwk = $ dpop ->headers ()->get ("jwk " );
9886
99- if (isset ($ jwk ['kid ' ])) {
100- $ kid = $ jwk ['kid ' ];
87+ if (isset ($ jwk ['kid ' ]) === false ) {
88+ throw new InvalidTokenException ('Key ID is missing from JWK header ' );
89+ }
90+
91+ return $ jwk ['kid ' ];
92+ }
93+
94+ /**
95+ * RFC7638 defines a method for computing the hash value (or "digest") of a JSON Web Key (JWK).
96+ *
97+ * The resulting hash value can be used for identifying the key represented by the JWK
98+ * that is the subject of the thumbprint.
99+ *
100+ * For instance by using the base64url-encoded JWK Thumbprint value as a key ID (or "kid") value.
101+ *
102+ * @see https://www.rfc-editor.org/rfc/rfc7638
103+ *
104+ * The thumbprint of a JWK is created by:
105+ *
106+ * 1. Constructing a JSON string (without whitespaces) with the required keys in alphabetical order.
107+ * 2. Hashing the JSON string using SHA-256 (or another hash function)
108+ *
109+ * @param string $jwk The JWK key to thumbprint
110+ * @return string the thumbprint
111+ * @throws InvalidTokenException
112+ */
113+ public function makeJwkThumbprint ($ jwk ) {
114+ if (!$ jwk || !isset ($ jwk ['kty ' ])) {
115+ throw new InvalidTokenException ('JWK has no "kty" key type ' );
101116 }
117+ if (!in_array ($ jwk ['kty ' ], ['RSA ' ,'EC ' ])) {
118+ throw new InvalidTokenException ('JWK "kty" key type value must be one of "RSA" or "EC", got " ' .$ jwk ['kty ' ].'" instead. ' );
119+ }
120+ if ($ jwk ['kty ' ]=='RSA ' ) {
121+ if (!isset ($ jwk ['e ' ]) || !isset ($ jwk ['n ' ])) {
122+ throw new InvalidTokenException ('JWK values do not match "RSA" key type ' );
123+ }
124+ $ json = vsprintf ('{"e":"%s","kty":"%s","n":"%s"} ' , [
125+ $ jwk ['e ' ],
126+ $ jwk ['kty ' ],
127+ $ jwk ['n ' ],
128+ ]);
102129
103- return $ kid ;
130+ } else { //EC
131+ if (!isset ($ jwk ['crv ' ]) || !isset ($ jwk ['x ' ]) || !isset ($ jwk ['y ' ])) {
132+ throw new InvalidTokenException ('JWK values doe not match "EC" key type ' );
133+ }
134+ //crv, kty, x, y
135+ $ json = vsprintf ('{"crv":"%s","kty":"%s","x":"%s","y":"%s"} ' , [
136+ $ jwk ['crv ' ],
137+ $ jwk ['kty ' ],
138+ $ jwk ['x ' ],
139+ $ jwk ['y ' ]
140+ ]);
141+ }
142+ $ hash = hash ('sha256 ' , $ json );
143+ $ encoded = Base64Url::encode ($ hash );
144+ return $ encoded ;
104145 }
105146
106- private function validateJwtDpop ($ jwt , $ dpopKey ) {
147+ /**
148+ * https://datatracker.ietf.org/doc/html/draft-ietf-oauth-dpop#section-4.2
149+ * When the DPoP proof is used in conjunction with the presentation of
150+ * an access token in protected resource access, see Section 7, the DPoP
151+ * proof MUST also contain the following claim:
152+ * ath: hash of the access token. The value MUST be the result of a
153+ * base64url encoding (as defined in Section 2 of [RFC7515]) the
154+ * SHA-256 [SHS] hash of the ASCII encoding of the associated access
155+ * token's value.
156+ * See also: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-dpop#section-7
157+ *
158+ * Validates the above part of the oauth dpop specification
159+ * @param string $jwt JWT access token, raw
160+ * @param string $dpop DPoP token, raw
161+ * @param ServerRequestInterface $request Server Request
162+ * @return bool true, if the dpop token "ath" claim matches the access token
163+ */
164+ public function validateJwtDpop ($ jwt , $ dpop , $ request ) {
165+ $ this ->validateDpop ($ dpop , $ request );
166+ $ jwtConfig = Configuration::forUnsecuredSigner ();
167+ $ dpopJWT = $ jwtConfig ->parser ()->parse ($ dpop );
168+
169+ $ ath = $ dpopJWT ->claims ()->get ('ath ' );
170+ if ($ ath === null ) {
171+ throw new InvalidTokenException ('DPoP "ath" claim is missing ' );
172+ }
173+
174+ $ hash = hash ('sha256 ' , $ jwt );
175+ $ encoded = Base64Url::encode ($ hash );
176+ return ($ ath === $ encoded );
177+ }
178+
179+ /**
180+ * https://solidproject.org/TR/oidc#tokens-id
181+ * validates that the provided OIDC ID Token matches the DPoP header
182+ * @param string $token The OIDS ID Token (raw)
183+ * @param string $dpop The DPoP Token (raw)
184+ * @param ServerRequestInterface $request Server Request
185+ * @return bool True if the id token jkt matches the dpop token jwk
186+ * @throws InvalidTokenException when the tokens do not match
187+ */
188+ public function validateIdTokenDpop ($ token , $ dpop , $ request ) {
189+ $ this ->validateDpop ($ dpop , $ request );
107190 $ jwtConfig = Configuration::forUnsecuredSigner ();
108- $ jwt = $ jwtConfig ->parser ()->parse ($ jwt );
191+ $ jwt = $ jwtConfig ->parser ()->parse ($ token );
109192 $ cnf = $ jwt ->claims ()->get ("cnf " );
110193
111194 if ($ cnf === null ) {
112195 throw new InvalidTokenException ('JWT Confirmation claim (cnf) is missing ' );
113196 }
114197
115- if (isset ($ cnf ['jkt ' ]) === false ) {
198+ if (! isset ($ cnf ['jkt ' ])) {
116199 throw new InvalidTokenException ('JWT Confirmation claim (cnf) is missing Thumbprint (jkt) ' );
117200 }
118201
119- // !!! HIER GEBLEVEN !!!
120-
121- // @CHECKME: If we are checking against the JKT this "if" can be removed
122- if ($ dpopKey !== '' ) {
123- $ keyTypes = [
124- /*ES256*/ 'EC ' => ['crv ' , 'kty ' , 'x ' , 'y ' ],
125- /*RS256*/ 'RSA ' => ['e ' , 'kty ' , 'n ' ],
126- ];
127-
128- $ jwk = $ jwt ->headers ()->get ('jwk ' );
129-
130- $ keyType = $ jwk ['kty ' ];
131-
132- if (array_key_exists ($ keyType , $ keyTypes ) === false ) {
133- $ message = vsprintf ('Unsupported key type "%s". Must be one of: %s ' , [
134- $ keyType ,
135- implode (', ' , array_keys ($ keyTypes )),
136- ]);
137- throw new InvalidTokenException ($ message );
138- }
202+ $ jkt = $ cnf ['jkt ' ];
139203
140- $ required = $ keyTypes [ $ keyType ] ;
141- $ missing = array_diff ( $ required , array_keys ( $ jwk ) );
204+ $ dpopJwt = $ jwtConfig -> parser ()-> parse ( $ dpop ) ;
205+ $ jwk = $ dpopJwt -> headers ()-> get ( ' jwk ' );
142206
143- if ($ missing !== []) {
144- throw new InvalidTokenException ('Required JWK values have not been set: ' . implode (', ' , $ missing ));
145- }
146-
147- /**
148- * RFC7638 defines a method for computing the hash value (or "digest") of a JSON Web Key (JWK).
149- *
150- * The resulting hash value can be used for identifying the key represented by the JWK
151- * that is the subject of the thumbprint.
152- *
153- * For instance by using the base64url-encoded JWK Thumbprint value as a key ID (or "kid") value.
154- *
155- * @see https://www.rfc-editor.org/rfc/rfc7638
156- *
157- * The thumbprint of a JWK is created by:
158- *
159- * 1. Constructing a JSON string (without whitespaces) with the required keys in alphabetical order.
160- * 2. Hashing the JSON string using SHA-256 (or another hash function)
161- *
162- */
163- // @FIXME: Add logic to build correct JSON string
164- $ json = vsprintf ('{"e":"%s","kty":"%s","n":"%s"} ' , [
165- $ jwk ['e ' ],
166- $ jwk ['kty ' ],
167- $ jwk ['n ' ],
168- ]);
169-
170- $ hash = hash ('sha256 ' , $ json );
171- $ encoded = Base64Url::encode ($ hash );
172-
173- // @FIXME: What are we comparing against? JKT? KID / $dpopKey?
174- if ($ encoded !== $ dpopKey /* or $cnf['jkt'] ?*/ ) {
175- // @CHECKME: What error message belongs here?
176- throw new InvalidTokenException ('JWT Confirmation claim (cnf) provided Thumbprint (jkt) does not match Key ID from JWK header ' );
177- }
207+ $ jwkThumbprint = $ this ->makeJwkThumbprint ($ jwk );
208+ if ($ jwkThumbprint !== $ jkt ) {
209+ throw new InvalidTokenException ('ID Token JWK Thumbprint (jkt) does not match the JWK from DPoP header ' );
178210 }
179211
180- //@FIXME: add check for "ath" claim in DPoP token, per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-dpop#section-7
181- return false ;
212+ return true ;
182213 }
183214
184215 /**
@@ -191,6 +222,7 @@ private function validateJwtDpop($jwt, $dpopKey) {
191222 * @return bool True if the DPOP token is valid, false otherwise
192223 *
193224 * @throws RequiredConstraintsViolated
225+ * @throws InvalidTokenException
194226 */
195227 public function validateDpop ($ dpop , $ request ) {
196228 /*
@@ -220,7 +252,11 @@ public function validateDpop($dpop, $request) {
220252 */
221253 // 1. the string value is a well-formed JWT,
222254 $ jwtConfig = Configuration::forUnsecuredSigner ();
223- $ dpop = $ jwtConfig ->parser ()->parse ($ dpop );
255+ try {
256+ $ dpop = $ jwtConfig ->parser ()->parse ($ dpop );
257+ } catch (\Exception $ e ) {
258+ throw new InvalidTokenException ('Invalid DPoP token ' , 400 , $ e );
259+ }
224260
225261 // 2. all required claims are contained in the JWT,
226262 $ htm = $ dpop ->claims ()->get ("htm " ); // http method
@@ -310,9 +346,6 @@ public function validateDpop($dpop, $request) {
310346 throw new InvalidTokenException ("jti is invalid " );
311347 }
312348
313- // 10. that, if used with an access token, it also contains the 'ath' claim, with a hash of the access token
314- // TODO: implement
315-
316349 return true ;
317350 }
318351
0 commit comments