From 94f11b4a79900dead3020abd0c2653fc8705fe82 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 11:06:14 -0600 Subject: [PATCH 01/42] oauth2_jwt: add JWT validation interface for OAuth2 server-side validation This commit introduces a new OAuth2 JWT validation interface that allows Fluent Bit to validate incoming JWT tokens on the server side. The interface supports: - JWT parsing and validation - JWKS (JSON Web Key Set) fetching and caching - RSA signature verification using flb_crypto abstraction - Configurable validation rules (issuer, audience, client ID) Signed-off-by: Eduardo Silva --- include/fluent-bit/flb_oauth2_jwt.h | 118 +++ src/CMakeLists.txt | 1 + src/flb_oauth2.c | 921 ++++++++++++------ src/flb_oauth2_jwt.c | 1389 +++++++++++++++++++++++++++ 4 files changed, 2132 insertions(+), 297 deletions(-) create mode 100644 include/fluent-bit/flb_oauth2_jwt.h create mode 100644 src/flb_oauth2_jwt.c diff --git a/include/fluent-bit/flb_oauth2_jwt.h b/include/fluent-bit/flb_oauth2_jwt.h new file mode 100644 index 00000000000..cae87c7804b --- /dev/null +++ b/include/fluent-bit/flb_oauth2_jwt.h @@ -0,0 +1,118 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* Fluent Bit + * ========== + * Copyright (C) 2015-2024 The Fluent Bit Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLB_OAUTH2_JWT_H +#define FLB_OAUTH2_JWT_H + +#include +#include +#include + +struct flb_config; +struct mk_list; + +enum flb_oauth2_jwt_status { + FLB_OAUTH2_JWT_OK = 0, + FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT = -1000, + FLB_OAUTH2_JWT_ERR_SEGMENT_COUNT = -1001, + FLB_OAUTH2_JWT_ERR_BASE64_HEADER = -1002, + FLB_OAUTH2_JWT_ERR_BASE64_PAYLOAD = -1003, + FLB_OAUTH2_JWT_ERR_BASE64_SIGNATURE = -1004, + FLB_OAUTH2_JWT_ERR_JSON_HEADER = -1005, + FLB_OAUTH2_JWT_ERR_JSON_PAYLOAD = -1006, + FLB_OAUTH2_JWT_ERR_MISSING_KID = -1007, + FLB_OAUTH2_JWT_ERR_ALG_UNSUPPORTED = -1008, + FLB_OAUTH2_JWT_ERR_MISSING_EXP = -1009, + FLB_OAUTH2_JWT_ERR_MISSING_ISS = -1010, + FLB_OAUTH2_JWT_ERR_MISSING_AUD = -1011, + FLB_OAUTH2_JWT_ERR_MISSING_BEARER_TOKEN = -1012, + FLB_OAUTH2_JWT_ERR_MISSING_AUTH_HEADER = -1013, + FLB_OAUTH2_JWT_ERR_VALIDATION_UNAVAILABLE = -1014 +}; + +struct flb_oauth2_jwt_claims { + flb_sds_t kid; + flb_sds_t alg; + flb_sds_t issuer; + flb_sds_t audience; + flb_sds_t client_id; + uint64_t expiration; +}; + +struct flb_oauth2_jwt { + flb_sds_t header_json; + flb_sds_t payload_json; + flb_sds_t signing_input; + unsigned char *signature; + size_t signature_len; + struct flb_oauth2_jwt_claims claims; +}; + +struct flb_oauth2_jwt_cfg { + int validate; /* enable validation */ + 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 */ + int jwks_refresh_interval; /* refresh cadence in seconds */ +}; + +struct flb_oauth2_jwt_validation_request { + const char *token; /* raw JWT token */ + size_t token_length; /* JWT length */ + flb_sds_t issuer; /* required issuer */ + flb_sds_t audience; /* required audience */ + flb_sds_t client_id; /* required client id/azp */ + int64_t current_time; /* optional unix time override */ + int64_t leeway; /* optional expiration leeway */ +}; + +struct flb_oauth2_jwt_validation_response { + int status; /* validation status */ +}; + +struct flb_oauth2_jwt_ctx; + +/* Allocate and populate a validation context from configuration. */ +struct flb_oauth2_jwt_ctx *flb_oauth2_jwt_context_create(struct flb_config *config, + struct flb_oauth2_jwt_cfg *cfg); + +/* Release validation resources. */ +void flb_oauth2_jwt_context_destroy(struct flb_oauth2_jwt_ctx *ctx); + +/* Validate a bearer token (JWT) using the supplied context. */ +int flb_oauth2_jwt_validate(struct flb_oauth2_jwt_ctx *ctx, + const char *authorization_header, + size_t authorization_header_len); + +/* Parse a JWT and populate the supplied structure. */ +int flb_oauth2_jwt_parse(const char *token, + size_t token_len, + struct flb_oauth2_jwt *jwt); + +/* Destroy a parsed JWT structure. */ +void flb_oauth2_jwt_destroy(struct flb_oauth2_jwt *jwt); + +/* Human readable error for logging. */ +const char *flb_oauth2_jwt_status_message(int status); + +/* Get OAuth2 JWT config map for input plugins */ +struct mk_list *flb_oauth2_jwt_get_config_map(struct flb_config *config); + +#endif diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ac5fe5ef863..8ba8440ce0c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -161,6 +161,7 @@ if(FLB_TLS) ${src} "tls/flb_tls.c" "flb_oauth2.c" + "flb_oauth2_jwt.c" ) # Make sure our output targets links to the TLS library diff --git a/src/flb_oauth2.c b/src/flb_oauth2.c index 866fbe8e132..480f0474283 100644 --- a/src/flb_oauth2.c +++ b/src/flb_oauth2.c @@ -21,11 +21,68 @@ #include #include #include +#include #include #include #include #include +#include +#include +#include + +/* Config map for OAuth2 configuration */ +struct flb_config_map oauth2_config_map[] = { + { + FLB_CONFIG_MAP_BOOL, "oauth2", "false", + 0, FLB_TRUE, offsetof(struct flb_oauth2_config, enabled), + "Enable OAuth2 client credentials for outgoing requests" + }, + { + FLB_CONFIG_MAP_STR, "oauth2.token_url", NULL, + 0, FLB_TRUE, offsetof(struct flb_oauth2_config, token_url), + "OAuth2 token endpoint URL" + }, + { + FLB_CONFIG_MAP_STR, "oauth2.client_id", NULL, + 0, FLB_TRUE, offsetof(struct flb_oauth2_config, client_id), + "OAuth2 client_id" + }, + { + FLB_CONFIG_MAP_STR, "oauth2.client_secret", NULL, + 0, FLB_TRUE, offsetof(struct flb_oauth2_config, client_secret), + "OAuth2 client_secret" + }, + { + FLB_CONFIG_MAP_STR, "oauth2.scope", NULL, + 0, FLB_TRUE, offsetof(struct flb_oauth2_config, scope), + "Optional OAuth2 scope" + }, + { + FLB_CONFIG_MAP_STR, "oauth2.audience", NULL, + 0, FLB_TRUE, offsetof(struct flb_oauth2_config, audience), + "Optional OAuth2 audience parameter" + }, + { + FLB_CONFIG_MAP_INT, "oauth2.refresh_skew_seconds", "60", + 0, FLB_TRUE, offsetof(struct flb_oauth2_config, refresh_skew), + "Seconds before expiry to refresh the access token" + }, + { + FLB_CONFIG_MAP_TIME, "oauth2.timeout", "0s", + 0, FLB_TRUE, offsetof(struct flb_oauth2_config, timeout), + "Timeout for OAuth2 token requests (defaults to response_timeout when unset)" + }, + { + FLB_CONFIG_MAP_TIME, "oauth2.connect_timeout", "0s", + 0, FLB_TRUE, offsetof(struct flb_oauth2_config, connect_timeout), + "Connect timeout for OAuth2 token requests" + }, + + /* EOF */ + {0} +}; + #define free_temporary_buffers() \ if (prot) { \ flb_free(prot); \ @@ -40,8 +97,8 @@ flb_free(uri); \ } -static inline int key_cmp(const char *str, int len, const char *cmp) { - +static inline int key_cmp(const char *str, int len, const char *cmp) +{ if (strlen(cmp) != len) { return -1; } @@ -49,130 +106,121 @@ static inline int key_cmp(const char *str, int len, const char *cmp) { return strncasecmp(str, cmp, len); } -int flb_oauth2_parse_json_response(const char *json_data, size_t json_size, - struct flb_oauth2 *ctx) +static void oauth2_reset_state(struct flb_oauth2 *ctx) { - int i; - int ret; - int key_len; - int val_len; - int tokens_size = 32; - const char *key; - const char *val; - jsmn_parser parser; - jsmntok_t *t; - jsmntok_t *tokens; + ctx->expires_in = 0; + ctx->expires_at = 0; - jsmn_init(&parser); - tokens = flb_calloc(1, sizeof(jsmntok_t) * tokens_size); - if (!tokens) { - flb_errno(); - return -1; + if (ctx->access_token) { + flb_sds_destroy(ctx->access_token); + ctx->access_token = NULL; } - ret = jsmn_parse(&parser, json_data, json_size, tokens, tokens_size); - if (ret <= 0) { - flb_error("[oauth2] cannot parse payload:\n%s", json_data); - flb_free(tokens); - return -1; + if (ctx->token_type) { + flb_sds_destroy(ctx->token_type); + ctx->token_type = NULL; } +} - t = &tokens[0]; - if (t->type != JSMN_OBJECT) { - flb_error("[oauth2] invalid JSON response:\n%s", json_data); - flb_free(tokens); - return -1; - } +static void oauth2_apply_defaults(struct flb_oauth2_config *cfg) +{ + cfg->enabled = FLB_TRUE; + cfg->auth_method = FLB_OAUTH2_AUTH_METHOD_BASIC; + cfg->refresh_skew = FLB_OAUTH2_DEFAULT_SKEW_SECS; + cfg->timeout = 0; + cfg->connect_timeout = 0; +} - /* Parse JSON tokens */ - for (i = 1; i < ret; i++) { - t = &tokens[i]; +static int oauth2_clone_config(struct flb_oauth2_config *dst, + const struct flb_oauth2_config *src) +{ + oauth2_apply_defaults(dst); - if (t->type != JSMN_STRING) { - continue; - } + if (!src) { + return 0; + } - if (t->start == -1 || t->end == -1 || (t->start == 0 && t->end == 0)){ - break; - } + dst->enabled = src->enabled; + dst->auth_method = src->auth_method; - /* Key */ - key = json_data + t->start; - key_len = (t->end - t->start); + if (src->refresh_skew > 0) { + dst->refresh_skew = src->refresh_skew; + } - /* Value */ - i++; - t = &tokens[i]; - val = json_data + t->start; - val_len = (t->end - t->start); + dst->timeout = src->timeout; + dst->connect_timeout = src->connect_timeout; - if (key_cmp(key, key_len, "access_token") == 0) { - ctx->access_token = flb_sds_create_len(val, val_len); + if (src->token_url) { + dst->token_url = flb_sds_create(src->token_url); + if (!dst->token_url) { + flb_errno(); + return -1; } - else if (key_cmp(key, key_len, "token_type") == 0) { - ctx->token_type = flb_sds_create_len(val, val_len); + } + + if (src->client_id) { + dst->client_id = flb_sds_create(src->client_id); + if (!dst->client_id) { + flb_errno(); + return -1; } - else if (key_cmp(key, key_len, "expires_in") == 0) { - ctx->expires_in = atol(val); - - /* - * Our internal expiration time must be lower that the one set - * by the remote end-point, so we can use valid cached values - * if a token renewal is in place. So we decrease the expire - * interval -10%. - */ - ctx->expires_in -= (ctx->expires_in * 0.10); + } + + if (src->client_secret) { + dst->client_secret = flb_sds_create(src->client_secret); + if (!dst->client_secret) { + flb_errno(); + return -1; } } - flb_free(tokens); - if (!ctx->access_token || !ctx->token_type || ctx->expires_in < 60) { - flb_sds_destroy(ctx->access_token); - flb_sds_destroy(ctx->token_type); - ctx->expires_in = 0; - return -1; + if (src->scope) { + dst->scope = flb_sds_create(src->scope); + if (!dst->scope) { + flb_errno(); + return -1; + } + } + + if (src->audience) { + dst->audience = flb_sds_create(src->audience); + if (!dst->audience) { + flb_errno(); + return -1; + } } return 0; } -struct flb_oauth2 *flb_oauth2_create(struct flb_config *config, - const char *auth_url, int expire_sec) +void flb_oauth2_config_destroy(struct flb_oauth2_config *cfg) +{ + if (!cfg) { + return; + } + + flb_sds_destroy(cfg->token_url); + cfg->token_url = NULL; + flb_sds_destroy(cfg->client_id); + cfg->client_id = NULL; + flb_sds_destroy(cfg->client_secret); + cfg->client_secret = NULL; + flb_sds_destroy(cfg->scope); + cfg->scope = NULL; + flb_sds_destroy(cfg->audience); + cfg->audience = NULL; +} + +static int oauth2_setup_upstream(struct flb_oauth2 *ctx, + struct flb_config *config, + const char *auth_url) { int ret; char *prot = NULL; char *host = NULL; char *port = NULL; char *uri = NULL; - struct flb_oauth2 *ctx; - - /* allocate context */ - ctx = flb_calloc(1, sizeof(struct flb_oauth2)); - if (!ctx) { - flb_errno(); - return NULL; - } - /* register token url */ - ctx->auth_url = flb_sds_create(auth_url); - if (!ctx->auth_url) { - flb_errno(); - flb_free(ctx); - return NULL; - } - - /* default payload size to 1kb */ - ctx->payload = flb_sds_create_size(1024); - if (!ctx->payload) { - flb_errno(); - flb_oauth2_destroy(ctx); - return NULL; - } - - ctx->issued = time(NULL); - ctx->expires = ctx->issued + expire_sec; - - /* Parse and split URL */ ret = flb_utils_url_split(auth_url, &prot, &host, &port, &uri); if (ret == -1) { flb_error("[oauth2] invalid URL: %s", auth_url); @@ -189,51 +237,49 @@ struct flb_oauth2 *flb_oauth2_create(struct flb_config *config, goto error; } - /* Populate context */ ctx->host = flb_sds_create(host); if (!ctx->host) { flb_errno(); goto error; } + if (port) { ctx->port = flb_sds_create(port); } else { ctx->port = flb_sds_create(FLB_OAUTH2_PORT); } + if (!ctx->port) { flb_errno(); goto error; } + ctx->uri = flb_sds_create(uri); if (!ctx->uri) { flb_errno(); goto error; } - /* Create TLS context */ ctx->tls = flb_tls_create(FLB_TLS_CLIENT_MODE, - FLB_TRUE, /* verify */ - -1, /* debug */ - NULL, /* vhost */ - NULL, /* ca_path */ - NULL, /* ca_file */ - NULL, /* crt_file */ - NULL, /* key_file */ - NULL); /* key_passwd */ + FLB_TRUE, + -1, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL); if (!ctx->tls) { flb_error("[oauth2] error initializing TLS context"); goto error; } - /* Create Upstream context */ if (strcmp(prot, "https") == 0) { - ctx->u = flb_upstream_create_url(config, auth_url, - FLB_IO_TLS, ctx->tls); + ctx->u = flb_upstream_create_url(config, auth_url, FLB_IO_TLS, ctx->tls); } - else if (strcmp(prot, "http") == 0) { - ctx->u = flb_upstream_create_url(config, auth_url, - FLB_IO_TCP, NULL); + else { + ctx->u = flb_upstream_create_url(config, auth_url, FLB_IO_TCP, NULL); } if (!ctx->u) { @@ -241,227 +287,217 @@ struct flb_oauth2 *flb_oauth2_create(struct flb_config *config, goto error; } - /* Remove Upstream Async flag */ flb_stream_disable_async_mode(&ctx->u->base); + if (ctx->cfg.connect_timeout > 0) { + ctx->u->base.net.connect_timeout = ctx->cfg.connect_timeout; + } + free_temporary_buffers(); - return ctx; - error: + return 0; + +error: free_temporary_buffers(); - flb_oauth2_destroy(ctx); - return NULL; + return -1; } -/* Clear the current payload and token */ -void flb_oauth2_payload_clear(struct flb_oauth2 *ctx) +int flb_oauth2_parse_json_response(const char *json_data, size_t json_size, + struct flb_oauth2 *ctx) { - flb_sds_len_set(ctx->payload, 0); - ctx->payload[0] = '\0'; - ctx->expires_in = 0; - if (ctx->access_token){ - flb_sds_destroy(ctx->access_token); - ctx->access_token = NULL; - } - if (ctx->token_type){ - flb_sds_destroy(ctx->token_type); - ctx->token_type = NULL; - } -} + int i; + int ret; + int key_len; + int val_len; + int tokens_size = 32; + const char *key; + const char *val; + jsmn_parser parser; + jsmntok_t *t; + jsmntok_t *tokens; + uint64_t expires_in = 0; + flb_sds_t access_token = NULL; + flb_sds_t token_type = NULL; -/* Append a key/value to the request body */ -int flb_oauth2_payload_append(struct flb_oauth2 *ctx, - const char *key_str, int key_len, - const char *val_str, int val_len) -{ - int size; - flb_sds_t tmp; + jsmn_init(&parser); + tokens = flb_calloc(1, sizeof(jsmntok_t) * tokens_size); + if (!tokens) { + flb_errno(); + return -1; + } - if (key_len == -1) { - key_len = strlen(key_str); + ret = jsmn_parse(&parser, json_data, json_size, tokens, tokens_size); + if (ret <= 0) { + flb_error("[oauth2] cannot parse payload"); + flb_free(tokens); + return -1; } - if (val_len == -1) { - val_len = strlen(val_str); + + t = &tokens[0]; + if (t->type != JSMN_OBJECT) { + flb_error("[oauth2] invalid JSON response"); + flb_free(tokens); + return -1; } - /* - * Make sure we have enough space in the sds buffer, otherwise - * add more capacity (so further flb_sds_cat calls do not - * realloc(). - */ - size = key_len + val_len + 2; - if (flb_sds_avail(ctx->payload) < size) { - tmp = flb_sds_increase(ctx->payload, size); - if (!tmp) { - flb_errno(); - return -1; + for (i = 1; i < ret; i++) { + t = &tokens[i]; + + if (t->type != JSMN_STRING) { + continue; } - if (tmp != ctx->payload) { - ctx->payload = tmp; + if (t->start == -1 || t->end == -1 || (t->start == 0 && t->end == 0)) { + break; } - } - if (flb_sds_len(ctx->payload) > 0) { - flb_sds_cat(ctx->payload, "&", 1); + key = json_data + t->start; + key_len = (t->end - t->start); + + i++; + t = &tokens[i]; + val = json_data + t->start; + val_len = (t->end - t->start); + + if (key_cmp(key, key_len, "access_token") == 0) { + access_token = flb_sds_create_len(val, val_len); + } + else if (key_cmp(key, key_len, "token_type") == 0) { + token_type = flb_sds_create_len(val, val_len); + } + else if (key_cmp(key, key_len, "expires_in") == 0) { + expires_in = strtoull(val, NULL, 10); + } } - /* Append key and value */ - flb_sds_cat(ctx->payload, key_str, key_len); - flb_sds_cat(ctx->payload, "=", 1); - flb_sds_cat(ctx->payload, val_str, val_len); + flb_free(tokens); - return 0; -} + if (!access_token) { + oauth2_reset_state(ctx); + return -1; + } -void flb_oauth2_destroy(struct flb_oauth2 *ctx) -{ - flb_sds_destroy(ctx->auth_url); - flb_sds_destroy(ctx->payload); + if (!token_type) { + token_type = flb_sds_create("Bearer"); + flb_debug("[oauth2] token_type missing; defaulting to Bearer"); + } - flb_sds_destroy(ctx->host); - flb_sds_destroy(ctx->port); - flb_sds_destroy(ctx->uri); + if (expires_in == 0) { + expires_in = FLB_OAUTH2_DEFAULT_EXPIRES; + flb_warn("[oauth2] expires_in missing; defaulting to %d seconds", + FLB_OAUTH2_DEFAULT_EXPIRES); + } - flb_sds_destroy(ctx->access_token); - flb_sds_destroy(ctx->token_type); + oauth2_reset_state(ctx); - flb_upstream_destroy(ctx->u); - flb_tls_destroy(ctx->tls); + ctx->access_token = access_token; + ctx->token_type = token_type; + ctx->expires_in = expires_in; + ctx->expires_at = time(NULL) + expires_in; - flb_free(ctx); + return 0; } -char *flb_oauth2_token_get_ng(struct flb_oauth2 *ctx) +static flb_sds_t oauth2_append_kv(flb_sds_t buffer, const char *key, + const char *value) { - int ret; - time_t now; - struct flb_http_client_ng http_client; - struct flb_http_response *response; - struct flb_http_request *request; - uint64_t http_client_flags; + flb_sds_t tmp; - now = time(NULL); - if (ctx->access_token) { - /* validate unexpired token */ - if (ctx->expires > now && flb_sds_len(ctx->access_token) > 0) { - return ctx->access_token; - } + if (!value) { + return buffer; } - http_client_flags = FLB_HTTP_CLIENT_FLAG_AUTO_DEFLATE | - FLB_HTTP_CLIENT_FLAG_AUTO_INFLATE; - - ret = flb_http_client_ng_init(&http_client, - NULL, - ctx->u, - HTTP_PROTOCOL_VERSION_11, - http_client_flags); + tmp = flb_uri_encode(value, strlen(value)); + if (!tmp) { + flb_errno(); + return NULL; + } - if (ret != 0) { - flb_debug("[oauth2] http client creation error"); + if (flb_sds_len(buffer) > 0) { + buffer = flb_sds_cat(buffer, "&", 1); + if (!buffer) { + flb_sds_destroy(tmp); + return NULL; + } + } + buffer = flb_sds_cat(buffer, key, strlen(key)); + if (!buffer) { + flb_sds_destroy(tmp); return NULL; } - request = flb_http_client_request_builder( - &http_client, - FLB_HTTP_CLIENT_ARGUMENT_METHOD(FLB_HTTP_POST), - FLB_HTTP_CLIENT_ARGUMENT_HOST(ctx->host), - FLB_HTTP_CLIENT_ARGUMENT_URI(ctx->uri), - FLB_HTTP_CLIENT_ARGUMENT_CONTENT_TYPE( - FLB_OAUTH2_HTTP_ENCODING), - FLB_HTTP_CLIENT_ARGUMENT_BODY(ctx->payload, - cfl_sds_len(ctx->payload), - NULL)); + buffer = flb_sds_cat(buffer, "=", 1); + if (!buffer) { + flb_sds_destroy(tmp); + return NULL; + } - if (request == NULL) { - flb_stream_enable_flags(&ctx->u->base, FLB_IO_IPV6); + buffer = flb_sds_cat(buffer, tmp, flb_sds_len(tmp)); + flb_sds_destroy(tmp); - request = flb_http_client_request_builder( - &http_client, - FLB_HTTP_CLIENT_ARGUMENT_METHOD(FLB_HTTP_POST), - FLB_HTTP_CLIENT_ARGUMENT_HOST(ctx->host), - FLB_HTTP_CLIENT_ARGUMENT_URI(ctx->uri), - FLB_HTTP_CLIENT_ARGUMENT_CONTENT_TYPE( - FLB_OAUTH2_HTTP_ENCODING), - FLB_HTTP_CLIENT_ARGUMENT_BODY(ctx->payload, - cfl_sds_len(ctx->payload), - NULL)); - if (request == NULL) { - flb_error("[oauth2] could not get an upstream connection to %s:%i", - ctx->u->tcp_host, ctx->u->tcp_port); + return buffer; +} - flb_stream_disable_flags(&ctx->u->base, FLB_IO_IPV6); - flb_http_client_request_destroy(request, FLB_TRUE); - flb_http_client_ng_destroy(&http_client); +static flb_sds_t oauth2_build_body(struct flb_oauth2 *ctx) +{ + flb_sds_t body; - return NULL; - } + if (ctx->payload_manual == FLB_TRUE && ctx->payload) { + return flb_sds_create_len(ctx->payload, flb_sds_len(ctx->payload)); } - response = flb_http_client_request_execute(request); - - if (response == NULL) { - flb_debug("[oauth2] http request execution error"); - - flb_http_client_request_destroy(request, FLB_TRUE); - flb_http_client_ng_destroy(&http_client); - + body = flb_sds_create_size(128); + if (!body) { return NULL; } - flb_info("[oauth2] HTTP Status=%i", response->status); - if (response->body != NULL && - cfl_sds_len(response->body) > 0) { - flb_info("[oauth2] payload:\n%s", response->body); + body = oauth2_append_kv(body, "grant_type", "client_credentials"); + if (!body) { + return NULL; } - /* Extract token */ - if (response->body != NULL && - cfl_sds_len(response->body) > 0 && - response->status == 200) { - ret = flb_oauth2_parse_json_response(response->body, - cfl_sds_len(response->body), - ctx); - if (ret == 0) { - flb_info("[oauth2] access token from '%s:%s' retrieved", - ctx->host, ctx->port); + if (ctx->cfg.scope) { + body = oauth2_append_kv(body, "scope", ctx->cfg.scope); + if (!body) { + return NULL; + } + } - flb_http_client_request_destroy(request, FLB_TRUE); - flb_http_client_ng_destroy(&http_client); + if (ctx->cfg.audience) { + body = oauth2_append_kv(body, "audience", ctx->cfg.audience); + if (!body) { + return NULL; + } + } - ctx->issued = time(NULL); - ctx->expires = ctx->issued + ctx->expires_in; + if (ctx->cfg.auth_method == FLB_OAUTH2_AUTH_METHOD_POST) { + if (ctx->cfg.client_id) { + body = oauth2_append_kv(body, "client_id", ctx->cfg.client_id); + if (!body) { + return NULL; + } + } - return ctx->access_token; + if (ctx->cfg.client_secret) { + body = oauth2_append_kv(body, "client_secret", ctx->cfg.client_secret); + if (!body) { + return NULL; + } } } - flb_http_client_request_destroy(request, FLB_TRUE); - flb_http_client_ng_destroy(&http_client); - - return NULL; + return body; } -char *flb_oauth2_token_get(struct flb_oauth2 *ctx) +static int oauth2_http_request(struct flb_oauth2 *ctx, flb_sds_t body) { int ret; - size_t b_sent; - time_t now; + size_t b_sent = 0; struct flb_connection *u_conn; struct flb_http_client *c; - now = time(NULL); - if (ctx->access_token) { - /* validate unexpired token */ - if (ctx->expires > now && flb_sds_len(ctx->access_token) > 0) { - return ctx->access_token; - } - } - - /* Get Token and store it in the context */ u_conn = flb_upstream_conn_get(ctx->u); if (!u_conn) { flb_stream_enable_flags(&ctx->u->base, FLB_IO_IPV6); @@ -470,64 +506,345 @@ char *flb_oauth2_token_get(struct flb_oauth2 *ctx) flb_error("[oauth2] could not get an upstream connection to %s:%i", ctx->u->tcp_host, ctx->u->tcp_port); flb_stream_disable_flags(&ctx->u->base, FLB_IO_IPV6); - return NULL; + return -1; } } - /* Create HTTP client context */ c = flb_http_client(u_conn, FLB_HTTP_POST, ctx->uri, - ctx->payload, flb_sds_len(ctx->payload), + body, flb_sds_len(body), ctx->host, atoi(ctx->port), NULL, 0); if (!c) { flb_error("[oauth2] error creating HTTP client context"); flb_upstream_conn_release(u_conn); - return NULL; + return -1; + } + + if (ctx->cfg.timeout > 0) { + flb_http_set_response_timeout(c, ctx->cfg.timeout); + flb_http_set_read_idle_timeout(c, ctx->cfg.timeout); } - /* Append HTTP Header */ flb_http_add_header(c, FLB_HTTP_HEADER_CONTENT_TYPE, - sizeof(FLB_HTTP_HEADER_CONTENT_TYPE) -1, + sizeof(FLB_HTTP_HEADER_CONTENT_TYPE) - 1, FLB_OAUTH2_HTTP_ENCODING, sizeof(FLB_OAUTH2_HTTP_ENCODING) - 1); - /* Issue request */ + if (ctx->cfg.auth_method == FLB_OAUTH2_AUTH_METHOD_BASIC && + ctx->cfg.client_id && ctx->cfg.client_secret) { + ret = flb_http_basic_auth(c, ctx->cfg.client_id, ctx->cfg.client_secret); + if (ret != 0) { + flb_error("[oauth2] could not compose basic authorization header"); + flb_http_client_destroy(c); + flb_upstream_conn_release(u_conn); + return -1; + } + } + ret = flb_http_do(c, &b_sent); if (ret != 0) { flb_warn("[oauth2] cannot issue request, http_do=%i", ret); } else { - flb_info("[oauth2] HTTP Status=%i", c->resp.status); - if (c->resp.payload_size > 0) { - if (c->resp.status == 200) { - flb_debug("[oauth2] payload:\n%s", c->resp.payload); - } - else { - flb_info("[oauth2] payload:\n%s", c->resp.payload); - } - } + flb_debug("[oauth2] HTTP Status=%i", c->resp.status); } - /* Extract token */ if (c->resp.payload_size > 0 && c->resp.status == 200) { ret = flb_oauth2_parse_json_response(c->resp.payload, c->resp.payload_size, ctx); if (ret == 0) { - flb_info("[oauth2] access token from '%s:%s' retrieved", - ctx->host, ctx->port); + flb_info("[oauth2] access token from '%s:%s' retrieved", ctx->host, ctx->port); flb_http_client_destroy(c); flb_upstream_conn_release(u_conn); - ctx->issued = time(NULL); - ctx->expires = ctx->issued + ctx->expires_in; - return ctx->access_token; + return 0; } } flb_http_client_destroy(c); flb_upstream_conn_release(u_conn); - return NULL; + return -1; +} + +static int oauth2_refresh_locked(struct flb_oauth2 *ctx) +{ + int ret; + flb_sds_t body; + + body = oauth2_build_body(ctx); + if (!body) { + flb_error("[oauth2] could not build request body"); + return -1; + } + + ret = oauth2_http_request(ctx, body); + flb_sds_destroy(body); + + return ret; +} + +static int oauth2_token_needs_refresh(struct flb_oauth2 *ctx, int force_refresh) +{ + time_t now; + + if (force_refresh) { + return FLB_TRUE; + } + + if (!ctx->access_token) { + return FLB_TRUE; + } + + now = time(NULL); + + if (ctx->expires_at == 0) { + return FLB_TRUE; + } + + if (now >= (ctx->expires_at - ctx->refresh_skew)) { + return FLB_TRUE; + } + + return FLB_FALSE; +} + +struct flb_oauth2 *flb_oauth2_create(struct flb_config *config, + const char *auth_url, int expire_sec) +{ + struct flb_oauth2_config cfg; + struct flb_oauth2 *ctx; + + (void) expire_sec; + + oauth2_apply_defaults(&cfg); + cfg.token_url = flb_sds_create(auth_url); + cfg.refresh_skew = FLB_OAUTH2_DEFAULT_SKEW_SECS; + + ctx = flb_oauth2_create_from_config(config, &cfg); + + flb_oauth2_config_destroy(&cfg); + + return ctx; +} + +struct flb_oauth2 *flb_oauth2_create_from_config(struct flb_config *config, + const struct flb_oauth2_config *cfg) +{ + int ret; + struct flb_oauth2 *ctx; + + ctx = flb_calloc(1, sizeof(struct flb_oauth2)); + if (!ctx) { + flb_errno(); + return NULL; + } + + oauth2_apply_defaults(&ctx->cfg); + + ret = oauth2_clone_config(&ctx->cfg, cfg); + if (ret != 0) { + flb_free(ctx); + return NULL; + } + + if (!ctx->cfg.token_url) { + flb_error("[oauth2] token_url is not set"); + flb_oauth2_destroy(ctx); + return NULL; + } + + ctx->auth_url = flb_sds_create(ctx->cfg.token_url); + if (!ctx->auth_url) { + flb_errno(); + flb_oauth2_destroy(ctx); + return NULL; + } + + ctx->payload = flb_sds_create_size(1024); + if (!ctx->payload) { + flb_errno(); + flb_oauth2_destroy(ctx); + return NULL; + } + + ctx->refresh_skew = ctx->cfg.refresh_skew; + if (ctx->refresh_skew <= 0) { + ctx->refresh_skew = FLB_OAUTH2_DEFAULT_SKEW_SECS; + } + + ret = flb_lock_init(&ctx->lock); + if (ret != 0) { + flb_oauth2_destroy(ctx); + return NULL; + } + + ret = oauth2_setup_upstream(ctx, config, ctx->auth_url); + if (ret != 0) { + flb_oauth2_destroy(ctx); + return NULL; + } + + return ctx; +} + +void flb_oauth2_destroy(struct flb_oauth2 *ctx) +{ + if (!ctx) { + return; + } + + oauth2_reset_state(ctx); + + flb_sds_destroy(ctx->auth_url); + flb_sds_destroy(ctx->payload); + flb_sds_destroy(ctx->host); + flb_sds_destroy(ctx->port); + flb_sds_destroy(ctx->uri); + + if (ctx->tls) { + flb_tls_destroy(ctx->tls); + } + + if (ctx->u) { + flb_upstream_destroy(ctx->u); + } + + flb_oauth2_config_destroy(&ctx->cfg); + flb_lock_destroy(&ctx->lock); + + flb_free(ctx); +} + +void flb_oauth2_payload_clear(struct flb_oauth2 *ctx) +{ + if (!ctx || !ctx->payload) { + return; + } + + flb_sds_len_set(ctx->payload, 0); + ctx->payload[0] = '\0'; + ctx->payload_manual = FLB_TRUE; + oauth2_reset_state(ctx); +} + +int flb_oauth2_payload_append(struct flb_oauth2 *ctx, + const char *key_str, int key_len, + const char *val_str, int val_len) +{ + int size; + flb_sds_t tmp; + + if (key_len == -1) { + key_len = strlen(key_str); + } + if (val_len == -1) { + val_len = strlen(val_str); + } + + size = key_len + val_len + 2; + if (flb_sds_avail(ctx->payload) < size) { + tmp = flb_sds_increase(ctx->payload, size); + if (!tmp) { + flb_errno(); + return -1; + } + + if (tmp != ctx->payload) { + ctx->payload = tmp; + } + } + + if (flb_sds_len(ctx->payload) > 0) { + flb_sds_cat(ctx->payload, "&", 1); + } + + flb_sds_cat(ctx->payload, key_str, key_len); + flb_sds_cat(ctx->payload, "=", 1); + flb_sds_cat(ctx->payload, val_str, val_len); + + ctx->payload_manual = FLB_TRUE; + return 0; +} + +static int oauth2_get_token_locked(struct flb_oauth2 *ctx, + flb_sds_t *token_out, + int force_refresh) +{ + int ret = 0; + + if (oauth2_token_needs_refresh(ctx, force_refresh) == FLB_TRUE) { + ret = oauth2_refresh_locked(ctx); + if (ret != 0) { + return ret; + } + } + + *token_out = ctx->access_token; + + return (*token_out != NULL) ? 0 : -1; +} + +int flb_oauth2_get_access_token(struct flb_oauth2 *ctx, + flb_sds_t *token_out, + int force_refresh) +{ + int ret; + + if (ctx->cfg.enabled == FLB_FALSE) { + return -1; + } + + ret = flb_lock_acquire(&ctx->lock, + FLB_LOCK_DEFAULT_RETRY_LIMIT, + FLB_LOCK_DEFAULT_RETRY_DELAY); + if (ret != 0) { + return -1; + } + + ret = oauth2_get_token_locked(ctx, token_out, force_refresh); + + flb_lock_release(&ctx->lock, + FLB_LOCK_DEFAULT_RETRY_LIMIT, + FLB_LOCK_DEFAULT_RETRY_DELAY); + + return ret; +} + +char *flb_oauth2_token_get(struct flb_oauth2 *ctx) +{ + flb_sds_t token = NULL; + int ret; + + ret = flb_oauth2_get_access_token(ctx, &token, FLB_FALSE); + if (ret != 0) { + return NULL; + } + + return token; +} + +char *flb_oauth2_token_get_ng(struct flb_oauth2 *ctx) +{ + return flb_oauth2_token_get(ctx); +} + +void flb_oauth2_invalidate_token(struct flb_oauth2 *ctx) +{ + int ret; + + ret = flb_lock_acquire(&ctx->lock, + FLB_LOCK_DEFAULT_RETRY_LIMIT, + FLB_LOCK_DEFAULT_RETRY_DELAY); + if (ret != 0) { + return; + } + + ctx->expires_at = 0; + + flb_lock_release(&ctx->lock, + FLB_LOCK_DEFAULT_RETRY_LIMIT, + FLB_LOCK_DEFAULT_RETRY_DELAY); } int flb_oauth2_token_len(struct flb_oauth2 *ctx) @@ -548,9 +865,19 @@ int flb_oauth2_token_expired(struct flb_oauth2 *ctx) } now = time(NULL); - if (ctx->expires <= now) { + if (ctx->expires_at <= now) { return FLB_TRUE; } return FLB_FALSE; } + +struct mk_list *flb_oauth2_get_config_map(struct flb_config *config) +{ + struct mk_list *config_map; + + config_map = flb_config_map_create(config, oauth2_config_map); + + return config_map; +} + diff --git a/src/flb_oauth2_jwt.c b/src/flb_oauth2_jwt.c new file mode 100644 index 00000000000..a57a15e3fda --- /dev/null +++ b/src/flb_oauth2_jwt.c @@ -0,0 +1,1389 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* Fluent Bit + * ========== + * Copyright (C) 2015-2025 The Fluent Bit Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + + + +struct flb_oauth2_jwks_key { + flb_sds_t kid; + flb_sds_t modulus; + flb_sds_t exponent; + time_t loaded_at; +}; + +struct flb_oauth2_jwks_cache { + struct flb_hash_table *entries; + time_t last_refresh; + int refresh_interval; +}; + +struct flb_oauth2_jwt_ctx { + struct flb_config *config; + struct flb_oauth2_jwt_cfg cfg; + struct flb_oauth2_jwks_cache jwks_cache; +}; + +const char *flb_oauth2_jwt_status_message(int status) +{ + switch (status) { + case FLB_OAUTH2_JWT_OK: + return "ok"; + case FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT: + return "invalid argument"; + case FLB_OAUTH2_JWT_ERR_SEGMENT_COUNT: + return "jwt must contain 3 segments"; + case FLB_OAUTH2_JWT_ERR_BASE64_HEADER: + return "unable to decode header"; + case FLB_OAUTH2_JWT_ERR_BASE64_PAYLOAD: + return "unable to decode payload"; + case FLB_OAUTH2_JWT_ERR_BASE64_SIGNATURE: + return "unable to decode signature"; + case FLB_OAUTH2_JWT_ERR_JSON_HEADER: + return "invalid header json"; + case FLB_OAUTH2_JWT_ERR_JSON_PAYLOAD: + return "invalid payload json"; + case FLB_OAUTH2_JWT_ERR_MISSING_KID: + return "missing kid in header"; + case FLB_OAUTH2_JWT_ERR_ALG_UNSUPPORTED: + return "unsupported alg"; + case FLB_OAUTH2_JWT_ERR_MISSING_EXP: + return "missing exp claim"; + case FLB_OAUTH2_JWT_ERR_MISSING_ISS: + return "missing iss claim"; + case FLB_OAUTH2_JWT_ERR_MISSING_AUD: + return "missing aud claim"; + case FLB_OAUTH2_JWT_ERR_MISSING_BEARER_TOKEN: + return "missing bearer token"; + case FLB_OAUTH2_JWT_ERR_MISSING_AUTH_HEADER: + return "missing authorization header"; + case FLB_OAUTH2_JWT_ERR_VALIDATION_UNAVAILABLE: + return "validation not implemented"; + default: + return "unknown error"; + } +} + +static void oauth2_jwks_key_destroy(struct flb_oauth2_jwks_key *key) +{ + if (!key) { + return; + } + + if (key->kid) { + flb_sds_destroy(key->kid); + } + + if (key->modulus) { + flb_sds_destroy(key->modulus); + } + + if (key->exponent) { + flb_sds_destroy(key->exponent); + } + + flb_free(key); +} + +static void oauth2_jwks_cache_destroy(struct flb_oauth2_jwks_cache *cache) +{ + int i; + struct mk_list *head; + struct mk_list *tmp; + struct flb_hash_table_entry *entry; + struct flb_hash_table_chain *table; + + if (!cache || !cache->entries) { + return; + } + + /* Iterate through all hash table chains and destroy keys */ + for (i = 0; i < cache->entries->size; i++) { + table = &cache->entries->table[i]; + mk_list_foreach_safe(head, tmp, &table->chains) { + entry = mk_list_entry(head, struct flb_hash_table_entry, _head); + if (entry->val) { + oauth2_jwks_key_destroy((struct flb_oauth2_jwks_key *)entry->val); + entry->val = NULL; /* Prevent double-free */ + } + } + } + + flb_hash_table_destroy(cache->entries); + cache->entries = NULL; +} + +static int oauth2_jwks_cache_init(struct flb_oauth2_jwks_cache *cache, + int refresh_interval) +{ + if (!cache) { + return -1; + } + + cache->entries = flb_hash_table_create(FLB_HASH_TABLE_EVICT_NONE, 64, 0); + if (!cache->entries) { + return -1; + } + + cache->last_refresh = 0; + cache->refresh_interval = refresh_interval; + + return 0; +} + +static void oauth2_jwt_destroy_claims(struct flb_oauth2_jwt_claims *claims) +{ + if (!claims) { + return; + } + + if (claims->kid) { + flb_sds_destroy(claims->kid); + } + + if (claims->alg) { + flb_sds_destroy(claims->alg); + } + + if (claims->issuer) { + flb_sds_destroy(claims->issuer); + } + + if (claims->audience) { + flb_sds_destroy(claims->audience); + } + + if (claims->client_id) { + flb_sds_destroy(claims->client_id); + } +} + +void flb_oauth2_jwt_destroy(struct flb_oauth2_jwt *jwt) +{ + if (!jwt) { + return; + } + + oauth2_jwt_destroy_claims(&jwt->claims); + + if (jwt->header_json) { + flb_sds_destroy(jwt->header_json); + } + + if (jwt->payload_json) { + flb_sds_destroy(jwt->payload_json); + } + + if (jwt->signing_input) { + flb_sds_destroy(jwt->signing_input); + } + + if (jwt->signature) { + flb_free(jwt->signature); + } +} + +static int oauth2_jwt_token_strcmp(const char *json, jsmntok_t *tok, const char *cmp) +{ + int len = (tok->end - tok->start); + + if (len != (int) strlen(cmp)) { + return -1; + } + + return strncmp(json + tok->start, cmp, len); +} + +static int oauth2_jwt_parse_json_tokens(const char *json, + size_t json_len, + jsmntok_t **tokens_out, + int *tokens_size_out, + int invalid_error) +{ + int ret; + jsmn_parser parser; + int tokens_size = 32; + jsmntok_t *tokens = NULL; + int max_iterations = 20; /* Prevent infinite loop */ + int iteration = 0; + + while (iteration < max_iterations) { + flb_free(tokens); + tokens = flb_calloc(1, sizeof(jsmntok_t) * tokens_size); + if (!tokens) { + flb_errno(); + return FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; + } + + /* Reinitialize parser for each attempt */ + jsmn_init(&parser); + ret = jsmn_parse(&parser, json, json_len, tokens, tokens_size); + + if (ret != JSMN_ERROR_NOMEM) { + break; + } + + /* Double the token size for next iteration */ + tokens_size *= 2; + iteration++; + } + + if (iteration >= max_iterations) { + flb_free(tokens); + return invalid_error; + } + + if (ret == JSMN_ERROR_INVAL || ret == JSMN_ERROR_PART) { + flb_free(tokens); + return invalid_error; + } + + if (ret < 1 || tokens[0].type != JSMN_OBJECT) { + flb_free(tokens); + return invalid_error; + } + + *tokens_out = tokens; + *tokens_size_out = ret; + return FLB_OAUTH2_JWT_OK; +} + +static int oauth2_jwt_base64url_decode(const char *segment, + size_t segment_len, + unsigned char **decoded, + size_t *decoded_len, + int base64_error_code) +{ + int ret; + size_t i; + size_t j = 0; + size_t padding = 0; + size_t padded_len; + size_t clean_len = 0; + char *padded; + unsigned char c; + + if (!segment || !decoded || !decoded_len) { + return FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; + } + + /* First, count non-whitespace characters */ + for (i = 0; i < segment_len; i++) { + if (!isspace((unsigned char)segment[i])) { + clean_len++; + } + } + + if (clean_len == 0) { + return base64_error_code; + } + + padding = (4 - (clean_len % 4)) % 4; + padded_len = clean_len + padding; + + padded = flb_malloc(padded_len + 1); + if (!padded) { + flb_errno(); + return FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; + } + + /* Copy and convert base64url to base64, skipping whitespace */ + for (i = 0; i < segment_len && j < clean_len; i++) { + c = (unsigned char)segment[i]; + + if (isspace(c)) { + continue; /* Skip whitespace */ + } + + /* Validate base64url character */ + if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '-' || c == '_')) { + flb_free(padded); + return base64_error_code; + } + + if (c == '-') { + padded[j] = '+'; + } + else if (c == '_') { + padded[j] = '/'; + } + else { + padded[j] = c; + } + j++; + } + + if (j != clean_len) { + flb_free(padded); + return base64_error_code; + } + + /* Add padding */ + for (i = 0; i < padding; i++) { + padded[clean_len + i] = '='; + } + padded[padded_len] = '\0'; + + /* First pass: get required buffer size */ + ret = flb_base64_decode(NULL, 0, decoded_len, + (unsigned char *) padded, padded_len); + /* Note: ret will be FLB_BASE64_ERR_BUFFER_TOO_SMALL (-42) on first pass, this is expected */ + if (ret != 0 && ret != FLB_BASE64_ERR_BUFFER_TOO_SMALL) { + flb_free(padded); + return base64_error_code; + } + + if (*decoded_len == 0) { + flb_free(padded); + return base64_error_code; + } + + *decoded = flb_malloc(*decoded_len + 1); + if (!*decoded) { + flb_errno(); + flb_free(padded); + return FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; + } + + ret = flb_base64_decode(*decoded, *decoded_len, decoded_len, + (unsigned char *) padded, padded_len); + flb_free(padded); + + if (ret != 0) { + flb_free(*decoded); + *decoded = NULL; + return base64_error_code; + } + + (*decoded)[*decoded_len] = '\0'; + return FLB_OAUTH2_JWT_OK; +} + +static flb_sds_t oauth2_jwt_token_to_sds(const char *json, jsmntok_t *tok) +{ + return flb_sds_create_len(json + tok->start, tok->end - tok->start); +} + +static int oauth2_jwt_parse_header(const char *json, size_t json_len, + struct flb_oauth2_jwt_claims *claims) +{ + int ret; + int root_type; + char *mp_buf = NULL; + size_t mp_size; + size_t off = 0; + msgpack_unpacked result; + msgpack_object map; + msgpack_object *k; + msgpack_object *v; + size_t i; + size_t map_size; + size_t key_len; + size_t val_len; + const char *key_str; + const char *val_str; + + /* Convert JSON to msgpack */ + ret = flb_pack_json_yyjson(json, json_len, &mp_buf, &mp_size, + &root_type, NULL); + if (ret != 0 || root_type != JSMN_OBJECT) { + if (mp_buf) { + flb_free(mp_buf); + } + return FLB_OAUTH2_JWT_ERR_JSON_HEADER; + } + + /* Unpack msgpack */ + msgpack_unpacked_init(&result); + if (msgpack_unpack_next(&result, mp_buf, mp_size, &off) != MSGPACK_UNPACK_SUCCESS) { + flb_free(mp_buf); + msgpack_unpacked_destroy(&result); + return FLB_OAUTH2_JWT_ERR_JSON_HEADER; + } + + map = result.data; + if (map.type != MSGPACK_OBJECT_MAP) { + flb_free(mp_buf); + msgpack_unpacked_destroy(&result); + return FLB_OAUTH2_JWT_ERR_JSON_HEADER; + } + + /* Extract fields from msgpack map */ + map_size = map.via.map.size; + for (i = 0; i < map_size; i++) { + k = &map.via.map.ptr[i].key; + v = &map.via.map.ptr[i].val; + + if (k->type != MSGPACK_OBJECT_STR) { + continue; + } + + if (v->type == MSGPACK_OBJECT_STR) { + key_len = k->via.str.size; + val_len = v->via.str.size; + key_str = (const char *)k->via.str.ptr; + val_str = (const char *)v->via.str.ptr; + + if (key_len == 3 && strncmp(key_str, "kid", 3) == 0) { + claims->kid = flb_sds_create_len(val_str, val_len); + } + else if (key_len == 3 && strncmp(key_str, "alg", 3) == 0) { + claims->alg = flb_sds_create_len(val_str, val_len); + } + } + } + + flb_free(mp_buf); + msgpack_unpacked_destroy(&result); + + if (!claims->kid) { + return FLB_OAUTH2_JWT_ERR_MISSING_KID; + } + + if (!claims->alg || strcmp(claims->alg, "RS256") != 0) { + return FLB_OAUTH2_JWT_ERR_ALG_UNSUPPORTED; + } + + return FLB_OAUTH2_JWT_OK; +} + +static int oauth2_jwt_parse_payload(const char *json, size_t json_len, + struct flb_oauth2_jwt_claims *claims) +{ + int ret; + int root_type; + char *mp_buf = NULL; + size_t mp_size; + size_t off = 0; + msgpack_unpacked result; + msgpack_object map; + msgpack_object *k; + msgpack_object *v; + size_t i; + size_t map_size; + size_t key_len; + const char *key_str; + msgpack_object *first; + + /* Convert JSON to msgpack */ + ret = flb_pack_json_yyjson(json, json_len, &mp_buf, &mp_size, + &root_type, NULL); + if (ret != 0 || root_type != JSMN_OBJECT) { + if (mp_buf) { + flb_free(mp_buf); + } + return FLB_OAUTH2_JWT_ERR_JSON_PAYLOAD; + } + + /* Unpack msgpack */ + msgpack_unpacked_init(&result); + if (msgpack_unpack_next(&result, mp_buf, mp_size, &off) != MSGPACK_UNPACK_SUCCESS) { + flb_free(mp_buf); + msgpack_unpacked_destroy(&result); + return FLB_OAUTH2_JWT_ERR_JSON_PAYLOAD; + } + + map = result.data; + if (map.type != MSGPACK_OBJECT_MAP) { + flb_free(mp_buf); + msgpack_unpacked_destroy(&result); + return FLB_OAUTH2_JWT_ERR_JSON_PAYLOAD; + } + + /* Extract fields from msgpack map */ + map_size = map.via.map.size; + for (i = 0; i < map_size; i++) { + k = &map.via.map.ptr[i].key; + v = &map.via.map.ptr[i].val; + + if (k->type != MSGPACK_OBJECT_STR) { + continue; + } + + key_len = k->via.str.size; + key_str = (const char *)k->via.str.ptr; + + if (key_len == 3 && strncmp(key_str, "exp", 3) == 0) { + if (v->type == MSGPACK_OBJECT_POSITIVE_INTEGER) { + claims->expiration = v->via.u64; + } + else if (v->type == MSGPACK_OBJECT_NEGATIVE_INTEGER) { + /* Negative integers are not valid for exp */ + continue; + } + } + else if (key_len == 3 && strncmp(key_str, "iss", 3) == 0) { + if (v->type == MSGPACK_OBJECT_STR) { + if (claims->issuer) { + flb_sds_destroy(claims->issuer); + } + claims->issuer = flb_sds_create_len((const char *)v->via.str.ptr, + v->via.str.size); + } + } + else if (key_len == 3 && strncmp(key_str, "aud", 3) == 0) { + if (v->type == MSGPACK_OBJECT_STR) { + if (claims->audience) { + flb_sds_destroy(claims->audience); + } + claims->audience = flb_sds_create_len((const char *)v->via.str.ptr, + v->via.str.size); + } + else if (v->type == MSGPACK_OBJECT_ARRAY && v->via.array.size > 0) { + /* Take first element of array */ + first = &v->via.array.ptr[0]; + if (first->type == MSGPACK_OBJECT_STR) { + if (claims->audience) { + flb_sds_destroy(claims->audience); + } + claims->audience = flb_sds_create_len((const char *)first->via.str.ptr, + first->via.str.size); + } + } + } + else if (key_len == 3 && strncmp(key_str, "azp", 3) == 0) { + if (v->type == MSGPACK_OBJECT_STR) { + 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); + } + } + else if (key_len == 9 && strncmp(key_str, "client_id", 9) == 0) { + if (v->type == MSGPACK_OBJECT_STR) { + 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); + } + } + } + + flb_free(mp_buf); + msgpack_unpacked_destroy(&result); + + if (claims->expiration == 0) { + return FLB_OAUTH2_JWT_ERR_MISSING_EXP; + } + + if (!claims->issuer) { + return FLB_OAUTH2_JWT_ERR_MISSING_ISS; + } + + if (!claims->audience) { + return FLB_OAUTH2_JWT_ERR_MISSING_AUD; + } + + return FLB_OAUTH2_JWT_OK; +} + +int flb_oauth2_jwt_parse(const char *token, size_t token_len, + struct flb_oauth2_jwt *jwt) +{ + int ret; + int segment = 0; + size_t i; + size_t start = 0; + const char *parts[3] = {0}; + size_t parts_len[3] = {0}; + unsigned char *decoded = NULL; + size_t decoded_len = 0; + + if (!token || token_len == 0 || !jwt) { + return FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; + } + + memset(jwt, 0, sizeof(struct flb_oauth2_jwt)); + + for (i = 0; i <= token_len; i++) { + if (i == token_len || token[i] == '.') { + if (segment >= 3) { + return FLB_OAUTH2_JWT_ERR_SEGMENT_COUNT; + } + + parts[segment] = token + start; + parts_len[segment] = i - start; + segment++; + start = i + 1; + } + } + + if (segment != 3) { + return FLB_OAUTH2_JWT_ERR_SEGMENT_COUNT; + } + + jwt->signing_input = flb_sds_create_len(token, parts_len[0] + parts_len[1] + 1); + if (!jwt->signing_input) { + return FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; + } + + memcpy(jwt->signing_input, parts[0], parts_len[0]); + jwt->signing_input[parts_len[0]] = '.'; + memcpy(jwt->signing_input + parts_len[0] + 1, parts[1], parts_len[1]); + jwt->signing_input[parts_len[0] + parts_len[1] + 1] = '\0'; + + ret = oauth2_jwt_base64url_decode(parts[0], parts_len[0], &decoded, &decoded_len, + FLB_OAUTH2_JWT_ERR_BASE64_HEADER); + if (ret != FLB_OAUTH2_JWT_OK) { + flb_oauth2_jwt_destroy(jwt); + return ret; + } + + jwt->header_json = flb_sds_create_len((const char *) decoded, decoded_len); + flb_free(decoded); + decoded = NULL; + decoded_len = 0; + if (!jwt->header_json) { + flb_oauth2_jwt_destroy(jwt); + return FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; + } + + ret = oauth2_jwt_parse_header(jwt->header_json, flb_sds_len(jwt->header_json), + &jwt->claims); + if (ret != FLB_OAUTH2_JWT_OK) { + flb_oauth2_jwt_destroy(jwt); + return ret; + } + + ret = oauth2_jwt_base64url_decode(parts[1], parts_len[1], &decoded, &decoded_len, + FLB_OAUTH2_JWT_ERR_BASE64_PAYLOAD); + if (ret != FLB_OAUTH2_JWT_OK) { + flb_oauth2_jwt_destroy(jwt); + return ret; + } + + jwt->payload_json = flb_sds_create_len((const char *) decoded, decoded_len); + flb_free(decoded); + decoded = NULL; + decoded_len = 0; + if (!jwt->payload_json) { + flb_oauth2_jwt_destroy(jwt); + return FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; + } + + ret = oauth2_jwt_parse_payload(jwt->payload_json, + flb_sds_len(jwt->payload_json), + &jwt->claims); + if (ret != FLB_OAUTH2_JWT_OK) { + flb_oauth2_jwt_destroy(jwt); + return ret; + } + + ret = oauth2_jwt_base64url_decode(parts[2], parts_len[2], &decoded, &decoded_len, + FLB_OAUTH2_JWT_ERR_BASE64_SIGNATURE); + if (ret != FLB_OAUTH2_JWT_OK) { + flb_oauth2_jwt_destroy(jwt); + return ret; + } + + jwt->signature = decoded; + jwt->signature_len = decoded_len; + + return FLB_OAUTH2_JWT_OK; +} + +static int oauth2_jwks_parse_key(const char *json, jsmntok_t *tokens, int tokens_size, int key_obj_idx, + struct flb_oauth2_jwks_key **key_out) +{ + int i; + flb_sds_t kid = NULL; + flb_sds_t n = NULL; + flb_sds_t e = NULL; + struct flb_oauth2_jwks_key *key = NULL; + jsmntok_t *key_obj; + + if (!json || !tokens || key_obj_idx < 0 || key_obj_idx >= tokens_size || !key_out) { + return -1; + } + + key_obj = &tokens[key_obj_idx]; + if (key_obj->type != JSMN_OBJECT) { + return -1; + } + + /* Find kty, kid, n, e in the key object */ + /* JSMN stores objects as: [object_token, key1, value1, key2, value2, ...] */ + for (i = key_obj_idx + 1; i < tokens_size && i < key_obj_idx + 1 + (key_obj->size * 2); i += 2) { + jsmntok_t *tok = &tokens[i]; + jsmntok_t *val; + + if (i + 1 >= tokens_size) { + break; + } + + val = &tokens[i + 1]; + + if (tok->type != JSMN_STRING) { + continue; + } + + if (oauth2_jwt_token_strcmp(json, tok, "kty") == 0) { + flb_sds_t kty = oauth2_jwt_token_to_sds(json, val); + if (kty && strcmp(kty, "RSA") != 0) { + flb_sds_destroy(kty); + if (kid) flb_sds_destroy(kid); + if (n) flb_sds_destroy(n); + if (e) flb_sds_destroy(e); + return -1; /* Not an RSA key */ + } + if (kty) { + flb_sds_destroy(kty); + } + } + else if (oauth2_jwt_token_strcmp(json, tok, "kid") == 0) { + kid = oauth2_jwt_token_to_sds(json, val); + } + else if (oauth2_jwt_token_strcmp(json, tok, "n") == 0) { + n = oauth2_jwt_token_to_sds(json, val); + } + else if (oauth2_jwt_token_strcmp(json, tok, "e") == 0) { + e = oauth2_jwt_token_to_sds(json, val); + } + } + + if (!kid || !n || !e) { + if (kid) flb_sds_destroy(kid); + if (n) flb_sds_destroy(n); + if (e) flb_sds_destroy(e); + return -1; + } + + key = flb_calloc(1, sizeof(struct flb_oauth2_jwks_key)); + if (!key) { + flb_sds_destroy(kid); + flb_sds_destroy(n); + flb_sds_destroy(e); + return -1; + } + + key->kid = kid; + key->modulus = n; + key->exponent = e; + key->loaded_at = time(NULL); + + *key_out = key; + return 0; +} + +static int oauth2_jwks_parse_json(flb_sds_t jwks_json, struct flb_oauth2_jwks_cache *cache) +{ + int ret; + int tokens_size; + jsmntok_t *tokens = NULL; + int i; + int keys_found = 0; + + ret = oauth2_jwt_parse_json_tokens(jwks_json, flb_sds_len(jwks_json), + &tokens, &tokens_size, + FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT); + if (ret != FLB_OAUTH2_JWT_OK) { + flb_error("[oauth2_jwt] failed to parse JWKS JSON tokens"); + return -1; + } + + + /* Find "keys" array in the JWKS */ + for (i = 1; i < tokens_size; i++) { + jsmntok_t *key = &tokens[i]; + jsmntok_t *val; + + if (key->type != JSMN_STRING) { + continue; + } + + i++; + if (i >= tokens_size) { + break; + } + + val = &tokens[i]; + + if (oauth2_jwt_token_strcmp(jwks_json, key, "keys") == 0 && + val->type == JSMN_ARRAY) { + int j; + int keys_count = val->size; + int key_idx = i + 1; + int key_obj_end; + jsmntok_t *key_obj; + struct flb_oauth2_jwks_key *jwks_key; + + /* Parse each key in the array */ + for (j = 0; j < keys_count && key_idx < tokens_size; j++) { + key_obj = &tokens[key_idx]; + jwks_key = NULL; + + if (key_obj->type != JSMN_OBJECT) { + break; + } + + /* For JSMN, an object with size N has N key-value pairs + * Each pair occupies 2 tokens (key + value) + * So total tokens = 1 (object) + N*2 (pairs) + * Since JWKS keys have simple string values (no nested objects), + * we can use this simple calculation */ + key_obj_end = key_idx + 1 + (key_obj->size * 2); + + /* Ensure we don't go beyond tokens_size */ + if (key_obj_end > tokens_size) { + key_obj_end = tokens_size; + } + + ret = oauth2_jwks_parse_key(jwks_json, tokens, tokens_size, key_idx, &jwks_key); + if (ret == 0 && jwks_key) { + /* Store key in cache using kid as hash key */ + flb_hash_table_add(cache->entries, jwks_key->kid, + flb_sds_len(jwks_key->kid), + jwks_key, 0); + keys_found++; + } + + /* Move to next key object */ + key_idx = key_obj_end; + } + break; + } + } + + flb_free(tokens); + + if (keys_found == 0) { + flb_error("[oauth2_jwt] No valid keys found in JWKS"); + return -1; + } + + return 0; +} + + +static int oauth2_jwt_verify_signature_rsa(const char *signing_input, + size_t signing_input_len, + const unsigned char *signature, + size_t signature_len, + flb_sds_t modulus_b64, + flb_sds_t exponent_b64) +{ + int ret; + unsigned char *modulus_bytes = NULL; + unsigned char *exponent_bytes = NULL; + size_t modulus_len = 0; + size_t exponent_len = 0; + + /* Decode base64url modulus and exponent */ + ret = oauth2_jwt_base64url_decode(modulus_b64, flb_sds_len(modulus_b64), + (unsigned char **)&modulus_bytes, &modulus_len, + FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT); + if (ret != FLB_OAUTH2_JWT_OK) { + goto cleanup; + } + + ret = oauth2_jwt_base64url_decode(exponent_b64, flb_sds_len(exponent_b64), + (unsigned char **)&exponent_bytes, &exponent_len, + FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT); + if (ret != FLB_OAUTH2_JWT_OK) { + goto cleanup; + } + + /* Use flb_crypto abstraction for signature verification */ + /* This handles OpenSSL 1.1.1 and 3.x compatibility internally */ + ret = flb_crypto_verify_simple(FLB_CRYPTO_PADDING_PKCS1, + FLB_HASH_SHA256, + modulus_bytes, modulus_len, + exponent_bytes, exponent_len, + (unsigned char *) signing_input, signing_input_len, + (unsigned char *) signature, signature_len); + + if (ret != FLB_CRYPTO_SUCCESS) { + flb_debug("[oauth2_jwt] Signature verification failed: ret=%d", ret); + } + +cleanup: + if (modulus_bytes) { + flb_free(modulus_bytes); + } + if (exponent_bytes) { + flb_free(exponent_bytes); + } + + return (ret == FLB_CRYPTO_SUCCESS) ? 0 : -1; +} + +static int oauth2_jwks_fetch_keys(struct flb_oauth2_jwt_ctx *ctx) +{ + int ret; + int port; + size_t b_sent; + char *protocol = NULL; + char *host = NULL; + char *port_str = NULL; + char *uri = NULL; + int io_flags = FLB_IO_TCP; + struct flb_upstream *u = NULL; + struct flb_connection *u_conn = NULL; + struct flb_http_client *c = NULL; + struct flb_tls *tls = NULL; + flb_sds_t jwks_json = NULL; + + if (!ctx || !ctx->cfg.jwks_url || !ctx->config) { + return -1; + } + + + ret = flb_utils_url_split(ctx->cfg.jwks_url, &protocol, &host, &port_str, &uri); + if (ret != 0) { + flb_error("[oauth2_jwt] invalid JWKS URL: %s", ctx->cfg.jwks_url); + return -1; + } + + if (!host || !port_str || !uri) { + flb_error("[oauth2_jwt] invalid JWKS URL components"); + goto cleanup; + } + + port = atoi(port_str); + if (port <= 0) { + flb_error("[oauth2_jwt] invalid port in JWKS URL"); + goto cleanup; + } + + if (protocol && strcasecmp(protocol, "https") == 0) { + io_flags = FLB_IO_TLS; + flb_tls_init(); + tls = flb_tls_create(FLB_TLS_CLIENT_MODE, FLB_TRUE, 0, + host, NULL, NULL, NULL, NULL, NULL); + if (!tls) { + flb_error("[oauth2_jwt] failed to create TLS context"); + goto cleanup; + } + flb_tls_set_verify_hostname(tls, FLB_TRUE); + ret = flb_tls_load_system_certificates(tls); + if (ret != 0) { + flb_error("[oauth2_jwt] failed to load system certificates"); + goto cleanup; + } + } + + u = flb_upstream_create(ctx->config, host, port, io_flags, tls); + if (!u) { + flb_error("[oauth2_jwt] failed to create upstream"); + goto cleanup; + } + + flb_stream_disable_async_mode(&u->base); + + u_conn = flb_upstream_conn_get(u); + if (!u_conn) { + flb_error("[oauth2_jwt] failed to get upstream connection"); + goto cleanup; + } + + c = flb_http_client(u_conn, FLB_HTTP_GET, uri, NULL, 0, host, port, NULL, 0); + if (!c) { + flb_error("[oauth2_jwt] failed to create HTTP client"); + goto cleanup; + } + + ret = flb_http_do(c, &b_sent); + if (ret != 0) { + flb_error("[oauth2_jwt] HTTP request failed"); + goto cleanup; + } + + if (c->resp.status != 200) { + flb_error("[oauth2_jwt] JWKS endpoint returned status %d", c->resp.status); + goto cleanup; + } + + if (c->resp.payload_size <= 0) { + flb_error("[oauth2_jwt] empty JWKS response"); + goto cleanup; + } + + jwks_json = flb_sds_create_len(c->resp.payload, c->resp.payload_size); + if (!jwks_json) { + flb_error("[oauth2_jwt] failed to create JWKS JSON buffer"); + goto cleanup; + } + + /* Parse JWKS JSON and store keys in cache */ + ret = oauth2_jwks_parse_json(jwks_json, &ctx->jwks_cache); + if (ret != 0) { + flb_error("[oauth2_jwt] failed to parse JWKS JSON"); + flb_sds_destroy(jwks_json); + jwks_json = NULL; + } + else { + ctx->jwks_cache.last_refresh = time(NULL); + } + +cleanup: + if (jwks_json) { + flb_sds_destroy(jwks_json); + } + if (c) { + flb_http_client_destroy(c); + } + if (u_conn) { + flb_upstream_conn_release(u_conn); + } + if (u) { + flb_upstream_destroy(u); + } + if (tls) { + flb_tls_destroy(tls); + } + if (protocol) { + flb_free(protocol); + } + if (host) { + flb_free(host); + } + if (port_str) { + flb_free(port_str); + } + if (uri) { + flb_free(uri); + } + + return (jwks_json != NULL) ? 0 : -1; +} + +static void oauth2_jwt_free_cfg(struct flb_oauth2_jwt_cfg *cfg) +{ + /* Note: cfg->issuer, cfg->jwks_url, and cfg->allowed_audience are pointers + * to strings owned by the Fluent Bit configuration system (flb_kv). + * They will be freed automatically when the input instance properties are + * destroyed, so we should NOT free them here to avoid double-free errors. + */ + (void) cfg; +} + +struct flb_oauth2_jwt_ctx *flb_oauth2_jwt_context_create(struct flb_config *config, + struct flb_oauth2_jwt_cfg *cfg) +{ + struct flb_oauth2_jwt_ctx *ctx; + + ctx = flb_calloc(1, sizeof(struct flb_oauth2_jwt_ctx)); + if (!ctx) { + flb_errno(); + return NULL; + } + + ctx->config = config; + + if (cfg != NULL) { + memcpy(&ctx->cfg, cfg, sizeof(struct flb_oauth2_jwt_cfg)); + } + + if (oauth2_jwks_cache_init(&ctx->jwks_cache, + ctx->cfg.jwks_refresh_interval) != 0) { + flb_free(ctx); + return NULL; + } + + /* Don't download JWKS during initialization - do it lazily on first validation */ + /* This avoids blocking the initialization thread */ + + return ctx; +} + +void flb_oauth2_jwt_context_destroy(struct flb_oauth2_jwt_ctx *ctx) +{ + if (!ctx) { + return; + } + + oauth2_jwks_cache_destroy(&ctx->jwks_cache); + oauth2_jwt_free_cfg(&ctx->cfg); + flb_free(ctx); +} + +int flb_oauth2_jwt_validate(struct flb_oauth2_jwt_ctx *ctx, + const char *authorization_header, + size_t authorization_header_len) +{ + int ret; + int status = FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; + int verify_ret; + int allowed_client_authorized; + int dot_count; + size_t token_start = 0; + size_t token_len; + size_t i; + time_t now; + uint64_t exp; + struct flb_oauth2_jwt jwt; + struct flb_oauth2_jwks_key *jwks_key; + struct mk_list *allowed_client_head; + struct flb_config_map_val *map_val; + struct mk_list *client_list_head; + struct flb_slist_entry *client_entry; + + verify_ret = 0; + allowed_client_authorized = FLB_FALSE; + dot_count = 0; + jwks_key = NULL; + allowed_client_head = NULL; + map_val = NULL; + client_list_head = NULL; + client_entry = NULL; + + if (!ctx) { + return FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; + } + + if (!ctx->cfg.validate) { + return FLB_OAUTH2_JWT_OK; + } + + if (!authorization_header || authorization_header_len == 0) { + return FLB_OAUTH2_JWT_ERR_MISSING_AUTH_HEADER; + } + + while (token_start < authorization_header_len && + isspace((unsigned char) authorization_header[token_start])) { + token_start++; + } + + if (authorization_header_len - token_start < sizeof("Bearer ") - 1 || + strncasecmp(&authorization_header[token_start], "Bearer ", sizeof("Bearer ") - 1) != 0) { + return FLB_OAUTH2_JWT_ERR_MISSING_BEARER_TOKEN; + } + + token_start += sizeof("Bearer ") - 1; + token_len = authorization_header_len - token_start; + + while (token_len > 0 && + isspace((unsigned char) authorization_header[token_start + token_len - 1])) { + token_len--; + } + + /* Check if token looks like a JWT (has dots) */ + if (token_len > 0) { + dot_count = 0; + for (i = 0; i < token_len; i++) { + if (authorization_header[token_start + i] == '.') { + dot_count++; + } + } + if (dot_count != 2) { + flb_debug("[oauth2_jwt] Token does not appear to be a JWT (expected 2 dots, found %d). " + "Keycloak may be returning opaque tokens instead of JWT access tokens.", dot_count); + return FLB_OAUTH2_JWT_ERR_SEGMENT_COUNT; + } + } + + memset(&jwt, 0, sizeof(struct flb_oauth2_jwt)); + + status = flb_oauth2_jwt_parse(&authorization_header[token_start], token_len, &jwt); + if (status != FLB_OAUTH2_JWT_OK) { + flb_debug("[oauth2_jwt] failed to parse token: %s", + flb_oauth2_jwt_status_message(status)); + return status; + } + + /* Verify signature using JWKS */ + if (jwt.claims.kid) { + now = time(NULL); + + /* Check if cache needs refresh or is empty */ + if (ctx->jwks_cache.last_refresh == 0 || + (now - ctx->jwks_cache.last_refresh) >= ctx->cfg.jwks_refresh_interval) { + ret = oauth2_jwks_fetch_keys(ctx); + if (ret != 0) { + flb_debug("[oauth2_jwt] Failed to fetch JWKS: %d", ret); + status = FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; + goto jwt_end; + } + } + + /* Lookup key by kid */ + jwks_key = (struct flb_oauth2_jwks_key *)flb_hash_table_get_ptr(ctx->jwks_cache.entries, + jwt.claims.kid, + flb_sds_len(jwt.claims.kid)); + if (!jwks_key) { + /* Try to refresh JWKS and lookup again */ + ret = oauth2_jwks_fetch_keys(ctx); + if (ret != 0) { + flb_debug("[oauth2_jwt] Failed to refresh JWKS: %d", ret); + status = FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; + goto jwt_end; + } + jwks_key = (struct flb_oauth2_jwks_key *)flb_hash_table_get_ptr(ctx->jwks_cache.entries, + jwt.claims.kid, + flb_sds_len(jwt.claims.kid)); + } + + if (!jwks_key) { + flb_debug("[oauth2_jwt] Key with kid '%s' not found in JWKS", jwt.claims.kid); + status = FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; + goto jwt_end; + } + + /* Verify RSA signature */ + verify_ret = oauth2_jwt_verify_signature_rsa( + jwt.signing_input, flb_sds_len(jwt.signing_input), + jwt.signature, jwt.signature_len, + jwks_key->modulus, jwks_key->exponent); + if (verify_ret != 0) { + flb_debug("[oauth2_jwt] Signature verification failed: ret=%d", verify_ret); + status = FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; + goto jwt_end; + } + } + + /* Check expiration */ + now = time(NULL); + exp = jwt.claims.expiration; + if (exp <= (uint64_t) now) { + flb_debug("[oauth2_jwt] Token expired: exp=%llu <= now=%ld", (unsigned long long)exp, (long)now); + status = FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; + goto jwt_end; + } + + /* Check issuer */ + if (ctx->cfg.issuer) { + if (!jwt.claims.issuer || strcmp(ctx->cfg.issuer, jwt.claims.issuer) != 0) { + flb_debug("[oauth2_jwt] Issuer mismatch: expected='%s', actual='%s'", + ctx->cfg.issuer, jwt.claims.issuer ? jwt.claims.issuer : "(null)"); + status = FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; + goto jwt_end; + } + } + + /* Check audience */ + if (ctx->cfg.allowed_audience) { + if (!jwt.claims.audience || strcmp(ctx->cfg.allowed_audience, jwt.claims.audience) != 0) { + flb_debug("[oauth2_jwt] Audience mismatch: expected='%s', actual='%s'", + ctx->cfg.allowed_audience, jwt.claims.audience ? jwt.claims.audience : "(null)"); + status = FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; + goto jwt_end; + } + } + + /* Check allowed clients */ + if (ctx->cfg.allowed_clients && mk_list_size(ctx->cfg.allowed_clients) > 0) { + allowed_client_authorized = FLB_FALSE; + + /* Iterate over flb_config_map_val entries (each contains a list of flb_slist_entry) */ + mk_list_foreach(allowed_client_head, ctx->cfg.allowed_clients) { + map_val = mk_list_entry(allowed_client_head, struct flb_config_map_val, _head); + if (!map_val || !map_val->val.list) { + continue; + } + + /* Iterate over flb_slist_entry in this map_val's list */ + mk_list_foreach(client_list_head, map_val->val.list) { + client_entry = mk_list_entry(client_list_head, struct flb_slist_entry, _head); + if (jwt.claims.client_id && client_entry && client_entry->str && + strcmp(client_entry->str, jwt.claims.client_id) == 0) { + allowed_client_authorized = FLB_TRUE; + goto client_check_done; + } + } + } + + client_check_done: + if (allowed_client_authorized == FLB_FALSE) { + flb_error("[oauth2_jwt] Client ID '%s' not in allowed list (rejecting request)", + jwt.claims.client_id ? jwt.claims.client_id : "(null)"); + status = FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; + goto jwt_end; + } + } + + status = FLB_OAUTH2_JWT_OK; + +jwt_end: + flb_oauth2_jwt_destroy(&jwt); + + return status; +} + +/* OAuth2 JWT config map for input plugins */ +static struct flb_config_map oauth2_jwt_config_map[] = { + { + FLB_CONFIG_MAP_BOOL, "oauth2.validate", "false", + 0, FLB_TRUE, offsetof(struct flb_oauth2_jwt_cfg, validate), + "Enable OAuth2 JWT validation for incoming requests" + }, + { + FLB_CONFIG_MAP_STR, "oauth2.issuer", NULL, + 0, FLB_TRUE, offsetof(struct flb_oauth2_jwt_cfg, issuer), + "Expected issuer claim for OAuth2 JWT validation" + }, + { + FLB_CONFIG_MAP_STR, "oauth2.jwks_url", NULL, + 0, FLB_TRUE, offsetof(struct flb_oauth2_jwt_cfg, jwks_url), + "JWKS endpoint URL for OAuth2 JWT validation" + }, + { + FLB_CONFIG_MAP_STR, "oauth2.allowed_audience", NULL, + 0, FLB_TRUE, offsetof(struct flb_oauth2_jwt_cfg, allowed_audience), + "Audience claim to enforce for OAuth2 JWT validation" + }, + { + FLB_CONFIG_MAP_SLIST_1, "oauth2.allowed_clients", NULL, + FLB_CONFIG_MAP_MULT, FLB_TRUE, offsetof(struct flb_oauth2_jwt_cfg, allowed_clients), + "Authorized client_id/azp values for OAuth2 JWT validation" + }, + { + FLB_CONFIG_MAP_INT, "oauth2.jwks_refresh_interval", "300", + 0, FLB_TRUE, offsetof(struct flb_oauth2_jwt_cfg, jwks_refresh_interval), + "JWKS cache refresh interval in seconds for OAuth2 JWT validation" + }, + + /* EOF */ + {0} +}; + +struct mk_list *flb_oauth2_jwt_get_config_map(struct flb_config *config) +{ + struct mk_list *config_map; + + config_map = flb_config_map_create(config, oauth2_jwt_config_map); + + return config_map; +} + From 1ded4ccfb8997ef262b3b831eca67fb5dccdeb97 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 11:08:08 -0600 Subject: [PATCH 02/42] crypto: extend flb_crypto abstraction for RSA signature verification This commit extends the flb_crypto abstraction layer to support RSA signature verification operations. The new functions handle OpenSSL 1.1.1 and 3.x compatibility internally, providing a unified API for cryptographic operations. New functions: - flb_crypto_build_rsa_public_key_from_components() - flb_crypto_init_from_rsa_components() - flb_crypto_verify() - flb_crypto_verify_simple() The implementation uses a hybrid approach for OpenSSL 3.x compatibility while maintaining functionality with OpenSSL 1.1.1. Signed-off-by: Eduardo Silva --- include/fluent-bit/flb_crypto.h | 63 ++++-- include/fluent-bit/flb_crypto_constants.h | 1 + src/flb_crypto.c | 236 +++++++++++++++++++++- 3 files changed, 280 insertions(+), 20 deletions(-) diff --git a/include/fluent-bit/flb_crypto.h b/include/fluent-bit/flb_crypto.h index e406388e457..3691cd8d1d9 100644 --- a/include/fluent-bit/flb_crypto.h +++ b/include/fluent-bit/flb_crypto.h @@ -54,48 +54,73 @@ int flb_crypto_transform(struct flb_crypto *context, unsigned char *output_buffer, size_t *output_length); -int flb_crypto_sign(struct flb_crypto *context, - unsigned char *input_buffer, +int flb_crypto_sign(struct flb_crypto *context, + unsigned char *input_buffer, size_t input_length, - unsigned char *output_buffer, + unsigned char *output_buffer, size_t *output_length); -int flb_crypto_encrypt(struct flb_crypto *context, - unsigned char *input_buffer, +int flb_crypto_encrypt(struct flb_crypto *context, + unsigned char *input_buffer, size_t input_length, - unsigned char *output_buffer, + unsigned char *output_buffer, size_t *output_length); -int flb_crypto_decrypt(struct flb_crypto *context, - unsigned char *input_buffer, +int flb_crypto_decrypt(struct flb_crypto *context, + unsigned char *input_buffer, size_t input_length, - unsigned char *output_buffer, + unsigned char *output_buffer, size_t *output_length); -int flb_crypto_sign_simple(int key_type, +int flb_crypto_sign_simple(int key_type, int padding_type, int digest_algorithm, unsigned char *key, - size_t key_length, - unsigned char *input_buffer, + size_t key_length, + unsigned char *input_buffer, size_t input_length, - unsigned char *output_buffer, + unsigned char *output_buffer, size_t *output_length); int flb_crypto_encrypt_simple(int padding_type, unsigned char *key, - size_t key_length, - unsigned char *input_buffer, + size_t key_length, + unsigned char *input_buffer, size_t input_length, - unsigned char *output_buffer, + unsigned char *output_buffer, size_t *output_length); int flb_crypto_decrypt_simple(int padding_type, unsigned char *key, - size_t key_length, - unsigned char *input_buffer, + size_t key_length, + unsigned char *input_buffer, size_t input_length, - unsigned char *output_buffer, + unsigned char *output_buffer, size_t *output_length); +int flb_crypto_init_from_rsa_components(struct flb_crypto *context, + int padding_type, + int digest_algorithm, + unsigned char *modulus_bytes, + size_t modulus_len, + unsigned char *exponent_bytes, + size_t exponent_len); + +int flb_crypto_verify(struct flb_crypto *context, + unsigned char *data, + size_t data_length, + unsigned char *signature, + size_t signature_length); + +int flb_crypto_verify_simple(int padding_type, + int digest_algorithm, + unsigned char *modulus_bytes, + size_t modulus_len, + unsigned char *exponent_bytes, + size_t exponent_len, + unsigned char *data, + size_t data_length, + unsigned char *signature, + size_t signature_length); + #endif \ No newline at end of file diff --git a/include/fluent-bit/flb_crypto_constants.h b/include/fluent-bit/flb_crypto_constants.h index 5728e0b4b62..d28e3a67297 100644 --- a/include/fluent-bit/flb_crypto_constants.h +++ b/include/fluent-bit/flb_crypto_constants.h @@ -52,5 +52,6 @@ #define FLB_CRYPTO_OPERATION_SIGN 1 #define FLB_CRYPTO_OPERATION_ENCRYPT 2 #define FLB_CRYPTO_OPERATION_DECRYPT 3 +#define FLB_CRYPTO_OPERATION_VERIFY 4 #endif \ No newline at end of file diff --git a/src/flb_crypto.c b/src/flb_crypto.c index c2811039a27..352aadc14d9 100644 --- a/src/flb_crypto.c +++ b/src/flb_crypto.c @@ -17,6 +17,9 @@ #include #include +#include +#include +#include #include static int flb_crypto_get_rsa_padding_type_by_id(int padding_type_id) @@ -111,6 +114,92 @@ static int flb_crypto_import_pem_key(int key_type, return result; } +/* Build RSA public key from modulus and exponent (base64url encoded) */ +static int flb_crypto_build_rsa_public_key_from_components(unsigned char *modulus_bytes, + size_t modulus_len, + unsigned char *exponent_bytes, + size_t exponent_len, + EVP_PKEY **pkey) +{ + BIGNUM *n = NULL; + BIGNUM *e = NULL; + RSA *rsa = NULL; + int ret = FLB_CRYPTO_BACKEND_ERROR; + + if (!modulus_bytes || !exponent_bytes || !pkey) { + return FLB_CRYPTO_INVALID_ARGUMENT; + } + + n = BN_bin2bn(modulus_bytes, modulus_len, NULL); + e = BN_bin2bn(exponent_bytes, exponent_len, NULL); + if (!n || !e) { + goto cleanup; + } + +#if OPENSSL_VERSION_MAJOR < 3 + /* OpenSSL 1.1.1: Build RSA key directly */ + rsa = RSA_new(); + if (!rsa) { + goto cleanup; + } + + if (RSA_set0_key(rsa, n, e, NULL) != 1) { + goto cleanup; + } + n = e = NULL; /* ownership transferred */ + + *pkey = EVP_PKEY_new(); + if (!*pkey) { + goto cleanup; + } + + if (EVP_PKEY_assign_RSA(*pkey, rsa) != 1) { + EVP_PKEY_free(*pkey); + *pkey = NULL; + goto cleanup; + } + rsa = NULL; /* ownership transferred */ +#else + /* OpenSSL 3.x: Build RSA key and wrap in EVP_PKEY */ + rsa = RSA_new(); + if (!rsa) { + goto cleanup; + } + + if (RSA_set0_key(rsa, BN_dup(n), BN_dup(e), NULL) != 1) { + goto cleanup; + } + + *pkey = EVP_PKEY_new(); + if (!*pkey) { + goto cleanup; + } + + if (EVP_PKEY_set1_RSA(*pkey, rsa) != 1) { + EVP_PKEY_free(*pkey); + *pkey = NULL; + goto cleanup; + } + RSA_free(rsa); + rsa = NULL; +#endif + + ret = FLB_CRYPTO_SUCCESS; + +cleanup: + if (rsa) { + RSA_free(rsa); + } + if (n) { + BN_free(n); + } + if (e) { + BN_free(e); + } + + return ret; +} + int flb_crypto_init(struct flb_crypto *context, int padding_type, int digest_algorithm, @@ -201,7 +290,8 @@ int flb_crypto_transform(struct flb_crypto *context, if (operation != FLB_CRYPTO_OPERATION_SIGN && operation != FLB_CRYPTO_OPERATION_ENCRYPT && - operation != FLB_CRYPTO_OPERATION_DECRYPT) { + operation != FLB_CRYPTO_OPERATION_DECRYPT && + operation != FLB_CRYPTO_OPERATION_VERIFY) { return FLB_CRYPTO_INVALID_ARGUMENT; } @@ -215,6 +305,9 @@ int flb_crypto_transform(struct flb_crypto *context, else if (operation == FLB_CRYPTO_OPERATION_DECRYPT) { result = EVP_PKEY_decrypt_init(context->backend_context); } + else if (operation == FLB_CRYPTO_OPERATION_VERIFY) { + result = EVP_PKEY_verify_init(context->backend_context); + } if (result == 1) { result = EVP_PKEY_CTX_set_rsa_padding(context->backend_context, @@ -259,6 +352,13 @@ int flb_crypto_transform(struct flb_crypto *context, output_buffer, output_length, input_buffer, input_length); } + else if(operation == FLB_CRYPTO_OPERATION_VERIFY) { + /* For verify, input_buffer is the signature, input_length is signature length */ + /* output_buffer is the data to verify, output_length is data length */ + result = EVP_PKEY_verify(context->backend_context, + input_buffer, input_length, + output_buffer, *output_length); + } if (result != 1) { context->last_error = ERR_get_error(); @@ -401,5 +501,139 @@ int flb_crypto_decrypt_simple(int padding_type, return result; } +int flb_crypto_init_from_rsa_components(struct flb_crypto *context, + int padding_type, + int digest_algorithm, + unsigned char *modulus_bytes, + size_t modulus_len, + unsigned char *exponent_bytes, + size_t exponent_len) +{ + int result; + + if (context == NULL) { + return FLB_CRYPTO_INVALID_ARGUMENT; + } + + if (modulus_bytes == NULL || exponent_bytes == NULL) { + return FLB_CRYPTO_INVALID_ARGUMENT; + } + + memset(context, 0, sizeof(struct flb_crypto)); + + result = flb_crypto_build_rsa_public_key_from_components(modulus_bytes, + modulus_len, + exponent_bytes, + exponent_len, + &context->key); + + if (result != FLB_CRYPTO_SUCCESS) { + if (result == FLB_CRYPTO_BACKEND_ERROR) { + context->last_error = ERR_get_error(); + } + flb_crypto_cleanup(context); + return result; + } + + context->backend_context = EVP_PKEY_CTX_new(context->key, NULL); + + if (context->backend_context == NULL) { + context->last_error = ERR_get_error(); + flb_crypto_cleanup(context); + return FLB_CRYPTO_BACKEND_ERROR; + } + + context->block_size = (size_t) EVP_PKEY_size(context->key); + context->padding_type = flb_crypto_get_rsa_padding_type_by_id(padding_type); + context->digest_algorithm = flb_crypto_get_digest_algorithm_instance_by_id(digest_algorithm); + + return FLB_CRYPTO_SUCCESS; +} + +int flb_crypto_verify(struct flb_crypto *context, + unsigned char *data, + size_t data_length, + unsigned char *signature, + size_t signature_length) +{ + EVP_MD_CTX *md_ctx = NULL; + EVP_PKEY_CTX *pkey_ctx = NULL; + int result = FLB_CRYPTO_BACKEND_ERROR; + + if (context == NULL || data == NULL || signature == NULL) { + return FLB_CRYPTO_INVALID_ARGUMENT; + } + + md_ctx = EVP_MD_CTX_new(); + if (!md_ctx) { + if (context) { + context->last_error = ERR_get_error(); + } + return FLB_CRYPTO_BACKEND_ERROR; + } + + if (EVP_DigestVerifyInit(md_ctx, &pkey_ctx, context->digest_algorithm, NULL, context->key) <= 0) { + if (context) { + context->last_error = ERR_get_error(); + } + EVP_MD_CTX_free(md_ctx); + return FLB_CRYPTO_BACKEND_ERROR; + } + + if (EVP_PKEY_CTX_set_rsa_padding(pkey_ctx, context->padding_type) <= 0) { + if (context) { + context->last_error = ERR_get_error(); + } + EVP_MD_CTX_free(md_ctx); + return FLB_CRYPTO_BACKEND_ERROR; + } + + result = EVP_DigestVerify(md_ctx, signature, signature_length, data, data_length); + EVP_MD_CTX_free(md_ctx); + + if (result == 1) { + return FLB_CRYPTO_SUCCESS; + } + else { + if (context) { + context->last_error = ERR_get_error(); + } + return FLB_CRYPTO_BACKEND_ERROR; + } +} + +int flb_crypto_verify_simple(int padding_type, + int digest_algorithm, + unsigned char *modulus_bytes, + size_t modulus_len, + unsigned char *exponent_bytes, + size_t exponent_len, + unsigned char *data, + size_t data_length, + unsigned char *signature, + size_t signature_length) +{ + struct flb_crypto context; + int result; + + result = flb_crypto_init_from_rsa_components(&context, + padding_type, + digest_algorithm, + modulus_bytes, + modulus_len, + exponent_bytes, + exponent_len); + + if (result == FLB_CRYPTO_SUCCESS) { + result = flb_crypto_verify(&context, + data, data_length, + signature, signature_length); + + flb_crypto_cleanup(&context); + } + + return result; +} + From 7384de58587831225640dd645a95a5cf4b2e0524 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 11:09:11 -0600 Subject: [PATCH 03/42] in_http: add OAuth2 JWT validation support This commit adds OAuth2 JWT validation to the HTTP input plugin, allowing Fluent Bit to validate incoming bearer tokens. The implementation uses an independent config map pattern similar to net.* properties. Configuration properties: - oauth2.validate: enable/disable validation - oauth2.issuer: expected issuer claim - oauth2.jwks_url: JWKS endpoint URL - oauth2.allowed_audience: required audience claim - oauth2.allowed_clients: list of authorized client IDs - oauth2.jwks_refresh_interval: JWKS cache refresh interval The validation is performed lazily (JWKS is fetched on first use) to avoid blocking plugin initialization. Signed-off-by: Eduardo Silva --- plugins/in_http/http.c | 19 +++++++++++++ plugins/in_http/http.h | 4 +++ plugins/in_http/http_config.c | 34 +++++++++++++++++++++++ plugins/in_http/http_prot.c | 52 +++++++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+) diff --git a/plugins/in_http/http.c b/plugins/in_http/http.c index f220f121454..472e4054bb7 100644 --- a/plugins/in_http/http.c +++ b/plugins/in_http/http.c @@ -88,6 +88,25 @@ static int in_http_init(struct flb_input_instance *ins, return -1; } + if (ctx->oauth2_cfg.validate) { + if (!ctx->oauth2_cfg.issuer || !ctx->oauth2_cfg.jwks_url) { + flb_plg_error(ctx->ins, "oauth2.issuer and oauth2.jwks_url are required when oauth2.validate is enabled"); + http_config_destroy(ctx); + return -1; + } + + if (ctx->oauth2_cfg.jwks_refresh_interval <= 0) { + ctx->oauth2_cfg.jwks_refresh_interval = 300; + } + + ctx->oauth2_ctx = flb_oauth2_jwt_context_create(config, &ctx->oauth2_cfg); + if (!ctx->oauth2_ctx) { + flb_plg_error(ctx->ins, "unable to create oauth2 jwt context"); + http_config_destroy(ctx); + return -1; + } + } + /* Set the context */ flb_input_set_context(ins, ctx); diff --git a/plugins/in_http/http.h b/plugins/in_http/http.h index 2e3796798c9..2306b532d4e 100644 --- a/plugins/in_http/http.h +++ b/plugins/in_http/http.h @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -51,6 +52,9 @@ struct flb_http { int enable_http2; struct flb_http_server http_server; + struct flb_oauth2_jwt_cfg oauth2_cfg; + struct flb_oauth2_jwt_ctx *oauth2_ctx; + /* Legacy HTTP server */ struct flb_downstream *downstream; /* Client manager */ struct mk_list connections; /* linked list of connections */ diff --git a/plugins/in_http/http_config.c b/plugins/in_http/http_config.c index 2459f004907..06a95878587 100644 --- a/plugins/in_http/http_config.c +++ b/plugins/in_http/http_config.c @@ -18,6 +18,7 @@ */ #include +#include #include "http.h" #include "http_config.h" @@ -42,6 +43,8 @@ struct flb_http *http_config_create(struct flb_input_instance *ins) ctx->ins = ins; mk_list_init(&ctx->connections); + ctx->oauth2_cfg.jwks_refresh_interval = 300; + /* Load the config map */ ret = flb_input_config_map_set(ins, (void *) ctx); if (ret == -1) { @@ -49,6 +52,16 @@ struct flb_http *http_config_create(struct flb_input_instance *ins) return NULL; } + /* Apply OAuth2 JWT config map properties if any */ + if (ins->oauth2_jwt_config_map && mk_list_size(&ins->oauth2_jwt_properties) > 0) { + ret = flb_config_map_set(&ins->oauth2_jwt_properties, ins->oauth2_jwt_config_map, + &ctx->oauth2_cfg); + if (ret == -1) { + flb_free(ctx); + return NULL; + } + } + /* Listen interface (if not set, defaults to 0.0.0.0:9880) */ flb_input_net_default_listener("0.0.0.0", 9880, ins); @@ -170,6 +183,27 @@ int http_config_destroy(struct flb_http *ctx) flb_sds_destroy(ctx->success_headers_str); } + if (ctx->oauth2_ctx) { + flb_oauth2_jwt_context_destroy(ctx->oauth2_ctx); + ctx->oauth2_ctx = NULL; + ctx->oauth2_cfg.issuer = NULL; + ctx->oauth2_cfg.jwks_url = NULL; + ctx->oauth2_cfg.allowed_audience = NULL; + } + else { + if (ctx->oauth2_cfg.issuer) { + flb_sds_destroy(ctx->oauth2_cfg.issuer); + } + + if (ctx->oauth2_cfg.jwks_url) { + flb_sds_destroy(ctx->oauth2_cfg.jwks_url); + } + + if (ctx->oauth2_cfg.allowed_audience) { + flb_sds_destroy(ctx->oauth2_cfg.allowed_audience); + } + } + flb_free(ctx->listen); flb_free(ctx->tcp_port); diff --git a/plugins/in_http/http_prot.c b/plugins/in_http/http_prot.c index e92220a51d9..939258b2128 100644 --- a/plugins/in_http/http_prot.c +++ b/plugins/in_http/http_prot.c @@ -153,6 +153,14 @@ static int send_response(struct http_conn *conn, int http_status, char *message) FLB_VERSION_STR, len, message); } + else if (http_status == 401) { + flb_sds_printf(&out, + "HTTP/1.1 401 Unauthorized\r\n" + "Server: Fluent Bit v%s\r\n" + "Content-Length: %i\r\n\r\n%s", + FLB_VERSION_STR, + len, message ? message : ""); + } /* We should check this operations result */ flb_io_net_write(conn->connection, @@ -866,6 +874,7 @@ int http_prot_handle(struct flb_http *ctx, struct http_conn *conn, { int ret; int len; + int auth_status; char *uri; char *qs; off_t diff; @@ -920,6 +929,26 @@ int http_prot_handle(struct flb_http *ctx, struct http_conn *conn, /* Check if we have a Host header: Hostname ; port */ mk_http_point_header(&request->host, &session->parser, MK_HEADER_HOST); + if (ctx->oauth2_ctx) { + header = &session->parser.headers[MK_HEADER_AUTHORIZATION]; + if (header->type == MK_HEADER_AUTHORIZATION) { + auth_status = flb_oauth2_jwt_validate(ctx->oauth2_ctx, + header->val.data, + header->val.len); + } + else { + auth_status = FLB_OAUTH2_JWT_ERR_MISSING_AUTH_HEADER; + } + + if (auth_status != FLB_OAUTH2_JWT_OK) { + flb_plg_error(ctx->ins, "OAuth2 validation failed: %s (rejecting request with 401)", + flb_oauth2_jwt_status_message(auth_status)); + flb_sds_destroy(tag); + send_response(conn, 401, NULL); + return -1; + } + } + /* Header: Connection */ mk_http_point_header(&request->connection, &session->parser, MK_HEADER_CONNECTION); @@ -1002,6 +1031,9 @@ static int send_response_ng(struct flb_http_response *response, else if (http_status == 400) { flb_http_response_set_message(response, "Bad Request"); } + else if (http_status == 401) { + flb_http_response_set_message(response, "Unauthorized"); + } else if (http_status == 413) { flb_http_response_set_message(response, "Payload Too Large"); } @@ -1231,9 +1263,13 @@ int http_prot_handle_ng(struct flb_http_request *request, int ret; int len; flb_sds_t tag; + const char *auth_header; + size_t auth_len; struct flb_http *ctx; ctx = (struct flb_http *) response->stream->user_data; + auth_header = NULL; + auth_len = 0; if (request->path[0] != '/') { send_response_ng(response, 400, "error: invalid request\n"); return -1; @@ -1269,6 +1305,22 @@ int http_prot_handle_ng(struct flb_http_request *request, return -1; } + if (ctx->oauth2_ctx) { + auth_header = flb_http_request_get_header(request, "authorization"); + if (auth_header != NULL) { + auth_len = strlen(auth_header); + } + + ret = flb_oauth2_jwt_validate(ctx->oauth2_ctx, auth_header, auth_len); + if (ret != FLB_OAUTH2_JWT_OK) { + flb_plg_error(ctx->ins, "OAuth2 validation failed: %s (rejecting request with 401)", + flb_oauth2_jwt_status_message(ret)); + flb_sds_destroy(tag); + send_response_ng(response, 401, NULL); + return -1; + } + } + if (request->method != HTTP_METHOD_POST) { send_response_ng(response, 400, "error: invalid HTTP method\n"); flb_sds_destroy(tag); From 5c167819dd21f13e38f717e3551ab23f29e919ef Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 11:09:58 -0600 Subject: [PATCH 04/42] out_http: add OAuth2 client credentials support This commit adds OAuth2 client credentials grant flow to the HTTP output plugin, allowing Fluent Bit to obtain and use OAuth2 access tokens for outgoing requests. The implementation uses an independent config map pattern similar to net.* properties. Configuration properties: - oauth2.enable: enable/disable OAuth2 - oauth2.token_url: OAuth2 token endpoint - oauth2.client_id: OAuth2 client ID - oauth2.client_secret: OAuth2 client secret - oauth2.scope: optional OAuth2 scope - oauth2.auth_method: authentication method (basic or post) - oauth2.refresh_skew_seconds: token refresh skew The implementation includes automatic token refresh on 401 responses and proper connection handling for retries. Signed-off-by: Eduardo Silva --- plugins/out_http/http.c | 52 +++++++++++++++++- plugins/out_http/http.h | 8 +++ plugins/out_http/http_conf.c | 101 +++++++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+), 1 deletion(-) diff --git a/plugins/out_http/http.c b/plugins/out_http/http.c index bfaa9d748c1..1d890d463c6 100644 --- a/plugins/out_http/http.c +++ b/plugins/out_http/http.c @@ -302,7 +302,7 @@ static int http_request(struct flb_out_http *ctx, #endif #endif - ret = flb_http_do(c, &b_sent); + ret = flb_http_do_with_oauth2(c, &b_sent, ctx->oauth2_ctx); if (ret == 0) { /* * Only allow the following HTTP status: @@ -723,6 +723,56 @@ static struct flb_config_map config_map[] = { 0, FLB_TRUE, offsetof(struct flb_out_http, http_passwd), "Set HTTP auth password" }, + { + FLB_CONFIG_MAP_BOOL, "oauth2.enable", "false", + 0, FLB_TRUE, offsetof(struct flb_out_http, oauth2_config.enabled), + "Enable OAuth2 client credentials for outgoing requests" + }, + { + FLB_CONFIG_MAP_STR, "oauth2.token_url", NULL, + 0, FLB_TRUE, offsetof(struct flb_out_http, oauth2_config.token_url), + "OAuth2 token endpoint URL" + }, + { + FLB_CONFIG_MAP_STR, "oauth2.client_id", NULL, + 0, FLB_TRUE, offsetof(struct flb_out_http, oauth2_config.client_id), + "OAuth2 client_id" + }, + { + FLB_CONFIG_MAP_STR, "oauth2.client_secret", NULL, + 0, FLB_TRUE, offsetof(struct flb_out_http, oauth2_config.client_secret), + "OAuth2 client_secret" + }, + { + FLB_CONFIG_MAP_STR, "oauth2.scope", NULL, + 0, FLB_TRUE, offsetof(struct flb_out_http, oauth2_config.scope), + "Optional OAuth2 scope" + }, + { + FLB_CONFIG_MAP_STR, "oauth2.audience", NULL, + 0, FLB_TRUE, offsetof(struct flb_out_http, oauth2_config.audience), + "Optional OAuth2 audience 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" + }, + { + FLB_CONFIG_MAP_INT, "oauth2.refresh_skew_seconds", "60", + 0, FLB_TRUE, offsetof(struct flb_out_http, oauth2_config.refresh_skew), + "Seconds before expiry to refresh the access token" + }, + { + FLB_CONFIG_MAP_TIME, "oauth2.timeout", "0s", + 0, FLB_TRUE, offsetof(struct flb_out_http, oauth2_config.timeout), + "Timeout for OAuth2 token requests (defaults to response_timeout when unset)" + }, + { + FLB_CONFIG_MAP_TIME, "oauth2.connect_timeout", "0s", + 0, FLB_TRUE, offsetof(struct flb_out_http, oauth2_config.connect_timeout), + "Connect timeout for OAuth2 token requests" + }, #ifdef FLB_HAVE_SIGNV4 #ifdef FLB_HAVE_AWS { diff --git a/plugins/out_http/http.h b/plugins/out_http/http.h index 2a0b3ca5c91..945c311751a 100644 --- a/plugins/out_http/http.h +++ b/plugins/out_http/http.h @@ -20,6 +20,9 @@ #ifndef FLB_OUT_HTTP_H #define FLB_OUT_HTTP_H +#include +#include + #define FLB_HTTP_OUT_MSGPACK FLB_PACK_JSON_FORMAT_NONE #define FLB_HTTP_OUT_GELF 20 @@ -111,6 +114,11 @@ struct flb_out_http { /* Plugin instance */ struct flb_output_instance *ins; + + /* OAuth2 */ + struct flb_oauth2_config oauth2_config; + struct flb_oauth2 *oauth2_ctx; + flb_sds_t oauth2_auth_method; }; #endif diff --git a/plugins/out_http/http_conf.c b/plugins/out_http/http_conf.c index a9aa2596cae..163338a8289 100644 --- a/plugins/out_http/http_conf.c +++ b/plugins/out_http/http_conf.c @@ -24,6 +24,7 @@ #include #include #include +#include #ifdef FLB_HAVE_SIGNV4 #ifdef FLB_HAVE_AWS @@ -55,6 +56,11 @@ struct flb_out_http *flb_http_conf_create(struct flb_output_instance *ins, return NULL; } ctx->ins = ins; + ctx->oauth2_config.enabled = FLB_FALSE; + ctx->oauth2_config.auth_method = FLB_OAUTH2_AUTH_METHOD_BASIC; + ctx->oauth2_config.refresh_skew = FLB_OAUTH2_DEFAULT_SKEW_SECS; + ctx->oauth2_ctx = NULL; + ctx->oauth2_auth_method = NULL; ret = flb_output_config_map_set(ins, (void *) ctx); if (ret == -1) { @@ -62,6 +68,27 @@ struct flb_out_http *flb_http_conf_create(struct flb_output_instance *ins, return NULL; } + /* Apply OAuth2 config map properties if any */ + if (ins->oauth2_config_map && mk_list_size(&ins->oauth2_properties) > 0) { + ret = flb_config_map_set(&ins->oauth2_properties, ins->oauth2_config_map, + &ctx->oauth2_config); + if (ret == -1) { + flb_free(ctx); + return NULL; + } + + /* Handle oauth2.auth_method separately since it's stored in a different field */ + tmp = flb_kv_get_key_value("oauth2.auth_method", &ins->oauth2_properties); + if (tmp) { + ctx->oauth2_auth_method = flb_sds_create(tmp); + if (!ctx->oauth2_auth_method) { + flb_errno(); + flb_free(ctx); + return NULL; + } + } + } + if (ctx->headers_key && !ctx->body_key) { flb_plg_error(ctx->ins, "when setting headers_key, body_key is also required"); flb_free(ctx); @@ -295,6 +322,62 @@ struct flb_out_http *flb_http_conf_create(struct flb_output_instance *ins, ctx->host = ins->host.name; ctx->port = ins->host.port; + if (ctx->oauth2_config.connect_timeout <= 0 && + ins->net_setup.connect_timeout > 0) { + ctx->oauth2_config.connect_timeout = ins->net_setup.connect_timeout; + } + + if (ctx->oauth2_config.timeout <= 0 && ctx->response_timeout > 0) { + ctx->oauth2_config.timeout = ctx->response_timeout; + } + + if (ctx->oauth2_config.enabled == FLB_TRUE) { + tmp = ctx->oauth2_auth_method ? ctx->oauth2_auth_method : + flb_output_get_property("oauth2.auth_method", ins); + + if (tmp) { + if (strcasecmp(tmp, "basic") == 0) { + ctx->oauth2_config.auth_method = FLB_OAUTH2_AUTH_METHOD_BASIC; + } + else if (strcasecmp(tmp, "post") == 0) { + ctx->oauth2_config.auth_method = FLB_OAUTH2_AUTH_METHOD_POST; + } + else { + flb_plg_error(ctx->ins, "invalid oauth2.auth_method '%s'", tmp); + flb_http_conf_destroy(ctx); + return NULL; + } + } + + 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"); + flb_http_conf_destroy(ctx); + return NULL; + } + + ctx->oauth2_ctx = flb_oauth2_create_from_config(config, &ctx->oauth2_config); + if (!ctx->oauth2_ctx) { + flb_plg_error(ctx->ins, "failed to initialize oauth2 context"); + flb_http_conf_destroy(ctx); + return NULL; + } + + /* Clear the oauth2_config strings since they're now owned by the OAuth2 context + * (cloned) and the original strings are owned by the config map. This prevents + * double-free when destroying. + */ + ctx->oauth2_config.token_url = NULL; + ctx->oauth2_config.client_id = NULL; + ctx->oauth2_config.client_secret = NULL; + ctx->oauth2_config.scope = NULL; + ctx->oauth2_config.audience = NULL; + + /* oauth2_auth_method is also owned by the config map, clear it to prevent double-free */ + ctx->oauth2_auth_method = NULL; + } + /* Set instance flags into upstream */ flb_output_upstream_set(ctx->u, ins); @@ -324,6 +407,24 @@ void flb_http_conf_destroy(struct flb_out_http *ctx) #endif #endif + if (ctx->oauth2_ctx) { + flb_oauth2_destroy(ctx->oauth2_ctx); + /* OAuth2 context owns cloned copies of the config strings, so we don't + * need to destroy ctx->oauth2_config here. The original strings in + * ctx->oauth2_config are owned by the config map and will be freed by + * flb_config_map_destroy. We set them to NULL after creating the context + * to prevent double-free. + */ + } + else { + /* Only destroy oauth2_config if OAuth2 context wasn't created, + * meaning the strings weren't cloned. But in this case, they're still + * owned by the config map, so we shouldn't free them either. + */ + } + + /* oauth2_auth_method is owned by the config map, don't free it here */ + flb_free(ctx->proxy_host); flb_free(ctx->uri); flb_free(ctx); From 7999b44ce0f91a112a996b63e1929c2557c78290 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 11:10:54 -0600 Subject: [PATCH 05/42] tests: internal: oauth2: new oauth2 and oauth2_jwt unit tests Signed-off-by: Eduardo Silva --- tests/internal/CMakeLists.txt | 2 + tests/internal/oauth2.c | 283 ++++++++++++++++++++++++++++++++++ tests/internal/oauth2_jwt.c | 223 +++++++++++++++++++++++++++ 3 files changed, 508 insertions(+) create mode 100644 tests/internal/oauth2.c create mode 100644 tests/internal/oauth2_jwt.c diff --git a/tests/internal/CMakeLists.txt b/tests/internal/CMakeLists.txt index 45d769b9c25..f832f8717fe 100644 --- a/tests/internal/CMakeLists.txt +++ b/tests/internal/CMakeLists.txt @@ -18,6 +18,8 @@ set(UNIT_TESTS_FILES unit_sizes.c hashtable.c http_client.c + oauth2_jwt.c + oauth2.c utils.c gzip.c zstd.c diff --git a/tests/internal/oauth2.c b/tests/internal/oauth2.c new file mode 100644 index 00000000000..855c16e1cac --- /dev/null +++ b/tests/internal/oauth2.c @@ -0,0 +1,283 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "flb_tests_internal.h" + +#define MOCK_BODY_SIZE 1024 + +struct oauth2_mock_server { + int listen_fd; + int port; + int stop; + int token_requests; + int resource_requests; + int resource_challenge; + int expires_in; + char latest_token[64]; + pthread_t thread; +}; + +static void compose_http_response(int fd, int status, const char *body) +{ + char buffer[MOCK_BODY_SIZE]; + int body_len = 0; + + if (body != NULL) { + body_len = strlen(body); + } + + snprintf(buffer, sizeof(buffer), + "HTTP/1.1 %d\r\n" + "Content-Length: %d\r\n" + "Content-Type: application/json\r\n" + "Connection: close\r\n\r\n" + "%s", + status, body_len, body ? body : ""); + + send(fd, buffer, strlen(buffer), 0); +} + +static void handle_token_request(struct oauth2_mock_server *server, int fd) +{ + char payload[MOCK_BODY_SIZE]; + + server->token_requests++; + snprintf(server->latest_token, sizeof(server->latest_token), + "mock-token-%d", server->token_requests); + + snprintf(payload, sizeof(payload), + "{\"access_token\":\"%s\",\"token_type\":\"Bearer\","\ + "\"expires_in\":%d}", + server->latest_token, server->expires_in); + + compose_http_response(fd, 200, payload); +} + +static void handle_resource_request(struct oauth2_mock_server *server, int fd, + const char *request) +{ + int authorized = 0; + const char *auth; + + server->resource_requests++; + + if (server->resource_challenge > 0) { + server->resource_challenge--; + compose_http_response(fd, 401, ""); + return; + } + + auth = strstr(request, "Authorization: "); + if (auth && strstr(auth, server->latest_token)) { + authorized = 1; + } + + if (authorized) { + compose_http_response(fd, 200, "{\"ok\":true}"); + } + else { + compose_http_response(fd, 401, ""); + } +} + +static void *oauth2_mock_server_thread(void *data) +{ + struct oauth2_mock_server *server = (struct oauth2_mock_server *) data; + int client_fd; + fd_set rfds; + struct timeval tv; + char buffer[MOCK_BODY_SIZE]; + + while (!server->stop) { + FD_ZERO(&rfds); + FD_SET(server->listen_fd, &rfds); + tv.tv_sec = 0; + tv.tv_usec = 200000; + + if (select(server->listen_fd + 1, &rfds, NULL, NULL, &tv) <= 0) { + continue; + } + + client_fd = accept(server->listen_fd, NULL, NULL); + if (client_fd < 0) { + continue; + } + + memset(buffer, 0, sizeof(buffer)); + recv(client_fd, buffer, sizeof(buffer) - 1, 0); + + if (strstr(buffer, "/token")) { + handle_token_request(server, client_fd); + } + else if (strstr(buffer, "/resource")) { + handle_resource_request(server, client_fd, buffer); + } + + close(client_fd); + } + + return NULL; +} + +static int oauth2_mock_server_start(struct oauth2_mock_server *server, int expires_in, + int resource_challenge) +{ + int flags; + int on = 1; + struct sockaddr_in addr; + socklen_t len; + + memset(server, 0, sizeof(struct oauth2_mock_server)); + server->expires_in = expires_in; + server->resource_challenge = resource_challenge; + + server->listen_fd = socket(AF_INET, SOCK_STREAM, 0); + if (server->listen_fd < 0) { + return -1; + } + + setsockopt(server->listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; + + if (bind(server->listen_fd, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + close(server->listen_fd); + return -1; + } + + len = sizeof(addr); + if (getsockname(server->listen_fd, (struct sockaddr *) &addr, &len) < 0) { + close(server->listen_fd); + return -1; + } + + server->port = ntohs(addr.sin_port); + + if (listen(server->listen_fd, 4) < 0) { + close(server->listen_fd); + return -1; + } + + flags = fcntl(server->listen_fd, F_GETFL, 0); + fcntl(server->listen_fd, F_SETFL, flags | O_NONBLOCK); + + if (pthread_create(&server->thread, NULL, oauth2_mock_server_thread, server) != 0) { + close(server->listen_fd); + return -1; + } + + return 0; +} + +static void oauth2_mock_server_stop(struct oauth2_mock_server *server) +{ + if (server->listen_fd > 0) { + server->stop = 1; + shutdown(server->listen_fd, SHUT_RDWR); + pthread_join(server->thread, NULL); + close(server->listen_fd); + } +} + +static struct flb_oauth2 *create_oauth_ctx(struct flb_config *config, + struct oauth2_mock_server *server, + int refresh_skew) +{ + 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_BASIC; + cfg.refresh_skew = refresh_skew; + cfg.client_id = flb_sds_create("id"); + cfg.client_secret = flb_sds_create("secret"); + + flb_sds_printf(&cfg.token_url, "http://127.0.0.1:%d/token", server->port); + + struct flb_oauth2 *ctx = flb_oauth2_create_from_config(config, &cfg); + + flb_oauth2_config_destroy(&cfg); + + return ctx; +} + +void test_parse_defaults(void) +{ + int ret; + struct flb_oauth2 ctx; + const char *payload = "{\"access_token\":\"abc\"}"; + + memset(&ctx, 0, sizeof(ctx)); + ctx.refresh_skew = FLB_OAUTH2_DEFAULT_SKEW_SECS; + + ret = flb_oauth2_parse_json_response(payload, strlen(payload), &ctx); + TEST_CHECK(ret == 0); + TEST_CHECK(ctx.access_token != NULL); + TEST_CHECK(strcmp(ctx.token_type, "Bearer") == 0); + TEST_CHECK(ctx.expires_in == FLB_OAUTH2_DEFAULT_EXPIRES); + + flb_sds_destroy(ctx.access_token); + flb_sds_destroy(ctx.token_type); +} + +void test_caching_and_refresh(void) +{ + int ret; + flb_sds_t token = NULL; + struct flb_config *config; + struct flb_oauth2 *ctx; + struct oauth2_mock_server server; + + config = flb_config_init(); + TEST_CHECK(config != NULL); + + ret = oauth2_mock_server_start(&server, 2, 0); + TEST_CHECK(ret == 0); + + ctx = create_oauth_ctx(config, &server, 1); + TEST_CHECK(ctx != NULL); + + ret = flb_oauth2_get_access_token(ctx, &token, FLB_FALSE); + TEST_CHECK(ret == 0); + TEST_CHECK(strcmp(token, "mock-token-1") == 0); + TEST_CHECK(server.token_requests == 1); + + ret = flb_oauth2_get_access_token(ctx, &token, FLB_FALSE); + TEST_CHECK(ret == 0); + TEST_CHECK(strcmp(token, "mock-token-1") == 0); + TEST_CHECK(server.token_requests == 1); + + sleep(2); + + ret = flb_oauth2_get_access_token(ctx, &token, FLB_FALSE); + TEST_CHECK(ret == 0); + TEST_CHECK(strcmp(token, "mock-token-2") == 0); + TEST_CHECK(server.token_requests == 2); + + flb_oauth2_destroy(ctx); + oauth2_mock_server_stop(&server); + flb_config_exit(config); +} + +TEST_LIST = { + {"parse_defaults", test_parse_defaults}, + {"caching_and_refresh", test_caching_and_refresh}, + {0} +}; + diff --git a/tests/internal/oauth2_jwt.c b/tests/internal/oauth2_jwt.c new file mode 100644 index 00000000000..87925f81c5f --- /dev/null +++ b/tests/internal/oauth2_jwt.c @@ -0,0 +1,223 @@ +#include +#include +#include +#include + +#include "flb_tests_internal.h" +#include + +static const char *VALID_JWT = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5In0.eyJleHAiOjE3MTAwMDAwMDAsImlzcyI6Imlzc3VlciIsImF1ZCI6ImF1ZGllbmNlIiwiYXpwIjoiY2xpZW50LTEifQ.c2ln"; +static const char *INVALID_SEGMENTS = "abc.def"; +static const char *BAD_BASE64 = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5In0#.eyJleHAiOjE3MTAwMDAwMDAsImlzcyI6Imlzc3VlciIsImF1ZCI6ImF1ZGllbmNlIiwiYXpwIjoiY2xpZW50LTEifQ.c2ln"; +static const char *MISSING_KID = "eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE3MTAwMDAwMDAsImlzcyI6Imlzc3VlciIsImF1ZCI6ImF1ZGllbmNlIiwiYXpwIjoiY2xpZW50LTEifQ.c2ln"; +static const char *BAD_ALG = "eyJhbGciOiJIUzI1NiIsImtpZCI6InRlc3Qta2V5In0.eyJleHAiOjE3MTAwMDAwMDAsImlzcyI6Imlzc3VlciIsImF1ZCI6ImF1ZGllbmNlIiwiYXpwIjoiY2xpZW50LTEifQ.c2ln"; + +static void test_valid_jwt_parses() +{ + int ret; + struct flb_oauth2_jwt jwt; + + ret = flb_oauth2_jwt_parse(VALID_JWT, strlen(VALID_JWT), &jwt); + TEST_CHECK(ret == FLB_OAUTH2_JWT_OK); + TEST_CHECK(jwt.signature != NULL && jwt.signature_len > 0); + TEST_CHECK(jwt.claims.expiration == 1710000000); + TEST_CHECK(strcmp(jwt.claims.kid, "test-key") == 0); + TEST_CHECK(strcmp(jwt.claims.alg, "RS256") == 0); + TEST_CHECK(strcmp(jwt.claims.issuer, "issuer") == 0); + TEST_CHECK(strcmp(jwt.claims.audience, "audience") == 0); + TEST_CHECK(strcmp(jwt.claims.client_id, "client-1") == 0); + TEST_CHECK(jwt.signing_input != NULL); + + flb_oauth2_jwt_destroy(&jwt); +} + +static void test_invalid_segments() +{ + int ret; + struct flb_oauth2_jwt jwt; + + ret = flb_oauth2_jwt_parse(INVALID_SEGMENTS, strlen(INVALID_SEGMENTS), &jwt); + TEST_CHECK(ret == FLB_OAUTH2_JWT_ERR_SEGMENT_COUNT); +} + +static void test_bad_base64() +{ + int ret; + struct flb_oauth2_jwt jwt; + + ret = flb_oauth2_jwt_parse(BAD_BASE64, strlen(BAD_BASE64), &jwt); + TEST_CHECK(ret == FLB_OAUTH2_JWT_ERR_BASE64_HEADER); +} + +static void test_missing_kid() +{ + int ret; + struct flb_oauth2_jwt jwt; + + ret = flb_oauth2_jwt_parse(MISSING_KID, strlen(MISSING_KID), &jwt); + TEST_CHECK(ret == FLB_OAUTH2_JWT_ERR_MISSING_KID); +} + +static void test_bad_alg() +{ + int ret; + struct flb_oauth2_jwt jwt; + + ret = flb_oauth2_jwt_parse(BAD_ALG, strlen(BAD_ALG), &jwt); + TEST_CHECK(ret == FLB_OAUTH2_JWT_ERR_ALG_UNSUPPORTED); +} + +static void test_static_key_validation() +{ + int ret; + struct flb_oauth2_jwt jwt; + unsigned char *modulus_bytes = NULL; + unsigned char *exponent_bytes = NULL; + size_t modulus_len = 0; + size_t exponent_len = 0; + + /* JWT signed with a known RSA key */ + const char *test_jwt = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5IiwidHlwIjoiSldUIn0.eyJleHAiOjE3MTAwMDAwMDAsImlzcyI6InRlc3QtaXNzdWVyIiwiYXVkIjoidGVzdC1hdWRpZW5jZSIsImF6cCI6InRlc3QtY2xpZW50Iiwia2lkIjoidGVzdC1rZXkifQ.mEfwBoPjhU-CbwduDcvuw_VoI6VMZsHFmHn9MeAYZ73raB7vMyMO85KBLJp9TN95iBNiKZa5Hcd7LXdTSvjQyF5QjHoZE1W0UOuPmBRDoQfkgKKhy-azMvX8RsyLU3zvXMP2v_D4CSrUkDYmLSE_DP48buMFs84C82PONkgm_0gWWqM_KH9E0QMlddL-9iWvqGkiXk-zJC0Qfuo-G98kHJC3XQRkyjqVOxVwRKey09uGgV1JlxoWoSMIwhGQq_I3G6UmbcVYhhh9Pf60NCs6SfEJ5BLyRrxwf6C8C9kvQdgmRRovbNY-BYBrX-4FrvNPChPZRnmMRpOCNgLEhcZucA"; + + /* RSA public key components (base64url encoded) */ + const char *modulus_b64url = "xrgu6hNnDaqehidqV2dotxx0zps6eYwcBpT5JLi83gYSboqesABz7ct1-F0Qtq43W2ISul0zuBMLolotvWFOOqPd6Kk_fVF3gDaHhqxdv1IQo84cznRUzpBYHhft6_JupHVhgdJBv2GuoJfvOR0q5qJkXlPgM3gNh4hQywLFRpDBtjg8hrKNAyq7pics2fjU4GEDVV8tIhP1bYsUIEt7o79u8ifdIl3ctq8PvvnElOeafabRdn-SEUuBRnGNFXwV9Iu163OqvsKp4riEs4z1oHpp2UCRDknOSfgsiFcbtx2JUiQil_wC5-5Rworlq0qAGmLela5wLd8sPy4dWL-Utw"; + const char *exponent_b64url = "AQAB"; + + /* Parse the JWT */ + ret = flb_oauth2_jwt_parse(test_jwt, strlen(test_jwt), &jwt); + TEST_CHECK(ret == FLB_OAUTH2_JWT_OK); + TEST_CHECK(jwt.signing_input != NULL); + TEST_CHECK(jwt.signature != NULL); + TEST_CHECK(jwt.signature_len > 0); + + /* Decode modulus from base64url */ + { + size_t i; + size_t j = 0; + size_t padding = 0; + size_t padded_len; + size_t clean_len = strlen(modulus_b64url); + char *padded; + + padding = (4 - (clean_len % 4)) % 4; + padded_len = clean_len + padding; + + padded = flb_malloc(padded_len + 1); + TEST_CHECK(padded != NULL); + + /* Convert base64url to base64 */ + for (i = 0; i < clean_len; i++) { + char c = modulus_b64url[i]; + if (c == '-') { + padded[j++] = '+'; + } + else if (c == '_') { + padded[j++] = '/'; + } + else { + padded[j++] = c; + } + } + + /* Add padding */ + for (i = 0; i < padding; i++) { + padded[clean_len + i] = '='; + } + padded[padded_len] = '\0'; + + /* Decode base64 */ + ret = flb_base64_decode(NULL, 0, &modulus_len, + (unsigned char *) padded, padded_len); + TEST_CHECK(ret == FLB_BASE64_ERR_BUFFER_TOO_SMALL || ret == 0); + + modulus_bytes = flb_malloc(modulus_len); + TEST_CHECK(modulus_bytes != NULL); + + ret = flb_base64_decode(modulus_bytes, modulus_len, &modulus_len, + (unsigned char *) padded, padded_len); + TEST_CHECK(ret == 0); + + flb_free(padded); + } + + /* Decode exponent from base64url */ + { + size_t i; + size_t j = 0; + size_t padding = 0; + size_t padded_len; + size_t clean_len = strlen(exponent_b64url); + char *padded; + + padding = (4 - (clean_len % 4)) % 4; + padded_len = clean_len + padding; + + padded = flb_malloc(padded_len + 1); + TEST_CHECK(padded != NULL); + + /* Convert base64url to base64 */ + for (i = 0; i < clean_len; i++) { + char c = exponent_b64url[i]; + if (c == '-') { + padded[j++] = '+'; + } + else if (c == '_') { + padded[j++] = '/'; + } + else { + padded[j++] = c; + } + } + + /* Add padding */ + for (i = 0; i < padding; i++) { + padded[clean_len + i] = '='; + } + padded[padded_len] = '\0'; + + /* Decode base64 */ + ret = flb_base64_decode(NULL, 0, &exponent_len, + (unsigned char *) padded, padded_len); + TEST_CHECK(ret == FLB_BASE64_ERR_BUFFER_TOO_SMALL || ret == 0); + + exponent_bytes = flb_malloc(exponent_len); + TEST_CHECK(exponent_bytes != NULL); + + ret = flb_base64_decode(exponent_bytes, exponent_len, &exponent_len, + (unsigned char *) padded, padded_len); + TEST_CHECK(ret == 0); + + flb_free(padded); + } + + /* Verify signature using flb_crypto_verify_simple */ + ret = flb_crypto_verify_simple(FLB_CRYPTO_PADDING_PKCS1, + FLB_HASH_SHA256, + modulus_bytes, modulus_len, + exponent_bytes, exponent_len, + (unsigned char *) jwt.signing_input, + flb_sds_len(jwt.signing_input), + (unsigned char *) jwt.signature, + jwt.signature_len); + + TEST_CHECK(ret == FLB_CRYPTO_SUCCESS); + + /* Cleanup */ + if (modulus_bytes) { + flb_free(modulus_bytes); + } + if (exponent_bytes) { + flb_free(exponent_bytes); + } + flb_oauth2_jwt_destroy(&jwt); +} + +TEST_LIST = { + {"valid_jwt_parses", test_valid_jwt_parses}, + {"invalid_segments", test_invalid_segments}, + {"bad_base64", test_bad_base64}, + {"missing_kid", test_missing_kid}, + {"bad_alg", test_bad_alg}, + {"static_key_validation", test_static_key_validation}, + {0} +}; From feaf46bb81c25c23419a20b50ba02c846954b0d4 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 11:12:41 -0600 Subject: [PATCH 06/42] out_stackdriver: use new renamed oauth2 property Signed-off-by: Eduardo Silva --- plugins/out_stackdriver/gce_metadata.c | 2 +- plugins/out_stackdriver/stackdriver.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/out_stackdriver/gce_metadata.c b/plugins/out_stackdriver/gce_metadata.c index 364cf9210e4..eaed6cb1ba5 100644 --- a/plugins/out_stackdriver/gce_metadata.c +++ b/plugins/out_stackdriver/gce_metadata.c @@ -133,7 +133,7 @@ int gce_metadata_read_token(struct flb_stackdriver *ctx) flb_plg_error(ctx->ins, "unable to parse token body"); return -1; } - ctx->o->expires = time(NULL) + ctx->o->expires_in; + ctx->o->expires_at = time(NULL) + ctx->o->expires_in; return 0; } diff --git a/plugins/out_stackdriver/stackdriver.c b/plugins/out_stackdriver/stackdriver.c index a322e8eca39..61ac209ecc4 100644 --- a/plugins/out_stackdriver/stackdriver.c +++ b/plugins/out_stackdriver/stackdriver.c @@ -396,7 +396,7 @@ static flb_sds_t get_google_token(struct flb_stackdriver *ctx) /* Copy string to prevent race conditions (get_oauth2 can free the string) */ if (ret == 0) { /* Update pthread keys cached values */ - oauth2_cache_set(ctx->o->token_type, ctx->o->access_token, ctx->o->expires); + oauth2_cache_set(ctx->o->token_type, ctx->o->access_token, ctx->o->expires_at); /* Compose outgoing buffer using cached values */ output = oauth2_cache_to_token(); From a6b8f7fdaed6fec270302c663bf2b3e94130a67e Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 11:15:33 -0600 Subject: [PATCH 07/42] http_client: add OAuth2 integration support This commit extends the HTTP client to support OAuth2 token management and automatic token refresh on 401 responses. The implementation adds: - flb_http_do_with_oauth2(): Execute HTTP request with automatic OAuth2 token management and retry on 401 responses - flb_http_remove_header(): Remove HTTP headers by key (used for token refresh) - base_header_len: Track base header length for proper header reset on retries The implementation handles connection reuse and proper cleanup when retrying requests after token refresh. Signed-off-by: Eduardo Silva --- include/fluent-bit/flb_http_client.h | 7 +++ src/flb_http_client.c | 81 ++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/include/fluent-bit/flb_http_client.h b/include/fluent-bit/flb_http_client.h index 1b4dd7d5f98..d5eedab8a23 100644 --- a/include/fluent-bit/flb_http_client.h +++ b/include/fluent-bit/flb_http_client.h @@ -222,6 +222,7 @@ struct flb_http_client { int method; int flags; int header_len; + int base_header_len; int header_size; char *header_buf; @@ -261,6 +262,8 @@ struct flb_http_client { void *cb_ctx; }; +struct flb_oauth2; + struct flb_http_client_ng { struct cfl_list sessions; @@ -377,6 +380,8 @@ int flb_http_proxy_auth(struct flb_http_client *c, const char *user, const char *passwd); int flb_http_bearer_auth(struct flb_http_client *c, const char *token); +int flb_http_remove_header(struct flb_http_client *c, + const char *key, size_t key_len); int flb_http_set_keepalive(struct flb_http_client *c); int flb_http_set_content_encoding_gzip(struct flb_http_client *c); int flb_http_set_content_encoding_zstd(struct flb_http_client *c); @@ -397,6 +402,8 @@ int flb_http_get_response_data(struct flb_http_client *c, size_t bytes_consumed) int flb_http_do_request(struct flb_http_client *c, size_t *bytes); int flb_http_do(struct flb_http_client *c, size_t *bytes); +int flb_http_do_with_oauth2(struct flb_http_client *c, size_t *bytes, + struct flb_oauth2 *oauth2); int flb_http_client_proxy_connect(struct flb_connection *u_conn); void flb_http_client_destroy(struct flb_http_client *c); int flb_http_buffer_size(struct flb_http_client *c, size_t size); diff --git a/src/flb_http_client.c b/src/flb_http_client.c index 810e172fe83..8d970eccb58 100644 --- a/src/flb_http_client.c +++ b/src/flb_http_client.c @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -752,6 +753,7 @@ struct flb_http_client *create_http_client(struct flb_connection *u_conn, c->header_buf = buf; c->header_size = FLB_HTTP_BUF_SIZE; c->header_len = ret; + c->base_header_len = ret; c->flags = flags; c->allow_dup_headers = FLB_TRUE; mk_list_init(&c->headers); @@ -994,6 +996,25 @@ int flb_http_add_header(struct flb_http_client *c, return 0; } +int flb_http_remove_header(struct flb_http_client *c, + const char *key, size_t key_len) +{ + int removed = 0; + struct flb_kv *kv; + struct mk_list *tmp; + struct mk_list *head; + + mk_list_foreach_safe(head, tmp, &c->headers) { + kv = mk_list_entry(head, struct flb_kv, _head); + if (flb_sds_casecmp(kv->key, key, key_len) == 0) { + flb_kv_item_destroy(kv); + removed++; + } + } + + return removed; +} + /* * flb_http_get_header looks up a first value of request header. * The return value should be destroyed after using. @@ -1383,6 +1404,8 @@ int flb_http_do_request(struct flb_http_client *c, size_t *bytes) size_t bytes_body = 0; char *tmp; + c->header_len = c->base_header_len; + /* Try to add keep alive header */ flb_http_set_keepalive(c); @@ -1631,6 +1654,64 @@ int flb_http_do(struct flb_http_client *c, size_t *bytes) return 0; } +int flb_http_do_with_oauth2(struct flb_http_client *c, size_t *bytes, + struct flb_oauth2 *oauth2) +{ + int ret; + flb_sds_t token = NULL; + + if (!oauth2 || oauth2->cfg.enabled == FLB_FALSE) { + return flb_http_do(c, bytes); + } + + flb_http_allow_duplicated_headers(c, FLB_FALSE); + + ret = flb_oauth2_get_access_token(oauth2, &token, FLB_FALSE); + if (ret != 0 || token == NULL) { + return -1; + } + + flb_http_remove_header(c, FLB_HTTP_HEADER_AUTH, strlen(FLB_HTTP_HEADER_AUTH)); + ret = flb_http_bearer_auth(c, token); + if (ret != 0) { + return ret; + } + + ret = flb_http_do(c, bytes); + if (ret != 0) { + return ret; + } + + if (c->resp.status == 401) { + flb_info("[http_client] 401 received; refreshing OAuth2 token and retrying once"); + flb_oauth2_invalidate_token(oauth2); + + /* If connection was closed, get a new one */ + if (c->resp.connection_close == FLB_TRUE && c->u_conn) { + flb_upstream_conn_release(c->u_conn); + c->u_conn = flb_upstream_conn_get(c->u_conn->upstream); + if (!c->u_conn) { + return -1; + } + } + + ret = flb_oauth2_get_access_token(oauth2, &token, FLB_TRUE); + if (ret != 0 || token == NULL) { + return -1; + } + + flb_http_remove_header(c, FLB_HTTP_HEADER_AUTH, strlen(FLB_HTTP_HEADER_AUTH)); + ret = flb_http_bearer_auth(c, token); + if (ret != 0) { + return ret; + } + + ret = flb_http_do(c, bytes); + } + + return ret; +} + /* * flb_http_client_proxy_connect opens a tunnel to a proxy server via * http `CONNECT` method. This is needed for https traffic through a From 1a406e02c3a16d6209a985fa83605aec1f91fbdf Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 11:16:19 -0600 Subject: [PATCH 08/42] input: add OAuth2 JWT config map support This commit extends the input plugin infrastructure to support OAuth2 JWT configuration properties using an independent config map pattern, similar to net.* properties. This allows input plugins (like in_http) to validate OAuth2 JWT tokens on incoming requests. The implementation adds: - oauth2_jwt_config_map and oauth2_jwt_properties to flb_input_instance - flb_input_oauth2_jwt_property_check(): Validate OAuth2 JWT properties - Automatic property extraction for oauth2.* keys - Proper cleanup of OAuth2 JWT config map resources Signed-off-by: Eduardo Silva --- include/fluent-bit/flb_input.h | 3 ++ src/flb_input.c | 59 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/include/fluent-bit/flb_input.h b/include/fluent-bit/flb_input.h index fc01d0e9dd4..e23dbbb8741 100644 --- a/include/fluent-bit/flb_input.h +++ b/include/fluent-bit/flb_input.h @@ -471,6 +471,9 @@ struct flb_input_instance { struct mk_list *net_config_map; struct mk_list net_properties; + struct mk_list *oauth2_jwt_config_map; + struct mk_list oauth2_jwt_properties; + flb_pipefd_t notification_channel; /* Keep a reference to the original context this instance belongs to */ diff --git a/src/flb_input.c b/src/flb_input.c index 97ebb1e4ad9..21cd8f44493 100644 --- a/src/flb_input.c +++ b/src/flb_input.c @@ -42,6 +42,7 @@ #include #include #include +#include /* input plugin macro helpers */ #include @@ -365,6 +366,7 @@ struct flb_input_instance *flb_input_new(struct flb_config *config, /* Initialize properties list */ flb_kv_init(&instance->properties); flb_kv_init(&instance->net_properties); + flb_kv_init(&instance->oauth2_jwt_properties); /* Plugin use networking */ if (plugin->flags & (FLB_INPUT_NET | FLB_INPUT_NET_SERVER)) { @@ -636,6 +638,16 @@ int flb_input_set_property(struct flb_input_instance *ins, } kv->val = tmp; } + else if (strncasecmp("oauth2", k, 6) == 0 && tmp) { + kv = flb_kv_item_create(&ins->oauth2_jwt_properties, (char *) k, NULL); + if (!kv) { + if (tmp) { + flb_sds_destroy(tmp); + } + return -1; + } + kv->val = tmp; + } #ifdef FLB_HAVE_TLS else if (prop_key_check("tls", k, len) == 0 && tmp) { @@ -878,6 +890,7 @@ void flb_input_instance_destroy(struct flb_input_instance *ins) /* release properties */ flb_kv_release(&ins->properties); flb_kv_release(&ins->net_properties); + flb_kv_release(&ins->oauth2_jwt_properties); #ifdef FLB_HAVE_CHUNK_TRACE @@ -908,6 +921,10 @@ void flb_input_instance_destroy(struct flb_input_instance *ins) flb_config_map_destroy(ins->net_config_map); } + if (ins->oauth2_jwt_config_map) { + flb_config_map_destroy(ins->oauth2_jwt_config_map); + } + /* hash table for chunks */ if (ins->ht_log_chunks) { flb_hash_table_destroy(ins->ht_log_chunks); @@ -1088,6 +1105,38 @@ int flb_input_plugin_property_check(struct flb_input_instance *ins, return 0; } +int flb_input_oauth2_jwt_property_check(struct flb_input_instance *ins, + struct flb_config *config) +{ + int ret = 0; + + /* Get OAuth2 JWT configmap */ + ins->oauth2_jwt_config_map = flb_oauth2_jwt_get_config_map(config); + if (!ins->oauth2_jwt_config_map) { + flb_input_instance_destroy(ins); + return -1; + } + + /* + * Validate 'oauth2*' properties: if the plugin uses OAuth2 JWT, + * it might receive OAuth2 JWT settings. + */ + if (mk_list_size(&ins->oauth2_jwt_properties) > 0) { + ret = flb_config_map_properties_check(ins->p->name, + &ins->oauth2_jwt_properties, + ins->oauth2_jwt_config_map); + if (ret == -1) { + if (config->program_name) { + flb_helper("try the command: %s -i %s -h\n", + config->program_name, ins->p->name); + } + return -1; + } + } + + return 0; +} + int flb_input_instance_init(struct flb_input_instance *ins, struct flb_config *config) { @@ -1281,6 +1330,16 @@ int flb_input_instance_init(struct flb_input_instance *ins, return -1; } + /* + * Validate 'oauth2*' properties: if the plugin uses OAuth2 JWT, + * it might receive OAuth2 JWT settings. + */ + if (mk_list_size(&ins->oauth2_jwt_properties) > 0) { + if (flb_input_oauth2_jwt_property_check(ins, config) == -1) { + return -1; + } + } + #ifdef FLB_HAVE_TLS if (ins->use_tls == FLB_TRUE) { if ((p->flags & FLB_INPUT_NET_SERVER) != 0) { From c9d2eb6891eced3d20495e8f671fe9164b6dfd5b Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 11:17:26 -0600 Subject: [PATCH 09/42] output: add OAuth2 config map support This commit extends the output plugin infrastructure to support OAuth2 configuration properties using an independent config map pattern, similar to net.* properties. This allows output plugins (like out_http) to use OAuth2 client credentials for outgoing requests. The implementation adds: - oauth2_config_map and oauth2_properties to flb_output_instance - flb_output_oauth2_property_check(): Validate OAuth2 properties - Automatic property extraction for oauth2.* keys - Proper cleanup of OAuth2 config map resources Signed-off-by: Eduardo Silva --- include/fluent-bit/flb_output.h | 9 ++++++ src/flb_output.c | 57 +++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/include/fluent-bit/flb_output.h b/include/fluent-bit/flb_output.h index 6ad593a3652..6ef3e44f3dd 100644 --- a/include/fluent-bit/flb_output.h +++ b/include/fluent-bit/flb_output.h @@ -434,6 +434,9 @@ struct flb_output_instance { struct mk_list *net_config_map; struct mk_list net_properties; + struct mk_list *oauth2_config_map; + struct mk_list oauth2_properties; + struct mk_list *tls_config_map; struct mk_list _head; /* link to config->inputs */ @@ -1322,6 +1325,10 @@ static inline int flb_output_config_map_set(struct flb_output_instance *ins, } } + /* OAuth2 properties are validated but not automatically applied here. + * Plugins should call flb_config_map_set() with &ctx->oauth2_config + * in their init callback after calling flb_output_config_map_set(). */ + return 0; } @@ -1358,6 +1365,8 @@ void flb_output_set_context(struct flb_output_instance *ins, void *context); int flb_output_instance_destroy(struct flb_output_instance *ins); int flb_output_net_property_check(struct flb_output_instance *ins, struct flb_config *config); +int flb_output_oauth2_property_check(struct flb_output_instance *ins, + struct flb_config *config); int flb_output_plugin_property_check(struct flb_output_instance *ins, struct flb_config *config); int flb_output_init_all(struct flb_config *config); diff --git a/src/flb_output.c b/src/flb_output.c index 307905b3379..ec9bbee793a 100644 --- a/src/flb_output.c +++ b/src/flb_output.c @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -165,6 +166,7 @@ static void flb_output_free_properties(struct flb_output_instance *ins) flb_kv_release(&ins->properties); flb_kv_release(&ins->net_properties); + flb_kv_release(&ins->oauth2_properties); #ifdef FLB_HAVE_TLS if (ins->tls_vhost) { @@ -510,6 +512,10 @@ int flb_output_instance_destroy(struct flb_output_instance *ins) flb_config_map_destroy(ins->net_config_map); } + if (ins->oauth2_config_map) { + flb_config_map_destroy(ins->oauth2_config_map); + } + if (ins->ch_events[0] > 0) { mk_event_closesocket(ins->ch_events[0]); } @@ -810,6 +816,7 @@ struct flb_output_instance *flb_output_new(struct flb_config *config, flb_kv_init(&instance->properties); flb_kv_init(&instance->net_properties); + flb_kv_init(&instance->oauth2_properties); mk_list_init(&instance->upstreams); mk_list_init(&instance->flush_list); mk_list_init(&instance->flush_list_destroy); @@ -939,6 +946,16 @@ int flb_output_set_property(struct flb_output_instance *ins, } kv->val = tmp; } + else if (strncasecmp("oauth2", k, 6) == 0 && tmp) { + kv = flb_kv_item_create(&ins->oauth2_properties, (char *) k, NULL); + if (!kv) { + if (tmp) { + flb_sds_destroy(tmp); + } + return -1; + } + kv->val = tmp; + } #ifdef FLB_HAVE_HTTP_CLIENT_DEBUG else if (strncasecmp("_debug.http.", k, 12) == 0 && tmp) { ret = flb_http_client_debug_property_is_valid((char *) k, tmp); @@ -1149,6 +1166,38 @@ int flb_output_net_property_check(struct flb_output_instance *ins, return 0; } +int flb_output_oauth2_property_check(struct flb_output_instance *ins, + struct flb_config *config) +{ + int ret = 0; + + /* Get OAuth2 configmap */ + ins->oauth2_config_map = flb_oauth2_get_config_map(config); + if (!ins->oauth2_config_map) { + flb_output_instance_destroy(ins); + return -1; + } + + /* + * Validate 'oauth2*' properties: if the plugin uses OAuth2, + * it might receive OAuth2 settings. + */ + if (mk_list_size(&ins->oauth2_properties) > 0) { + ret = flb_config_map_properties_check(ins->p->name, + &ins->oauth2_properties, + ins->oauth2_config_map); + if (ret == -1) { + if (config->program_name) { + flb_helper("try the command: %s -o %s -h\n", + config->program_name, ins->p->name); + } + return -1; + } + } + + return 0; +} + int flb_output_plugin_property_check(struct flb_output_instance *ins, struct flb_config *config) { @@ -1504,6 +1553,14 @@ int flb_output_init_all(struct flb_config *config) return -1; } + /* Check OAuth2 properties if any */ + if (mk_list_size(&ins->oauth2_properties) > 0) { + if (flb_output_oauth2_property_check(ins, config) == -1) { + flb_output_instance_destroy(ins); + return -1; + } + } + /* Initialize plugin through it 'init callback' */ ret = p->cb_init(ins, config, ins->data); if (ret == -1) { From 08b30b9fe828770f0eb983c4170d95a61df8f4dc Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 11:18:15 -0600 Subject: [PATCH 10/42] oauth2: extend interface for config-based OAuth2 and token management This commit extends the OAuth2 interface to support configuration-based OAuth2 creation and improved token management. The changes include: - flb_oauth2_config structure: Configuration structure for OAuth2 settings - flb_oauth2_create_from_config(): Create OAuth2 context from config structure - flb_oauth2_get_access_token(): Get access token with force refresh option - flb_oauth2_invalidate_token(): Invalidate cached token - flb_oauth2_config_destroy(): Cleanup configuration structure - expires_at field: Replace expires with expires_at for better time handling - refresh_skew support: Configurable token refresh skew - auth_method enum: Support for basic and post authentication methods Also updates the config map to use oauth2.enable instead of oauth2 for consistency with the naming convention. Signed-off-by: Eduardo Silva --- include/fluent-bit/flb_oauth2.h | 49 +++++++++++++++++++++++++++++---- src/flb_oauth2.c | 2 +- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/include/fluent-bit/flb_oauth2.h b/include/fluent-bit/flb_oauth2.h index ea6266e95c7..8de2584bd2f 100644 --- a/include/fluent-bit/flb_oauth2.h +++ b/include/fluent-bit/flb_oauth2.h @@ -21,11 +21,35 @@ #define FLB_OAUTH2_H #include +#include #include +#include #include -#define FLB_OAUTH2_PORT "443" -#define FLB_OAUTH2_HTTP_ENCODING "application/x-www-form-urlencoded" +#define FLB_OAUTH2_PORT "443" +#define FLB_OAUTH2_HTTP_ENCODING "application/x-www-form-urlencoded" +#define FLB_OAUTH2_DEFAULT_SKEW_SECS 60 +#define FLB_OAUTH2_DEFAULT_EXPIRES 300 + +enum flb_oauth2_auth_method { + FLB_OAUTH2_AUTH_METHOD_BASIC = 0, + FLB_OAUTH2_AUTH_METHOD_POST = 1 +}; + +struct flb_oauth2_config { + int enabled; + flb_sds_t token_url; + flb_sds_t client_id; + flb_sds_t client_secret; + flb_sds_t scope; + flb_sds_t audience; + + enum flb_oauth2_auth_method auth_method; + + int refresh_skew; + int timeout; + int connect_timeout; +}; struct flb_oauth2 { flb_sds_t auth_url; @@ -36,9 +60,14 @@ struct flb_oauth2 { flb_sds_t port; flb_sds_t uri; - /* Token times set by the caller */ - time_t issued; - time_t expires; + /* Configuration */ + struct flb_oauth2_config cfg; + + /* Internal state */ + int payload_manual; + flb_lock_t lock; + time_t expires_at; + int refresh_skew; /* Token info after successful auth */ flb_sds_t access_token; @@ -58,7 +87,11 @@ struct flb_oauth2 { struct flb_oauth2 *flb_oauth2_create(struct flb_config *config, const char *auth_url, int expire_sec); +struct flb_oauth2 *flb_oauth2_create_from_config( + struct flb_config *config, + const struct flb_oauth2_config *cfg); void flb_oauth2_destroy(struct flb_oauth2 *ctx); +void flb_oauth2_config_destroy(struct flb_oauth2_config *cfg); int flb_oauth2_token_len(struct flb_oauth2 *ctx); void flb_oauth2_payload_clear(struct flb_oauth2 *ctx); int flb_oauth2_payload_append(struct flb_oauth2 *ctx, @@ -66,9 +99,15 @@ int flb_oauth2_payload_append(struct flb_oauth2 *ctx, const char *val_str, int val_len); char *flb_oauth2_token_get_ng(struct flb_oauth2 *ctx); char *flb_oauth2_token_get(struct flb_oauth2 *ctx); +int flb_oauth2_get_access_token(struct flb_oauth2 *ctx, + flb_sds_t *token_out, + int force_refresh); +void flb_oauth2_invalidate_token(struct flb_oauth2 *ctx); int flb_oauth2_token_expired(struct flb_oauth2 *ctx); int flb_oauth2_parse_json_response(const char *json_data, size_t json_size, struct flb_oauth2 *ctx); +struct mk_list *flb_oauth2_get_config_map(struct flb_config *config); + #endif diff --git a/src/flb_oauth2.c b/src/flb_oauth2.c index 480f0474283..056a989dd0e 100644 --- a/src/flb_oauth2.c +++ b/src/flb_oauth2.c @@ -34,7 +34,7 @@ /* Config map for OAuth2 configuration */ struct flb_config_map oauth2_config_map[] = { { - FLB_CONFIG_MAP_BOOL, "oauth2", "false", + FLB_CONFIG_MAP_BOOL, "oauth2.enable", "false", 0, FLB_TRUE, offsetof(struct flb_oauth2_config, enabled), "Enable OAuth2 client credentials for outgoing requests" }, From 474f502beb8d5fd4a1a267e3e0dceabb31868574 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 11:19:01 -0600 Subject: [PATCH 11/42] out_azure_kusto: update oauth2 interface usage Signed-off-by: Eduardo Silva --- plugins/out_azure_kusto/azure_msiauth.c | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/plugins/out_azure_kusto/azure_msiauth.c b/plugins/out_azure_kusto/azure_msiauth.c index d3121346b33..ebcd811429f 100644 --- a/plugins/out_azure_kusto/azure_msiauth.c +++ b/plugins/out_azure_kusto/azure_msiauth.c @@ -34,15 +34,15 @@ char *flb_azure_msiauth_token_get(struct flb_oauth2 *ctx) time_t now; struct flb_connection *u_conn; struct flb_http_client *c; - + now = time(NULL); if (ctx->access_token) { /* validate unexpired token */ - if (ctx->expires > now && flb_sds_len(ctx->access_token) > 0) { + if (ctx->expires_at > now && flb_sds_len(ctx->access_token) > 0) { return ctx->access_token; } } - + /* Get Token and store it in the context */ u_conn = flb_upstream_conn_get(ctx->u); if (!u_conn) { @@ -50,7 +50,7 @@ char *flb_azure_msiauth_token_get(struct flb_oauth2 *ctx) ctx->u->tcp_host, ctx->u->tcp_port); return NULL; } - + /* Create HTTP client context */ c = flb_http_client(u_conn, FLB_HTTP_GET, ctx->uri, NULL, 0, @@ -61,10 +61,10 @@ char *flb_azure_msiauth_token_get(struct flb_oauth2 *ctx) flb_upstream_conn_release(u_conn); return NULL; } - + /* Append HTTP Header */ flb_http_add_header(c, "Metadata", 8, "true", 4); - + /* Issue request */ ret = flb_http_do(c, &b_sent); if (ret != 0) { @@ -81,7 +81,7 @@ char *flb_azure_msiauth_token_get(struct flb_oauth2 *ctx) } } } - + /* Extract token */ if (c->resp.payload_size > 0 && c->resp.status == 200) { ret = flb_oauth2_parse_json_response(c->resp.payload, @@ -91,15 +91,14 @@ char *flb_azure_msiauth_token_get(struct flb_oauth2 *ctx) ctx->host, ctx->port); flb_http_client_destroy(c); flb_upstream_conn_release(u_conn); - ctx->issued = time(NULL); - ctx->expires = ctx->issued + ctx->expires_in; + ctx->expires_at = time(NULL) + ctx->expires_in; return ctx->access_token; } } - + flb_http_client_destroy(c); flb_upstream_conn_release(u_conn); - + return NULL; } @@ -258,8 +257,7 @@ int flb_azure_workload_identity_token_get(struct flb_oauth2 *ctx, const char *to flb_upstream_conn_release(u_conn); flb_sds_destroy(federated_token); /* body already destroyed */ - ctx->issued = time(NULL); - ctx->expires = ctx->issued + ctx->expires_in; + ctx->expires_at = time(NULL) + ctx->expires_in; return 0; } } From db60c162b81ece4e18c5bc7187dd79e64cbed8ce Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 11:25:36 -0600 Subject: [PATCH 12/42] tests: runtime: http: add oauth2 and oauth2_jwt tests Signed-off-by: Eduardo Silva --- tests/runtime/in_http.c | 272 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 272 insertions(+) diff --git a/tests/runtime/in_http.c b/tests/runtime/in_http.c index 66ddaea5230..6a3f37db944 100644 --- a/tests/runtime/in_http.c +++ b/tests/runtime/in_http.c @@ -19,6 +19,14 @@ */ #include +#include +#include +#include +#include +#include +#include +#include + #include #include #include @@ -29,6 +37,123 @@ #define JSON_CONTENT_TYPE "application/json" #define JSON_CHARSET_CONTENT_TYPE "application/json; charset=utf-8" +#define MOCK_JWKS_BODY "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"test\",\"n\":\"xCUx72fXOyrjUZiiPJZIa7HtYHdQo_LAAkYG3yAcl1mwmh8pXrXB71xSDBI5SZDtKW4g6FEzYmP0jv3xwBdrZO2HQYwdxpCLhiMKEF0neC5w4NsjFlZKpnO53GN5W_c95bEhlVbh7O2q3PZVDhF5x9bdjlDS84NA0CY2l10UbSvIz12XR8uXqt6w9WVznrCe7ucSex3YPBTwll8Tm5H1rs1tPSx_9D0CJtZvxhKfgJtDyJJmV9syI6hlRgXnAsOonycOGSLryaIBtttxKUwy6QQkA-qSLZe2EcG2XoeBy10geOZ4WKGRiGubuuDpB1yFFy4mXQULJF6anO2osE31SQ\",\"e\":\"AQAB\"}]}" +#define MOCK_VALID_JWT "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3QiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjE4OTM0NTYwMDAsImlzcyI6Imlzc3VlciIsImF1ZCI6ImF1ZGllbmNlIiwiYXpwIjoiY2xpZW50MSJ9.TqWs06LUpQa0FGLejnOkWAD6v562d5CUh2NwsJ7iAuae9-WNFBKU6mP1zAaoafla6o5npee7RfbSzZNFI4PKhqAj69789JjAYV7IW-GSuMwJejHdVOWmCc5lmcZPH0EVxEkHA6lFQxYQwDCrfQ8Sd4Q3vYCV6sLPENcuNpQi9ytjVjaZs_7ONH2oA-sZ7EUchqJJoIBPfjit2yYsq9NeemxCzYMtngiC-IX12eEfaQ1cVYPIjhhN_NaMvapznp-BW4gnXkNoAZ1S-p1axWWY-6UgRdMYOr0Hy5PHQ9fCuHJ6Z-blYdtuGavCUGHK5ghX-JdH1WJ51F89992dQ5yF_w" + +struct jwks_mock_server { + int listen_fd; + int port; + int stop; + pthread_t thread; +}; + +static void jwks_mock_send_response(int fd) +{ + char buffer[512]; + + snprintf(buffer, sizeof(buffer), + "HTTP/1.1 200 OK\r\n" + "Content-Length: %zu\r\n" + "Content-Type: application/json\r\n" + "Connection: close\r\n\r\n" + "%s", + strlen(MOCK_JWKS_BODY), MOCK_JWKS_BODY); + + send(fd, buffer, strlen(buffer), 0); +} + +static void *jwks_mock_server_thread(void *data) +{ + struct jwks_mock_server *server = (struct jwks_mock_server *) data; + fd_set rfds; + struct timeval tv; + int client_fd; + + client_fd = -1; + while (!server->stop) { + FD_ZERO(&rfds); + FD_SET(server->listen_fd, &rfds); + tv.tv_sec = 0; + tv.tv_usec = 200000; + + if (select(server->listen_fd + 1, &rfds, NULL, NULL, &tv) <= 0) { + continue; + } + + client_fd = accept(server->listen_fd, NULL, NULL); + if (client_fd < 0) { + continue; + } + + jwks_mock_send_response(client_fd); + close(client_fd); + } + + return NULL; +} + +static int jwks_mock_server_start(struct jwks_mock_server *server) +{ + int on = 1; + struct sockaddr_in addr; + socklen_t len; + int flags; + + memset(server, 0, sizeof(struct jwks_mock_server)); + + server->listen_fd = socket(AF_INET, SOCK_STREAM, 0); + if (server->listen_fd < 0) { + return -1; + } + + setsockopt(server->listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; + + if (bind(server->listen_fd, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + close(server->listen_fd); + return -1; + } + + len = sizeof(addr); + if (getsockname(server->listen_fd, (struct sockaddr *) &addr, &len) < 0) { + close(server->listen_fd); + return -1; + } + + server->port = ntohs(addr.sin_port); + + if (listen(server->listen_fd, 4) < 0) { + close(server->listen_fd); + return -1; + } + + flags = fcntl(server->listen_fd, F_GETFL, 0); + if (flags >= 0) { + fcntl(server->listen_fd, F_SETFL, flags | O_NONBLOCK); + } + + if (pthread_create(&server->thread, NULL, jwks_mock_server_thread, server) != 0) { + close(server->listen_fd); + return -1; + } + + return 0; +} + +static void jwks_mock_server_stop(struct jwks_mock_server *server) +{ + if (server->listen_fd <= 0) { + return; + } + + server->stop = 1; + pthread_join(server->thread, NULL); + close(server->listen_fd); +} struct http_client_ctx { struct flb_upstream *u; @@ -671,6 +796,151 @@ void flb_test_http_tag_key_with_array_input() test_http_tag_key("[{\"tag\":\"new_tag\",\"test\":\"msg\"}]"); } +void flb_test_http_oauth2_requires_token() +{ + struct flb_lib_out_cb cb_data; + struct test_ctx *ctx; + struct flb_http_client *c; + struct jwks_mock_server jwks; + char jwks_url[64]; + int ret; + size_t b_sent; + + clear_output_num(); + + cb_data.cb = cb_check_result_json; + cb_data.data = "\"test\":\"msg\""; + + if (!TEST_CHECK(jwks_mock_server_start(&jwks) == 0)) { + TEST_MSG("unable to start mock jwks server"); + return; + } + + snprintf(jwks_url, sizeof(jwks_url), "http://127.0.0.1:%d/jwks", jwks.port); + + ctx = test_ctx_create(&cb_data); + if (!TEST_CHECK(ctx != NULL)) { + jwks_mock_server_stop(&jwks); + return; + } + + ret = flb_input_set(ctx->flb, ctx->i_ffd, + "oauth2.validate", "true", + "oauth2.issuer", "issuer", + "oauth2.jwks_url", jwks_url, + "oauth2.allowed_audience", "audience", + "oauth2.allowed_clients", "client1", + NULL); + TEST_CHECK(ret == 0); + + ret = flb_output_set(ctx->flb, ctx->o_ffd, + "match", "*", + "format", "json", + NULL); + TEST_CHECK(ret == 0); + + ret = flb_start(ctx->flb); + TEST_CHECK(ret == 0); + + ctx->httpc = http_client_ctx_create(); + TEST_CHECK(ctx->httpc != NULL); + + c = flb_http_client(ctx->httpc->u_conn, FLB_HTTP_POST, "/", "{\"test\":\"msg\"}", 15, + "127.0.0.1", 9880, NULL, 0); + TEST_CHECK(c != NULL); + + ret = flb_http_add_header(c, FLB_HTTP_HEADER_CONTENT_TYPE, strlen(FLB_HTTP_HEADER_CONTENT_TYPE), + JSON_CONTENT_TYPE, strlen(JSON_CONTENT_TYPE)); + TEST_CHECK(ret == 0); + + ret = flb_http_do(c, &b_sent); + TEST_CHECK(ret == 0); + TEST_CHECK(c->resp.status == 401); + + flb_time_msleep(500); + TEST_CHECK(get_output_num() == 0); + + flb_http_client_destroy(c); + flb_upstream_conn_release(ctx->httpc->u_conn); + test_ctx_destroy(ctx); + jwks_mock_server_stop(&jwks); +} + +void flb_test_http_oauth2_accepts_valid_token() +{ + struct flb_lib_out_cb cb_data; + struct test_ctx *ctx; + struct flb_http_client *c; + struct jwks_mock_server jwks; + char jwks_url[64]; + int ret; + size_t b_sent; + + clear_output_num(); + + cb_data.cb = cb_check_result_json; + cb_data.data = "\"test\":\"msg\""; + + if (!TEST_CHECK(jwks_mock_server_start(&jwks) == 0)) { + TEST_MSG("unable to start mock jwks server"); + return; + } + + snprintf(jwks_url, sizeof(jwks_url), "http://127.0.0.1:%d/jwks", jwks.port); + + ctx = test_ctx_create(&cb_data); + if (!TEST_CHECK(ctx != NULL)) { + jwks_mock_server_stop(&jwks); + return; + } + + ret = flb_input_set(ctx->flb, ctx->i_ffd, + "oauth2.validate", "true", + "oauth2.issuer", "issuer", + "oauth2.jwks_url", jwks_url, + "oauth2.allowed_audience", "audience", + "oauth2.allowed_clients", "client1", + NULL); + TEST_CHECK(ret == 0); + + ret = flb_output_set(ctx->flb, ctx->o_ffd, + "match", "*", + "format", "json", + NULL); + TEST_CHECK(ret == 0); + + ret = flb_start(ctx->flb); + TEST_CHECK(ret == 0); + + ctx->httpc = http_client_ctx_create(); + TEST_CHECK(ctx->httpc != NULL); + + c = flb_http_client(ctx->httpc->u_conn, FLB_HTTP_POST, "/", "{\"test\":\"msg\"}", 15, + "127.0.0.1", 9880, NULL, 0); + TEST_CHECK(c != NULL); + + ret = flb_http_add_header(c, FLB_HTTP_HEADER_CONTENT_TYPE, strlen(FLB_HTTP_HEADER_CONTENT_TYPE), + JSON_CONTENT_TYPE, strlen(JSON_CONTENT_TYPE)); + TEST_CHECK(ret == 0); + + ret = flb_http_add_header(c, FLB_HTTP_HEADER_AUTH, strlen(FLB_HTTP_HEADER_AUTH), + "Bearer " MOCK_VALID_JWT, + strlen("Bearer " MOCK_VALID_JWT)); + TEST_CHECK(ret == 0); + + ret = flb_http_do(c, &b_sent); + TEST_CHECK(ret == 0); + TEST_CHECK(c->resp.status == 201); + + flb_time_msleep(1500); + TEST_CHECK(get_output_num() > 0); + + flb_http_client_destroy(c); + flb_upstream_conn_release(ctx->httpc->u_conn); + test_ctx_destroy(ctx); + jwks_mock_server_stop(&jwks); +} + TEST_LIST = { {"http", flb_test_http}, {"successful_response_code_200", flb_test_http_successful_response_code_200}, @@ -679,5 +949,7 @@ TEST_LIST = { {"failure_response_code_400_bad_disk_write", flb_test_http_failure_400_bad_disk_write}, {"tag_key_with_map_input", flb_test_http_tag_key_with_map_input}, {"tag_key_with_array_input", flb_test_http_tag_key_with_array_input}, + {"oauth2_requires_token", flb_test_http_oauth2_requires_token}, + {"oauth2_accepts_valid_token", flb_test_http_oauth2_accepts_valid_token}, {NULL, NULL} }; From 6e93ced08b50eda15fa2aa78e2613c15258da5b5 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 11:42:52 -0600 Subject: [PATCH 13/42] out_http: fix oauth2_auth_method cleanup Signed-off-by: Eduardo Silva --- plugins/out_http/http_conf.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/plugins/out_http/http_conf.c b/plugins/out_http/http_conf.c index 163338a8289..737915585d9 100644 --- a/plugins/out_http/http_conf.c +++ b/plugins/out_http/http_conf.c @@ -374,8 +374,11 @@ struct flb_out_http *flb_http_conf_create(struct flb_output_instance *ins, ctx->oauth2_config.scope = NULL; ctx->oauth2_config.audience = NULL; - /* oauth2_auth_method is also owned by the config map, clear it to prevent double-free */ - ctx->oauth2_auth_method = NULL; + /* oauth2_auth_method was allocated with flb_sds_create, free it before clearing */ + if (ctx->oauth2_auth_method) { + flb_sds_destroy(ctx->oauth2_auth_method); + ctx->oauth2_auth_method = NULL; + } } /* Set instance flags into upstream */ @@ -423,7 +426,11 @@ void flb_http_conf_destroy(struct flb_out_http *ctx) */ } - /* oauth2_auth_method is owned by the config map, don't free it here */ + /* oauth2_auth_method was allocated with flb_sds_create, free it if present */ + if (ctx->oauth2_auth_method) { + flb_sds_destroy(ctx->oauth2_auth_method); + ctx->oauth2_auth_method = NULL; + } flb_free(ctx->proxy_host); flb_free(ctx->uri); From e32d32fe886041488ed03ecb5b681b4106891026 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 11:49:43 -0600 Subject: [PATCH 14/42] crypto: fix BIGNUM exception handling in OpenSSL 3 RSA key building Signed-off-by: Eduardo Silva --- src/flb_crypto.c | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/flb_crypto.c b/src/flb_crypto.c index 352aadc14d9..d8ae4028126 100644 --- a/src/flb_crypto.c +++ b/src/flb_crypto.c @@ -123,6 +123,8 @@ static int flb_crypto_build_rsa_public_key_from_components(unsigned char *modulu { BIGNUM *n = NULL; BIGNUM *e = NULL; + BIGNUM *n_dup = NULL; + BIGNUM *e_dup = NULL; RSA *rsa = NULL; int ret = FLB_CRYPTO_BACKEND_ERROR; @@ -166,10 +168,26 @@ static int flb_crypto_build_rsa_public_key_from_components(unsigned char *modulu goto cleanup; } - if (RSA_set0_key(rsa, BN_dup(n), BN_dup(e), NULL) != 1) { + n_dup = BN_dup(n); + if (!n_dup) { goto cleanup; } + e_dup = BN_dup(e); + if (!e_dup) { + BN_free(n_dup); + n_dup = NULL; + goto cleanup; + } + + if (RSA_set0_key(rsa, n_dup, e_dup, NULL) != 1) { + BN_free(n_dup); + BN_free(e_dup); + n_dup = e_dup = NULL; + goto cleanup; + } + n_dup = e_dup = NULL; /* ownership transferred to RSA */ + *pkey = EVP_PKEY_new(); if (!*pkey) { goto cleanup; @@ -190,6 +208,12 @@ static int flb_crypto_build_rsa_public_key_from_components(unsigned char *modulu if (rsa) { RSA_free(rsa); } + if (n_dup) { + BN_free(n_dup); + } + if (e_dup) { + BN_free(e_dup); + } if (n) { BN_free(n); } From abb07f91e4aac5cb6f655dbc38d3840fda7f2a43 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 11:55:01 -0600 Subject: [PATCH 15/42] crypto: remove unused VERIFY code from flb_crypto_transform Signed-off-by: Eduardo Silva --- src/flb_crypto.c | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/flb_crypto.c b/src/flb_crypto.c index d8ae4028126..86a981d4288 100644 --- a/src/flb_crypto.c +++ b/src/flb_crypto.c @@ -314,8 +314,7 @@ int flb_crypto_transform(struct flb_crypto *context, if (operation != FLB_CRYPTO_OPERATION_SIGN && operation != FLB_CRYPTO_OPERATION_ENCRYPT && - operation != FLB_CRYPTO_OPERATION_DECRYPT && - operation != FLB_CRYPTO_OPERATION_VERIFY) { + operation != FLB_CRYPTO_OPERATION_DECRYPT) { return FLB_CRYPTO_INVALID_ARGUMENT; } @@ -329,9 +328,6 @@ int flb_crypto_transform(struct flb_crypto *context, else if (operation == FLB_CRYPTO_OPERATION_DECRYPT) { result = EVP_PKEY_decrypt_init(context->backend_context); } - else if (operation == FLB_CRYPTO_OPERATION_VERIFY) { - result = EVP_PKEY_verify_init(context->backend_context); - } if (result == 1) { result = EVP_PKEY_CTX_set_rsa_padding(context->backend_context, @@ -376,13 +372,6 @@ int flb_crypto_transform(struct flb_crypto *context, output_buffer, output_length, input_buffer, input_length); } - else if(operation == FLB_CRYPTO_OPERATION_VERIFY) { - /* For verify, input_buffer is the signature, input_length is signature length */ - /* output_buffer is the data to verify, output_length is data length */ - result = EVP_PKEY_verify(context->backend_context, - input_buffer, input_length, - output_buffer, *output_length); - } if (result != 1) { context->last_error = ERR_get_error(); From 609bd7c153578a6f96d7d907b34e5619a512d4d0 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 11:59:27 -0600 Subject: [PATCH 16/42] http_client: fix use-after-free in OAuth2 401 retry path Fix a use-after-free bug in flb_http_do_with_oauth2() where the code was dereferencing c->u_conn->upstream after releasing c->u_conn when handling a 401 response that requires connection replacement. The fix saves a pointer to the upstream (struct flb_upstream *u) before calling flb_upstream_conn_release(), then uses that saved pointer to obtain a new connection via flb_upstream_conn_get(u). This ensures we have a valid upstream reference when the server indicates it will close the connection (Connection: close header) and we need to retry the request with a refreshed OAuth2 token. Signed-off-by: Eduardo Silva --- src/flb_http_client.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/flb_http_client.c b/src/flb_http_client.c index 8d970eccb58..4b08aa9a733 100644 --- a/src/flb_http_client.c +++ b/src/flb_http_client.c @@ -1659,6 +1659,7 @@ int flb_http_do_with_oauth2(struct flb_http_client *c, size_t *bytes, { int ret; flb_sds_t token = NULL; + struct flb_upstream *u; if (!oauth2 || oauth2->cfg.enabled == FLB_FALSE) { return flb_http_do(c, bytes); @@ -1688,8 +1689,9 @@ int flb_http_do_with_oauth2(struct flb_http_client *c, size_t *bytes, /* If connection was closed, get a new one */ if (c->resp.connection_close == FLB_TRUE && c->u_conn) { + u = c->u_conn->upstream; flb_upstream_conn_release(c->u_conn); - c->u_conn = flb_upstream_conn_get(c->u_conn->upstream); + c->u_conn = flb_upstream_conn_get(u); if (!c->u_conn) { return -1; } From e91351ec395ac09dc6e94618dcb2b54ca14d58da Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 12:03:43 -0600 Subject: [PATCH 17/42] oauth2: release variables on exception Signed-off-by: Eduardo Silva --- src/flb_oauth2.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/flb_oauth2.c b/src/flb_oauth2.c index 056a989dd0e..bec2a318240 100644 --- a/src/flb_oauth2.c +++ b/src/flb_oauth2.c @@ -154,6 +154,7 @@ static int oauth2_clone_config(struct flb_oauth2_config *dst, dst->token_url = flb_sds_create(src->token_url); if (!dst->token_url) { flb_errno(); + flb_oauth2_config_destroy(dst); return -1; } } @@ -162,6 +163,7 @@ static int oauth2_clone_config(struct flb_oauth2_config *dst, dst->client_id = flb_sds_create(src->client_id); if (!dst->client_id) { flb_errno(); + flb_oauth2_config_destroy(dst); return -1; } } @@ -170,6 +172,7 @@ static int oauth2_clone_config(struct flb_oauth2_config *dst, dst->client_secret = flb_sds_create(src->client_secret); if (!dst->client_secret) { flb_errno(); + flb_oauth2_config_destroy(dst); return -1; } } @@ -178,6 +181,7 @@ static int oauth2_clone_config(struct flb_oauth2_config *dst, dst->scope = flb_sds_create(src->scope); if (!dst->scope) { flb_errno(); + flb_oauth2_config_destroy(dst); return -1; } } @@ -186,6 +190,7 @@ static int oauth2_clone_config(struct flb_oauth2_config *dst, dst->audience = flb_sds_create(src->audience); if (!dst->audience) { flb_errno(); + flb_oauth2_config_destroy(dst); return -1; } } From 8ae47eb1e78f5efa6a98629909ad288a7aeb0251 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 12:05:46 -0600 Subject: [PATCH 18/42] output: do not destroy instance on oauth2 init failure Signed-off-by: Eduardo Silva --- src/flb_output.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/flb_output.c b/src/flb_output.c index ec9bbee793a..e5958b683e8 100644 --- a/src/flb_output.c +++ b/src/flb_output.c @@ -1174,7 +1174,6 @@ int flb_output_oauth2_property_check(struct flb_output_instance *ins, /* Get OAuth2 configmap */ ins->oauth2_config_map = flb_oauth2_get_config_map(config); if (!ins->oauth2_config_map) { - flb_output_instance_destroy(ins); return -1; } From dc10438a6dad3f2f22024280b27fb99ab8f1bc67 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 12:08:27 -0600 Subject: [PATCH 19/42] tests: internal: conditionally include OAuth2 tests when FLB_TLS is enabled Signed-off-by: Eduardo Silva --- tests/internal/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/internal/CMakeLists.txt b/tests/internal/CMakeLists.txt index f832f8717fe..44034b127b3 100644 --- a/tests/internal/CMakeLists.txt +++ b/tests/internal/CMakeLists.txt @@ -18,8 +18,6 @@ set(UNIT_TESTS_FILES unit_sizes.c hashtable.c http_client.c - oauth2_jwt.c - oauth2.c utils.c gzip.c zstd.c @@ -66,6 +64,8 @@ if(FLB_TLS) set(UNIT_TESTS_FILES ${UNIT_TESTS_FILES} upstream_tls.c + oauth2_jwt.c + oauth2.c ) endif() From d09c143aa905a7b9481d3c134517976995cf2dd1 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 12:10:39 -0600 Subject: [PATCH 20/42] oauth2_jwt: do not enforce port use defaults Signed-off-by: Eduardo Silva --- src/flb_oauth2_jwt.c | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/flb_oauth2_jwt.c b/src/flb_oauth2_jwt.c index a57a15e3fda..36930275c6a 100644 --- a/src/flb_oauth2_jwt.c +++ b/src/flb_oauth2_jwt.c @@ -976,15 +976,31 @@ static int oauth2_jwks_fetch_keys(struct flb_oauth2_jwt_ctx *ctx) return -1; } - if (!host || !port_str || !uri) { + if (!host || !uri) { flb_error("[oauth2_jwt] invalid JWKS URL components"); goto cleanup; } - port = atoi(port_str); - if (port <= 0) { - flb_error("[oauth2_jwt] invalid port in JWKS URL"); - goto cleanup; + /* Determine port: use explicit port if provided, otherwise use protocol defaults */ + if (port_str && port_str[0] != '\0') { + port = atoi(port_str); + if (port <= 0) { + flb_error("[oauth2_jwt] invalid port in JWKS URL"); + goto cleanup; + } + } + else { + /* No explicit port: use default based on protocol */ + if (protocol && strcasecmp(protocol, "https") == 0) { + port = 443; + } + else if (protocol && strcasecmp(protocol, "http") == 0) { + port = 80; + } + else { + flb_error("[oauth2_jwt] unsupported protocol in JWKS URL: %s", protocol ? protocol : "(null)"); + goto cleanup; + } } if (protocol && strcasecmp(protocol, "https") == 0) { From 8d512ca5dc7bbcaa63036193cb9d672f3e748d2b Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 12:42:18 -0600 Subject: [PATCH 21/42] out_http: fix cleanup of oauth2 context Signed-off-by: Eduardo Silva --- plugins/out_http/http_conf.c | 30 ++---------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/plugins/out_http/http_conf.c b/plugins/out_http/http_conf.c index 737915585d9..b59817c1e84 100644 --- a/plugins/out_http/http_conf.c +++ b/plugins/out_http/http_conf.c @@ -80,12 +80,8 @@ struct flb_out_http *flb_http_conf_create(struct flb_output_instance *ins, /* Handle oauth2.auth_method separately since it's stored in a different field */ tmp = flb_kv_get_key_value("oauth2.auth_method", &ins->oauth2_properties); if (tmp) { - ctx->oauth2_auth_method = flb_sds_create(tmp); - if (!ctx->oauth2_auth_method) { - flb_errno(); - flb_free(ctx); - return NULL; - } + /* Store pointer directly - config map owns this string and will free it */ + ctx->oauth2_auth_method = (flb_sds_t) tmp; } } @@ -363,22 +359,6 @@ struct flb_out_http *flb_http_conf_create(struct flb_output_instance *ins, flb_http_conf_destroy(ctx); return NULL; } - - /* Clear the oauth2_config strings since they're now owned by the OAuth2 context - * (cloned) and the original strings are owned by the config map. This prevents - * double-free when destroying. - */ - ctx->oauth2_config.token_url = NULL; - ctx->oauth2_config.client_id = NULL; - ctx->oauth2_config.client_secret = NULL; - ctx->oauth2_config.scope = NULL; - ctx->oauth2_config.audience = NULL; - - /* oauth2_auth_method was allocated with flb_sds_create, free it before clearing */ - if (ctx->oauth2_auth_method) { - flb_sds_destroy(ctx->oauth2_auth_method); - ctx->oauth2_auth_method = NULL; - } } /* Set instance flags into upstream */ @@ -426,12 +406,6 @@ void flb_http_conf_destroy(struct flb_out_http *ctx) */ } - /* oauth2_auth_method was allocated with flb_sds_create, free it if present */ - if (ctx->oauth2_auth_method) { - flb_sds_destroy(ctx->oauth2_auth_method); - ctx->oauth2_auth_method = NULL; - } - flb_free(ctx->proxy_host); flb_free(ctx->uri); flb_free(ctx); From bf1c1968b0ffce8ea074d8aef43304b750b32717 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 12:59:17 -0600 Subject: [PATCH 22/42] github: scripts: commit_linter: fix handling of multiple prefixes Signed-off-by: Eduardo Silva --- .github/scripts/commit_prefix_check.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/scripts/commit_prefix_check.py b/.github/scripts/commit_prefix_check.py index fffba6d87ae..f1475c57884 100644 --- a/.github/scripts/commit_prefix_check.py +++ b/.github/scripts/commit_prefix_check.py @@ -183,14 +183,19 @@ def validate_commit(commit): umbrella_prefixes = {"lib:"} # If more than one non-build prefix is inferred AND the subject is not an umbrella - # prefix, require split commits. + # prefix, check if the subject prefix is in the expected list. If it is, allow it + # (because the corresponding file exists). Only reject if it's not in the expected list + # or if it's an umbrella prefix that doesn't match. if len(non_build_prefixes) > 1 and subj_lower not in umbrella_prefixes: - expected_list = sorted(expected) - expected_str = ", ".join(expected_list) - return False, ( - f"Subject prefix '{subject_prefix}' does not match files changed.\n" - f"Expected one of: {expected_str}" - ) + # If subject prefix is in expected list, it's valid (the corresponding file exists) + if subj_lower not in expected_lower: + expected_list = sorted(expected) + expected_str = ", ".join(expected_list) + return False, ( + f"Subject prefix '{subject_prefix}' does not match files changed.\n" + f"Expected one of: {expected_str}" + ) + # Subject prefix is in expected list, so it's valid - no need to check further # Subject prefix must be one of the expected ones if subj_lower not in expected_lower: From 589c253ede3144cadf9bd2f328aca957390618bc Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 17:37:49 -0600 Subject: [PATCH 23/42] oauth2_jwt: add handling of audience when value is an array Signed-off-by: Eduardo Silva --- src/flb_oauth2_jwt.c | 111 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 107 insertions(+), 4 deletions(-) diff --git a/src/flb_oauth2_jwt.c b/src/flb_oauth2_jwt.c index 36930275c6a..0a194725ce6 100644 --- a/src/flb_oauth2_jwt.c +++ b/src/flb_oauth2_jwt.c @@ -570,7 +570,11 @@ static int oauth2_jwt_parse_payload(const char *json, size_t json_len, v->via.str.size); } else if (v->type == MSGPACK_OBJECT_ARRAY && v->via.array.size > 0) { - /* Take first element of array */ + /* + * Store first element of array for backward compatibility and existence check. + * Note: During validation, all elements in the array are checked against + * allowed_audience (see oauth2_jwt_check_audience() in flb_oauth2_jwt_validate()). + */ first = &v->via.array.ptr[0]; if (first->type == MSGPACK_OBJECT_STR) { if (claims->audience) { @@ -1105,6 +1109,100 @@ static int oauth2_jwks_fetch_keys(struct flb_oauth2_jwt_ctx *ctx) return (jwks_json != NULL) ? 0 : -1; } +static int oauth2_jwt_check_audience(const char *json, size_t json_len, + const char *allowed_audience) +{ + int ret; + int root_type; + char *mp_buf = NULL; + size_t mp_size; + size_t off = 0; + msgpack_unpacked result; + msgpack_object map; + msgpack_object *k; + msgpack_object *v; + size_t i; + size_t j; + size_t map_size; + size_t key_len; + const char *key_str; + int found_match = 0; + + if (!json || json_len == 0 || !allowed_audience) { + return 0; + } + + /* Convert JSON to msgpack */ + ret = flb_pack_json_yyjson(json, json_len, &mp_buf, &mp_size, + &root_type, NULL); + if (ret != 0 || root_type != JSMN_OBJECT) { + if (mp_buf) { + flb_free(mp_buf); + } + return 0; + } + + /* Unpack msgpack */ + msgpack_unpacked_init(&result); + if (msgpack_unpack_next(&result, mp_buf, mp_size, &off) != MSGPACK_UNPACK_SUCCESS) { + flb_free(mp_buf); + msgpack_unpacked_destroy(&result); + return 0; + } + + map = result.data; + if (map.type != MSGPACK_OBJECT_MAP) { + flb_free(mp_buf); + msgpack_unpacked_destroy(&result); + return 0; + } + + /* Find and check the 'aud' claim */ + map_size = map.via.map.size; + for (i = 0; i < map_size; i++) { + k = &map.via.map.ptr[i].key; + v = &map.via.map.ptr[i].val; + + if (k->type != MSGPACK_OBJECT_STR) { + continue; + } + + key_len = k->via.str.size; + key_str = (const char *)k->via.str.ptr; + + if (key_len == 3 && strncmp(key_str, "aud", 3) == 0) { + if (v->type == MSGPACK_OBJECT_STR) { + /* Single string audience */ + if (v->via.str.size == strlen(allowed_audience) && + strncmp((const char *)v->via.str.ptr, allowed_audience, + v->via.str.size) == 0) { + found_match = 1; + } + } + else if (v->type == MSGPACK_OBJECT_ARRAY) { + /* Array of audiences - check if any element matches */ + for (j = 0; j < v->via.array.size; j++) { + msgpack_object *elem = &v->via.array.ptr[j]; + if (elem->type == MSGPACK_OBJECT_STR) { + if (elem->via.str.size == strlen(allowed_audience) && + strncmp((const char *)elem->via.str.ptr, allowed_audience, + elem->via.str.size) == 0) { + found_match = 1; + break; + } + } + } + } + break; + } + } + + flb_free(mp_buf); + msgpack_unpacked_destroy(&result); + + return found_match; +} + static void oauth2_jwt_free_cfg(struct flb_oauth2_jwt_cfg *cfg) { /* Note: cfg->issuer, cfg->jwks_url, and cfg->allowed_audience are pointers @@ -1310,9 +1408,14 @@ int flb_oauth2_jwt_validate(struct flb_oauth2_jwt_ctx *ctx, /* Check audience */ if (ctx->cfg.allowed_audience) { - if (!jwt.claims.audience || strcmp(ctx->cfg.allowed_audience, jwt.claims.audience) != 0) { - flb_debug("[oauth2_jwt] Audience mismatch: expected='%s', actual='%s'", - ctx->cfg.allowed_audience, jwt.claims.audience ? jwt.claims.audience : "(null)"); + /* Check audience by re-parsing the payload to handle both string and array formats. + * Per JWT spec (RFC 7519), when aud is an array, the token is valid if ANY element + * in the array matches the expected audience. */ + if (!oauth2_jwt_check_audience(jwt.payload_json, + flb_sds_len(jwt.payload_json), + ctx->cfg.allowed_audience)) { + flb_debug("[oauth2_jwt] Audience mismatch: expected='%s' not found in token audiences", + ctx->cfg.allowed_audience); status = FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; goto jwt_end; } From e0358bc506921b937626f06b2dd381e761ee9616 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 17:42:31 -0600 Subject: [PATCH 24/42] oauth2_jwt: fix signing_input construction Signed-off-by: Eduardo Silva --- src/flb_oauth2_jwt.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/flb_oauth2_jwt.c b/src/flb_oauth2_jwt.c index 0a194725ce6..7d13475be37 100644 --- a/src/flb_oauth2_jwt.c +++ b/src/flb_oauth2_jwt.c @@ -658,7 +658,7 @@ int flb_oauth2_jwt_parse(const char *token, size_t token_len, return FLB_OAUTH2_JWT_ERR_SEGMENT_COUNT; } - jwt->signing_input = flb_sds_create_len(token, parts_len[0] + parts_len[1] + 1); + jwt->signing_input = flb_sds_create_size(parts_len[0] + 1 + parts_len[1] + 1); if (!jwt->signing_input) { return FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; } @@ -667,6 +667,7 @@ int flb_oauth2_jwt_parse(const char *token, size_t token_len, jwt->signing_input[parts_len[0]] = '.'; memcpy(jwt->signing_input + parts_len[0] + 1, parts[1], parts_len[1]); jwt->signing_input[parts_len[0] + parts_len[1] + 1] = '\0'; + flb_sds_len_set(jwt->signing_input, parts_len[0] + 1 + parts_len[1]); ret = oauth2_jwt_base64url_decode(parts[0], parts_len[0], &decoded, &decoded_len, FLB_OAUTH2_JWT_ERR_BASE64_HEADER); From 871b30db20d22d47418721b0094e9e44decc433a Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 17:50:31 -0600 Subject: [PATCH 25/42] oauth2_jwt: handle JWKS keys in a msgpack buffer Signed-off-by: Eduardo Silva --- src/flb_oauth2_jwt.c | 277 +++++++++++++++++++------------------------ 1 file changed, 120 insertions(+), 157 deletions(-) diff --git a/src/flb_oauth2_jwt.c b/src/flb_oauth2_jwt.c index 7d13475be37..025af3f2b07 100644 --- a/src/flb_oauth2_jwt.c +++ b/src/flb_oauth2_jwt.c @@ -222,71 +222,6 @@ void flb_oauth2_jwt_destroy(struct flb_oauth2_jwt *jwt) } } -static int oauth2_jwt_token_strcmp(const char *json, jsmntok_t *tok, const char *cmp) -{ - int len = (tok->end - tok->start); - - if (len != (int) strlen(cmp)) { - return -1; - } - - return strncmp(json + tok->start, cmp, len); -} - -static int oauth2_jwt_parse_json_tokens(const char *json, - size_t json_len, - jsmntok_t **tokens_out, - int *tokens_size_out, - int invalid_error) -{ - int ret; - jsmn_parser parser; - int tokens_size = 32; - jsmntok_t *tokens = NULL; - int max_iterations = 20; /* Prevent infinite loop */ - int iteration = 0; - - while (iteration < max_iterations) { - flb_free(tokens); - tokens = flb_calloc(1, sizeof(jsmntok_t) * tokens_size); - if (!tokens) { - flb_errno(); - return FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; - } - - /* Reinitialize parser for each attempt */ - jsmn_init(&parser); - ret = jsmn_parse(&parser, json, json_len, tokens, tokens_size); - - if (ret != JSMN_ERROR_NOMEM) { - break; - } - - /* Double the token size for next iteration */ - tokens_size *= 2; - iteration++; - } - - if (iteration >= max_iterations) { - flb_free(tokens); - return invalid_error; - } - - if (ret == JSMN_ERROR_INVAL || ret == JSMN_ERROR_PART) { - flb_free(tokens); - return invalid_error; - } - - if (ret < 1 || tokens[0].type != JSMN_OBJECT) { - flb_free(tokens); - return invalid_error; - } - - *tokens_out = tokens; - *tokens_size_out = ret; - return FLB_OAUTH2_JWT_OK; -} - static int oauth2_jwt_base64url_decode(const char *segment, size_t segment_len, unsigned char **decoded, @@ -399,11 +334,6 @@ static int oauth2_jwt_base64url_decode(const char *segment, return FLB_OAUTH2_JWT_OK; } -static flb_sds_t oauth2_jwt_token_to_sds(const char *json, jsmntok_t *tok) -{ - return flb_sds_create_len(json + tok->start, tok->end - tok->start); -} - static int oauth2_jwt_parse_header(const char *json, size_t json_len, struct flb_oauth2_jwt_claims *claims) { @@ -729,74 +659,97 @@ int flb_oauth2_jwt_parse(const char *token, size_t token_len, return FLB_OAUTH2_JWT_OK; } -static int oauth2_jwks_parse_key(const char *json, jsmntok_t *tokens, int tokens_size, int key_obj_idx, +static int oauth2_jwks_parse_key(msgpack_object *key_obj, struct flb_oauth2_jwks_key **key_out) { - int i; flb_sds_t kid = NULL; flb_sds_t n = NULL; flb_sds_t e = NULL; + msgpack_object *k; + msgpack_object *v; + size_t i; + size_t map_size; + size_t key_len; + const char *key_str; struct flb_oauth2_jwks_key *key = NULL; - jsmntok_t *key_obj; - if (!json || !tokens || key_obj_idx < 0 || key_obj_idx >= tokens_size || !key_out) { + if (!key_obj || !key_out) { return -1; } - key_obj = &tokens[key_obj_idx]; - if (key_obj->type != JSMN_OBJECT) { + if (key_obj->type != MSGPACK_OBJECT_MAP) { return -1; } - /* Find kty, kid, n, e in the key object */ - /* JSMN stores objects as: [object_token, key1, value1, key2, value2, ...] */ - for (i = key_obj_idx + 1; i < tokens_size && i < key_obj_idx + 1 + (key_obj->size * 2); i += 2) { - jsmntok_t *tok = &tokens[i]; - jsmntok_t *val; - - if (i + 1 >= tokens_size) { - break; - } - - val = &tokens[i + 1]; + /* Extract kty, kid, n, e from the key object */ + map_size = key_obj->via.map.size; + for (i = 0; i < map_size; i++) { + k = &key_obj->via.map.ptr[i].key; + v = &key_obj->via.map.ptr[i].val; - if (tok->type != JSMN_STRING) { + if (k->type != MSGPACK_OBJECT_STR) { continue; } - if (oauth2_jwt_token_strcmp(json, tok, "kty") == 0) { - flb_sds_t kty = oauth2_jwt_token_to_sds(json, val); - if (kty && strcmp(kty, "RSA") != 0) { - flb_sds_destroy(kty); - if (kid) flb_sds_destroy(kid); - if (n) flb_sds_destroy(n); - if (e) flb_sds_destroy(e); - return -1; /* Not an RSA key */ - } - if (kty) { - flb_sds_destroy(kty); + key_len = k->via.str.size; + key_str = (const char *) k->via.str.ptr; + + if (key_len == 3 && strncmp(key_str, "kty", 3) == 0) { + if (v->type == MSGPACK_OBJECT_STR) { + if (v->via.str.size == 3 && + strncmp((const char *) v->via.str.ptr, "RSA", 3) == 0) { + /* Valid RSA key type */ + } + else { + /* Not an RSA key */ + if (kid) { + flb_sds_destroy(kid); + } + if (n) { + flb_sds_destroy(n); + } + if (e) { + flb_sds_destroy(e); + } + return -1; + } } } - else if (oauth2_jwt_token_strcmp(json, tok, "kid") == 0) { - kid = oauth2_jwt_token_to_sds(json, val); + else if (key_len == 3 && strncmp(key_str, "kid", 3) == 0) { + if (v->type == MSGPACK_OBJECT_STR) { + kid = flb_sds_create_len((const char *) v->via.str.ptr, + v->via.str.size); + } } - else if (oauth2_jwt_token_strcmp(json, tok, "n") == 0) { - n = oauth2_jwt_token_to_sds(json, val); + else if (key_len == 1 && strncmp(key_str, "n", 1) == 0) { + if (v->type == MSGPACK_OBJECT_STR) { + n = flb_sds_create_len((const char *) v->via.str.ptr, v->via.str.size); + } } - else if (oauth2_jwt_token_strcmp(json, tok, "e") == 0) { - e = oauth2_jwt_token_to_sds(json, val); + else if (key_len == 1 && strncmp(key_str, "e", 1) == 0) { + if (v->type == MSGPACK_OBJECT_STR) { + e = flb_sds_create_len((const char *) v->via.str.ptr, v->via.str.size); + } } + /* Ignore other fields (e.g. x5c array) - msgpack handles nested structures automatically */ } if (!kid || !n || !e) { - if (kid) flb_sds_destroy(kid); - if (n) flb_sds_destroy(n); - if (e) flb_sds_destroy(e); + if (kid) { + flb_sds_destroy(kid); + } + if (n) { + flb_sds_destroy(n); + } + if (e) { + flb_sds_destroy(e); + } return -1; } key = flb_calloc(1, sizeof(struct flb_oauth2_jwks_key)); if (!key) { + flb_errno(); flb_sds_destroy(kid); flb_sds_destroy(n); flb_sds_destroy(e); @@ -815,67 +768,79 @@ static int oauth2_jwks_parse_key(const char *json, jsmntok_t *tokens, int tokens static int oauth2_jwks_parse_json(flb_sds_t jwks_json, struct flb_oauth2_jwks_cache *cache) { int ret; - int tokens_size; - jsmntok_t *tokens = NULL; - int i; + int root_type; + char *mp_buf = NULL; + size_t mp_size; + size_t off = 0; + msgpack_unpacked result; + msgpack_object root; + msgpack_object *k; + msgpack_object *v; + msgpack_object *key_obj; + size_t i; + size_t j; + size_t map_size; + size_t array_size; + size_t key_len; + const char *key_str; int keys_found = 0; + struct flb_oauth2_jwks_key *jwks_key; - ret = oauth2_jwt_parse_json_tokens(jwks_json, flb_sds_len(jwks_json), - &tokens, &tokens_size, - FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT); - if (ret != FLB_OAUTH2_JWT_OK) { - flb_error("[oauth2_jwt] failed to parse JWKS JSON tokens"); + /* Convert JSON to msgpack */ + ret = flb_pack_json_yyjson(jwks_json, flb_sds_len(jwks_json), + &mp_buf, &mp_size, + &root_type, NULL); + if (ret != 0 || root_type != JSMN_OBJECT) { + if (mp_buf) { + flb_free(mp_buf); + } + flb_error("[oauth2_jwt] failed to parse JWKS JSON"); return -1; } + /* Unpack msgpack */ + msgpack_unpacked_init(&result); + if (msgpack_unpack_next(&result, mp_buf, mp_size, &off) != MSGPACK_UNPACK_SUCCESS) { + flb_free(mp_buf); + msgpack_unpacked_destroy(&result); + flb_error("[oauth2_jwt] failed to unpack JWKS msgpack"); + return -1; + } + + root = result.data; + if (root.type != MSGPACK_OBJECT_MAP) { + flb_free(mp_buf); + msgpack_unpacked_destroy(&result); + flb_error("[oauth2_jwt] JWKS root is not an object"); + return -1; + } /* Find "keys" array in the JWKS */ - for (i = 1; i < tokens_size; i++) { - jsmntok_t *key = &tokens[i]; - jsmntok_t *val; + map_size = root.via.map.size; + for (i = 0; i < map_size; i++) { + k = &root.via.map.ptr[i].key; + v = &root.via.map.ptr[i].val; - if (key->type != JSMN_STRING) { + if (k->type != MSGPACK_OBJECT_STR) { continue; } - i++; - if (i >= tokens_size) { - break; - } - - val = &tokens[i]; - - if (oauth2_jwt_token_strcmp(jwks_json, key, "keys") == 0 && - val->type == JSMN_ARRAY) { - int j; - int keys_count = val->size; - int key_idx = i + 1; - int key_obj_end; - jsmntok_t *key_obj; - struct flb_oauth2_jwks_key *jwks_key; + key_len = k->via.str.size; + key_str = (const char *) k->via.str.ptr; + if (key_len == 4 && strncmp(key_str, "keys", 4) == 0 && + v->type == MSGPACK_OBJECT_ARRAY) { /* Parse each key in the array */ - for (j = 0; j < keys_count && key_idx < tokens_size; j++) { - key_obj = &tokens[key_idx]; + array_size = v->via.array.size; + for (j = 0; j < array_size; j++) { + key_obj = &v->via.array.ptr[j]; jwks_key = NULL; - if (key_obj->type != JSMN_OBJECT) { - break; + if (key_obj->type != MSGPACK_OBJECT_MAP) { + continue; } - /* For JSMN, an object with size N has N key-value pairs - * Each pair occupies 2 tokens (key + value) - * So total tokens = 1 (object) + N*2 (pairs) - * Since JWKS keys have simple string values (no nested objects), - * we can use this simple calculation */ - key_obj_end = key_idx + 1 + (key_obj->size * 2); - - /* Ensure we don't go beyond tokens_size */ - if (key_obj_end > tokens_size) { - key_obj_end = tokens_size; - } - - ret = oauth2_jwks_parse_key(jwks_json, tokens, tokens_size, key_idx, &jwks_key); + ret = oauth2_jwks_parse_key(key_obj, &jwks_key); if (ret == 0 && jwks_key) { /* Store key in cache using kid as hash key */ flb_hash_table_add(cache->entries, jwks_key->kid, @@ -883,15 +848,13 @@ static int oauth2_jwks_parse_json(flb_sds_t jwks_json, struct flb_oauth2_jwks_ca jwks_key, 0); keys_found++; } - - /* Move to next key object */ - key_idx = key_obj_end; } break; } } - flb_free(tokens); + flb_free(mp_buf); + msgpack_unpacked_destroy(&result); if (keys_found == 0) { flb_error("[oauth2_jwt] No valid keys found in JWKS"); @@ -974,7 +937,6 @@ static int oauth2_jwks_fetch_keys(struct flb_oauth2_jwt_ctx *ctx) return -1; } - ret = flb_utils_url_split(ctx->cfg.jwks_url, &protocol, &host, &port_str, &uri); if (ret != 0) { flb_error("[oauth2_jwt] invalid JWKS URL: %s", ctx->cfg.jwks_url); @@ -1507,3 +1469,4 @@ struct mk_list *flb_oauth2_jwt_get_config_map(struct flb_config *config) return config_map; } + From 103a4125ae3a2b0f1f9cf595c55f9affc6f69ad3 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 17:52:43 -0600 Subject: [PATCH 26/42] oauth2_jwt: split ret code handling when fetching keys Signed-off-by: Eduardo Silva --- src/flb_oauth2_jwt.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/flb_oauth2_jwt.c b/src/flb_oauth2_jwt.c index 025af3f2b07..ab3b3a75d04 100644 --- a/src/flb_oauth2_jwt.c +++ b/src/flb_oauth2_jwt.c @@ -920,6 +920,7 @@ static int oauth2_jwt_verify_signature_rsa(const char *signing_input, static int oauth2_jwks_fetch_keys(struct flb_oauth2_jwt_ctx *ctx) { int ret; + int ret_code = -1; int port; size_t b_sent; char *protocol = NULL; @@ -1038,6 +1039,7 @@ static int oauth2_jwks_fetch_keys(struct flb_oauth2_jwt_ctx *ctx) } else { ctx->jwks_cache.last_refresh = time(NULL); + ret_code = 0; } cleanup: @@ -1069,7 +1071,7 @@ static int oauth2_jwks_fetch_keys(struct flb_oauth2_jwt_ctx *ctx) flb_free(uri); } - return (jwks_json != NULL) ? 0 : -1; + return ret_code; } static int oauth2_jwt_check_audience(const char *json, size_t json_len, From a3ebd081a3e53e8aa1437b1d3362f851a261ab2e Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 17:56:34 -0600 Subject: [PATCH 27/42] oauth2: use flb_sds_cat_safe() for payload handling Signed-off-by: Eduardo Silva --- src/flb_oauth2.c | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/flb_oauth2.c b/src/flb_oauth2.c index bec2a318240..0a215b07ce5 100644 --- a/src/flb_oauth2.c +++ b/src/flb_oauth2.c @@ -737,6 +737,7 @@ int flb_oauth2_payload_append(struct flb_oauth2 *ctx, const char *key_str, int key_len, const char *val_str, int val_len) { + int ret; int size; flb_sds_t tmp; @@ -761,12 +762,26 @@ int flb_oauth2_payload_append(struct flb_oauth2 *ctx, } if (flb_sds_len(ctx->payload) > 0) { - flb_sds_cat(ctx->payload, "&", 1); + ret = flb_sds_cat_safe(&ctx->payload, "&", 1); + if (ret != 0) { + return -1; + } + } + + ret = flb_sds_cat_safe(&ctx->payload, key_str, key_len); + if (ret != 0) { + return -1; } - flb_sds_cat(ctx->payload, key_str, key_len); - flb_sds_cat(ctx->payload, "=", 1); - flb_sds_cat(ctx->payload, val_str, val_len); + ret = flb_sds_cat_safe(&ctx->payload, "=", 1); + if (ret != 0) { + return -1; + } + + ret = flb_sds_cat_safe(&ctx->payload, val_str, val_len); + if (ret != 0) { + return -1; + } ctx->payload_manual = FLB_TRUE; return 0; From ddb8a98af3e7f7db567546889a72a5b26959eee5 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 18:11:37 -0600 Subject: [PATCH 28/42] crypto: add support for OpenSSL v1.0.2 Signed-off-by: Eduardo Silva --- src/flb_crypto.c | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/flb_crypto.c b/src/flb_crypto.c index 86a981d4288..5158d148f0b 100644 --- a/src/flb_crypto.c +++ b/src/flb_crypto.c @@ -19,9 +19,21 @@ #include #include #include +#include #include #include +/* + * OpenSSL version compatibility macros + * + * EVP_MD_CTX_new/free were introduced in OpenSSL 1.1.0 + * For OpenSSL 1.0.2, use EVP_MD_CTX_create/destroy + */ +#if OPENSSL_VERSION_NUMBER < 0x10100000L +#define EVP_MD_CTX_new() EVP_MD_CTX_create() +#define EVP_MD_CTX_free(ctx) EVP_MD_CTX_destroy(ctx) +#endif + static int flb_crypto_get_rsa_padding_type_by_id(int padding_type_id) { int result; @@ -601,7 +613,20 @@ int flb_crypto_verify(struct flb_crypto *context, return FLB_CRYPTO_BACKEND_ERROR; } +#if OPENSSL_VERSION_NUMBER >= 0x10100000L + /* OpenSSL 1.1.0+: Use the convenient EVP_DigestVerify() function */ result = EVP_DigestVerify(md_ctx, signature, signature_length, data, data_length); +#else + /* OpenSSL 1.0.2: Use Init/Update/Final pattern */ + if (EVP_DigestVerifyUpdate(md_ctx, data, data_length) <= 0) { + if (context) { + context->last_error = ERR_get_error(); + } + EVP_MD_CTX_free(md_ctx); + return FLB_CRYPTO_BACKEND_ERROR; + } + result = EVP_DigestVerifyFinal(md_ctx, signature, signature_length); +#endif EVP_MD_CTX_free(md_ctx); if (result == 1) { From bf1b348f4e6f873eb19e3e3140ec48a3a0241f33 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 18:13:12 -0600 Subject: [PATCH 29/42] tests: internal: input_chunk_routes: initialize oauth2_jwt properties Signed-off-by: Eduardo Silva --- tests/internal/input_chunk_routes.c | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/internal/input_chunk_routes.c b/tests/internal/input_chunk_routes.c index 489e04a0bf1..d901ccd83ff 100644 --- a/tests/internal/input_chunk_routes.c +++ b/tests/internal/input_chunk_routes.c @@ -115,6 +115,7 @@ static int init_test_config(struct flb_config *config, /* Initialize properties list (required by flb_input_instance_init) */ mk_list_init(&in->properties); mk_list_init(&in->net_properties); + mk_list_init(&in->oauth2_jwt_properties); /* Initialize hash tables for chunks (required by flb_input_chunk_destroy) */ in->ht_log_chunks = flb_hash_table_create(FLB_HASH_TABLE_EVICT_NONE, 512, 0); From 0337a851f19f7f012a0a6405e09614db1c14a287 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 18:16:09 -0600 Subject: [PATCH 30/42] oauth2_jwt: enhance clearing cache entries functionality Signed-off-by: Eduardo Silva --- src/flb_oauth2_jwt.c | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/flb_oauth2_jwt.c b/src/flb_oauth2_jwt.c index ab3b3a75d04..45a1c3f0e00 100644 --- a/src/flb_oauth2_jwt.c +++ b/src/flb_oauth2_jwt.c @@ -124,6 +124,45 @@ static void oauth2_jwks_key_destroy(struct flb_oauth2_jwks_key *key) flb_free(key); } +static void oauth2_jwks_cache_clear(struct flb_oauth2_jwks_cache *cache) +{ + int i; + struct mk_list *head; + struct mk_list *tmp; + struct flb_hash_table_entry *entry; + struct flb_hash_table_chain *table; + int refresh_interval; + + if (!cache || !cache->entries) { + return; + } + + /* Save refresh interval before destroying */ + refresh_interval = cache->refresh_interval; + + /* Iterate through all hash table chains and destroy keys */ + for (i = 0; i < cache->entries->size; i++) { + table = &cache->entries->table[i]; + mk_list_foreach_safe(head, tmp, &table->chains) { + entry = mk_list_entry(head, struct flb_hash_table_entry, _head); + if (entry->val) { + oauth2_jwks_key_destroy((struct flb_oauth2_jwks_key *)entry->val); + entry->val = NULL; /* Prevent double-free */ + } + } + } + + /* Destroy and recreate the hash table to clear all entries */ + flb_hash_table_destroy(cache->entries); + cache->entries = flb_hash_table_create(FLB_HASH_TABLE_EVICT_NONE, 64, 0); + if (!cache->entries) { + flb_error("[oauth2_jwt] failed to recreate JWKS cache after clear"); + } + + /* Restore refresh interval */ + cache->refresh_interval = refresh_interval; +} + static void oauth2_jwks_cache_destroy(struct flb_oauth2_jwks_cache *cache) { int i; @@ -1030,6 +1069,9 @@ static int oauth2_jwks_fetch_keys(struct flb_oauth2_jwt_ctx *ctx) goto cleanup; } + /* Clear existing cache entries before refreshing to ensure revoked/rotated keys are removed */ + oauth2_jwks_cache_clear(&ctx->jwks_cache); + /* Parse JWKS JSON and store keys in cache */ ret = oauth2_jwks_parse_json(jwks_json, &ctx->jwks_cache); if (ret != 0) { From 67392d63f9f659d88549ec3aabab128fbf49865b Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 19:12:56 -0600 Subject: [PATCH 31/42] oauth2: fix missing initialization Signed-off-by: Eduardo Silva --- src/flb_oauth2.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/flb_oauth2.c b/src/flb_oauth2.c index 0a215b07ce5..c4f83cdb449 100644 --- a/src/flb_oauth2.c +++ b/src/flb_oauth2.c @@ -129,6 +129,12 @@ static void oauth2_apply_defaults(struct flb_oauth2_config *cfg) cfg->refresh_skew = FLB_OAUTH2_DEFAULT_SKEW_SECS; cfg->timeout = 0; cfg->connect_timeout = 0; + /* Initialize all pointer fields to NULL to avoid using uninitialized values */ + cfg->token_url = NULL; + cfg->client_id = NULL; + cfg->client_secret = NULL; + cfg->scope = NULL; + cfg->audience = NULL; } static int oauth2_clone_config(struct flb_oauth2_config *dst, From 4174835ffee88730d1d63d27bf647d7c6fb1e186 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 17 Dec 2025 19:13:20 -0600 Subject: [PATCH 32/42] oauth2_jwt: initialize defaults Signed-off-by: Eduardo Silva --- src/flb_oauth2_jwt.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/flb_oauth2_jwt.c b/src/flb_oauth2_jwt.c index 45a1c3f0e00..94fca319385 100644 --- a/src/flb_oauth2_jwt.c +++ b/src/flb_oauth2_jwt.c @@ -1236,6 +1236,11 @@ struct flb_oauth2_jwt_ctx *flb_oauth2_jwt_context_create(struct flb_config *conf if (cfg != NULL) { memcpy(&ctx->cfg, cfg, sizeof(struct flb_oauth2_jwt_cfg)); } + else { + /* Initialize with defaults when cfg is NULL */ + memset(&ctx->cfg, 0, sizeof(struct flb_oauth2_jwt_cfg)); + ctx->cfg.jwks_refresh_interval = 300; /* Default from config map */ + } if (oauth2_jwks_cache_init(&ctx->jwks_cache, ctx->cfg.jwks_refresh_interval) != 0) { From 8884521c92d63b535a13c0b460bee2f64f930d8e Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Fri, 19 Dec 2025 12:41:03 -0600 Subject: [PATCH 33/42] tests: internal: oauth2: add macOS and Windows compat Signed-off-by: Eduardo Silva --- tests/internal/oauth2.c | 154 ++++++++++++++++++++++++++++++++-------- 1 file changed, 125 insertions(+), 29 deletions(-) diff --git a/tests/internal/oauth2.c b/tests/internal/oauth2.c index 855c16e1cac..259f350872f 100644 --- a/tests/internal/oauth2.c +++ b/tests/internal/oauth2.c @@ -1,24 +1,32 @@ /* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ -#include -#include -#include -#include -#include -#include - +#include +#include +#include #include #include #include #include +#include #include +#include + +#ifndef _WIN32 +#include +#include +#include +#else +#include +#include +#endif + #include "flb_tests_internal.h" #define MOCK_BODY_SIZE 1024 struct oauth2_mock_server { - int listen_fd; + flb_sockfd_t listen_fd; int port; int stop; int token_requests; @@ -29,10 +37,13 @@ struct oauth2_mock_server { pthread_t thread; }; -static void compose_http_response(int fd, int status, const char *body) +static void compose_http_response(flb_sockfd_t fd, int status, const char *body) { char buffer[MOCK_BODY_SIZE]; int body_len = 0; + ssize_t sent = 0; + ssize_t total = 0; + ssize_t len; if (body != NULL) { body_len = strlen(body); @@ -46,10 +57,18 @@ static void compose_http_response(int fd, int status, const char *body) "%s", status, body_len, body ? body : ""); - send(fd, buffer, strlen(buffer), 0); + len = strlen(buffer); + /* Ensure we send all data - loop until complete */ + while (total < len) { + sent = send(fd, buffer + total, len - total, 0); + if (sent <= 0) { + break; + } + total += sent; + } } -static void handle_token_request(struct oauth2_mock_server *server, int fd) +static void handle_token_request(struct oauth2_mock_server *server, flb_sockfd_t fd) { char payload[MOCK_BODY_SIZE]; @@ -65,7 +84,7 @@ static void handle_token_request(struct oauth2_mock_server *server, int fd) compose_http_response(fd, 200, payload); } -static void handle_resource_request(struct oauth2_mock_server *server, int fd, +static void handle_resource_request(struct oauth2_mock_server *server, flb_sockfd_t fd, const char *request) { int authorized = 0; @@ -95,10 +114,12 @@ static void handle_resource_request(struct oauth2_mock_server *server, int fd, static void *oauth2_mock_server_thread(void *data) { struct oauth2_mock_server *server = (struct oauth2_mock_server *) data; - int client_fd; + flb_sockfd_t client_fd; fd_set rfds; struct timeval tv; char buffer[MOCK_BODY_SIZE]; + ssize_t total; + ssize_t n; while (!server->stop) { FD_ZERO(&rfds); @@ -106,17 +127,35 @@ static void *oauth2_mock_server_thread(void *data) tv.tv_sec = 0; tv.tv_usec = 200000; - if (select(server->listen_fd + 1, &rfds, NULL, NULL, &tv) <= 0) { + if (select((int)(server->listen_fd + 1), &rfds, NULL, NULL, &tv) <= 0) { continue; } client_fd = accept(server->listen_fd, NULL, NULL); - if (client_fd < 0) { + if (client_fd == FLB_INVALID_SOCKET) { continue; } + /* Read the full HTTP request - loop until we get the complete request */ memset(buffer, 0, sizeof(buffer)); - recv(client_fd, buffer, sizeof(buffer) - 1, 0); + total = 0; + + /* 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) */ + while (total < sizeof(buffer) - 1) { + n = recv(client_fd, buffer + total, (int)(sizeof(buffer) - 1 - total), 0); + if (n <= 0) { + /* Connection closed or error */ + break; + } + total += n; + /* Check if we've received the complete HTTP request */ + if (strstr(buffer, "\r\n\r\n") != NULL) { + break; + } + } if (strstr(buffer, "/token")) { handle_token_request(server, client_fd); @@ -125,7 +164,7 @@ static void *oauth2_mock_server_thread(void *data) handle_resource_request(server, client_fd, buffer); } - close(client_fd); + flb_socket_close(client_fd); } return NULL; @@ -134,7 +173,6 @@ static void *oauth2_mock_server_thread(void *data) static int oauth2_mock_server_start(struct oauth2_mock_server *server, int expires_in, int resource_challenge) { - int flags; int on = 1; struct sockaddr_in addr; socklen_t len; @@ -144,11 +182,12 @@ static int oauth2_mock_server_start(struct oauth2_mock_server *server, int expir server->resource_challenge = resource_challenge; server->listen_fd = socket(AF_INET, SOCK_STREAM, 0); - if (server->listen_fd < 0) { + if (server->listen_fd == FLB_INVALID_SOCKET) { + flb_errno(); return -1; } - setsockopt(server->listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); + setsockopt(server->listen_fd, SOL_SOCKET, SO_REUSEADDR, (const char *)&on, sizeof(on)); memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; @@ -156,41 +195,89 @@ static int oauth2_mock_server_start(struct oauth2_mock_server *server, int expir addr.sin_port = 0; if (bind(server->listen_fd, (struct sockaddr *) &addr, sizeof(addr)) < 0) { - close(server->listen_fd); + flb_errno(); + flb_socket_close(server->listen_fd); return -1; } len = sizeof(addr); if (getsockname(server->listen_fd, (struct sockaddr *) &addr, &len) < 0) { - close(server->listen_fd); + flb_errno(); + flb_socket_close(server->listen_fd); return -1; } server->port = ntohs(addr.sin_port); if (listen(server->listen_fd, 4) < 0) { - close(server->listen_fd); + flb_errno(); + flb_socket_close(server->listen_fd); return -1; } - flags = fcntl(server->listen_fd, F_GETFL, 0); - fcntl(server->listen_fd, F_SETFL, flags | O_NONBLOCK); + flb_net_socket_nonblocking(server->listen_fd); if (pthread_create(&server->thread, NULL, oauth2_mock_server_thread, server) != 0) { - close(server->listen_fd); + printf("pthread_create failed: %s\n", strerror(errno)); + flb_socket_close(server->listen_fd); return -1; } - + printf("server started on port %d\n", server->port); return 0; } +static int oauth2_mock_server_wait_ready(struct oauth2_mock_server *server) +{ + /* On macOS, we need to give the server thread time to start and enter + * its select() loop. A simple delay is sufficient since pthread_create + * returns when the thread is created, but the thread may not have + * started executing yet. */ + int retries = 50; + flb_sockfd_t test_fd; + struct sockaddr_in addr; + int ret; + + while (retries-- > 0) { + /* Check if server is listening by attempting a non-blocking connect */ + test_fd = socket(AF_INET, SOCK_STREAM, 0); + if (test_fd != FLB_INVALID_SOCKET) { + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = htons(server->port); + + flb_net_socket_nonblocking(test_fd); + + ret = connect(test_fd, (struct sockaddr *) &addr, sizeof(addr)); + + /* If connect succeeds or is in progress, server is ready */ +#ifdef _WIN32 + if (ret == 0 || (ret < 0 && WSAGetLastError() == WSAEWOULDBLOCK)) { +#else + if (ret == 0 || (ret < 0 && (errno == EINPROGRESS || errno == EWOULDBLOCK))) { +#endif + flb_socket_close(test_fd); + /* Give the server thread one more moment to be fully ready */ + flb_time_msleep(10); + return 0; + } + + flb_socket_close(test_fd); + } + + flb_time_msleep(20); + } + + return -1; +} + static void oauth2_mock_server_stop(struct oauth2_mock_server *server) { - if (server->listen_fd > 0) { + if (server->listen_fd != FLB_INVALID_SOCKET) { server->stop = 1; shutdown(server->listen_fd, SHUT_RDWR); pthread_join(server->thread, NULL); - close(server->listen_fd); + flb_socket_close(server->listen_fd); } } @@ -253,6 +340,15 @@ void test_caching_and_refresh(void) ctx = create_oauth_ctx(config, &server, 1); TEST_CHECK(ctx != NULL); +#ifdef FLB_SYSTEM_MACOS + /* On macOS, wait for the server thread to be ready to accept connections. + * This ensures the server has entered its select() loop before we make requests. */ + ret = oauth2_mock_server_wait_ready(&server); + TEST_CHECK(ret == 0); + /* Give the server a moment to finish processing the test connection */ + flb_time_msleep(50); +#endif + ret = flb_oauth2_get_access_token(ctx, &token, FLB_FALSE); TEST_CHECK(ret == 0); TEST_CHECK(strcmp(token, "mock-token-1") == 0); From d54214e36b464c06e2b8aaec795dc3bad189637e Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Fri, 19 Dec 2025 12:45:55 -0600 Subject: [PATCH 34/42] compat: expose strcasecmp() Signed-off-by: Eduardo Silva --- include/fluent-bit/flb_compat.h | 38 +++++++++++++++++---------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/include/fluent-bit/flb_compat.h b/include/fluent-bit/flb_compat.h index 2e7f28273a2..fdecd19728c 100644 --- a/include/fluent-bit/flb_compat.h +++ b/include/fluent-bit/flb_compat.h @@ -62,6 +62,7 @@ */ #define timezone _timezone #define tzname _tzname +#define strcasecmp _stricmp #define strncasecmp _strnicmp #define timegm _mkgmtime @@ -138,6 +139,7 @@ static inline int usleep(LONGLONG usec) #include #include #include +#include #define FLB_DIRCHAR '/' #endif @@ -148,33 +150,33 @@ static inline int usleep(LONGLONG usec) #ifdef FLB_ENFORCE_ALIGNMENT -/* Please do not modify these functions without a very solid understanding of +/* Please do not modify these functions without a very solid understanding of * the reasoning behind. * * These functions deliverately abuse the volatile qualifier in order to prevent - * the compiler from mistakenly optimizing the memory accesses into a singled - * DWORD read (which in some architecture and compiler combinations it does regardless + * the compiler from mistakenly optimizing the memory accesses into a singled + * DWORD read (which in some architecture and compiler combinations it does regardless * of the flags). - * - * The reason why we decided to include this is that according to PR 9096, - * when the linux kernel is built and configured to pass through memory alignment - * exceptions rather than remediate them fluent-bit generates one while accessing a - * packed field in the msgpack wire format (which we cannot modify due to interoperability + * + * The reason why we decided to include this is that according to PR 9096, + * when the linux kernel is built and configured to pass through memory alignment + * exceptions rather than remediate them fluent-bit generates one while accessing a + * packed field in the msgpack wire format (which we cannot modify due to interoperability * reasons). - * - * Because of this, a potential patch using memcpy was suggested, however, this patch did - * not yield consistent machine code accross architecture and compiler versions with most + * + * Because of this, a potential patch using memcpy was suggested, however, this patch did + * not yield consistent machine code accross architecture and compiler versions with most * of them still generating optimized misaligned memory access instructions. - * + * * Keep in mind that these functions transform a single memory read into seven plus a few - * writes as this was the only way to prevent the compiler from mistakenly optimizing the + * writes as this was the only way to prevent the compiler from mistakenly optimizing the * operations. - * - * In most cases, FLB_ENFORCE_ALIGNMENT should not be enabled and the operating system - * kernel should be left to handle these scenarios, however, this option is present for - * those users who deliverately and knowingly choose to set up their operating system in + * + * In most cases, FLB_ENFORCE_ALIGNMENT should not be enabled and the operating system + * kernel should be left to handle these scenarios, however, this option is present for + * those users who deliverately and knowingly choose to set up their operating system in * a way that requires it. - * + * */ #if FLB_BYTE_ORDER == FLB_LITTLE_ENDIAN From ce77bf9fc4171e5935cd4e90fad61e9691e6abf5 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Fri, 19 Dec 2025 12:46:55 -0600 Subject: [PATCH 35/42] oauth2_jwt: fix conditioanls and enhance azp handling Signed-off-by: Eduardo Silva --- include/fluent-bit/flb_oauth2_jwt.h | 1 + src/flb_oauth2_jwt.c | 43 +++++++++++++++++------------ 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/include/fluent-bit/flb_oauth2_jwt.h b/include/fluent-bit/flb_oauth2_jwt.h index cae87c7804b..99412cd7c10 100644 --- a/include/fluent-bit/flb_oauth2_jwt.h +++ b/include/fluent-bit/flb_oauth2_jwt.h @@ -53,6 +53,7 @@ struct flb_oauth2_jwt_claims { flb_sds_t audience; flb_sds_t client_id; uint64_t expiration; + int has_azp; }; struct flb_oauth2_jwt { diff --git a/src/flb_oauth2_jwt.c b/src/flb_oauth2_jwt.c index 94fca319385..d587cff5276 100644 --- a/src/flb_oauth2_jwt.c +++ b/src/flb_oauth2_jwt.c @@ -39,9 +39,8 @@ #include #include -#include #include - +#include struct flb_oauth2_jwks_key { @@ -124,7 +123,7 @@ static void oauth2_jwks_key_destroy(struct flb_oauth2_jwks_key *key) flb_free(key); } -static void oauth2_jwks_cache_clear(struct flb_oauth2_jwks_cache *cache) +static int oauth2_jwks_cache_clear(struct flb_oauth2_jwks_cache *cache) { int i; struct mk_list *head; @@ -134,7 +133,7 @@ static void oauth2_jwks_cache_clear(struct flb_oauth2_jwks_cache *cache) int refresh_interval; if (!cache || !cache->entries) { - return; + return 0; } /* Save refresh interval before destroying */ @@ -157,10 +156,12 @@ static void oauth2_jwks_cache_clear(struct flb_oauth2_jwks_cache *cache) cache->entries = flb_hash_table_create(FLB_HASH_TABLE_EVICT_NONE, 64, 0); if (!cache->entries) { flb_error("[oauth2_jwt] failed to recreate JWKS cache after clear"); + return -1; } /* Restore refresh interval */ cache->refresh_interval = refresh_interval; + return 0; } static void oauth2_jwks_cache_destroy(struct flb_oauth2_jwks_cache *cache) @@ -561,15 +562,19 @@ 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_azp = FLB_TRUE; } } else if (key_len == 9 && strncmp(key_str, "client_id", 9) == 0) { if (v->type == MSGPACK_OBJECT_STR) { - if (claims->client_id) { - flb_sds_destroy(claims->client_id); + /* Only assign client_id if azp was not already set */ + if (claims->has_azp == 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); } - claims->client_id = flb_sds_create_len((const char *)v->via.str.ptr, - v->via.str.size); } } } @@ -1070,7 +1075,11 @@ static int oauth2_jwks_fetch_keys(struct flb_oauth2_jwt_ctx *ctx) } /* Clear existing cache entries before refreshing to ensure revoked/rotated keys are removed */ - oauth2_jwks_cache_clear(&ctx->jwks_cache); + ret = oauth2_jwks_cache_clear(&ctx->jwks_cache); + if (ret != 0) { + flb_error("[oauth2_jwt] failed to clear JWKS cache"); + goto cleanup; + } /* Parse JWKS JSON and store keys in cache */ ret = oauth2_jwks_parse_json(jwks_json, &ctx->jwks_cache); @@ -1359,8 +1368,8 @@ int flb_oauth2_jwt_validate(struct flb_oauth2_jwt_ctx *ctx, ret = oauth2_jwks_fetch_keys(ctx); if (ret != 0) { flb_debug("[oauth2_jwt] Failed to fetch JWKS: %d", ret); - status = FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; - goto jwt_end; + status = FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; + goto jwt_end; } } @@ -1373,19 +1382,19 @@ int flb_oauth2_jwt_validate(struct flb_oauth2_jwt_ctx *ctx, ret = oauth2_jwks_fetch_keys(ctx); if (ret != 0) { flb_debug("[oauth2_jwt] Failed to refresh JWKS: %d", ret); - status = FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; - goto jwt_end; + status = FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; + goto jwt_end; } jwks_key = (struct flb_oauth2_jwks_key *)flb_hash_table_get_ptr(ctx->jwks_cache.entries, jwt.claims.kid, flb_sds_len(jwt.claims.kid)); - } + } if (!jwks_key) { flb_debug("[oauth2_jwt] Key with kid '%s' not found in JWKS", jwt.claims.kid); - status = FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; - goto jwt_end; - } + status = FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; + goto jwt_end; + } /* Verify RSA signature */ verify_ret = oauth2_jwt_verify_signature_rsa( From 4bcf6c4adf8a4c8284c383be90440119b3293cf7 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Fri, 19 Dec 2025 13:04:14 -0600 Subject: [PATCH 36/42] tests: internal: input_chunk_routes: initialize new list Signed-off-by: Eduardo Silva --- tests/internal/input_chunk_routes.c | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/internal/input_chunk_routes.c b/tests/internal/input_chunk_routes.c index d901ccd83ff..acd1ae2dd8e 100644 --- a/tests/internal/input_chunk_routes.c +++ b/tests/internal/input_chunk_routes.c @@ -241,6 +241,7 @@ static void cleanup_test_routing_scenario(struct flb_input_chunk *ic, /* Release properties */ flb_kv_release(&in->properties); flb_kv_release(&in->net_properties); + flb_kv_release(&in->oauth2_jwt_properties); /* Destroy metrics (created by flb_input_instance_init) */ #ifdef FLB_HAVE_METRICS From 6bf92e96f7856d46c9a8def4f546fb4d4923fdcb Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Fri, 19 Dec 2025 13:04:37 -0600 Subject: [PATCH 37/42] oauth2: always validate creation of config_map Signed-off-by: Eduardo Silva --- src/flb_oauth2.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/flb_oauth2.c b/src/flb_oauth2.c index c4f83cdb449..cd7068a2687 100644 --- a/src/flb_oauth2.c +++ b/src/flb_oauth2.c @@ -124,7 +124,7 @@ static void oauth2_reset_state(struct flb_oauth2 *ctx) static void oauth2_apply_defaults(struct flb_oauth2_config *cfg) { - cfg->enabled = FLB_TRUE; + cfg->enabled = FLB_FALSE; cfg->auth_method = FLB_OAUTH2_AUTH_METHOD_BASIC; cfg->refresh_skew = FLB_OAUTH2_DEFAULT_SKEW_SECS; cfg->timeout = 0; @@ -903,6 +903,10 @@ struct mk_list *flb_oauth2_get_config_map(struct flb_config *config) struct mk_list *config_map; config_map = flb_config_map_create(config, oauth2_config_map); + if (!config_map) { + flb_error("[oauth2] error loading OAuth2 config map"); + return NULL; + } return config_map; } From e3df744379f6caf147e695eba40cdc56a63ffe84 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Fri, 19 Dec 2025 13:05:17 -0600 Subject: [PATCH 38/42] oauth2_jwt: code cleanup Signed-off-by: Eduardo Silva --- src/flb_oauth2_jwt.c | 66 +++++++++++++++++--------------------------- 1 file changed, 26 insertions(+), 40 deletions(-) diff --git a/src/flb_oauth2_jwt.c b/src/flb_oauth2_jwt.c index d587cff5276..e623cd011c9 100644 --- a/src/flb_oauth2_jwt.c +++ b/src/flb_oauth2_jwt.c @@ -283,7 +283,7 @@ static int oauth2_jwt_base64url_decode(const char *segment, /* First, count non-whitespace characters */ for (i = 0; i < segment_len; i++) { - if (!isspace((unsigned char)segment[i])) { + if (!isspace((unsigned char) segment[i])) { clean_len++; } } @@ -303,7 +303,7 @@ static int oauth2_jwt_base64url_decode(const char *segment, /* Copy and convert base64url to base64, skipping whitespace */ for (i = 0; i < segment_len && j < clean_len; i++) { - c = (unsigned char)segment[i]; + c = (unsigned char) segment[i]; if (isspace(c)) { continue; /* Skip whitespace */ @@ -340,8 +340,8 @@ static int oauth2_jwt_base64url_decode(const char *segment, padded[padded_len] = '\0'; /* First pass: get required buffer size */ - ret = flb_base64_decode(NULL, 0, decoded_len, - (unsigned char *) padded, padded_len); + ret = flb_base64_decode(NULL, 0, decoded_len, (unsigned char *) padded, padded_len); + /* Note: ret will be FLB_BASE64_ERR_BUFFER_TOO_SMALL (-42) on first pass, this is expected */ if (ret != 0 && ret != FLB_BASE64_ERR_BUFFER_TOO_SMALL) { flb_free(padded); @@ -360,8 +360,7 @@ static int oauth2_jwt_base64url_decode(const char *segment, return FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; } - ret = flb_base64_decode(*decoded, *decoded_len, decoded_len, - (unsigned char *) padded, padded_len); + ret = flb_base64_decode(*decoded, *decoded_len, decoded_len, (unsigned char *) padded, padded_len); flb_free(padded); if (ret != 0) { @@ -431,8 +430,8 @@ static int oauth2_jwt_parse_header(const char *json, size_t json_len, if (v->type == MSGPACK_OBJECT_STR) { key_len = k->via.str.size; val_len = v->via.str.size; - key_str = (const char *)k->via.str.ptr; - val_str = (const char *)v->via.str.ptr; + key_str = (const char *) k->via.str.ptr; + val_str = (const char *) v->via.str.ptr; if (key_len == 3 && strncmp(key_str, "kid", 3) == 0) { claims->kid = flb_sds_create_len(val_str, val_len); @@ -511,7 +510,7 @@ static int oauth2_jwt_parse_payload(const char *json, size_t json_len, } key_len = k->via.str.size; - key_str = (const char *)k->via.str.ptr; + key_str = (const char *) k->via.str.ptr; if (key_len == 3 && strncmp(key_str, "exp", 3) == 0) { if (v->type == MSGPACK_OBJECT_POSITIVE_INTEGER) { @@ -924,14 +923,14 @@ static int oauth2_jwt_verify_signature_rsa(const char *signing_input, /* Decode base64url modulus and exponent */ ret = oauth2_jwt_base64url_decode(modulus_b64, flb_sds_len(modulus_b64), - (unsigned char **)&modulus_bytes, &modulus_len, + (unsigned char **) &modulus_bytes, &modulus_len, FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT); if (ret != FLB_OAUTH2_JWT_OK) { goto cleanup; } ret = oauth2_jwt_base64url_decode(exponent_b64, flb_sds_len(exponent_b64), - (unsigned char **)&exponent_bytes, &exponent_len, + (unsigned char **) &exponent_bytes, &exponent_len, FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT); if (ret != FLB_OAUTH2_JWT_OK) { goto cleanup; @@ -1184,13 +1183,13 @@ static int oauth2_jwt_check_audience(const char *json, size_t json_len, } key_len = k->via.str.size; - key_str = (const char *)k->via.str.ptr; + key_str = (const char *) k->via.str.ptr; if (key_len == 3 && strncmp(key_str, "aud", 3) == 0) { if (v->type == MSGPACK_OBJECT_STR) { /* Single string audience */ if (v->via.str.size == strlen(allowed_audience) && - strncmp((const char *)v->via.str.ptr, allowed_audience, + strncmp((const char *) v->via.str.ptr, allowed_audience, v->via.str.size) == 0) { found_match = 1; } @@ -1201,7 +1200,7 @@ static int oauth2_jwt_check_audience(const char *json, size_t json_len, msgpack_object *elem = &v->via.array.ptr[j]; if (elem->type == MSGPACK_OBJECT_STR) { if (elem->via.str.size == strlen(allowed_audience) && - strncmp((const char *)elem->via.str.ptr, allowed_audience, + strncmp((const char *) elem->via.str.ptr, allowed_audience, elem->via.str.size) == 0) { found_match = 1; break; @@ -1221,7 +1220,8 @@ static int oauth2_jwt_check_audience(const char *json, size_t json_len, static void oauth2_jwt_free_cfg(struct flb_oauth2_jwt_cfg *cfg) { - /* Note: cfg->issuer, cfg->jwks_url, and cfg->allowed_audience are pointers + /* + * Note: cfg->issuer, cfg->jwks_url, and cfg->allowed_audience are pointers * to strings owned by the Fluent Bit configuration system (flb_kv). * They will be freed automatically when the input instance properties are * destroyed, so we should NOT free them here to avoid double-free errors. @@ -1257,9 +1257,6 @@ struct flb_oauth2_jwt_ctx *flb_oauth2_jwt_context_create(struct flb_config *conf return NULL; } - /* Don't download JWKS during initialization - do it lazily on first validation */ - /* This avoids blocking the initialization thread */ - return ctx; } @@ -1374,33 +1371,24 @@ int flb_oauth2_jwt_validate(struct flb_oauth2_jwt_ctx *ctx, } /* Lookup key by kid */ - jwks_key = (struct flb_oauth2_jwks_key *)flb_hash_table_get_ptr(ctx->jwks_cache.entries, - jwt.claims.kid, - flb_sds_len(jwt.claims.kid)); - if (!jwks_key) { - /* Try to refresh JWKS and lookup again */ - ret = oauth2_jwks_fetch_keys(ctx); - if (ret != 0) { - flb_debug("[oauth2_jwt] Failed to refresh JWKS: %d", ret); - status = FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; - goto jwt_end; - } - jwks_key = (struct flb_oauth2_jwks_key *)flb_hash_table_get_ptr(ctx->jwks_cache.entries, - jwt.claims.kid, - flb_sds_len(jwt.claims.kid)); - } - + jwks_key = (struct flb_oauth2_jwks_key *) flb_hash_table_get_ptr(ctx->jwks_cache.entries, + jwt.claims.kid, + flb_sds_len(jwt.claims.kid)); if (!jwks_key) { + /* + * Key not found in cache - reject the token. + * JWKS refresh is time-based only to prevent DoS attacks from malicious tokens with + * random kid values. If key rotation requires faster refresh, configure a shorter + * jwks_refresh_interval. */ flb_debug("[oauth2_jwt] Key with kid '%s' not found in JWKS", jwt.claims.kid); status = FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; goto jwt_end; } /* Verify RSA signature */ - verify_ret = oauth2_jwt_verify_signature_rsa( - jwt.signing_input, flb_sds_len(jwt.signing_input), - jwt.signature, jwt.signature_len, - jwks_key->modulus, jwks_key->exponent); + verify_ret = oauth2_jwt_verify_signature_rsa(jwt.signing_input, flb_sds_len(jwt.signing_input), + jwt.signature, jwt.signature_len, + jwks_key->modulus, jwks_key->exponent); if (verify_ret != 0) { flb_debug("[oauth2_jwt] Signature verification failed: ret=%d", verify_ret); status = FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; @@ -1526,5 +1514,3 @@ struct mk_list *flb_oauth2_jwt_get_config_map(struct flb_config *config) return config_map; } - - From 2ea1e4e64542877ecfc2af10c149a6809d481dd2 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Fri, 19 Dec 2025 13:18:03 -0600 Subject: [PATCH 39/42] oauth2: enforce check of return values Signed-off-by: Eduardo Silva --- src/flb_oauth2.c | 69 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/src/flb_oauth2.c b/src/flb_oauth2.c index cd7068a2687..e4fc4dfd5f7 100644 --- a/src/flb_oauth2.c +++ b/src/flb_oauth2.c @@ -414,6 +414,7 @@ static flb_sds_t oauth2_append_kv(flb_sds_t buffer, const char *key, const char *value) { flb_sds_t tmp; + flb_sds_t result; if (!value) { return buffer; @@ -422,38 +423,60 @@ static flb_sds_t oauth2_append_kv(flb_sds_t buffer, const char *key, tmp = flb_uri_encode(value, strlen(value)); if (!tmp) { flb_errno(); + if (buffer) { + flb_sds_destroy(buffer); + } return NULL; } if (flb_sds_len(buffer) > 0) { - buffer = flb_sds_cat(buffer, "&", 1); - if (!buffer) { + result = flb_sds_cat(buffer, "&", 1); + if (!result) { flb_sds_destroy(tmp); + if (buffer) { + flb_sds_destroy(buffer); + } return NULL; } + buffer = result; } - buffer = flb_sds_cat(buffer, key, strlen(key)); - if (!buffer) { + result = flb_sds_cat(buffer, key, strlen(key)); + if (!result) { flb_sds_destroy(tmp); + if (buffer) { + flb_sds_destroy(buffer); + } return NULL; } + buffer = result; - buffer = flb_sds_cat(buffer, "=", 1); - if (!buffer) { + result = flb_sds_cat(buffer, "=", 1); + if (!result) { flb_sds_destroy(tmp); + if (buffer) { + flb_sds_destroy(buffer); + } return NULL; } + buffer = result; - buffer = flb_sds_cat(buffer, tmp, flb_sds_len(tmp)); + result = flb_sds_cat(buffer, tmp, flb_sds_len(tmp)); flb_sds_destroy(tmp); + if (!result) { + if (buffer) { + flb_sds_destroy(buffer); + } + return NULL; + } - return buffer; + return result; } static flb_sds_t oauth2_build_body(struct flb_oauth2 *ctx) { flb_sds_t body; + flb_sds_t tmp; if (ctx->payload_manual == FLB_TRUE && ctx->payload) { return flb_sds_create_len(ctx->payload, flb_sds_len(ctx->payload)); @@ -464,38 +487,48 @@ static flb_sds_t oauth2_build_body(struct flb_oauth2 *ctx) return NULL; } - body = oauth2_append_kv(body, "grant_type", "client_credentials"); - if (!body) { + tmp = oauth2_append_kv(body, "grant_type", "client_credentials"); + if (!tmp) { + flb_sds_destroy(body); return NULL; } + body = tmp; if (ctx->cfg.scope) { - body = oauth2_append_kv(body, "scope", ctx->cfg.scope); - if (!body) { + tmp = oauth2_append_kv(body, "scope", ctx->cfg.scope); + if (!tmp) { + flb_sds_destroy(body); return NULL; } + body = tmp; } if (ctx->cfg.audience) { - body = oauth2_append_kv(body, "audience", ctx->cfg.audience); - if (!body) { + tmp = oauth2_append_kv(body, "audience", ctx->cfg.audience); + 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) { - body = oauth2_append_kv(body, "client_id", ctx->cfg.client_id); - if (!body) { + tmp = oauth2_append_kv(body, "client_id", ctx->cfg.client_id); + if (!tmp) { + flb_sds_destroy(body); return NULL; } + body = tmp; } if (ctx->cfg.client_secret) { - body = oauth2_append_kv(body, "client_secret", ctx->cfg.client_secret); - if (!body) { + tmp = oauth2_append_kv(body, "client_secret", ctx->cfg.client_secret); + if (!tmp) { + flb_sds_destroy(body); return NULL; } + body = tmp; } } From 7b149f728bc93c91627ede7e453d08ab2ceb7552 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Fri, 19 Dec 2025 13:38:09 -0600 Subject: [PATCH 40/42] crypto: add RSA_set0_key compat for OpenSSL v1.0.2 Signed-off-by: Eduardo Silva --- src/flb_crypto.c | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/flb_crypto.c b/src/flb_crypto.c index 5158d148f0b..5c9329db223 100644 --- a/src/flb_crypto.c +++ b/src/flb_crypto.c @@ -32,6 +32,33 @@ #if OPENSSL_VERSION_NUMBER < 0x10100000L #define EVP_MD_CTX_new() EVP_MD_CTX_create() #define EVP_MD_CTX_free(ctx) EVP_MD_CTX_destroy(ctx) + +/* + * RSA_set0_key was introduced in OpenSSL 1.1.0 + * For OpenSSL 1.0.2, provide compatibility implementation + */ +static int RSA_set0_key(RSA *rsa, BIGNUM *n, BIGNUM *e, BIGNUM *d) +{ + if (n == NULL || e == NULL) { + return 0; + } + + if (rsa->n) { + BN_free(rsa->n); + } + if (rsa->e) { + BN_free(rsa->e); + } + if (rsa->d) { + BN_free(rsa->d); + } + + rsa->n = n; + rsa->e = e; + rsa->d = d; + + return 1; +} #endif static int flb_crypto_get_rsa_padding_type_by_id(int padding_type_id) From 5cdafcefdd033cb5d6468d171c43bb8953e0791b Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Fri, 19 Dec 2025 21:31:16 -0600 Subject: [PATCH 41/42] tests: internal: oauth2: fix windows socket race condition Signed-off-by: Eduardo Silva --- tests/internal/oauth2.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/internal/oauth2.c b/tests/internal/oauth2.c index 259f350872f..a01f9b82f82 100644 --- a/tests/internal/oauth2.c +++ b/tests/internal/oauth2.c @@ -200,6 +200,12 @@ static int oauth2_mock_server_start(struct oauth2_mock_server *server, int expir return -1; } + if (listen(server->listen_fd, 4) < 0) { + flb_errno(); + flb_socket_close(server->listen_fd); + return -1; + } + len = sizeof(addr); if (getsockname(server->listen_fd, (struct sockaddr *) &addr, &len) < 0) { flb_errno(); @@ -208,8 +214,7 @@ static int oauth2_mock_server_start(struct oauth2_mock_server *server, int expir } server->port = ntohs(addr.sin_port); - - if (listen(server->listen_fd, 4) < 0) { + if (server->port == 0) { flb_errno(); flb_socket_close(server->listen_fd); return -1; From d071378ca9e6db218778faf725a4e4a05fa6808f Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Tue, 23 Dec 2025 08:47:02 -0600 Subject: [PATCH 42/42] tests: internal: oauth2: add windows guards for init and cleanup Signed-off-by: Eduardo Silva --- tests/internal/oauth2.c | 62 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/internal/oauth2.c b/tests/internal/oauth2.c index a01f9b82f82..b82f93c06e9 100644 --- a/tests/internal/oauth2.c +++ b/tests/internal/oauth2.c @@ -19,6 +19,7 @@ #else #include #include +#include #endif #include "flb_tests_internal.h" @@ -35,6 +36,9 @@ struct oauth2_mock_server { int expires_in; char latest_token[64]; pthread_t thread; +#ifdef _WIN32 + int wsa_initialized; +#endif }; static void compose_http_response(flb_sockfd_t fd, int status, const char *body) @@ -176,14 +180,34 @@ static int oauth2_mock_server_start(struct oauth2_mock_server *server, int expir int on = 1; struct sockaddr_in addr; socklen_t len; +#ifdef _WIN32 + WSADATA wsa_data; + int wsa_result; +#endif memset(server, 0, sizeof(struct oauth2_mock_server)); server->expires_in = expires_in; server->resource_challenge = resource_challenge; +#ifdef _WIN32 + /* Initialize Winsock on Windows */ + wsa_result = WSAStartup(MAKEWORD(2, 2), &wsa_data); + if (wsa_result != 0) { + flb_errno(); + return -1; + } + server->wsa_initialized = 1; +#endif + server->listen_fd = socket(AF_INET, SOCK_STREAM, 0); if (server->listen_fd == FLB_INVALID_SOCKET) { flb_errno(); +#ifdef _WIN32 + if (server->wsa_initialized) { + WSACleanup(); + server->wsa_initialized = 0; + } +#endif return -1; } @@ -197,19 +221,38 @@ static int oauth2_mock_server_start(struct oauth2_mock_server *server, int expir if (bind(server->listen_fd, (struct sockaddr *) &addr, sizeof(addr)) < 0) { flb_errno(); flb_socket_close(server->listen_fd); +#ifdef _WIN32 + if (server->wsa_initialized) { + WSACleanup(); + server->wsa_initialized = 0; + } +#endif return -1; } if (listen(server->listen_fd, 4) < 0) { flb_errno(); flb_socket_close(server->listen_fd); +#ifdef _WIN32 + if (server->wsa_initialized) { + WSACleanup(); + server->wsa_initialized = 0; + } +#endif return -1; } len = sizeof(addr); + memset(&addr, 0, sizeof(addr)); if (getsockname(server->listen_fd, (struct sockaddr *) &addr, &len) < 0) { flb_errno(); flb_socket_close(server->listen_fd); +#ifdef _WIN32 + if (server->wsa_initialized) { + WSACleanup(); + server->wsa_initialized = 0; + } +#endif return -1; } @@ -217,6 +260,12 @@ static int oauth2_mock_server_start(struct oauth2_mock_server *server, int expir if (server->port == 0) { flb_errno(); flb_socket_close(server->listen_fd); +#ifdef _WIN32 + if (server->wsa_initialized) { + WSACleanup(); + server->wsa_initialized = 0; + } +#endif return -1; } @@ -225,6 +274,12 @@ static int oauth2_mock_server_start(struct oauth2_mock_server *server, int expir if (pthread_create(&server->thread, NULL, oauth2_mock_server_thread, server) != 0) { printf("pthread_create failed: %s\n", strerror(errno)); flb_socket_close(server->listen_fd); +#ifdef _WIN32 + if (server->wsa_initialized) { + WSACleanup(); + server->wsa_initialized = 0; + } +#endif return -1; } printf("server started on port %d\n", server->port); @@ -283,7 +338,14 @@ static void oauth2_mock_server_stop(struct oauth2_mock_server *server) shutdown(server->listen_fd, SHUT_RDWR); pthread_join(server->thread, NULL); flb_socket_close(server->listen_fd); + server->listen_fd = FLB_INVALID_SOCKET; + } +#ifdef _WIN32 + if (server->wsa_initialized) { + WSACleanup(); + server->wsa_initialized = 0; } +#endif } static struct flb_oauth2 *create_oauth_ctx(struct flb_config *config,