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: 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 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/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/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/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/include/fluent-bit/flb_oauth2_jwt.h b/include/fluent-bit/flb_oauth2_jwt.h new file mode 100644 index 00000000000..99412cd7c10 --- /dev/null +++ b/include/fluent-bit/flb_oauth2_jwt.h @@ -0,0 +1,119 @@ +/* -*- 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; + int has_azp; +}; + +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/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/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); 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; } } 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..b59817c1e84 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,23 @@ 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) { + /* Store pointer directly - config map owns this string and will free it */ + ctx->oauth2_auth_method = (flb_sds_t) tmp; + } + } + 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 +318,49 @@ 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; + } + } + /* Set instance flags into upstream */ flb_output_upstream_set(ctx->u, ins); @@ -324,6 +390,22 @@ 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. + */ + } + flb_free(ctx->proxy_host); flb_free(ctx->uri); flb_free(ctx); 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(); 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_crypto.c b/src/flb_crypto.c index c2811039a27..5c9329db223 100644 --- a/src/flb_crypto.c +++ b/src/flb_crypto.c @@ -17,8 +17,50 @@ #include #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) + +/* + * 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) { int result; @@ -111,6 +153,116 @@ 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; + BIGNUM *n_dup = NULL; + BIGNUM *e_dup = 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; + } + + 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; + } + + 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_dup) { + BN_free(n_dup); + } + if (e_dup) { + BN_free(e_dup); + } + 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, @@ -401,5 +553,152 @@ 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; + } + +#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) { + 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; +} + diff --git a/src/flb_http_client.c b/src/flb_http_client.c index 810e172fe83..4b08aa9a733 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,66 @@ 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; + struct flb_upstream *u; + + 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) { + u = c->u_conn->upstream; + flb_upstream_conn_release(c->u_conn); + c->u_conn = flb_upstream_conn_get(u); + 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 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) { diff --git a/src/flb_oauth2.c b/src/flb_oauth2.c index 866fbe8e132..e4fc4dfd5f7 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.enable", "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,132 @@ 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_FALSE; + cfg->auth_method = FLB_OAUTH2_AUTH_METHOD_BASIC; + 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; +} - /* 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(); + flb_oauth2_config_destroy(dst); + 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(); + flb_oauth2_config_destroy(dst); + 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(); + flb_oauth2_config_destroy(dst); + 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(); + flb_oauth2_config_destroy(dst); + return -1; + } + } + + if (src->audience) { + dst->audience = flb_sds_create(src->audience); + if (!dst->audience) { + flb_errno(); + flb_oauth2_config_destroy(dst); + 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 +248,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 +298,250 @@ 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; + flb_sds_t result; - 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); - - if (ret != 0) { - flb_debug("[oauth2] http client creation error"); - + tmp = flb_uri_encode(value, strlen(value)); + if (!tmp) { + flb_errno(); + if (buffer) { + flb_sds_destroy(buffer); + } 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)); - - if (request == NULL) { - flb_stream_enable_flags(&ctx->u->base, FLB_IO_IPV6); + if (flb_sds_len(buffer) > 0) { + result = flb_sds_cat(buffer, "&", 1); + if (!result) { + flb_sds_destroy(tmp); + if (buffer) { + flb_sds_destroy(buffer); + } + return NULL; + } + buffer = result; + } - 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); + result = flb_sds_cat(buffer, key, strlen(key)); + if (!result) { + flb_sds_destroy(tmp); + if (buffer) { + flb_sds_destroy(buffer); + } + return NULL; + } + buffer = result; - 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); + result = flb_sds_cat(buffer, "=", 1); + if (!result) { + flb_sds_destroy(tmp); + if (buffer) { + flb_sds_destroy(buffer); + } + return NULL; + } + buffer = result; - return NULL; + result = flb_sds_cat(buffer, tmp, flb_sds_len(tmp)); + flb_sds_destroy(tmp); + if (!result) { + if (buffer) { + flb_sds_destroy(buffer); } + return NULL; } - response = flb_http_client_request_execute(request); + return result; +} - if (response == NULL) { - flb_debug("[oauth2] http request execution error"); +static flb_sds_t oauth2_build_body(struct flb_oauth2 *ctx) +{ + flb_sds_t body; + flb_sds_t tmp; - flb_http_client_request_destroy(request, FLB_TRUE); - flb_http_client_ng_destroy(&http_client); + if (ctx->payload_manual == FLB_TRUE && ctx->payload) { + return flb_sds_create_len(ctx->payload, flb_sds_len(ctx->payload)); + } + 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); + tmp = oauth2_append_kv(body, "grant_type", "client_credentials"); + if (!tmp) { + flb_sds_destroy(body); + return NULL; } + body = tmp; - /* 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) { + tmp = oauth2_append_kv(body, "scope", ctx->cfg.scope); + if (!tmp) { + flb_sds_destroy(body); + return NULL; + } + body = tmp; + } - flb_http_client_request_destroy(request, FLB_TRUE); - flb_http_client_ng_destroy(&http_client); + if (ctx->cfg.audience) { + tmp = oauth2_append_kv(body, "audience", ctx->cfg.audience); + if (!tmp) { + flb_sds_destroy(body); + return NULL; + } + body = tmp; + } - 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) { + tmp = oauth2_append_kv(body, "client_id", ctx->cfg.client_id); + if (!tmp) { + flb_sds_destroy(body); + return NULL; + } + body = tmp; + } - return ctx->access_token; + if (ctx->cfg.client_secret) { + tmp = oauth2_append_kv(body, "client_secret", ctx->cfg.client_secret); + if (!tmp) { + flb_sds_destroy(body); + return NULL; + } + body = tmp; } } - 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 +550,360 @@ 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 ret; + 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) { + 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; + } + + 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; +} + +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 +924,23 @@ 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); + if (!config_map) { + flb_error("[oauth2] error loading OAuth2 config map"); + return NULL; + } + + return config_map; +} + diff --git a/src/flb_oauth2_jwt.c b/src/flb_oauth2_jwt.c new file mode 100644 index 00000000000..e623cd011c9 --- /dev/null +++ b/src/flb_oauth2_jwt.c @@ -0,0 +1,1516 @@ +/* -*- 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 int 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 0; + } + + /* 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"); + return -1; + } + + /* Restore refresh interval */ + cache->refresh_interval = refresh_interval; + return 0; +} + +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_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 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) { + /* + * 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) { + 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); + claims->has_azp = FLB_TRUE; + } + } + else if (key_len == 9 && strncmp(key_str, "client_id", 9) == 0) { + if (v->type == MSGPACK_OBJECT_STR) { + /* 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); + } + } + } + } + + 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_size(parts_len[0] + 1 + 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'; + 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); + 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(msgpack_object *key_obj, + struct flb_oauth2_jwks_key **key_out) +{ + 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; + + if (!key_obj || !key_out) { + return -1; + } + + if (key_obj->type != MSGPACK_OBJECT_MAP) { + return -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 (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, "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 (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 (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 (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); + } + 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); + 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 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; + + /* 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 */ + 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 (k->type != MSGPACK_OBJECT_STR) { + continue; + } + + 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 */ + 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 != MSGPACK_OBJECT_MAP) { + continue; + } + + 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, + flb_sds_len(jwks_key->kid), + jwks_key, 0); + keys_found++; + } + } + break; + } + } + + flb_free(mp_buf); + msgpack_unpacked_destroy(&result); + + 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 ret_code = -1; + 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 || !uri) { + flb_error("[oauth2_jwt] invalid JWKS URL components"); + 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) { + 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; + } + + /* Clear existing cache entries before refreshing to ensure revoked/rotated keys are removed */ + 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); + 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); + ret_code = 0; + } + +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 ret_code; +} + +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 + * 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)); + } + 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) { + flb_free(ctx); + return NULL; + } + + 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) { + /* + * 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); + 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) { + /* 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; + } + } + + /* 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; +} diff --git a/src/flb_output.c b/src/flb_output.c index 307905b3379..e5958b683e8 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,37 @@ 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) { + 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 +1552,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) { diff --git a/tests/internal/CMakeLists.txt b/tests/internal/CMakeLists.txt index 45d769b9c25..44034b127b3 100644 --- a/tests/internal/CMakeLists.txt +++ b/tests/internal/CMakeLists.txt @@ -64,6 +64,8 @@ if(FLB_TLS) set(UNIT_TESTS_FILES ${UNIT_TESTS_FILES} upstream_tls.c + oauth2_jwt.c + oauth2.c ) endif() diff --git a/tests/internal/input_chunk_routes.c b/tests/internal/input_chunk_routes.c index 489e04a0bf1..acd1ae2dd8e 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); @@ -240,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 diff --git a/tests/internal/oauth2.c b/tests/internal/oauth2.c new file mode 100644 index 00000000000..b82f93c06e9 --- /dev/null +++ b/tests/internal/oauth2.c @@ -0,0 +1,446 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#ifndef _WIN32 +#include +#include +#include +#else +#include +#include +#include +#endif + +#include "flb_tests_internal.h" + +#define MOCK_BODY_SIZE 1024 + +struct oauth2_mock_server { + flb_sockfd_t 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; +#ifdef _WIN32 + int wsa_initialized; +#endif +}; + +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); + } + + 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 : ""); + + 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, flb_sockfd_t 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, flb_sockfd_t 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; + 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); + FD_SET(server->listen_fd, &rfds); + tv.tv_sec = 0; + tv.tv_usec = 200000; + + if (select((int)(server->listen_fd + 1), &rfds, NULL, NULL, &tv) <= 0) { + continue; + } + + client_fd = accept(server->listen_fd, NULL, NULL); + if (client_fd == FLB_INVALID_SOCKET) { + continue; + } + + /* Read the full HTTP request - loop until we get the complete request */ + memset(buffer, 0, sizeof(buffer)); + 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); + } + else if (strstr(buffer, "/resource")) { + handle_resource_request(server, client_fd, buffer); + } + + flb_socket_close(client_fd); + } + + return NULL; +} + +static int oauth2_mock_server_start(struct oauth2_mock_server *server, int expires_in, + int resource_challenge) +{ + 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; + } + + setsockopt(server->listen_fd, SOL_SOCKET, SO_REUSEADDR, (const char *)&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) { + 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; + } + + server->port = ntohs(addr.sin_port); + 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; + } + + flb_net_socket_nonblocking(server->listen_fd); + + 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); + 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 != FLB_INVALID_SOCKET) { + server->stop = 1; + 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, + 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); + +#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); + 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} +}; 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} };