3131#define OAUTH_DPOP_HDR_JWK "jwk"
3232#define OAUTH_DPOP_CLAIM_HTM "htm"
3333#define OAUTH_DPOP_CLAIM_HTU "htu"
34+ #define OAUTH_DPOP_CLAIM_ATH "ath"
35+ #define OAUTH_DPOP_CLAIM_NONCE "nonce"
3436#define OAUTH_DPOP_HDR_TYP_VALUE "dpop+jwt"
3537
3638static bool _oauth2_dpop_jwt_validate (oauth2_log_t * log , const char * s_dpop ,
@@ -84,12 +86,13 @@ static bool _oauth2_dpop_claims_validate(oauth2_log_t *log, cjose_header_t *hdr,
8486 cjose_jwk_t * * jwk ,
8587 const char * * hdr_typ ,
8688 const char * * hdr_alg , char * * clm_htm ,
87- char * * clm_htu , char * * clm_jti )
89+ char * * clm_htu , char * * clm_jti ,
90+ char * * clm_ath , char * * clm_nonce )
8891{
8992 bool rc = false;
9093 cjose_err err ;
9194 char * hdr_jwk = NULL ;
92- json_int_t clm_iat ;
95+ json_int_t clm_iat = 0 ;
9396
9497 * hdr_typ = cjose_header_get (hdr , OAUTH2_JOSE_HDR_TYP , & err );
9598 if (* hdr_typ == NULL ) {
@@ -121,38 +124,59 @@ static bool _oauth2_dpop_claims_validate(oauth2_log_t *log, cjose_header_t *hdr,
121124 goto end ;
122125 }
123126
124- if (oauth2_json_string_get (log , dpop_payload , OAUTH_DPOP_CLAIM_HTU ,
125- clm_htu , NULL ) == false) {
127+ if ((oauth2_json_string_get (log , dpop_payload , OAUTH_DPOP_CLAIM_HTU ,
128+ clm_htu , NULL ) == false) ||
129+ (* clm_htu == NULL )) {
126130 oauth2_error (log ,
127131 "required claim \"%s\" not found in DPOP payload" ,
128132 OAUTH_DPOP_CLAIM_HTU );
129133 goto end ;
130134 }
131135
132- if (oauth2_json_string_get (log , dpop_payload , OAUTH_DPOP_CLAIM_HTM ,
133- clm_htm , NULL ) == false) {
136+ if ((oauth2_json_string_get (log , dpop_payload , OAUTH_DPOP_CLAIM_HTM ,
137+ clm_htm , NULL ) == false) ||
138+ (* clm_htm == NULL )) {
134139 oauth2_error (log ,
135140 "required claim \"%s\" not found in DPOP payload" ,
136141 OAUTH_DPOP_CLAIM_HTM );
137142 goto end ;
138143 }
139144
140- if (oauth2_json_string_get (log , dpop_payload , OAUTH2_CLAIM_JTI , clm_jti ,
141- NULL ) == false) {
145+ if ((oauth2_json_string_get (log , dpop_payload , OAUTH2_CLAIM_JTI ,
146+ clm_jti , NULL ) == false) ||
147+ (* clm_jti == NULL )) {
142148 oauth2_error (log ,
143149 "required claim \"%s\" not found in DPOP payload" ,
144150 OAUTH2_CLAIM_JTI );
145151 goto end ;
146152 }
147153
148- if (oauth2_json_number_get (log , dpop_payload , OAUTH2_CLAIM_IAT ,
149- & clm_iat , 0 ) == false) {
154+ if ((oauth2_json_number_get (log , dpop_payload , OAUTH2_CLAIM_IAT ,
155+ & clm_iat , 0 ) == false) ||
156+ (clm_iat == 0 )) {
150157 oauth2_error (log ,
151158 "required claim \"%s\" not found in DPOP payload" ,
152159 OAUTH2_CLAIM_IAT );
153160 goto end ;
154161 }
155162
163+ if ((oauth2_json_string_get (log , dpop_payload , OAUTH_DPOP_CLAIM_ATH ,
164+ clm_ath , NULL ) == false) ||
165+ (* clm_ath == NULL )) {
166+ oauth2_error (log ,
167+ "required claim \"%s\" not found in DPOP payload" ,
168+ OAUTH_DPOP_CLAIM_ATH );
169+ goto end ;
170+ }
171+
172+ if ((oauth2_json_string_get (log , dpop_payload , OAUTH_DPOP_CLAIM_NONCE ,
173+ clm_nonce , NULL ) == false) ||
174+ (* clm_nonce == NULL )) {
175+ oauth2_debug (
176+ log , "(optional) claim \"%s\" not found in DPOP payload" ,
177+ OAUTH_DPOP_CLAIM_NONCE );
178+ }
179+
156180 rc = true;
157181
158182end :
@@ -328,14 +352,99 @@ static bool _oauth2_dpop_jti_validate(oauth2_log_t *log,
328352 return rc ;
329353}
330354
355+ static bool (_oauth2_dpp_hdr_count )(oauth2_log_t * log , void * rec ,
356+ const char * key , const char * value )
357+ {
358+ int * n = (int * )rec ;
359+ if (strcasecmp (key , OAUTH2_HTTP_HDR_DPOP ) == 0 )
360+ (* n )++ ;
361+ return true;
362+ }
363+
364+ static bool _oauth2_dpop_header_count (oauth2_log_t * log ,
365+ oauth2_http_request_t * request )
366+ {
367+ bool rc = false;
368+ int n = 0 ;
369+
370+ oauth2_http_request_headers_loop (log , request , _oauth2_dpp_hdr_count ,
371+ & n );
372+
373+ if (n > 1 ) {
374+ oauth2_error (log , "more than one %s header found" ,
375+ OAUTH2_HTTP_HDR_DPOP );
376+ goto end ;
377+ }
378+
379+ if (n == 0 ) {
380+ oauth2_error (log , "no %s header found" , OAUTH2_HTTP_HDR_DPOP );
381+ goto end ;
382+ }
383+
384+ rc = true;
385+
386+ end :
387+
388+ return rc ;
389+ }
390+
391+ static bool _oauth2_dpop_ath_validate (oauth2_log_t * log ,
392+ oauth2_cfg_dpop_verify_t * verify ,
393+ const char * clm_ath ,
394+ const char * access_token )
395+ {
396+ bool rc = false;
397+ unsigned char * calc = NULL ;
398+ unsigned int calc_len = 0 ;
399+ uint8_t * dec = NULL ;
400+ size_t dec_len = 0 ;
401+
402+ if ((clm_ath == NULL ) || (access_token == NULL ))
403+ goto end ;
404+
405+ if (oauth2_jose_hash_bytes (
406+ log , "sha256" , (const unsigned char * )access_token ,
407+ strlen (access_token ), & calc , & calc_len ) == false)
408+ goto end ;
409+
410+ if (oauth2_base64url_decode (log , clm_ath , & dec , & dec_len ) == false)
411+ goto end ;
412+
413+ if ((calc_len != dec_len ) || (memcmp (dec , calc , dec_len ) != 0 )) {
414+ oauth2_error (log ,
415+ "provided \"ath\" hash value (%s) does not match "
416+ "the calculated value (dec_len=%d, calc_len=%d)" ,
417+ clm_ath , dec_len , calc_len );
418+ goto end ;
419+ }
420+
421+ oauth2_debug (log ,
422+ "successfully validated the provided \"ath\" hash value "
423+ "(%s) against the calculated value" ,
424+ clm_ath );
425+
426+ rc = true;
427+
428+ end :
429+
430+ if (dec )
431+ oauth2_mem_free (dec );
432+ if (calc )
433+ oauth2_mem_free (calc );
434+
435+ return rc ;
436+ }
437+
331438static bool _oauth2_dpop_parse_and_validate (oauth2_log_t * log ,
332439 oauth2_cfg_dpop_verify_t * verify ,
333440 oauth2_http_request_t * request ,
441+ const char * access_token ,
334442 cjose_jwk_t * * jwk )
335443{
336444 bool rc = false;
337445 const char * hdr_typ = NULL , * hdr_alg = NULL ;
338- char * clm_htm = NULL , * clm_htu = NULL , * clm_jti = NULL ;
446+ char * clm_htm = NULL , * clm_htu = NULL , * clm_jti = NULL , * clm_ath = NULL ,
447+ * clm_nonce = NULL ;
339448 const char * s_dpop = NULL ;
340449 cjose_jws_t * jws = NULL ;
341450 cjose_header_t * hdr = NULL ;
@@ -345,6 +454,12 @@ static bool _oauth2_dpop_parse_and_validate(oauth2_log_t *log,
345454 if ((request == NULL ) || (verify == NULL ) || (jwk == NULL ))
346455 goto end ;
347456
457+ /*
458+ * 1. that there is not more than one DPoP header in the request,
459+ */
460+ if (_oauth2_dpop_header_count (log , request ) == false)
461+ goto end ;
462+
348463 s_dpop =
349464 oauth2_http_request_header_get (log , request , OAUTH2_HTTP_HDR_DPOP );
350465 if (s_dpop == NULL )
@@ -355,71 +470,99 @@ static bool _oauth2_dpop_parse_and_validate(oauth2_log_t *log,
355470 oauth2_debug (log , "DPOP header: %s" , s_peek );
356471
357472 /*
358- * 1. the string value is a well-formed JWT
473+ * 2. the string value of the header field is a well-formed JWT,
359474 */
360475 if (_oauth2_dpop_jwt_validate (log , s_dpop , & jws , & hdr , & dpop_payload ) ==
361476 false)
362477 goto end ;
363478
364479 /*
365- * 2 . all required claims are contained in the JWT,
480+ * 3 . all required claims per Section 4.2 are contained in the JWT,
366481 */
367482 if (_oauth2_dpop_claims_validate (log , hdr , dpop_payload , jwk , & hdr_typ ,
368- & hdr_alg , & clm_htm , & clm_htu ,
369- & clm_jti ) == false)
483+ & hdr_alg , & clm_htm , & clm_htu , & clm_jti ,
484+ & clm_ath , & clm_nonce ) == false)
370485 goto end ;
371486
372487 /*
373- * 3 . the " typ" field in the header has the value " dpop+jwt" ,
488+ * 4 . the typ field in the header has the value dpop+jwt,
374489 */
375490 if (_oauth2_dpop_hdr_typ_validate (log , hdr_typ ) == false)
376491 goto end ;
377492
378493 /*
379- * 4. the algorithm in the header of the JWT indicates an asymmetric
380- * digital signature algorithm, is not " none" , is supported by the
381- * application, and is deemed secure,
494+ * 5. the algorithm in the header of the JWT indicates an asymmetric
495+ * digital signature algorithm, is not none, is supported by the
496+ * application, and is deemed secure,
382497 */
383498 if (_oauth2_dpop_hdr_alg_validate (log , hdr_alg ) == false)
384499 goto end ;
385500
386501 /*
387- * 5 . that the JWT is signed using the public key contained in the
388- * " jwk" header of the JWT,
502+ * 6 . the JWT signature verifies with the public key contained in the
503+ * jwk header of the JWT,
389504 */
390505 if (_oauth2_dpop_sig_verify (log , jws , * jwk ) == false)
391506 goto end ;
392507
393508 /*
394- * 6. the "htm" claim matches the HTTP method value of the HTTP request
395- * in which the JWT was received (case-insensitive),
509+ * 7. the jwk header of the JWT does not contain a private key,
510+ */
511+ // TODO:
512+
513+ /*
514+ * 8. the htm claim matches the HTTP method value of the HTTP request
515+ * in which the JWT was received,
396516 */
397517 if (_oauth2_dpop_htm_validate (log , request , clm_htm ) == false)
398518 goto end ;
399519
400520 /*
401- * 7 . the " htu" claims matches the HTTP URI value for the HTTP request
402- * in which the JWT was received, ignoring any query and fragment
403- * parts,
521+ * 9 . the htu claim matches the HTTPS URI value for the HTTP request
522+ * in which the JWT was received, ignoring any query and fragment
523+ * parts,
404524 */
405525 if (_oauth2_dpop_htu_validate (log , request , clm_htu ) == false)
406526 goto end ;
407527
408528 /*
409- * 8. the token was issued within an acceptable timeframe (see
410- * Section 9.1), and
529+ * 10. if the server provided a nonce value to the client, the nonce
530+ * claim matches the server-provided nonce value,
531+ */
532+ // TODO:
533+
534+ /*
535+ * 11. the iat claim value is within an acceptable timeframe and,
536+ * within a reasonable consideration of accuracy and resource
537+ * utilization, a proof JWT with the same jti value has not
538+ * previously been received at the same resource during that time
539+ * period (see Section 11.1),
411540 */
412541 if (_oauth2_dpop_iat_validate (log , verify , dpop_payload ) == false)
413542 goto end ;
414543
544+ if (_oauth2_dpop_jti_validate (log , verify , clm_jti , s_dpop ) == false)
545+ goto end ;
546+
415547 /*
416- * 9. that, within a reasonable consideration of accuracy and resource
417- * utilization, a JWT with the same "jti" value has not been
418- * received previously (see Section 9.1).
548+ * 12. if presented to a protected resource in conjunction with an
549+ * access token,
550+ *
551+ * 1. ensure that the value of the ath claim equals the hash of
552+ * that access token,
419553 */
420- if (_oauth2_dpop_jti_validate (log , verify , s_dpop , clm_jti ) == false)
554+ if (_oauth2_dpop_ath_validate (log , verify , clm_ath , access_token ) ==
555+ false)
421556 goto end ;
422557
558+ /*
559+ * 2. confirm that the public key to which the access token is
560+ * bound matches the public key from the DPoP proof.
561+ *
562+ */
563+ // done in the calling function oauth2_dpop_token_verify with the "jkt"
564+ // claim
565+
423566 rc = true;
424567
425568end :
@@ -432,6 +575,10 @@ static bool _oauth2_dpop_parse_and_validate(oauth2_log_t *log,
432575 oauth2_mem_free (clm_htm );
433576 if (clm_jti )
434577 oauth2_mem_free (clm_jti );
578+ if (clm_ath )
579+ oauth2_mem_free (clm_ath );
580+ if (clm_nonce )
581+ oauth2_mem_free (clm_nonce );
435582 if (dpop_payload )
436583 json_decref (dpop_payload );
437584 if (jws )
@@ -446,7 +593,7 @@ static bool _oauth2_dpop_parse_and_validate(oauth2_log_t *log,
446593bool oauth2_dpop_token_verify (oauth2_log_t * log ,
447594 oauth2_cfg_dpop_verify_t * verify ,
448595 oauth2_http_request_t * request ,
449- json_t * json_payload )
596+ const char * access_token , json_t * json_payload )
450597{
451598 bool rc = false;
452599 cjose_jwk_t * jwk = NULL ;
@@ -461,8 +608,8 @@ bool oauth2_dpop_token_verify(oauth2_log_t *log,
461608 if ((request == NULL ) || (json_payload == NULL ))
462609 goto end ;
463610
464- if (_oauth2_dpop_parse_and_validate (log , verify , request , & jwk ) ==
465- false)
611+ if (_oauth2_dpop_parse_and_validate (log , verify , request , access_token ,
612+ & jwk ) == false)
466613 goto end ;
467614
468615 if (oauth2_jose_jwk_thumbprint (log , jwk , & hash_bytes ,
@@ -490,7 +637,7 @@ bool oauth2_dpop_token_verify(oauth2_log_t *log,
490637 (memcmp (hash_bytes , dst , hash_bytes_len )) != 0 ) {
491638 oauth2_error (log ,
492639 "public key thumbprint in DPOP \"%s\" does not "
493- "match \"%s\" claim \%s\" in JWT token" ,
640+ "match \"%s\" claim \%s\" for the access token" ,
494641 calc_thumb , OAUTH_DPOP_CLAIM_CNF_JKT , prov_thumb );
495642 goto end ;
496643 }
0 commit comments