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