diff --git a/include/fluent-bit/flb_oauth2.h b/include/fluent-bit/flb_oauth2.h
index 3d237e5fcc4..b6eb07d56d5 100644
--- a/include/fluent-bit/flb_oauth2.h
+++ b/include/fluent-bit/flb_oauth2.h
@@ -33,7 +33,8 @@
 
 enum flb_oauth2_auth_method {
     FLB_OAUTH2_AUTH_METHOD_BASIC = 0,
-    FLB_OAUTH2_AUTH_METHOD_POST  = 1
+    FLB_OAUTH2_AUTH_METHOD_POST  = 1,
+    FLB_OAUTH2_AUTH_METHOD_PRIVATE_KEY_JWT = 2
 };
 
 struct flb_oauth2_config {
@@ -43,9 +44,15 @@ struct flb_oauth2_config {
     flb_sds_t client_secret;
     flb_sds_t scope;
     flb_sds_t audience;
+    flb_sds_t resource;
+    flb_sds_t jwt_key_file;
+    flb_sds_t jwt_cert_file;
+    flb_sds_t jwt_aud;
+    flb_sds_t jwt_header;
 
     enum flb_oauth2_auth_method auth_method;
 
+    int jwt_ttl;
     int refresh_skew;
     int timeout;
     int connect_timeout;
diff --git a/include/fluent-bit/flb_oauth2_jwt.h b/include/fluent-bit/flb_oauth2_jwt.h
index 99412cd7c10..2da8fffc02e 100644
--- a/include/fluent-bit/flb_oauth2_jwt.h
+++ b/include/fluent-bit/flb_oauth2_jwt.h
@@ -54,6 +54,7 @@ struct flb_oauth2_jwt_claims {
     flb_sds_t client_id;
     uint64_t expiration;
     int has_azp;
+    int has_client_id_claim;
 };
 
 struct flb_oauth2_jwt {
@@ -70,7 +71,7 @@ struct flb_oauth2_jwt_cfg {
     flb_sds_t   issuer;                   /* expected issuer */
     flb_sds_t   jwks_url;                 /* JWKS endpoint */
     flb_sds_t   allowed_audience;         /* audience claim to enforce */
-    struct mk_list *allowed_clients;      /* list of authorized azp/client_id */
+    struct mk_list *allowed_clients;      /* list of authorized azp/client_id/appid */
     int         jwks_refresh_interval;    /* refresh cadence in seconds */
 };
 
diff --git a/plugins/out_http/http.c b/plugins/out_http/http.c
index bf594d28336..63e9552b6c9 100644
--- a/plugins/out_http/http.c
+++ b/plugins/out_http/http.c
@@ -753,10 +753,45 @@ static struct flb_config_map config_map[] = {
      0, FLB_TRUE, offsetof(struct flb_out_http, oauth2_config.audience),
      "Optional OAuth2 audience parameter"
     },
+    {
+     FLB_CONFIG_MAP_STR, "oauth2.resource", NULL,
+     0, FLB_TRUE, offsetof(struct flb_out_http, oauth2_config.resource),
+     "Optional OAuth2 resource parameter"
+    },
     {
      FLB_CONFIG_MAP_STR, "oauth2.auth_method", "basic",
      0, FLB_TRUE, offsetof(struct flb_out_http, oauth2_auth_method),
-     "OAuth2 client authentication method: basic or post"
+     "OAuth2 client authentication method: basic, post or private_key_jwt"
+    },
+    {
+     FLB_CONFIG_MAP_STR, "oauth2.jwt_key_file", NULL,
+     0, FLB_TRUE, offsetof(struct flb_out_http,
+                           oauth2_config.jwt_key_file),
+     "Path to PEM private key used by private_key_jwt"
+    },
+    {
+     FLB_CONFIG_MAP_STR, "oauth2.jwt_cert_file", NULL,
+     0, FLB_TRUE, offsetof(struct flb_out_http,
+                           oauth2_config.jwt_cert_file),
+     "Path to certificate file used by private_key_jwt"
+    },
+    {
+     FLB_CONFIG_MAP_STR, "oauth2.jwt_aud", NULL,
+     0, FLB_TRUE, offsetof(struct flb_out_http,
+                           oauth2_config.jwt_aud),
+     "Audience for private_key_jwt assertion (defaults to oauth2.token_url)"
+    },
+    {
+     FLB_CONFIG_MAP_STR, "oauth2.jwt_header", "kid",
+     0, FLB_TRUE, offsetof(struct flb_out_http,
+                           oauth2_config.jwt_header),
+     "JWT header claim name for private_key_jwt thumbprint (kid or x5t)"
+    },
+    {
+     FLB_CONFIG_MAP_INT, "oauth2.jwt_ttl_seconds", "300",
+     0, FLB_TRUE, offsetof(struct flb_out_http,
+                           oauth2_config.jwt_ttl),
+     "Lifetime in seconds for private_key_jwt client assertions"
     },
     {
      FLB_CONFIG_MAP_INT, "oauth2.refresh_skew_seconds", "60",
diff --git a/plugins/out_http/http_conf.c b/plugins/out_http/http_conf.c
index 779d6bf1828..bc33334961b 100644
--- a/plugins/out_http/http_conf.c
+++ b/plugins/out_http/http_conf.c
@@ -338,6 +338,10 @@ struct flb_out_http *flb_http_conf_create(struct flb_output_instance *ins,
             else if (strcasecmp(tmp, "post") == 0) {
                 ctx->oauth2_config.auth_method = FLB_OAUTH2_AUTH_METHOD_POST;
             }
+            else if (strcasecmp(tmp, "private_key_jwt") == 0) {
+                ctx->oauth2_config.auth_method =
+                    FLB_OAUTH2_AUTH_METHOD_PRIVATE_KEY_JWT;
+            }
             else {
                 flb_plg_error(ctx->ins, "invalid oauth2.auth_method '%s'", tmp);
                 flb_http_conf_destroy(ctx);
@@ -345,10 +349,24 @@ struct flb_out_http *flb_http_conf_create(struct flb_output_instance *ins,
             }
         }
 
-        if (!ctx->oauth2_config.token_url ||
-            !ctx->oauth2_config.client_id ||
-            !ctx->oauth2_config.client_secret) {
-            flb_plg_error(ctx->ins, "oauth2 requires token_url, client_id and client_secret");
+        if (!ctx->oauth2_config.token_url || !ctx->oauth2_config.client_id) {
+            flb_plg_error(ctx->ins, "oauth2 requires token_url and client_id");
+            flb_http_conf_destroy(ctx);
+            return NULL;
+        }
+
+        if (ctx->oauth2_config.auth_method == FLB_OAUTH2_AUTH_METHOD_PRIVATE_KEY_JWT) {
+            if (!ctx->oauth2_config.jwt_key_file ||
+                !ctx->oauth2_config.jwt_cert_file) {
+                flb_plg_error(ctx->ins, "oauth2 private_key_jwt requires "
+                              "jwt_key_file and "
+                              "jwt_cert_file");
+                flb_http_conf_destroy(ctx);
+                return NULL;
+            }
+        }
+        else if (!ctx->oauth2_config.client_secret) {
+            flb_plg_error(ctx->ins, "oauth2 basic/post require client_secret");
             flb_http_conf_destroy(ctx);
             return NULL;
         }
diff --git a/src/flb_oauth2.c b/src/flb_oauth2.c
index a92cce7477e..1dbc11b2918 100644
--- a/src/flb_oauth2.c
+++ b/src/flb_oauth2.c
@@ -26,10 +26,23 @@
 #include <fluent-bit/flb_upstream.h>
 #include <fluent-bit/flb_http_client.h>
 #include <fluent-bit/flb_jsmn.h>
+#include <fluent-bit/flb_base64.h>
+#include <fluent-bit/flb_hash.h>
+#include <fluent-bit/flb_crypto.h>
 
 #include <time.h>
 #include <string.h>
 #include <stddef.h>
+#include <stdio.h>
+#include <limits.h>
+
+#include <openssl/bio.h>
+#include <openssl/x509.h>
+#include <openssl/pem.h>
+
+#define FLB_OAUTH2_DEFAULT_ASSERTION_TTL 300
+#define FLB_OAUTH2_DEFAULT_ASSERTION_HEADER "kid"
+#define FLB_OAUTH2_ASSERTION_UUID_LEN 37
 
 /* Config map for OAuth2 configuration */
 struct flb_config_map oauth2_config_map[] = {
@@ -63,6 +76,43 @@ struct flb_config_map oauth2_config_map[] = {
      0, FLB_TRUE, offsetof(struct flb_oauth2_config, audience),
      "Optional OAuth2 audience parameter"
     },
+    {
+     FLB_CONFIG_MAP_STR, "oauth2.resource", NULL,
+     0, FLB_TRUE, offsetof(struct flb_oauth2_config, resource),
+     "Optional OAuth2 resource parameter"
+    },
+    {
+     FLB_CONFIG_MAP_STR, "oauth2.auth_method", "basic",
+     0, FLB_FALSE, 0,
+     "OAuth2 client authentication method: basic, post or private_key_jwt"
+    },
+    {
+     FLB_CONFIG_MAP_STR, "oauth2.jwt_key_file", NULL,
+     0, FLB_TRUE, offsetof(struct flb_oauth2_config,
+                           jwt_key_file),
+     "Path to PEM private key for private_key_jwt authentication"
+    },
+    {
+     FLB_CONFIG_MAP_STR, "oauth2.jwt_cert_file", NULL,
+     0, FLB_TRUE, offsetof(struct flb_oauth2_config,
+                           jwt_cert_file),
+     "Path to certificate file for private_key_jwt kid/x5t derivation"
+    },
+    {
+     FLB_CONFIG_MAP_STR, "oauth2.jwt_aud", NULL,
+     0, FLB_TRUE, offsetof(struct flb_oauth2_config, jwt_aud),
+     "Audience used in private_key_jwt assertion (defaults to token_url)"
+    },
+    {
+     FLB_CONFIG_MAP_STR, "oauth2.jwt_header", "kid",
+     0, FLB_TRUE, offsetof(struct flb_oauth2_config, jwt_header),
+     "JWT header claim name for thumbprint in private_key_jwt (e.g., kid, x5t)"
+    },
+    {
+     FLB_CONFIG_MAP_INT, "oauth2.jwt_ttl_seconds", "300",
+     0, FLB_TRUE, offsetof(struct flb_oauth2_config, jwt_ttl),
+     "Lifetime in seconds for private_key_jwt client assertions"
+    },
     {
      FLB_CONFIG_MAP_INT, "oauth2.refresh_skew_seconds", "60",
      0, FLB_TRUE, offsetof(struct flb_oauth2_config, refresh_skew),
@@ -126,6 +176,7 @@ static void oauth2_apply_defaults(struct flb_oauth2_config *cfg)
 {
     cfg->enabled = FLB_FALSE;
     cfg->auth_method = FLB_OAUTH2_AUTH_METHOD_BASIC;
+    cfg->jwt_ttl = FLB_OAUTH2_DEFAULT_ASSERTION_TTL;
     cfg->refresh_skew = FLB_OAUTH2_DEFAULT_SKEW_SECS;
     cfg->timeout = 0;
     cfg->connect_timeout = 0;
@@ -135,6 +186,11 @@ static void oauth2_apply_defaults(struct flb_oauth2_config *cfg)
     cfg->client_secret = NULL;
     cfg->scope = NULL;
     cfg->audience = NULL;
+    cfg->resource = NULL;
+    cfg->jwt_key_file = NULL;
+    cfg->jwt_cert_file = NULL;
+    cfg->jwt_aud = NULL;
+    cfg->jwt_header = NULL;
 }
 
 static int oauth2_clone_config(struct flb_oauth2_config *dst,
@@ -155,6 +211,9 @@ static int oauth2_clone_config(struct flb_oauth2_config *dst,
 
     dst->timeout = src->timeout;
     dst->connect_timeout = src->connect_timeout;
+    if (src->jwt_ttl > 0) {
+        dst->jwt_ttl = src->jwt_ttl;
+    }
 
     if (src->token_url) {
         dst->token_url = flb_sds_create(src->token_url);
@@ -201,6 +260,54 @@ static int oauth2_clone_config(struct flb_oauth2_config *dst,
         }
     }
 
+    if (src->resource) {
+        dst->resource = flb_sds_create(src->resource);
+        if (!dst->resource) {
+            flb_errno();
+            flb_oauth2_config_destroy(dst);
+            return -1;
+        }
+    }
+
+    if (src->jwt_key_file) {
+        dst->jwt_key_file =
+            flb_sds_create(src->jwt_key_file);
+        if (!dst->jwt_key_file) {
+            flb_errno();
+            flb_oauth2_config_destroy(dst);
+            return -1;
+        }
+    }
+
+    if (src->jwt_cert_file) {
+        dst->jwt_cert_file =
+            flb_sds_create(src->jwt_cert_file);
+        if (!dst->jwt_cert_file) {
+            flb_errno();
+            flb_oauth2_config_destroy(dst);
+            return -1;
+        }
+    }
+
+    if (src->jwt_aud) {
+        dst->jwt_aud =
+            flb_sds_create(src->jwt_aud);
+        if (!dst->jwt_aud) {
+            flb_errno();
+            flb_oauth2_config_destroy(dst);
+            return -1;
+        }
+    }
+
+    if (src->jwt_header) {
+        dst->jwt_header = flb_sds_create(src->jwt_header);
+        if (!dst->jwt_header) {
+            flb_errno();
+            flb_oauth2_config_destroy(dst);
+            return -1;
+        }
+    }
+
     return 0;
 }
 
@@ -220,6 +327,16 @@ void flb_oauth2_config_destroy(struct flb_oauth2_config *cfg)
     cfg->scope = NULL;
     flb_sds_destroy(cfg->audience);
     cfg->audience = NULL;
+    flb_sds_destroy(cfg->resource);
+    cfg->resource = NULL;
+    flb_sds_destroy(cfg->jwt_key_file);
+    cfg->jwt_key_file = NULL;
+    flb_sds_destroy(cfg->jwt_cert_file);
+    cfg->jwt_cert_file = NULL;
+    flb_sds_destroy(cfg->jwt_aud);
+    cfg->jwt_aud = NULL;
+    flb_sds_destroy(cfg->jwt_header);
+    cfg->jwt_header = NULL;
 }
 
 static int oauth2_setup_upstream(struct flb_oauth2 *ctx,
@@ -473,15 +590,361 @@ static flb_sds_t oauth2_append_kv(flb_sds_t buffer, const char *key,
     return result;
 }
 
+static int oauth2_base64_url_encode(const unsigned char *input, size_t input_size,
+                                    flb_sds_t *output)
+{
+    int i;
+    int ret;
+    size_t olen;
+    size_t encoded_size;
+    unsigned char *encoded;
+
+    if (!input || !output) {
+        return -1;
+    }
+
+    encoded_size = ((input_size + 2) / 3) * 4 + 4;
+
+    encoded = flb_malloc(encoded_size);
+    if (!encoded) {
+        flb_errno();
+        return -1;
+    }
+
+    ret = flb_base64_encode(encoded, encoded_size - 1, &olen,
+                            (unsigned char *) input, input_size);
+    if (ret != 0) {
+        flb_free(encoded);
+        return -1;
+    }
+
+    for (i = 0; i < (int) olen && encoded[i] != '='; i++) {
+        if (encoded[i] == '+') {
+            encoded[i] = '-';
+        }
+        else if (encoded[i] == '/') {
+            encoded[i] = '_';
+        }
+    }
+
+    *output = flb_sds_create_len((char *) encoded, i);
+    flb_free(encoded);
+
+    if (!*output) {
+        flb_errno();
+        return -1;
+    }
+
+    return 0;
+}
+
+static int oauth2_private_key_jwt_thumbprint(const char *certificate_file,
+                                             const char *header_name,
+                                             flb_sds_t *thumbprint)
+{
+    int i;
+    int ret;
+    const EVP_MD *digest_type;
+    char hex[EVP_MAX_MD_SIZE * 2 + 1];
+    BIO *bio = NULL;
+    X509 *cert = NULL;
+    char *file_buf = NULL;
+    size_t file_size;
+    unsigned int digest_len = 0;
+    unsigned char digest[EVP_MAX_MD_SIZE];
+
+    ret = flb_utils_read_file((char *) certificate_file, &file_buf, &file_size);
+    if (ret != 0 || !file_buf || file_size == 0) {
+        flb_error("[oauth2] failed to read certificate file '%s'", certificate_file);
+        return -1;
+    }
+
+    if (file_size > INT_MAX) {
+        flb_error("[oauth2] certificate file '%s' is too large",
+                  certificate_file);
+        flb_free(file_buf);
+        return -1;
+    }
+
+    bio = BIO_new_mem_buf(file_buf, (int) file_size);
+    if (!bio) {
+        flb_error("[oauth2] failed to initialize certificate buffer");
+        flb_free(file_buf);
+        return -1;
+    }
+
+    cert = PEM_read_bio_X509(bio, NULL, NULL, NULL);
+    if (!cert) {
+        BIO_free(bio);
+        bio = BIO_new_mem_buf(file_buf, (int) file_size);
+        if (!bio) {
+            flb_error("[oauth2] failed to reload certificate buffer");
+            flb_free(file_buf);
+            return -1;
+        }
+        cert = d2i_X509_bio(bio, NULL);
+    }
+
+    if (!cert) {
+        flb_error("[oauth2] certificate '%s' is not valid PEM/DER X509",
+                  certificate_file);
+        BIO_free(bio);
+        flb_free(file_buf);
+        return -1;
+    }
+
+    digest_type = EVP_sha1();
+    if (strcasecmp(header_name, "x5t#S256") == 0) {
+        digest_type = EVP_sha256();
+    }
+
+    ret = X509_digest(cert, digest_type, digest, &digest_len);
+    if (ret != 1 || digest_len == 0) {
+        flb_error("[oauth2] failed to compute certificate thumbprint");
+        X509_free(cert);
+        BIO_free(bio);
+        flb_free(file_buf);
+        return -1;
+    }
+
+    if (strcasecmp(header_name, "x5t") == 0 ||
+        strcasecmp(header_name, "x5t#S256") == 0) {
+        ret = oauth2_base64_url_encode(digest, digest_len, thumbprint);
+        if (ret != 0) {
+            X509_free(cert);
+            BIO_free(bio);
+            flb_free(file_buf);
+            return -1;
+        }
+    }
+    else {
+        for (i = 0; i < (int) digest_len; i++) {
+            snprintf(&hex[i * 2], 3, "%02X", digest[i]);
+        }
+        hex[digest_len * 2] = '\0';
+
+        *thumbprint = flb_sds_create(hex);
+        if (!*thumbprint) {
+            flb_errno();
+            X509_free(cert);
+            BIO_free(bio);
+            flb_free(file_buf);
+            return -1;
+        }
+    }
+
+    X509_free(cert);
+    BIO_free(bio);
+    flb_free(file_buf);
+
+    return 0;
+}
+
+static int oauth2_private_key_jwt_sign(const char *private_key_file,
+                                       const char *data, size_t data_len,
+                                       flb_sds_t *signature)
+{
+    int ret;
+    char *key_buf = NULL;
+    size_t key_size;
+    size_t sig_len;
+    unsigned char digest[32];
+    unsigned char sig[4096];
+
+    ret = flb_utils_read_file((char *) private_key_file, &key_buf, &key_size);
+    if (ret != 0 || !key_buf || key_size == 0) {
+        flb_error("[oauth2] failed to read private key file '%s'", private_key_file);
+        return -1;
+    }
+
+    ret = flb_hash_simple(FLB_HASH_SHA256,
+                          (unsigned char *) data, data_len,
+                          digest, sizeof(digest));
+    if (ret != FLB_CRYPTO_SUCCESS) {
+        flb_error("[oauth2] failed to hash JWT assertion payload");
+        flb_free(key_buf);
+        return -1;
+    }
+
+    sig_len = sizeof(sig);
+    ret = flb_crypto_sign_simple(FLB_CRYPTO_PRIVATE_KEY,
+                                 FLB_CRYPTO_PADDING_PKCS1,
+                                 FLB_HASH_SHA256,
+                                 (unsigned char *) key_buf, key_size,
+                                 digest, sizeof(digest),
+                                 sig, &sig_len);
+    flb_free(key_buf);
+    if (ret != FLB_CRYPTO_SUCCESS) {
+        flb_error("[oauth2] failed to sign JWT assertion");
+        return -1;
+    }
+
+    return oauth2_base64_url_encode(sig, sig_len, signature);
+}
+
+static flb_sds_t oauth2_private_key_jwt_create_assertion(struct flb_oauth2 *ctx)
+{
+    int ret;
+    int ttl;
+    time_t now;
+    char jti[FLB_OAUTH2_ASSERTION_UUID_LEN] = {0};
+    const char *header_name;
+    const char *audience;
+    flb_sds_t thumbprint = NULL;
+    flb_sds_t header_json = NULL;
+    flb_sds_t payload_json = NULL;
+    flb_sds_t header_b64 = NULL;
+    flb_sds_t payload_b64 = NULL;
+    flb_sds_t signing_input = NULL;
+    flb_sds_t signature_b64 = NULL;
+    flb_sds_t assertion = NULL;
+    flb_sds_t tmp = NULL;
+
+    if (!ctx->cfg.client_id || !ctx->cfg.jwt_key_file ||
+        !ctx->cfg.jwt_cert_file) {
+        flb_error("[oauth2] private_key_jwt requires client_id, "
+                  "jwt_key_file and "
+                  "jwt_cert_file");
+        return NULL;
+    }
+
+    header_name = ctx->cfg.jwt_header ?
+                  ctx->cfg.jwt_header :
+                  FLB_OAUTH2_DEFAULT_ASSERTION_HEADER;
+    audience = ctx->cfg.jwt_aud ?
+               ctx->cfg.jwt_aud :
+               ctx->cfg.token_url;
+    ttl = ctx->cfg.jwt_ttl > 0 ?
+          ctx->cfg.jwt_ttl :
+          FLB_OAUTH2_DEFAULT_ASSERTION_TTL;
+
+    if (flb_utils_uuid_v4_gen(jti) != 0) {
+        flb_error("[oauth2] failed to generate JWT jti");
+        return NULL;
+    }
+
+    ret = oauth2_private_key_jwt_thumbprint(
+            ctx->cfg.jwt_cert_file,
+            header_name, &thumbprint);
+    if (ret != 0) {
+        return NULL;
+    }
+
+    header_json = flb_sds_create_size(256);
+    if (!header_json) {
+        flb_errno();
+        goto error;
+    }
+    tmp = flb_sds_printf(&header_json,
+                         "{\"alg\":\"RS256\",\"typ\":\"JWT\",\"%s\":\"%s\"}",
+                         header_name, thumbprint);
+    if (!tmp) {
+        goto error;
+    }
+    header_json = tmp;
+
+    now = time(NULL);
+    payload_json = flb_sds_create_size(512);
+    if (!payload_json) {
+        flb_errno();
+        goto error;
+    }
+    tmp = flb_sds_printf(&payload_json,
+                         "{\"iss\":\"%s\",\"sub\":\"%s\",\"aud\":\"%s\","
+                         "\"iat\":%lu,\"exp\":%lu,\"jti\":\"%s\"}",
+                         ctx->cfg.client_id, ctx->cfg.client_id, audience,
+                         (unsigned long) now, (unsigned long) (now + ttl),
+                         jti);
+    if (!tmp) {
+        goto error;
+    }
+    payload_json = tmp;
+
+    ret = oauth2_base64_url_encode((unsigned char *) header_json,
+                                   flb_sds_len(header_json), &header_b64);
+    if (ret != 0) {
+        goto error;
+    }
+
+    ret = oauth2_base64_url_encode((unsigned char *) payload_json,
+                                   flb_sds_len(payload_json), &payload_b64);
+    if (ret != 0) {
+        goto error;
+    }
+
+    signing_input = flb_sds_create_size(flb_sds_len(header_b64) +
+                                        flb_sds_len(payload_b64) + 2);
+    if (!signing_input) {
+        flb_errno();
+        goto error;
+    }
+
+    tmp = flb_sds_printf(&signing_input, "%s.%s", header_b64, payload_b64);
+    if (!tmp) {
+        goto error;
+    }
+    signing_input = tmp;
+
+    ret = oauth2_private_key_jwt_sign(ctx->cfg.jwt_key_file,
+                                      signing_input, flb_sds_len(signing_input),
+                                      &signature_b64);
+    if (ret != 0) {
+        goto error;
+    }
+
+    assertion = flb_sds_create_size(flb_sds_len(signing_input) +
+                                    flb_sds_len(signature_b64) + 2);
+    if (!assertion) {
+        flb_errno();
+        goto error;
+    }
+
+    tmp = flb_sds_printf(&assertion, "%s.%s", signing_input, signature_b64);
+    if (!tmp) {
+        goto error;
+    }
+    assertion = tmp;
+
+    flb_sds_destroy(thumbprint);
+    flb_sds_destroy(header_json);
+    flb_sds_destroy(payload_json);
+    flb_sds_destroy(header_b64);
+    flb_sds_destroy(payload_b64);
+    flb_sds_destroy(signing_input);
+    flb_sds_destroy(signature_b64);
+
+    return assertion;
+
+error:
+    flb_sds_destroy(assertion);
+    flb_sds_destroy(signature_b64);
+    flb_sds_destroy(signing_input);
+    flb_sds_destroy(payload_b64);
+    flb_sds_destroy(header_b64);
+    flb_sds_destroy(payload_json);
+    flb_sds_destroy(header_json);
+    flb_sds_destroy(thumbprint);
+
+    return NULL;
+}
+
 static flb_sds_t oauth2_build_body(struct flb_oauth2 *ctx)
 {
     flb_sds_t body;
     flb_sds_t tmp;
+    flb_sds_t assertion = NULL;
 
     if (ctx->payload_manual == FLB_TRUE && ctx->payload) {
         return flb_sds_create_len(ctx->payload, flb_sds_len(ctx->payload));
     }
 
+    if ((ctx->cfg.auth_method == FLB_OAUTH2_AUTH_METHOD_BASIC ||
+         ctx->cfg.auth_method == FLB_OAUTH2_AUTH_METHOD_POST) &&
+        (!ctx->cfg.client_id || !ctx->cfg.client_secret)) {
+        flb_error("[oauth2] auth method requires client_id and client_secret");
+        return NULL;
+    }
+
     body = flb_sds_create_size(128);
     if (!body) {
         return NULL;
@@ -512,6 +975,15 @@ static flb_sds_t oauth2_build_body(struct flb_oauth2 *ctx)
         body = tmp;
     }
 
+    if (ctx->cfg.resource) {
+        tmp = oauth2_append_kv(body, "resource", ctx->cfg.resource);
+        if (!tmp) {
+            flb_sds_destroy(body);
+            return NULL;
+        }
+        body = tmp;
+    }
+
     if (ctx->cfg.auth_method == FLB_OAUTH2_AUTH_METHOD_POST) {
         if (ctx->cfg.client_id) {
             tmp = oauth2_append_kv(body, "client_id", ctx->cfg.client_id);
@@ -531,6 +1003,38 @@ static flb_sds_t oauth2_build_body(struct flb_oauth2 *ctx)
             body = tmp;
         }
     }
+    else if (ctx->cfg.auth_method == FLB_OAUTH2_AUTH_METHOD_PRIVATE_KEY_JWT) {
+        if (ctx->cfg.client_id) {
+            tmp = oauth2_append_kv(body, "client_id", ctx->cfg.client_id);
+            if (!tmp) {
+                flb_sds_destroy(body);
+                return NULL;
+            }
+            body = tmp;
+        }
+
+        tmp = oauth2_append_kv(body, "client_assertion_type",
+                               "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
+        if (!tmp) {
+            flb_sds_destroy(body);
+            return NULL;
+        }
+        body = tmp;
+
+        assertion = oauth2_private_key_jwt_create_assertion(ctx);
+        if (!assertion) {
+            flb_sds_destroy(body);
+            return NULL;
+        }
+
+        tmp = oauth2_append_kv(body, "client_assertion", assertion);
+        flb_sds_destroy(assertion);
+        if (!tmp) {
+            flb_sds_destroy(body);
+            return NULL;
+        }
+        body = tmp;
+    }
 
     return body;
 }
@@ -698,6 +1202,17 @@ struct flb_oauth2 *flb_oauth2_create_from_config(struct flb_config *config,
         return NULL;
     }
 
+    if (ctx->cfg.auth_method == FLB_OAUTH2_AUTH_METHOD_PRIVATE_KEY_JWT) {
+        if (!ctx->cfg.client_id ||
+            !ctx->cfg.jwt_key_file ||
+            !ctx->cfg.jwt_cert_file) {
+            flb_error("[oauth2] private_key_jwt requires client_id, "
+                      "jwt_key_file and "
+                      "jwt_cert_file");
+            flb_oauth2_destroy(ctx);
+            return NULL;
+        }
+    }
     ctx->auth_url = flb_sds_create(ctx->cfg.token_url);
     if (!ctx->auth_url) {
         flb_errno();
@@ -943,4 +1458,3 @@ struct mk_list *flb_oauth2_get_config_map(struct flb_config *config)
 
     return config_map;
 }
-
diff --git a/src/flb_oauth2_jwt.c b/src/flb_oauth2_jwt.c
index e623cd011c9..9cd28db83c8 100644
--- a/src/flb_oauth2_jwt.c
+++ b/src/flb_oauth2_jwt.c
@@ -573,6 +573,23 @@ static int oauth2_jwt_parse_payload(const char *json, size_t json_len,
                     }
                     claims->client_id = flb_sds_create_len((const char *)v->via.str.ptr,
                                                            v->via.str.size);
+                    claims->has_client_id_claim = FLB_TRUE;
+                }
+            }
+        }
+        else if (key_len == 5 && strncmp(key_str, "appid", 5) == 0) {
+            if (v->type == MSGPACK_OBJECT_STR) {
+                /*
+                 * ADFS commonly emits appid for application identity.
+                 * Use it as a fallback when neither azp nor client_id are present.
+                 */
+                if (claims->has_azp == FLB_FALSE &&
+                    claims->has_client_id_claim == FLB_FALSE) {
+                    if (claims->client_id) {
+                        flb_sds_destroy(claims->client_id);
+                    }
+                    claims->client_id = flb_sds_create_len((const char *)v->via.str.ptr,
+                                                           v->via.str.size);
                 }
             }
         }
diff --git a/tests/internal/oauth2.c b/tests/internal/oauth2.c
index b82f93c06e9..9ca68c1048a 100644
--- a/tests/internal/oauth2.c
+++ b/tests/internal/oauth2.c
@@ -9,8 +9,11 @@
 #include <fluent-bit/flb_oauth2.h>
 #include <fluent-bit/flb_time.h>
 #include <fluent-bit/flb_upstream.h>
+#include <fluent-bit/flb_base64.h>
 
 #include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
 
 #ifndef _WIN32
 #include <sys/socket.h>
@@ -24,7 +27,60 @@
 
 #include "flb_tests_internal.h"
 
-#define MOCK_BODY_SIZE 1024
+#define MOCK_BODY_SIZE 16384
+#define TEST_CERT_FILENAME "oauth2_private_key_jwt_test_cert.pem"
+#define TEST_KEY_FILENAME  "oauth2_private_key_jwt_test_key.pem"
+
+static const char *TEST_PRIVATE_KEY_PEM =
+"-----BEGIN PRIVATE KEY-----\n"
+"MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDQ2q+ICzQ7U1dm\n"
+"sEQVvbQjVlv3iZITTrXcX0Hkxu5qh/L41QIG3ZKEkih3S0rDtumiHUDrQVlG9Ioz\n"
+"9s7zOY3sbwEC1c9UzqowI0urm9wJVkC8rDPhKTCD6pAAMwP27npxSmI6EfFRCefg\n"
+"42z5o/KKZGBzrR9FK+Mbik821eLJG67bT4ElclnSeIUC8+/rUgAMygCLH+o2BsTn\n"
+"BklBqDdnOTzMKOkH/rp8eC2EXimyokWb/jVUfMG6dw8Dg3WNNKIZ5Ye756qjdOsY\n"
+"lV6ptQpbyKK2RBDxK5yJnqP30IMHOcDvf4Ohko4jZpKB9gdC58Lqi5w/J1pI3gO4\n"
+"WUQxmcmVAgMBAAECggEADRIamSuGVc4l8qXGZQvtyaZedBP2geHTrNqDT7OJeT5P\n"
+"3PXLvi1Ava49/RVHtQ7t+TjWdsKsuS2VtqHUGtG3yZu61vgVlSun6AJVeoRzFVyC\n"
+"CazHRGilAiR8ZZ7LuTj8jbmHgzXbQeSaT+87wzXY+INGrAaiJdyUxoT15yskmcxW\n"
+"L7qxnXimxvGjYLn67xOJwhN6/JP+L1DV1TE6l9aSDlYAXR6Mb3mXDDcvaVDsddGF\n"
+"uoDyErJKANy37DlsC6GIkDoq/lR8GI0yy4pUzrwP2ANWNlG+hExhksrw9+e8sTgL\n"
+"GFIOZLTvL1GU5k3kspDOQakHZz80YuQBgYfQvRptsQKBgQD635AKe2LbduqnG2vQ\n"
+"8Y7BUTAaUuVftfL2XeuNZ4kuBq7cqBrNIrPZWIEmkuU4imNhQ3gCpmQ5XXbQqfqz\n"
+"DbE6p8eBMtK1K3iyN7P5myP94PlzJ0VPbcKt24xILrAkw3ePObgPaVzGIcWAR38k\n"
+"ZpNRlN1LDy3mjbj44AYNEQUKeQKBgQDVH00ock5kwtO4vniBxuUtt59zTsscmOry\n"
+"NGbdHZ03sh1qZpijZlYc6ocLZ+rdkxpwIA7SfOOnV+5v6WnI1Hzuj4A5P66J3GT/\n"
+"UNjiamuQHFzDSbdEtaFNWr7EV6QXbNBP1ZoYY5GiKpa6SoSuFyAuJVJfxov3PGpU\n"
+"rlx/oPXw/QKBgQD6iFionx/SW6dqyo+ZUiJmHFYVc8NtGZ9ROeoKhOMR+8qUwaxC\n"
+"P+2rmB8iDoCrPkiQ0Xf/7XsZbqVBLP8X4QykrvklpUOXeZpHICmzk6MV3p4+yXEG\n"
+"KW7JgP9O9pEhpbK4bcPKYEYt93vs53mpOGbWifuVAcus+stGfzKLyftmwQKBgQCa\n"
+"mohojPNdmQ/p9xKIYnaigZBEH6asaioV5fmw8ei5HJbGNwMHlhdmBqRMm+f/MNV+\n"
+"/WKDQ2IKZXls6dB5hdvTW3pTDWVaUO1bYZTUOwsokcqhSHqQd4o6CVhWKpW5AJDl\n"
+"OTj99E0TbP3GyoQRnmkT0LM/E1M52TPxlkM3utZvKQKBgFoEeP8L8gO9Tvc4TaBp\n"
+"S6arcO3FIObR8Hb9FxHu+lK+M/y+y77PmrHr2l1BFkiX8dWPk9OZLsC6tno6PfCn\n"
+"dooXSFG+U6C+kDQjwtXQCXMW6Vry7AuRY8dbaHH0f1o2fePM08ZdzcHlwWQmwGsL\n"
+"mzHuWItixgGqYX2cs3yslCQ/\n"
+"-----END PRIVATE KEY-----\n";
+
+static const char *TEST_CERT_PEM =
+"-----BEGIN CERTIFICATE-----\n"
+"MIIDFTCCAf2gAwIBAgIUIg54y3h7UiGzRc50dFWpdMdzLqYwDQYJKoZIhvcNAQEL\n"
+"BQAwGjEYMBYGA1UEAwwPZmxiLW9hdXRoMi10ZXN0MB4XDTI2MDIyMDIxNDIwMloX\n"
+"DTI2MDIyMTIxNDIwMlowGjEYMBYGA1UEAwwPZmxiLW9hdXRoMi10ZXN0MIIBIjAN\n"
+"BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0NqviAs0O1NXZrBEFb20I1Zb94mS\n"
+"E0613F9B5Mbuaofy+NUCBt2ShJIod0tKw7bpoh1A60FZRvSKM/bO8zmN7G8BAtXP\n"
+"VM6qMCNLq5vcCVZAvKwz4Skwg+qQADMD9u56cUpiOhHxUQnn4ONs+aPyimRgc60f\n"
+"RSvjG4pPNtXiyRuu20+BJXJZ0niFAvPv61IADMoAix/qNgbE5wZJQag3Zzk8zCjp\n"
+"B/66fHgthF4psqJFm/41VHzBuncPA4N1jTSiGeWHu+eqo3TrGJVeqbUKW8iitkQQ\n"
+"8SuciZ6j99CDBznA73+DoZKOI2aSgfYHQufC6oucPydaSN4DuFlEMZnJlQIDAQAB\n"
+"o1MwUTAdBgNVHQ4EFgQUWcCHMz10eB1evM0LeU9OCOMIwXkwHwYDVR0jBBgwFoAU\n"
+"WcCHMz10eB1evM0LeU9OCOMIwXkwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B\n"
+"AQsFAAOCAQEAKoZHiuU2UzO1RRmQD5js3HEuTd1YrMtGzm/k2D6YDrTWDH0au7vj\n"
+"DitXp41XqE0t4BOPoF+Ee9FJDRViKLCEIEnqX+KtJzoNuHgaOFAva6Ja5uxj+ws1\n"
+"iJnhj1Dwg50mhLsIe4Mb8tVvZPsEKVo5szFlJLi4KNMtIwCVmSS68bdGcGYB6ia1\n"
+"6i07IhmDRGJr5Mi2b+8maDLVKzrNp7caF19vnNI7juaQXbIutGKLXAmZlgvmd4EL\n"
+"TnkYGS36JMp9WK7IoLpvdn28KyV4LRysFJITxpBuM3MvinVlhSioDxzunLuNvjNm\n"
+"gDmvspwf6GKRi6hV6WhahalSGz8itHJ5VQ==\n"
+"-----END CERTIFICATE-----\n";
 
 struct oauth2_mock_server {
     flb_sockfd_t listen_fd;
@@ -35,6 +91,7 @@ struct oauth2_mock_server {
     int resource_challenge;
     int expires_in;
     char latest_token[64];
+    char latest_token_request[MOCK_BODY_SIZE];
     pthread_t thread;
 #ifdef _WIN32
     int wsa_initialized;
@@ -72,14 +129,54 @@ static void compose_http_response(flb_sockfd_t fd, int status, const char *body)
     }
 }
 
-static void handle_token_request(struct oauth2_mock_server *server, flb_sockfd_t fd)
+static int request_content_length(const char *request)
+{
+    int len;
+    const char *p;
+
+    p = strstr(request, "Content-Length:");
+    if (!p) {
+        return 0;
+    }
+
+    p += sizeof("Content-Length:") - 1;
+    while (*p == ' ') {
+        p++;
+    }
+
+    len = atoi(p);
+    if (len < 0) {
+        len = 0;
+    }
+
+    return len;
+}
+
+static void handle_token_request(struct oauth2_mock_server *server, flb_sockfd_t fd,
+                                 const char *request)
 {
     char payload[MOCK_BODY_SIZE];
+    char *body;
+    size_t body_len;
 
     server->token_requests++;
     snprintf(server->latest_token, sizeof(server->latest_token),
              "mock-token-%d", server->token_requests);
 
+    body = strstr(request, "\r\n\r\n");
+    if (body) {
+        body += 4;
+        body_len = strlen(body);
+        if (body_len >= sizeof(server->latest_token_request)) {
+            body_len = sizeof(server->latest_token_request) - 1;
+        }
+        memcpy(server->latest_token_request, body, body_len);
+        server->latest_token_request[body_len] = '\0';
+    }
+    else {
+        server->latest_token_request[0] = '\0';
+    }
+
     snprintf(payload, sizeof(payload),
              "{\"access_token\":\"%s\",\"token_type\":\"Bearer\","\
              "\"expires_in\":%d}",
@@ -117,7 +214,9 @@ static void handle_resource_request(struct oauth2_mock_server *server, flb_sockf
 
 static void *oauth2_mock_server_thread(void *data)
 {
+    int content_len;
     struct oauth2_mock_server *server = (struct oauth2_mock_server *) data;
+    const char *headers_end;
     flb_sockfd_t client_fd;
     fd_set rfds;
     struct timeval tv;
@@ -147,7 +246,7 @@ static void *oauth2_mock_server_thread(void *data)
         /* Make socket blocking for both read and write to ensure reliable operation */
         flb_net_socket_blocking(client_fd);
 
-        /* Read until we get the full HTTP request (ends with \r\n\r\n) */
+        /* Read until we get headers first */
         while (total < sizeof(buffer) - 1) {
             n = recv(client_fd, buffer + total, (int)(sizeof(buffer) - 1 - total), 0);
             if (n <= 0) {
@@ -161,8 +260,25 @@ static void *oauth2_mock_server_thread(void *data)
             }
         }
 
+        headers_end = strstr(buffer, "\r\n\r\n");
+        if (headers_end != NULL) {
+            content_len = request_content_length(buffer);
+            while (content_len > 0 && total < sizeof(buffer) - 1) {
+                if (total >= ((headers_end - buffer) + 4 + content_len)) {
+                    break;
+                }
+                n = recv(client_fd, buffer + total,
+                         (int)(sizeof(buffer) - 1 - total), 0);
+                if (n <= 0) {
+                    break;
+                }
+                total += n;
+            }
+            buffer[total] = '\0';
+        }
+
         if (strstr(buffer, "/token")) {
-            handle_token_request(server, client_fd);
+            handle_token_request(server, client_fd, buffer);
         }
         else if (strstr(buffer, "/resource")) {
             handle_resource_request(server, client_fd, buffer);
@@ -371,6 +487,270 @@ static struct flb_oauth2 *create_oauth_ctx(struct flb_config *config,
     return ctx;
 }
 
+static int write_text_file(const char *path, const char *content)
+{
+    FILE *fp;
+    size_t expected;
+    size_t written;
+
+    fp = fopen(path, "wb");
+    if (!fp) {
+        return -1;
+    }
+
+    expected = strlen(content);
+    written = fwrite(content, 1, expected, fp);
+    fclose(fp);
+
+    if (written != expected) {
+        return -1;
+    }
+
+    return 0;
+}
+
+static int test_setup_private_key_jwt_files(char *key_path, size_t key_path_size,
+                                            char *cert_path, size_t cert_path_size)
+{
+    int ret;
+
+    ret = snprintf(key_path, key_path_size, "/tmp/%s.%d",
+                   TEST_KEY_FILENAME, (int) getpid());
+    if (ret < 0 || (size_t) ret >= key_path_size) {
+        return -1;
+    }
+
+    ret = snprintf(cert_path, cert_path_size, "/tmp/%s.%d",
+                   TEST_CERT_FILENAME, (int) getpid());
+    if (ret < 0 || (size_t) ret >= cert_path_size) {
+        return -1;
+    }
+
+    ret = write_text_file(key_path, TEST_PRIVATE_KEY_PEM);
+    if (ret != 0) {
+        return -1;
+    }
+
+    ret = write_text_file(cert_path, TEST_CERT_PEM);
+    if (ret != 0) {
+        unlink(key_path);
+        return -1;
+    }
+
+    return 0;
+}
+
+static void test_cleanup_private_key_jwt_files(const char *key_path,
+                                               const char *cert_path)
+{
+    unlink(key_path);
+    unlink(cert_path);
+}
+
+static int extract_form_value(const char *body, const char *key, char *out,
+                              size_t out_size)
+{
+    int key_len;
+    const char *end;
+    const char *start;
+    size_t val_len;
+    char pattern[128];
+
+    key_len = snprintf(pattern, sizeof(pattern), "%s=", key);
+    if (key_len <= 0 || key_len >= (int) sizeof(pattern)) {
+        return -1;
+    }
+
+    start = strstr(body, pattern);
+    if (!start) {
+        return -1;
+    }
+    start += key_len;
+
+    end = strchr(start, '&');
+    if (!end) {
+        end = start + strlen(start);
+    }
+
+    val_len = end - start;
+    if (val_len >= out_size) {
+        return -1;
+    }
+
+    memcpy(out, start, val_len);
+    out[val_len] = '\0';
+
+    return 0;
+}
+
+static int base64_url_decode(const char *input, char *out, size_t out_size)
+{
+    int pad;
+    int ret;
+    int in_len;
+    size_t decoded_size;
+    size_t i;
+    size_t normalized_len;
+    unsigned char *decoded = NULL;
+    unsigned char *normalized = NULL;
+
+    if (!input || !out || out_size == 0) {
+        return -1;
+    }
+
+    in_len = strlen(input);
+    pad = (4 - (in_len % 4)) % 4;
+    normalized_len = in_len + pad;
+
+    normalized = flb_calloc(1, normalized_len + 1);
+    if (!normalized) {
+        flb_errno();
+        return -1;
+    }
+
+    memcpy(normalized, input, in_len);
+    for (i = 0; i < (size_t) in_len; i++) {
+        if (normalized[i] == '-') {
+            normalized[i] = '+';
+        }
+        else if (normalized[i] == '_') {
+            normalized[i] = '/';
+        }
+    }
+
+    for (i = 0; i < (size_t) pad; i++) {
+        normalized[in_len + i] = '=';
+    }
+
+    decoded = flb_calloc(1, normalized_len + 1);
+    if (!decoded) {
+        flb_errno();
+        flb_free(normalized);
+        return -1;
+    }
+
+    ret = flb_base64_decode(decoded, normalized_len, &decoded_size,
+                            normalized, normalized_len);
+    flb_free(normalized);
+    if (ret != 0) {
+        flb_free(decoded);
+        return -1;
+    }
+
+    if (decoded_size >= out_size) {
+        flb_free(decoded);
+        return -1;
+    }
+
+    memcpy(out, decoded, decoded_size);
+    out[decoded_size] = '\0';
+    flb_free(decoded);
+
+    return 0;
+}
+
+static int base64_url_decode_bytes(const char *input, unsigned char *out,
+                                   size_t out_size, size_t *decoded_size)
+{
+    int pad;
+    int ret;
+    int in_len;
+    size_t i;
+    size_t normalized_len;
+    unsigned char *normalized = NULL;
+
+    if (!input || !out || !decoded_size || out_size == 0) {
+        return -1;
+    }
+
+    in_len = strlen(input);
+    pad = (4 - (in_len % 4)) % 4;
+    normalized_len = in_len + pad;
+
+    normalized = flb_calloc(1, normalized_len + 1);
+    if (!normalized) {
+        flb_errno();
+        return -1;
+    }
+
+    memcpy(normalized, input, in_len);
+    for (i = 0; i < (size_t) in_len; i++) {
+        if (normalized[i] == '-') {
+            normalized[i] = '+';
+        }
+        else if (normalized[i] == '_') {
+            normalized[i] = '/';
+        }
+    }
+
+    for (i = 0; i < (size_t) pad; i++) {
+        normalized[in_len + i] = '=';
+    }
+
+    ret = flb_base64_decode(out, out_size, decoded_size,
+                            normalized, normalized_len);
+    flb_free(normalized);
+
+    if (ret != 0) {
+        return -1;
+    }
+
+    return 0;
+}
+
+static int parse_jwt_header(const char *jwt, char *header_json,
+                            size_t header_json_size)
+{
+    const char *dot;
+    size_t header_len;
+    char header_b64[4096];
+
+    dot = strchr(jwt, '.');
+    if (!dot) {
+        return -1;
+    }
+
+    header_len = dot - jwt;
+    if (header_len == 0 || header_len >= sizeof(header_b64)) {
+        return -1;
+    }
+
+    memcpy(header_b64, jwt, header_len);
+    header_b64[header_len] = '\0';
+
+    return base64_url_decode(header_b64, header_json, header_json_size);
+}
+
+static struct flb_oauth2 *create_private_key_jwt_ctx(struct flb_config *config,
+                                                     struct oauth2_mock_server *server,
+                                                     const char *key_path,
+                                                     const char *cert_path,
+                                                     const char *header_name)
+{
+    struct flb_oauth2 *ctx;
+    struct flb_oauth2_config cfg;
+
+    memset(&cfg, 0, sizeof(cfg));
+
+    cfg.enabled = FLB_TRUE;
+    cfg.token_url = flb_sds_create_size(64);
+    cfg.auth_method = FLB_OAUTH2_AUTH_METHOD_PRIVATE_KEY_JWT;
+    cfg.client_id = flb_sds_create("id");
+    cfg.resource = flb_sds_create("urn:resource:test");
+    cfg.jwt_key_file = flb_sds_create(key_path);
+    cfg.jwt_cert_file = flb_sds_create(cert_path);
+    cfg.jwt_header = flb_sds_create(header_name);
+    cfg.jwt_ttl = 300;
+
+    flb_sds_printf(&cfg.token_url, "http://127.0.0.1:%d/token", server->port);
+    cfg.jwt_aud = flb_sds_create(cfg.token_url);
+
+    ctx = flb_oauth2_create_from_config(config, &cfg);
+    flb_oauth2_config_destroy(&cfg);
+
+    return ctx;
+}
+
 void test_parse_defaults(void)
 {
     int ret;
@@ -438,9 +818,131 @@ void test_caching_and_refresh(void)
     flb_config_exit(config);
 }
 
+void test_private_key_jwt_body(void)
+{
+    int ret;
+    flb_sds_t token = NULL;
+    char cert_path[256];
+    char key_path[256];
+    struct flb_config *config;
+    struct flb_oauth2 *ctx;
+    struct oauth2_mock_server server;
+
+    config = flb_config_init();
+    TEST_CHECK(config != NULL);
+
+    ret = test_setup_private_key_jwt_files(key_path, sizeof(key_path),
+                                           cert_path, sizeof(cert_path));
+    TEST_CHECK(ret == 0);
+
+    ret = oauth2_mock_server_start(&server, 30, 0);
+    TEST_CHECK(ret == 0);
+
+    ctx = create_private_key_jwt_ctx(config, &server, key_path, cert_path, "kid");
+    TEST_CHECK(ctx != NULL);
+
+#ifdef FLB_SYSTEM_MACOS
+    ret = oauth2_mock_server_wait_ready(&server);
+    TEST_CHECK(ret == 0);
+#endif
+
+    ret = flb_oauth2_get_access_token(ctx, &token, FLB_FALSE);
+    TEST_CHECK(ret == 0);
+    TEST_CHECK(token != NULL);
+
+    TEST_CHECK(strstr(server.latest_token_request,
+                      "grant_type=client_credentials") != NULL);
+    TEST_CHECK(strstr(server.latest_token_request,
+                      "resource=urn%3Aresource%3Atest") != NULL);
+    TEST_CHECK(strstr(server.latest_token_request,
+                      "client_assertion_type="
+                      "urn%3Aietf%3Aparams%3Aoauth%3A"
+                      "client-assertion-type%3Ajwt-bearer") != NULL);
+    TEST_CHECK(strstr(server.latest_token_request, "client_assertion=") != NULL);
+
+    flb_oauth2_destroy(ctx);
+    oauth2_mock_server_stop(&server);
+    test_cleanup_private_key_jwt_files(key_path, cert_path);
+    flb_config_exit(config);
+}
+
+void test_private_key_jwt_x5t_header(void)
+{
+    int ret;
+    char *x5t_end;
+    char *x5t_start;
+    flb_sds_t token = NULL;
+    char cert_path[256];
+    char key_path[256];
+    char x5t_b64[256];
+    char assertion[8192];
+    char header_json[4096];
+    size_t decoded_len;
+    unsigned char decoded_digest[64];
+    struct flb_config *config;
+    struct flb_oauth2 *ctx;
+    struct oauth2_mock_server server;
+
+    config = flb_config_init();
+    TEST_CHECK(config != NULL);
+
+    ret = test_setup_private_key_jwt_files(key_path, sizeof(key_path),
+                                           cert_path, sizeof(cert_path));
+    TEST_CHECK(ret == 0);
+
+    ret = oauth2_mock_server_start(&server, 30, 0);
+    TEST_CHECK(ret == 0);
+
+    ctx = create_private_key_jwt_ctx(config, &server, key_path, cert_path, "x5t");
+    TEST_CHECK(ctx != NULL);
+
+#ifdef FLB_SYSTEM_MACOS
+    ret = oauth2_mock_server_wait_ready(&server);
+    TEST_CHECK(ret == 0);
+#endif
+
+    ret = flb_oauth2_get_access_token(ctx, &token, FLB_FALSE);
+    TEST_CHECK(ret == 0);
+    TEST_CHECK(token != NULL);
+
+    ret = extract_form_value(server.latest_token_request, "client_assertion",
+                             assertion, sizeof(assertion));
+    TEST_CHECK(ret == 0);
+
+    ret = parse_jwt_header(assertion, header_json, sizeof(header_json));
+    TEST_CHECK(ret == 0);
+    TEST_CHECK(strstr(header_json, "\"x5t\":\"") != NULL);
+
+    x5t_start = strstr(header_json, "\"x5t\":\"");
+    TEST_CHECK(x5t_start != NULL);
+    if (x5t_start != NULL) {
+        x5t_start += 7;
+        x5t_end = strchr(x5t_start, '"');
+        TEST_CHECK(x5t_end != NULL);
+        if (x5t_end != NULL) {
+            TEST_CHECK((size_t) (x5t_end - x5t_start) < sizeof(x5t_b64));
+            if ((size_t) (x5t_end - x5t_start) < sizeof(x5t_b64)) {
+                memcpy(x5t_b64, x5t_start, x5t_end - x5t_start);
+                x5t_b64[x5t_end - x5t_start] = '\0';
+
+                ret = base64_url_decode_bytes(x5t_b64, decoded_digest,
+                                              sizeof(decoded_digest), &decoded_len);
+                TEST_CHECK(ret == 0);
+                TEST_CHECK(decoded_len == 20);
+            }
+        }
+    }
+
+    flb_oauth2_destroy(ctx);
+    oauth2_mock_server_stop(&server);
+    test_cleanup_private_key_jwt_files(key_path, cert_path);
+    flb_config_exit(config);
+}
+
 TEST_LIST = {
     {"parse_defaults", test_parse_defaults},
     {"caching_and_refresh", test_caching_and_refresh},
+    {"private_key_jwt_body", test_private_key_jwt_body},
+    {"private_key_jwt_x5t_header", test_private_key_jwt_x5t_header},
     {0}
 };
-
diff --git a/tests/internal/oauth2_jwt.c b/tests/internal/oauth2_jwt.c
index 87925f81c5f..4f3d7c0b6b3 100644
--- a/tests/internal/oauth2_jwt.c
+++ b/tests/internal/oauth2_jwt.c
@@ -7,6 +7,9 @@
 #include <string.h>
 
 static const char *VALID_JWT = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5In0.eyJleHAiOjE3MTAwMDAwMDAsImlzcyI6Imlzc3VlciIsImF1ZCI6ImF1ZGllbmNlIiwiYXpwIjoiY2xpZW50LTEifQ.c2ln";
+static const char *VALID_JWT_APPID = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5In0.eyJleHAiOjE3MTAwMDAwMDAsImlzcyI6Imlzc3VlciIsImF1ZCI6ImF1ZGllbmNlIiwiYXBwaWQiOiJjbGllbnQtYXBwaWQifQ.c2ln";
+static const char *VALID_JWT_APPID_CLIENT_ID = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5In0.eyJleHAiOjE3MTAwMDAwMDAsImlzcyI6Imlzc3VlciIsImF1ZCI6ImF1ZGllbmNlIiwiYXBwaWQiOiJjbGllbnQtYXBwaWQiLCJjbGllbnRfaWQiOiJjbGllbnQtZGlyZWN0In0.c2ln";
+static const char *VALID_JWT_APPID_AZP = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5In0.eyJleHAiOjE3MTAwMDAwMDAsImlzcyI6Imlzc3VlciIsImF1ZCI6ImF1ZGllbmNlIiwiYXBwaWQiOiJjbGllbnQtYXBwaWQiLCJhenAiOiJjbGllbnQtYXpwIn0.c2ln";
 static const char *INVALID_SEGMENTS = "abc.def";
 static const char *BAD_BASE64 = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5In0#.eyJleHAiOjE3MTAwMDAwMDAsImlzcyI6Imlzc3VlciIsImF1ZCI6ImF1ZGllbmNlIiwiYXpwIjoiY2xpZW50LTEifQ.c2ln";
 static const char *MISSING_KID = "eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE3MTAwMDAwMDAsImlzcyI6Imlzc3VlciIsImF1ZCI6ImF1ZGllbmNlIiwiYXpwIjoiY2xpZW50LTEifQ.c2ln";
@@ -40,6 +43,49 @@ static void test_invalid_segments()
     TEST_CHECK(ret == FLB_OAUTH2_JWT_ERR_SEGMENT_COUNT);
 }
 
+static void test_client_id_falls_back_to_appid()
+{
+    int ret;
+    struct flb_oauth2_jwt jwt;
+
+    ret = flb_oauth2_jwt_parse(VALID_JWT_APPID, strlen(VALID_JWT_APPID), &jwt);
+    TEST_CHECK(ret == FLB_OAUTH2_JWT_OK);
+    TEST_CHECK(strcmp(jwt.claims.client_id, "client-appid") == 0);
+    TEST_CHECK(jwt.claims.has_azp == FLB_FALSE);
+    TEST_CHECK(jwt.claims.has_client_id_claim == FLB_FALSE);
+
+    flb_oauth2_jwt_destroy(&jwt);
+}
+
+static void test_client_id_claim_overrides_appid()
+{
+    int ret;
+    struct flb_oauth2_jwt jwt;
+
+    ret = flb_oauth2_jwt_parse(VALID_JWT_APPID_CLIENT_ID,
+                               strlen(VALID_JWT_APPID_CLIENT_ID), &jwt);
+    TEST_CHECK(ret == FLB_OAUTH2_JWT_OK);
+    TEST_CHECK(strcmp(jwt.claims.client_id, "client-direct") == 0);
+    TEST_CHECK(jwt.claims.has_azp == FLB_FALSE);
+    TEST_CHECK(jwt.claims.has_client_id_claim == FLB_TRUE);
+
+    flb_oauth2_jwt_destroy(&jwt);
+}
+
+static void test_azp_overrides_appid()
+{
+    int ret;
+    struct flb_oauth2_jwt jwt;
+
+    ret = flb_oauth2_jwt_parse(VALID_JWT_APPID_AZP,
+                               strlen(VALID_JWT_APPID_AZP), &jwt);
+    TEST_CHECK(ret == FLB_OAUTH2_JWT_OK);
+    TEST_CHECK(strcmp(jwt.claims.client_id, "client-azp") == 0);
+    TEST_CHECK(jwt.claims.has_azp == FLB_TRUE);
+
+    flb_oauth2_jwt_destroy(&jwt);
+}
+
 static void test_bad_base64()
 {
     int ret;
@@ -215,6 +261,9 @@ static void test_static_key_validation()
 TEST_LIST = {
     {"valid_jwt_parses", test_valid_jwt_parses},
     {"invalid_segments", test_invalid_segments},
+    {"client_id_falls_back_to_appid", test_client_id_falls_back_to_appid},
+    {"client_id_claim_overrides_appid", test_client_id_claim_overrides_appid},
+    {"azp_overrides_appid", test_azp_overrides_appid},
     {"bad_base64", test_bad_base64},
     {"missing_kid", test_missing_kid},
     {"bad_alg", test_bad_alg},
