diff --git a/include/fluent-bit/flb_input.h b/include/fluent-bit/flb_input.h index 7a92b2dad0b..8fdea0a6e1e 100644 --- a/include/fluent-bit/flb_input.h +++ b/include/fluent-bit/flb_input.h @@ -74,6 +74,7 @@ * private key and certificate are required. */ #define FLB_INPUT_HTTP_SERVER 4096 /* input uses the generic HTTP server */ +#define FLB_INPUT_OAUTH2_JWT 8192 /* input supports OAuth2 JWT validation */ /* Input status */ #define FLB_INPUT_RUNNING 1 diff --git a/include/fluent-bit/flb_output.h b/include/fluent-bit/flb_output.h index 8e2b8fc3512..c78a8b6b4f2 100644 --- a/include/fluent-bit/flb_output.h +++ b/include/fluent-bit/flb_output.h @@ -85,6 +85,7 @@ int flb_chunk_trace_output(struct flb_chunk_trace *trace, struct flb_output_inst #define FLB_OUTPUT_PRIVATE 1024 #define FLB_OUTPUT_SYNCHRONOUS 2048 /* run one task at a time, no flush cycle limit */ #define FLB_OUTPUT_HTTP_SERVER 4096 /* output uses the generic HTTP server */ +#define FLB_OUTPUT_OAUTH2_CLIENT 8192 /* output supports OAuth2 authentication */ /* diff --git a/plugins/in_http/http.c b/plugins/in_http/http.c index c8e4d3cae82..09a43d870f7 100644 --- a/plugins/in_http/http.c +++ b/plugins/in_http/http.c @@ -192,5 +192,6 @@ struct flb_input_plugin in_http_plugin = { .cb_resume = NULL, .cb_exit = in_http_exit, .config_map = config_map, - .flags = FLB_INPUT_NET_SERVER | FLB_INPUT_HTTP_SERVER | FLB_IO_OPT_TLS + .flags = FLB_INPUT_NET_SERVER | FLB_INPUT_HTTP_SERVER | + FLB_INPUT_OAUTH2_JWT | FLB_IO_OPT_TLS }; diff --git a/plugins/in_opentelemetry/opentelemetry.c b/plugins/in_opentelemetry/opentelemetry.c index d4229941d4f..e5bd8c885f3 100644 --- a/plugins/in_opentelemetry/opentelemetry.c +++ b/plugins/in_opentelemetry/opentelemetry.c @@ -201,5 +201,6 @@ struct flb_input_plugin in_opentelemetry_plugin = { .cb_resume = NULL, .cb_exit = in_opentelemetry_exit, .config_map = config_map, - .flags = FLB_INPUT_NET_SERVER | FLB_INPUT_HTTP_SERVER | FLB_IO_OPT_TLS + .flags = FLB_INPUT_NET_SERVER | FLB_INPUT_HTTP_SERVER | + FLB_INPUT_OAUTH2_JWT | FLB_IO_OPT_TLS }; diff --git a/plugins/out_http/http.c b/plugins/out_http/http.c index 79f7d9dfa1a..09f9dc533ef 100644 --- a/plugins/out_http/http.c +++ b/plugins/out_http/http.c @@ -943,6 +943,6 @@ struct flb_output_plugin out_http_plugin = { /* for testing */ .test_formatter.callback = cb_http_format_test, - .flags = FLB_OUTPUT_NET | FLB_IO_OPT_TLS, + .flags = FLB_OUTPUT_NET | FLB_OUTPUT_OAUTH2_CLIENT | FLB_IO_OPT_TLS, .workers = 2 }; diff --git a/plugins/out_opentelemetry/opentelemetry.c b/plugins/out_opentelemetry/opentelemetry.c index 50bd7e05edc..3b24642bb8b 100644 --- a/plugins/out_opentelemetry/opentelemetry.c +++ b/plugins/out_opentelemetry/opentelemetry.c @@ -1288,7 +1288,7 @@ struct flb_output_plugin out_opentelemetry_plugin = { .cb_exit = cb_opentelemetry_exit, .config_map = config_map, .event_type = FLB_OUTPUT_LOGS | FLB_OUTPUT_METRICS | FLB_OUTPUT_TRACES | FLB_OUTPUT_PROFILES, - .flags = FLB_OUTPUT_NET | FLB_IO_OPT_TLS, + .flags = FLB_OUTPUT_NET | FLB_OUTPUT_OAUTH2_CLIENT | FLB_IO_OPT_TLS, .test_formatter.callback = opentelemetry_format_test, }; diff --git a/src/flb_input.c b/src/flb_input.c index e990eee238c..abed1db9b00 100644 --- a/src/flb_input.c +++ b/src/flb_input.c @@ -771,7 +771,14 @@ int flb_input_set_property(struct flb_input_instance *ins, } kv->val = tmp; } - else if (strncasecmp("oauth2", k, 6) == 0 && tmp) { + else if (strncasecmp("oauth2.", k, 7) == 0 && tmp) { + if ((ins->p->flags & FLB_INPUT_OAUTH2_JWT) == 0) { + flb_error("[config] input plugin '%s' does not support OAuth2 JWT validation", + ins->p->name); + flb_sds_destroy(tmp); + return -1; + } + kv = flb_kv_item_create(&ins->oauth2_jwt_properties, (char *) k, NULL); if (!kv) { if (tmp) { @@ -1331,6 +1338,12 @@ int flb_input_oauth2_jwt_property_check(struct flb_input_instance *ins, * it might receive OAuth2 JWT settings. */ if (mk_list_size(&ins->oauth2_jwt_properties) > 0) { + if ((ins->p->flags & FLB_INPUT_OAUTH2_JWT) == 0) { + flb_error("[config] input plugin '%s' does not support OAuth2 JWT validation", + ins->p->name); + return -1; + } + ret = flb_config_map_properties_check(ins->p->name, &ins->oauth2_jwt_properties, ins->oauth2_jwt_config_map); diff --git a/src/flb_oauth2.c b/src/flb_oauth2.c index 90e63ecae58..44dcafb9d93 100644 --- a/src/flb_oauth2.c +++ b/src/flb_oauth2.c @@ -469,6 +469,7 @@ int flb_oauth2_parse_json_response(const char *json_data, size_t json_size, jsmntok_t *tokens; char tmp_num[32]; uint64_t new_expires_in = 0; + int expires_in_present = FLB_FALSE; jsmn_init(&parser); tokens = flb_calloc(1, sizeof(jsmntok_t) * tokens_size); @@ -537,6 +538,8 @@ int flb_oauth2_parse_json_response(const char *json_data, size_t json_size, } } else if (key_cmp(key, key_len, "expires_in") == 0) { + expires_in_present = FLB_TRUE; + if (val_len <= 0 || val_len >= sizeof(tmp_num)) { break; } @@ -557,18 +560,22 @@ int flb_oauth2_parse_json_response(const char *json_data, size_t json_size, } new_expires_in = parsed_expires_in; - new_expires_in -= (new_expires_in / 10); } } flb_free(tokens); - if (!new_access_token || !new_token_type || new_expires_in <= ctx->refresh_skew) { + if (!new_access_token || !new_token_type || + (expires_in_present == FLB_TRUE && new_expires_in == 0)) { flb_sds_destroy(new_access_token); flb_sds_destroy(new_token_type); return -1; } + if (expires_in_present == FLB_FALSE) { + new_expires_in = FLB_OAUTH2_DEFAULT_EXPIRES; + } + oauth2_reset_state(ctx); ctx->access_token = new_access_token; @@ -1195,6 +1202,7 @@ static int oauth2_refresh_locked(struct flb_oauth2 *ctx) static int oauth2_token_needs_refresh(struct flb_oauth2 *ctx, int force_refresh) { time_t now; + time_t refresh_skew; if (force_refresh) { return FLB_TRUE; @@ -1210,7 +1218,12 @@ static int oauth2_token_needs_refresh(struct flb_oauth2 *ctx, int force_refresh) return FLB_TRUE; } - if (now >= (ctx->expires_at - ctx->refresh_skew)) { + refresh_skew = ctx->refresh_skew; + if (ctx->expires_in <= (uint64_t) refresh_skew) { + refresh_skew = ctx->expires_in / 10; + } + + if (now >= (ctx->expires_at - refresh_skew)) { return FLB_TRUE; } diff --git a/src/flb_oauth2_jwt.c b/src/flb_oauth2_jwt.c index 57557c7c1e1..43edd162e0a 100644 --- a/src/flb_oauth2_jwt.c +++ b/src/flb_oauth2_jwt.c @@ -125,47 +125,6 @@ static void oauth2_jwks_key_destroy(struct flb_oauth2_jwks_key *key) 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; @@ -904,10 +863,24 @@ static int oauth2_jwks_parse_json(flb_sds_t jwks_json, struct flb_oauth2_jwks_ca ret = oauth2_jwks_parse_key(key_obj, &jwks_key); if (ret == 0 && jwks_key) { + if (flb_hash_table_get_ptr(cache->entries, jwks_key->kid, + flb_sds_len(jwks_key->kid))) { + flb_error("[oauth2_jwt] duplicate key ID in JWKS: %s", + jwks_key->kid); + oauth2_jwks_key_destroy(jwks_key); + keys_found = -1; + break; + } + /* 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); + ret = flb_hash_table_add(cache->entries, jwks_key->kid, + flb_sds_len(jwks_key->kid), + jwks_key, 0); + if (ret < 0) { + oauth2_jwks_key_destroy(jwks_key); + keys_found = -1; + break; + } keys_found++; } } @@ -918,6 +891,11 @@ static int oauth2_jwks_parse_json(flb_sds_t jwks_json, struct flb_oauth2_jwks_ca flb_free(mp_buf); msgpack_unpacked_destroy(&result); + if (keys_found < 0) { + flb_error("[oauth2_jwt] failed to build JWKS cache"); + return -1; + } + if (keys_found == 0) { flb_error("[oauth2_jwt] No valid keys found in JWKS"); return -1; @@ -994,7 +972,9 @@ static int oauth2_jwks_fetch_keys(struct flb_oauth2_jwt_ctx *ctx) struct flb_connection *u_conn = NULL; struct flb_http_client *c = NULL; struct flb_tls *tls = NULL; + struct flb_oauth2_jwks_cache new_cache = {0}; flb_sds_t jwks_json = NULL; + int new_cache_initialized = FLB_FALSE; if (!ctx || !ctx->cfg.jwks_url || !ctx->config) { return -1; @@ -1092,26 +1072,31 @@ static int oauth2_jwks_fetch_keys(struct flb_oauth2_jwt_ctx *ctx) goto cleanup; } - /* Clear existing cache entries before refreshing to ensure revoked/rotated keys are removed */ - ret = oauth2_jwks_cache_clear(&ctx->jwks_cache); + ret = oauth2_jwks_cache_init(&new_cache, ctx->jwks_cache.refresh_interval); if (ret != 0) { - flb_error("[oauth2_jwt] failed to clear JWKS cache"); + flb_error("[oauth2_jwt] failed to initialize refreshed JWKS cache"); goto cleanup; } + new_cache_initialized = FLB_TRUE; - /* Parse JWKS JSON and store keys in cache */ - ret = oauth2_jwks_parse_json(jwks_json, &ctx->jwks_cache); + /* Build the replacement cache completely before discarding valid keys. */ + ret = oauth2_jwks_parse_json(jwks_json, &new_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; + goto cleanup; } + new_cache.last_refresh = time(NULL); + oauth2_jwks_cache_destroy(&ctx->jwks_cache); + ctx->jwks_cache = new_cache; + memset(&new_cache, 0, sizeof(new_cache)); + new_cache_initialized = FLB_FALSE; + ret_code = 0; + cleanup: + if (new_cache_initialized == FLB_TRUE) { + oauth2_jwks_cache_destroy(&new_cache); + } if (jwks_json) { flb_sds_destroy(jwks_json); } @@ -1392,8 +1377,12 @@ int flb_oauth2_jwt_validate(struct flb_oauth2_jwt_ctx *ctx, ret = oauth2_jwks_fetch_keys(ctx); if (ret != 0) { flb_debug("[oauth2_jwt] Failed to fetch JWKS: %d", ret); - status = FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; - goto jwt_end; + if (ctx->jwks_cache.last_refresh == 0) { + status = FLB_OAUTH2_JWT_ERR_INVALID_ARGUMENT; + goto jwt_end; + } + + flb_warn("[oauth2_jwt] using cached keys after JWKS refresh failure"); } } diff --git a/src/flb_output.c b/src/flb_output.c index 26f66ad9e36..f87e135c59c 100644 --- a/src/flb_output.c +++ b/src/flb_output.c @@ -976,7 +976,14 @@ int flb_output_set_property(struct flb_output_instance *ins, } kv->val = tmp; } - else if (strncasecmp("oauth2", k, 6) == 0 && tmp) { + else if (strncasecmp("oauth2.", k, 7) == 0 && tmp) { + if ((ins->p->flags & FLB_OUTPUT_OAUTH2_CLIENT) == 0) { + flb_error("[config] output plugin '%s' does not support OAuth2 authentication", + ins->p->name); + flb_sds_destroy(tmp); + return -1; + } + kv = flb_kv_item_create(&ins->oauth2_properties, (char *) k, NULL); if (!kv) { if (tmp) { @@ -1234,6 +1241,12 @@ int flb_output_oauth2_property_check(struct flb_output_instance *ins, * it might receive OAuth2 settings. */ if (mk_list_size(&ins->oauth2_properties) > 0) { + if ((ins->p->flags & FLB_OUTPUT_OAUTH2_CLIENT) == 0) { + flb_error("[config] output plugin '%s' does not support OAuth2 authentication", + ins->p->name); + return -1; + } + ret = flb_config_map_properties_check(ins->p->name, &ins->oauth2_properties, ins->oauth2_config_map); diff --git a/tests/integration/scenarios/in_http/config/in_http_oauth2_refresh.yaml b/tests/integration/scenarios/in_http/config/in_http_oauth2_refresh.yaml new file mode 100644 index 00000000000..c4bbd31e2a3 --- /dev/null +++ b/tests/integration/scenarios/in_http/config/in_http_oauth2_refresh.yaml @@ -0,0 +1,25 @@ +service: + flush: 1 + log_level: info + http_server: on + http_port: ${FLUENT_BIT_HTTP_MONITORING_PORT} + +pipeline: + inputs: + - name: http + port: ${FLUENT_BIT_TEST_LISTENER_PORT} + oauth2.validate: true + oauth2.issuer: issuer + oauth2.jwks_url: http://127.0.0.1:${TEST_SUITE_HTTP_PORT}/jwks + oauth2.jwks_refresh_interval: 1 + oauth2.allowed_audience: audience + oauth2.allowed_clients: client1 + + outputs: + - name: http + match: '*' + host: 127.0.0.1 + port: ${TEST_SUITE_HTTP_PORT} + uri: /data + format: json + json_date_key: false diff --git a/tests/integration/scenarios/in_http/tests/test_in_http_001.py b/tests/integration/scenarios/in_http/tests/test_in_http_001.py index 519af7ae538..08a8dda7779 100644 --- a/tests/integration/scenarios/in_http/tests/test_in_http_001.py +++ b/tests/integration/scenarios/in_http/tests/test_in_http_001.py @@ -7,7 +7,7 @@ import pytest import requests -from server.http_server import data_storage, http_server_run +from server.http_server import configure_jwks_response, data_storage, http_server_run from utils.http_matrix import PROTOCOL_CASES, run_curl_request from utils.test_service import FluentBitTestService @@ -229,6 +229,36 @@ def test_in_http_oauth2_accepts_valid_jwt(): assert forwarded_payloads[0][0]["message"] == "Este es un mensaje de prueba" +def test_in_http_oauth2_keeps_cached_keys_after_bad_jwks_refresh(): + service = Service("in_http_oauth2_refresh.yaml") + service.start() + headers = [ + "Content-Type: application/json", + f"Authorization: Bearer {MOCK_VALID_JWT}", + ] + + first_result = run_curl_request( + f"http://localhost:{service.flb_listener_port}/", + create_payload("sample_data.json"), + headers=headers, + http_mode="http1.1", + ) + + configure_jwks_response(body="{malformed", content_type="application/json") + time.sleep(1.2) + + second_result = run_curl_request( + f"http://localhost:{service.flb_listener_port}/", + create_payload("sample_data.json"), + headers=headers, + http_mode="http1.1", + ) + service.stop() + + assert first_result["status_code"] == 201 + assert second_result["status_code"] == 201 + + class Service: def __init__(self, config_file): self.config_file = os.path.abspath(os.path.join(os.path.dirname(__file__), '../config/', config_file)) diff --git a/tests/integration/scenarios/out_http/config/unsupported_oauth2_input.yaml b/tests/integration/scenarios/out_http/config/unsupported_oauth2_input.yaml new file mode 100644 index 00000000000..e4b0459e14b --- /dev/null +++ b/tests/integration/scenarios/out_http/config/unsupported_oauth2_input.yaml @@ -0,0 +1,12 @@ +service: + flush: 1 + log_level: info + +pipeline: + inputs: + - name: elasticsearch + oauth2.validate: true + + outputs: + - name: 'null' + match: '*' diff --git a/tests/integration/scenarios/out_http/config/unsupported_oauth2_output.yaml b/tests/integration/scenarios/out_http/config/unsupported_oauth2_output.yaml new file mode 100644 index 00000000000..06d3e932443 --- /dev/null +++ b/tests/integration/scenarios/out_http/config/unsupported_oauth2_output.yaml @@ -0,0 +1,13 @@ +service: + flush: 1 + log_level: info + +pipeline: + inputs: + - name: dummy + tag: test + + outputs: + - name: stdout + match: '*' + oauth2.enable: true diff --git a/tests/integration/scenarios/out_http/tests/test_out_http_001.py b/tests/integration/scenarios/out_http/tests/test_out_http_001.py index 21c963643b8..80e0555e29f 100644 --- a/tests/integration/scenarios/out_http/tests/test_out_http_001.py +++ b/tests/integration/scenarios/out_http/tests/test_out_http_001.py @@ -4,6 +4,7 @@ import time import requests +import pytest from server.http_server import ( configure_http_response, @@ -13,6 +14,7 @@ server_instances, ) from utils.test_service import FluentBitTestService +from utils.fluent_bit_manager import FluentBitManager, FluentBitStartupError logger = logging.getLogger(__name__) @@ -213,6 +215,51 @@ def test_out_http_oauth2_basic_adds_bearer_token(): assert data_request["headers"].get("Authorization") == "Bearer oauth-access-token" +@pytest.mark.parametrize("expires_in", [30, None]) +def test_out_http_oauth2_accepts_short_or_missing_token_lifetime(expires_in): + body = {"access_token": "oauth-access-token", "token_type": "Bearer"} + if expires_in is not None: + body["expires_in"] = expires_in + + service = Service("out_http_oauth2_basic.yaml") + service.start() + configure_http_response(status_code=200, body={"status": "received"}) + configure_oauth_token_response(status_code=200, body=body) + + requests_seen = service.wait_for_requests(2) + service.stop() + + data_request = next(request for request in requests_seen if request["path"] == "/data") + assert data_request["headers"].get("Authorization") == "Bearer oauth-access-token" + + +@pytest.mark.parametrize( + "config_file, plugin_type, plugin_name", + [ + ("unsupported_oauth2_input.yaml", "input", "elasticsearch"), + ("unsupported_oauth2_output.yaml", "output", "stdout"), + ], +) +def test_oauth2_properties_fail_for_unsupported_plugins( + config_file, plugin_type, plugin_name +): + config_path = os.path.abspath( + os.path.join(os.path.dirname(__file__), "../config", config_file) + ) + manager = FluentBitManager(config_path) + + with pytest.raises(FluentBitStartupError): + try: + manager.start() + finally: + manager.stop() + + with open(manager.log_file, encoding="utf-8") as log_file: + log_text = log_file.read() + + assert f"{plugin_type} plugin '{plugin_name}' does not support OAuth2" in log_text + + def test_out_http_oauth2_private_key_jwt_adds_bearer_token(): service = Service("out_http_oauth2_private_key_jwt.yaml") service.start() diff --git a/tests/integration/src/server/http_server.py b/tests/integration/src/server/http_server.py index 7295fffe12d..43f16f12362 100644 --- a/tests/integration/src/server/http_server.py +++ b/tests/integration/src/server/http_server.py @@ -59,6 +59,11 @@ "expires_in": 300, }, } +jwks_response = { + "status_code": 200, + "body": MOCK_JWKS_BODY, + "content_type": "application/json", +} logger = logging.getLogger(__name__) server_thread = None server_instances = [] @@ -114,6 +119,13 @@ def reset_http_server_state(): }, } ) + jwks_response.update( + { + "status_code": 200, + "body": MOCK_JWKS_BODY, + "content_type": "application/json", + } + ) def configure_http_response(*, status_code=UNSET, body=UNSET, content_type=UNSET, @@ -164,6 +176,15 @@ def configure_oauth_token_response(*, status_code=UNSET, body=UNSET, oauth_token_response["hang_after_fragment_index"] = hang_after_fragment_index +def configure_jwks_response(*, status_code=UNSET, body=UNSET, content_type=UNSET): + if status_code is not UNSET: + jwks_response["status_code"] = status_code + if body is not UNSET: + jwks_response["body"] = body + if content_type is not UNSET: + jwks_response["content_type"] = content_type + + def _stream_fragments(config): hang_after_fragment_index = config["hang_after_fragment_index"] @@ -272,7 +293,13 @@ def receive_splunk_hec(): @app.route('/jwks', methods=['GET']) def jwks(): - return jsonify(MOCK_JWKS_BODY), 200 + body = jwks_response["body"] + status_code = jwks_response["status_code"] + + if isinstance(body, (dict, list)): + return jsonify(body), status_code + + return Response(body, status=status_code, content_type=jwks_response["content_type"]) @app.route('/oauth/token', methods=['POST']) diff --git a/tests/internal/oauth2.c b/tests/internal/oauth2.c index 9df0817d821..b9506f0a182 100644 --- a/tests/internal/oauth2.c +++ b/tests/internal/oauth2.c @@ -925,7 +925,7 @@ void test_parse_refreshes_token_transactionally(void) TEST_CHECK(ret == 0); TEST_CHECK(strcmp(ctx.access_token, "new-token") == 0); TEST_CHECK(strcmp(ctx.token_type, "Bearer") == 0); - TEST_CHECK(ctx.expires_in == 3240); + TEST_CHECK(ctx.expires_in == 3600); destroy_parse_ctx(&ctx); } @@ -944,7 +944,7 @@ void test_parse_accepts_quoted_expires_in(void) TEST_CHECK(ret == 0); TEST_CHECK(strcmp(ctx.access_token, "quoted-token") == 0); TEST_CHECK(strcmp(ctx.token_type, "Bearer") == 0); - TEST_CHECK(ctx.expires_in == 3240); + TEST_CHECK(ctx.expires_in == 3600); destroy_parse_ctx(&ctx); } @@ -964,7 +964,7 @@ void test_parse_duplicate_keys_last_wins(void) TEST_CHECK(ret == 0); TEST_CHECK(strcmp(ctx.access_token, "second") == 0); TEST_CHECK(strcmp(ctx.token_type, "Bearer") == 0); - TEST_CHECK(ctx.expires_in == 3240); + TEST_CHECK(ctx.expires_in == 3600); destroy_parse_ctx(&ctx); } @@ -977,11 +977,10 @@ void test_parse_rejects_missing_required_fields(void) const char *payloads[] = { "{\"token_type\":\"Bearer\",\"expires_in\":3600}", "{\"access_token\":\"new-token\",\"expires_in\":3600}", - "{\"access_token\":\"new-token\",\"token_type\":\"Bearer\"}", "{\"error\":\"invalid_request\"}" }; - for (index = 0; index < 4; index++) { + for (index = 0; index < 3; index++) { memset(&ctx, 0, sizeof(ctx)); populate_parse_ctx(&ctx, "old-token", "OldBearer", 1200); ctx.refresh_skew = FLB_OAUTH2_DEFAULT_SKEW_SECS; @@ -999,6 +998,42 @@ void test_parse_rejects_missing_required_fields(void) } } +void test_parse_defaults_missing_expires_in(void) +{ + int ret; + struct flb_oauth2 ctx = {0}; + const char *payload = "{\"access_token\":\"new-token\"," + "\"token_type\":\"Bearer\"}"; + + ctx.refresh_skew = FLB_OAUTH2_DEFAULT_SKEW_SECS; + ret = flb_oauth2_parse_json_response(payload, strlen(payload), &ctx); + + TEST_CHECK(ret == 0); + TEST_CHECK(strcmp(ctx.access_token, "new-token") == 0); + TEST_CHECK(strcmp(ctx.token_type, "Bearer") == 0); + TEST_CHECK(ctx.expires_in == FLB_OAUTH2_DEFAULT_EXPIRES); + + destroy_parse_ctx(&ctx); +} + +void test_parse_accepts_short_expires_in(void) +{ + int ret; + struct flb_oauth2 ctx = {0}; + const char *payload = "{\"access_token\":\"short-token\"," + "\"token_type\":\"Bearer\"," + "\"expires_in\":30}"; + + ctx.refresh_skew = FLB_OAUTH2_DEFAULT_SKEW_SECS; + ret = flb_oauth2_parse_json_response(payload, strlen(payload), &ctx); + + TEST_CHECK(ret == 0); + TEST_CHECK(strcmp(ctx.access_token, "short-token") == 0); + TEST_CHECK(ctx.expires_in == 30); + + destroy_parse_ctx(&ctx); +} + void test_parse_rejects_invalid_expires_in(void) { int index; @@ -1008,12 +1043,10 @@ void test_parse_rejects_invalid_expires_in(void) "{\"access_token\":\"t\",\"token_type\":\"Bearer\",\"expires_in\":\"\"}", "{\"access_token\":\"t\",\"token_type\":\"Bearer\",\"expires_in\":-1}", "{\"access_token\":\"t\",\"token_type\":\"Bearer\",\"expires_in\":\"3600x\"}", - "{\"access_token\":\"t\",\"token_type\":\"Bearer\",\"expires_in\":0}", - "{\"access_token\":\"t\",\"token_type\":\"Bearer\",\"expires_in\":50}", - "{\"access_token\":\"t\",\"token_type\":\"Bearer\",\"expires_in\":66}" + "{\"access_token\":\"t\",\"token_type\":\"Bearer\",\"expires_in\":0}" }; - for (index = 0; index < 6; index++) { + for (index = 0; index < 4; index++) { memset(&ctx, 0, sizeof(ctx)); populate_parse_ctx(&ctx, "old-token", "OldBearer", 1200); ctx.refresh_skew = FLB_OAUTH2_DEFAULT_SKEW_SECS; @@ -1042,10 +1075,10 @@ void test_caching_and_refresh(void) config = flb_config_init(); TEST_CHECK(config != NULL); - ret = oauth2_mock_server_start(&server, 65, 0); + ret = oauth2_mock_server_start(&server, 3, 0); TEST_CHECK(ret == 0); - ctx = create_oauth_ctx(config, &server, 58); + ctx = create_oauth_ctx(config, &server, 1); TEST_CHECK(ctx != NULL); #ifdef FLB_SYSTEM_MACOS @@ -1258,6 +1291,8 @@ TEST_LIST = { {"parse_duplicate_keys_last_wins", test_parse_duplicate_keys_last_wins}, {"parse_rejects_missing_required_fields", test_parse_rejects_missing_required_fields}, + {"parse_defaults_missing_expires_in", test_parse_defaults_missing_expires_in}, + {"parse_accepts_short_expires_in", test_parse_accepts_short_expires_in}, {"parse_rejects_invalid_expires_in", test_parse_rejects_invalid_expires_in}, {"caching_and_refresh", test_caching_and_refresh}, {"user_agent_header_optional", test_user_agent_header_optional},