Skip to content

Commit 2c8f93f

Browse files
committed
1.5.2: update DPoP support to RFC 9449
Signed-off-by: Hans Zandbelt <hans.zandbelt@openidc.com>
1 parent 387419f commit 2c8f93f

7 files changed

Lines changed: 238 additions & 77 deletions

File tree

ChangeLog

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
11/08/2023
2+
- update DPoP support to RFC 9449
3+
- release 1.5.2
4+
15
08/31/2023
26
- printout more cjose error details when errors occur verifying JWT access tokens
37

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ Generic library to build C-based OAuth 2.x and OpenID Connect servers and client
2121
- JWT bearer token validation using JWK, JWKS URI, shared symmetric key, X.509 cert, and RSA public key ([RFC 6750](https://tools.ietf.org/html/rfc6750))
2222
- OAuth 2.0 Authorization Server Metadata ([RFC 8414](https://tools.ietf.org/html/rfc8414))
2323
- Proof Key for Code Exchange (PKCE) by OAuth Public Clients ([RFC 7636](https://tools.ietf.org/html/rfc7636))
24-
- OAuth 2.0 Mutual-TLS (MTLS) Certificate-Bound Access Tokens ([RFC 8705](https://tools.ietf.org/html/rfc8705))
25-
- OAuth 2.0 Demonstration of Proof-of-Possession (DPoP) at the Application Layer ([Internet-Draft](https://tools.ietf.org/html/draft-ietf-oauth-dpop))
24+
- OAuth 2.0 Mutual-TLS (MTLS) Certificate-Bound Access Tokens ([RFC 8705](https://tools.ietf.org/html/rfc8705))
25+
- OAuth 2.0 Demonstrating Proof of Possession (DPoP) ([RFC9449](https://tools.ietf.org/html/rfc9449))
2626
- Amazon ALB [EC key URL based `x-amzn-oidc-data` JWT verification](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/listener-authenticate-users.html)
2727
- endpoint authentication methods: `client_secret_basic`, `client_secret_post`, [`client_secret_jwt`, `private_key_jwt`](https://tools.ietf.org/html/rfc7523), [TLS client certificate](https://tools.ietf.org/id/draft-ietf-oauth-mtls), and HTTP basic authentication
2828
- configurable cache backends: shared memory, file-based, memcache, and Redis

configure.ac

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
AC_INIT([liboauth2],[1.5.1],[hans.zandbelt@openidc.com])
1+
AC_INIT([liboauth2],[1.5.2],[hans.zandbelt@openidc.com])
22

33
AM_INIT_AUTOMAKE([foreign no-define subdir-objects])
44
AC_CONFIG_MACRO_DIR([m4])

src/dpop.c

Lines changed: 183 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
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

3638
static 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

158182
end:
@@ -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+
331438
static 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

425568
end:
@@ -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,
446593
bool 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

Comments
 (0)