]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MINOR: jwt: Improve 'jwt_tokenize' function
authorRemi Tricot-Le Breton <rlebreton@haproxy.com>
Tue, 10 Mar 2026 13:11:55 +0000 (14:11 +0100)
committerWilliam Lallemand <wlallemand@haproxy.com>
Tue, 10 Mar 2026 13:20:42 +0000 (14:20 +0100)
The 'jwt_tokenize' function that can be used to split a JWT token into
its subparts can either fully process the token (from beginning to end)
when we need to check its signature, or only partially when using the
jwt_header_query or jwt_member_query converters. In this case we relied
on the fact that the return value of the 'jwt_tokenize' function was not
checked because a '-1' was returned (which was not actually an error).

In order to make this logic more explicit, the 'jwt_tokenize' function
now has a way to warn the caller that the token was invalid (less
subparts than the specified 'item_num') or that the token was not
processed in full (enough subparts found without parsing the token all
the way).
The function will now only return 0 if we found strictly the same number
of subparts as 'item_num'.

include/haproxy/jwt.h
reg-tests/jwt/jws_verify.vtc
src/jwe.c
src/jwt.c
src/sample.c

index 10e928cefd8364946bd8279b72505b1ced5b79b0..cf8410b94e8307027788b41d08931323a9627473 100644 (file)
@@ -27,7 +27,7 @@
 
 #ifdef USE_OPENSSL
 enum jwt_alg jwt_parse_alg(const char *alg_str, unsigned int alg_len);
-int jwt_tokenize(const struct buffer *jwt, struct jwt_item *items, unsigned int *item_num);
+int jwt_tokenize(const struct buffer *jwt, struct jwt_item *items, unsigned int item_num);
 int jwt_tree_load_cert(char *path, int pathlen, int tryload_cert, const char *file, int line, char **err);
 
 enum jwt_vrfy_status jwt_verify(const struct buffer *token, const struct buffer *alg,
index 38017db8d3a27fead41e00e3eafd11aeaa939f21..64587a7bc3d1a116b99eb0c7459467d6400919c5 100644 (file)
@@ -16,7 +16,7 @@ feature cmd "$HAPROXY_PROGRAM -cc 'feature(OPENSSL)'"
 feature cmd "command -v socat"
 feature ignore_unknown_macro
 
-server s1 -repeat 27 {
+server s1 -repeat 40 {
   rxreq
   txresp
 } -start
@@ -542,3 +542,44 @@ client c27 -connect ${h1_mainfe_sock} {
     expect resp.http.x-jwt-verify-RS256-var2 == "1"
 
 } -run
+
+client c28 -connect ${h1_mainfe_sock} {
+    # Token content : {"alg":"none"}
+    #                 {"iss":"joe", "exp":1300819380, "http://example.com/is_root":true}
+    txreq -url "/none" -hdr "Authorization: Bearer eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ."
+    rxresp
+    expect resp.status == 200
+    expect resp.http.x-jwt-alg == "none"
+    expect resp.http.x-jwt-verify == "1"
+} -run
+
+client c29 -connect ${h1_mainfe_sock} {
+    # Invalid Token : too many subparts
+    txreq -url "/errors" -hdr "Authorization: Bearer eyJhbGciOiJub25lIn0.aa.aa.aa"
+    rxresp
+    expect resp.status == 200
+    expect resp.http.x-jwt-alg == "none"
+    expect resp.http.x-jwt-verify == "-3"
+
+    # Invalid Token : too many subparts
+    txreq -url "/errors" -hdr "Authorization: Bearer eyJhbGciOiJub25lIn0.aa.aa."
+    rxresp
+    expect resp.status == 200
+    expect resp.http.x-jwt-alg == "none"
+    expect resp.http.x-jwt-verify == "-3"
+
+    # Invalid Token : too few subparts
+    txreq -url "/errors" -hdr "Authorization: Bearer eyJhbGciOiJub25lIn0.aa"
+    rxresp
+    expect resp.status == 200
+    expect resp.http.x-jwt-alg == "none"
+    expect resp.http.x-jwt-verify == "-3"
+
+    # Invalid Token : no signature but alg different than "none"
+    txreq -url "/errors" -hdr "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ."
+    rxresp
+    expect resp.status == 200
+    expect resp.http.x-jwt-alg == "RS256"
+    expect resp.http.x-jwt-verify == "-3"
+} -run
+
index bfa73d901dbae28b99528bfcca15ab540ded40f1..91050a2c0ef578b4a4cce393c667614455ebcf24 100644 (file)
--- a/src/jwe.c
+++ b/src/jwe.c
@@ -534,7 +534,7 @@ static int sample_conv_jwt_decrypt_secret(const struct arg *args, struct sample
        if (!chunk_cpy(input, &smp->data.u.str))
                goto end;
 
-       if (jwt_tokenize(input, items, &item_num) || item_num != JWE_ELT_MAX)
+       if (jwt_tokenize(input, items, item_num))
                goto end;
 
        alg_tag = alloc_trash_chunk();
@@ -789,7 +789,7 @@ static int sample_conv_jwt_decrypt_cert(const struct arg *args, struct sample *s
        if (!chunk_cpy(input, &smp->data.u.str))
                goto end;
 
-       if (jwt_tokenize(input, items, &item_num) || item_num != JWE_ELT_MAX)
+       if (jwt_tokenize(input, items, item_num))
                goto end;
 
        /* Base64Url decode the JOSE header */
@@ -1302,7 +1302,7 @@ static int sample_conv_jwt_decrypt_jwk(const struct arg *args, struct sample *sm
        if (!chunk_cpy(input, &smp->data.u.str))
                goto end;
 
-       if (jwt_tokenize(input, items, &item_num) || item_num != JWE_ELT_MAX)
+       if (jwt_tokenize(input, items, item_num))
                goto end;
 
        alg_tag = alloc_trash_chunk();
index f8c33c5b3e1481aaafb7703e57af257c75f81e52..3dc04b1fbb9ba5ee0c28ad39fddabc2a5f1d55ef 100644 (file)
--- a/src/jwt.c
+++ b/src/jwt.c
@@ -94,26 +94,30 @@ enum jwt_alg jwt_parse_alg(const char *alg_str, unsigned int alg_len)
  * now, we don't need to manage more than three subparts in the tokens.
  * See section 3.1 of RFC7515 for more information about JWS Compact
  * Serialization.
- * Returns 0 in case of success.
+ * Returns -1 in case of error, 0 if the token has exactly <item_num> parts, a
+ * positive value otherwise.
  */
-int jwt_tokenize(const struct buffer *jwt, struct jwt_item *items, unsigned int *item_num)
+int jwt_tokenize(const struct buffer *jwt, struct jwt_item *items, unsigned int item_num)
 {
        char *ptr = jwt->area;
        char *jwt_end = jwt->area + jwt->data;
        unsigned int index = 0;
        unsigned int length = 0;
 
-       if (index < *item_num) {
-               items[index].start = ptr;
-               items[index].length = 0;
-       }
+       if (item_num == 0)
+               return -1;
 
-       while (index < *item_num && ptr < jwt_end) {
+       items[index].start = ptr;
+       items[index].length = 0;
+
+       while (ptr < jwt_end) {
                if (*ptr++ == '.') {
                        items[index++].length = length;
+                       /* We found enough items, no need to keep looking for
+                        * separators. */
+                       if (index == item_num)
+                               return 1;
 
-                       if (index == *item_num)
-                               return -1;
                        items[index].start = ptr;
                        items[index].length = 0;
                        length = 0;
@@ -121,10 +125,11 @@ int jwt_tokenize(const struct buffer *jwt, struct jwt_item *items, unsigned int
                        ++length;
        }
 
-       if (index < *item_num)
-               items[index].length = length;
+       /* We might not have found enough items */
+       if (index < item_num - 1)
+               return -1;
 
-       *item_num = (index+1);
+       items[index].length = length;
 
        return (ptr != jwt_end);
 }
@@ -493,13 +498,9 @@ enum jwt_vrfy_status jwt_verify(const struct buffer *token, const struct buffer
        if (ctx.alg == JWT_ALG_DEFAULT)
                return JWT_VRFY_UNKNOWN_ALG;
 
-       if (jwt_tokenize(token, items, &item_num))
+       if (jwt_tokenize(token, items, item_num))
                return JWT_VRFY_INVALID_TOKEN;
 
-       if (item_num != JWT_ELT_MAX)
-               if (ctx.alg != JWS_ALG_NONE || item_num != JWT_ELT_SIG)
-                       return JWT_VRFY_INVALID_TOKEN;
-
        ctx.jose = items[JWT_ELT_JOSE];
        ctx.claims = items[JWT_ELT_CLAIMS];
        ctx.signature = items[JWT_ELT_SIG];
index f8150eaa53be633eed41506aaa5b4a04ef4fe4e1..b29f6f3d3763b11ff38c803b271d2c881a75dbbf 100644 (file)
@@ -4795,9 +4795,10 @@ static int sample_conv_jwt_member_query(const struct arg *args, struct sample *s
        int retval = 0;
        int ret;
 
-       jwt_tokenize(&smp->data.u.str, items, &item_num);
-
-       if (item_num < member + 1)
+       /* We don't need to extract all the parts from the token, we only need a
+        * specific one.
+        */
+       if (jwt_tokenize(&smp->data.u.str, items, item_num) < 0)
                goto end;
 
        decoded_header = get_trash_chunk_sz(items[member].length);