/**
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0.
 */

#include <aws/auth/credentials.h>

#include <aws/auth/private/credentials_utils.h>
#include <aws/common/clock.h>
#include <aws/common/json.h>
#include <aws/common/string.h>
#include <aws/http/connection.h>
#include <aws/http/connection_manager.h>
#include <aws/http/request_response.h>
#include <aws/http/status_code.h>
#include <aws/io/channel_bootstrap.h>
#include <aws/io/retry_strategy.h>
#include <aws/io/socket.h>
#include <aws/io/stream.h>
#include <aws/io/tls_channel_handler.h>

#include <inttypes.h>

#define COGNITO_CONNECT_TIMEOUT_DEFAULT_IN_SECONDS 5
#define COGNITO_MAX_RETRIES 3
#define HTTP_REQUEST_BODY_INITIAL_SIZE 1024
#define HTTP_RESPONSE_BODY_INITIAL_SIZE 4096

static void s_on_connection_manager_shutdown(void *user_data);
static void s_on_connection_setup_fn(struct aws_http_connection *connection, int error_code, void *user_data);

struct aws_cognito_login {
    struct aws_byte_cursor identity_provider_name;
    struct aws_byte_cursor identity_provider_token;
    struct aws_byte_buf login_buffer;
};

static int s_aws_cognito_login_init(
    struct aws_cognito_login *login,
    struct aws_allocator *allocator,
    struct aws_byte_cursor identity_provider_name,
    struct aws_byte_cursor identity_provider_token) {
    AWS_ZERO_STRUCT(*login);

    login->identity_provider_name = identity_provider_name;
    login->identity_provider_token = identity_provider_token;

    return aws_byte_buf_init_cache_and_update_cursors(
        &login->login_buffer, allocator, &login->identity_provider_name, &login->identity_provider_token, NULL);
}

static void s_aws_cognito_login_clean_up(struct aws_cognito_login *login) {
    aws_byte_buf_clean_up(&login->login_buffer);

    AWS_ZERO_STRUCT(*login);
}

struct aws_credentials_provider_cognito_impl {
    struct aws_http_connection_manager *connection_manager;
    struct aws_retry_strategy *retry_strategy;
    const struct aws_auth_http_system_vtable *function_table;

    struct aws_string *endpoint;

    struct aws_string *identity;

    struct aws_array_list static_logins;

    struct aws_string *custom_role_arn;

    aws_credentials_provider_cognito_get_token_pairs_async_fn *get_token_pairs;
    void *get_token_pairs_user_data;
};

struct cognito_user_data {
    struct aws_allocator *allocator;

    struct aws_credentials_provider *provider;

    aws_on_get_credentials_callback_fn *original_callback;
    void *original_user_data;

    struct aws_array_list dynamic_logins;

    struct aws_http_connection *connection;
    struct aws_http_message *get_credentials_request;
    struct aws_byte_buf request_body_buffer;
    struct aws_input_stream *request_body_stream;

    struct aws_retry_token *retry_token;
    struct aws_credentials *credentials;
    struct aws_byte_buf response_body;
};

static void s_user_data_reset(struct cognito_user_data *user_data) {
    aws_byte_buf_clean_up(&user_data->request_body_buffer);

    user_data->request_body_stream = aws_input_stream_release(user_data->request_body_stream);
    user_data->get_credentials_request = aws_http_message_release(user_data->get_credentials_request);

    struct aws_credentials_provider_cognito_impl *impl = user_data->provider->impl;
    if (user_data->connection != NULL) {
        impl->function_table->aws_http_connection_manager_release_connection(
            impl->connection_manager, user_data->connection);
        user_data->connection = NULL;
    }

    aws_byte_buf_reset(&user_data->response_body, false);
}

static void s_user_data_destroy(struct cognito_user_data *user_data) {
    if (user_data == NULL) {
        return;
    }

    s_user_data_reset(user_data);

    aws_byte_buf_clean_up(&user_data->response_body);
    aws_retry_token_release(user_data->retry_token);
    aws_credentials_provider_release(user_data->provider);
    aws_credentials_release(user_data->credentials);

    size_t dynamic_logins_count = aws_array_list_length(&user_data->dynamic_logins);
    for (size_t i = 0; i < dynamic_logins_count; ++i) {
        struct aws_cognito_login login;
        AWS_ZERO_STRUCT(login);

        aws_array_list_get_at(&user_data->dynamic_logins, &login, i);

        s_aws_cognito_login_clean_up(&login);
    }
    aws_array_list_clean_up(&user_data->dynamic_logins);

    aws_mem_release(user_data->allocator, user_data);
}

static struct cognito_user_data *s_user_data_new(
    struct aws_credentials_provider *provider,
    aws_on_get_credentials_callback_fn callback,
    void *user_data) {

    struct aws_allocator *allocator = provider->allocator;
    struct cognito_user_data *cognito_user_data = aws_mem_calloc(allocator, 1, sizeof(struct cognito_user_data));
    cognito_user_data->allocator = allocator;

    aws_byte_buf_init(&cognito_user_data->response_body, cognito_user_data->allocator, HTTP_RESPONSE_BODY_INITIAL_SIZE);

    cognito_user_data->provider = aws_credentials_provider_acquire(provider);
    cognito_user_data->original_callback = callback;
    cognito_user_data->original_user_data = user_data;

    aws_array_list_init_dynamic(&cognito_user_data->dynamic_logins, allocator, 0, sizeof(struct aws_cognito_login));

    return cognito_user_data;
}

static void s_finalize_credentials_query(struct cognito_user_data *user_data, int error_code) {
    AWS_FATAL_ASSERT(user_data != NULL);

    if (user_data->credentials == NULL && error_code == AWS_ERROR_SUCCESS) {
        error_code = AWS_AUTH_CREDENTIALS_PROVIDER_COGNITO_SOURCE_FAILURE;
    }

    (user_data->original_callback)(user_data->credentials, error_code, user_data->original_user_data);

    s_user_data_destroy(user_data);
}

/* Keys per Cognito-Identity service model */
AWS_STATIC_STRING_FROM_LITERAL(s_credentials_key, "Credentials");
AWS_STATIC_STRING_FROM_LITERAL(s_access_key_id_name, "AccessKeyId");
AWS_STATIC_STRING_FROM_LITERAL(s_secret_access_key_name, "SecretKey");
AWS_STATIC_STRING_FROM_LITERAL(s_session_token_name, "SessionToken");
AWS_STATIC_STRING_FROM_LITERAL(s_expiration_name, "Expiration");

static int s_parse_credentials_from_response(struct cognito_user_data *user_data) {

    int result = AWS_OP_ERR;

    struct aws_json_value *response_document =
        aws_json_value_new_from_string(user_data->allocator, aws_byte_cursor_from_buf(&user_data->response_body));
    if (response_document == NULL) {
        goto done;
    }

    struct aws_json_value *credentials_entry =
        aws_json_value_get_from_object(response_document, aws_byte_cursor_from_string(s_credentials_key));
    if (credentials_entry == NULL) {
        goto done;
    }

    struct aws_parse_credentials_from_json_doc_options credentials_parse_options = {
        .access_key_id_name = aws_string_c_str(s_access_key_id_name),
        .secret_access_key_name = aws_string_c_str(s_secret_access_key_name),
        .token_name = aws_string_c_str(s_session_token_name),
        .expiration_name = aws_string_c_str(s_expiration_name),
        .expiration_format = AWS_PCEF_NUMBER_UNIX_EPOCH,
        .token_required = true,
        .expiration_required = true,
    };

    user_data->credentials =
        aws_parse_credentials_from_aws_json_object(user_data->allocator, credentials_entry, &credentials_parse_options);
    if (user_data->credentials == NULL) {
        goto done;
    }

    result = AWS_OP_SUCCESS;

done:

    aws_json_value_destroy(response_document);

    if (result != AWS_OP_SUCCESS) {
        aws_raise_error(AWS_AUTH_PROVIDER_PARSER_UNEXPECTED_RESPONSE);
    }

    return result;
}

static void s_on_retry_ready(struct aws_retry_token *token, int error_code, void *user_data) {
    (void)token;
    struct cognito_user_data *provider_user_data = user_data;

    if (error_code != AWS_ERROR_SUCCESS) {
        AWS_LOGF_ERROR(
            AWS_LS_AUTH_CREDENTIALS_PROVIDER,
            "(id=%p): Cognito credentials provider retry task failed: %s",
            (void *)provider_user_data->provider,
            aws_error_str(error_code));
        s_finalize_credentials_query(user_data, error_code);
        return;
    }

    s_user_data_reset(provider_user_data);

    struct aws_credentials_provider_cognito_impl *impl = provider_user_data->provider->impl;

    impl->function_table->aws_http_connection_manager_acquire_connection(
        impl->connection_manager, s_on_connection_setup_fn, provider_user_data);
}

static void s_on_stream_complete_fn(struct aws_http_stream *stream, int error_code, void *user_data) {

    struct cognito_user_data *provider_user_data = user_data;
    struct aws_credentials_provider_cognito_impl *impl = provider_user_data->provider->impl;

    int http_response_code = 0;
    impl->function_table->aws_http_stream_get_incoming_response_status(stream, &http_response_code);

    if (http_response_code != 200) {
        error_code = AWS_AUTH_CREDENTIALS_PROVIDER_HTTP_STATUS_FAILURE;
    }

    impl->function_table->aws_http_stream_release(stream);

    AWS_LOGF_DEBUG(
        AWS_LS_AUTH_CREDENTIALS_PROVIDER,
        "(id=%p): GetCredentialsForIdentity call completed with http status %d",
        (void *)provider_user_data->provider,
        http_response_code);

    if (http_response_code == AWS_HTTP_STATUS_CODE_200_OK) {
        aws_retry_token_record_success(provider_user_data->retry_token);

        if (s_parse_credentials_from_response(provider_user_data) == AWS_OP_SUCCESS) {
            s_finalize_credentials_query(user_data, AWS_ERROR_SUCCESS);
            return;
        }

        AWS_LOGF_ERROR(
            AWS_LS_AUTH_CREDENTIALS_PROVIDER,
            "(id=%p): Cognito credentials provider failed to parse GetCredentialsForIdentity response",
            (void *)provider_user_data->provider);

        error_code = AWS_AUTH_PROVIDER_PARSER_UNEXPECTED_RESPONSE;
    }

    /* Success path is done, error-only from here on out */

    /* Unsure if this should be unconditional or a function of status code. STS does this unconditionally. */
    impl->function_table->aws_http_connection_close(provider_user_data->connection);

    enum aws_retry_error_type error_type =
        aws_credentials_provider_compute_retry_error_type(http_response_code, error_code);
    bool can_retry = http_response_code == 0 || error_type != AWS_RETRY_ERROR_TYPE_CLIENT_ERROR;
    if (!can_retry) {
        s_finalize_credentials_query(user_data, error_code);
        return;
    }

    if (aws_retry_strategy_schedule_retry(
            provider_user_data->retry_token, error_type, s_on_retry_ready, provider_user_data)) {
        error_code = aws_last_error();
        AWS_LOGF_ERROR(
            AWS_LS_AUTH_CREDENTIALS_PROVIDER,
            "(id=%p): Cognito credentials provider failed to schedule retry: %s",
            (void *)provider_user_data->provider,
            aws_error_str(error_code));
        s_finalize_credentials_query(user_data, error_code);
        return;
    }
}

static int s_on_incoming_body_fn(struct aws_http_stream *stream, const struct aws_byte_cursor *data, void *user_data) {
    (void)stream;

    struct cognito_user_data *provider_user_data = user_data;
    return aws_byte_buf_append_dynamic(&provider_user_data->response_body, data);
}

static int s_add_login_tokens_to_json(
    struct aws_json_value *json_value,
    struct aws_array_list *logins,
    struct aws_allocator *allocator) {
    size_t login_count = aws_array_list_length(logins);
    for (size_t i = 0; i < login_count; ++i) {
        struct aws_cognito_login login;
        if (aws_array_list_get_at(logins, &login, i)) {
            return AWS_OP_ERR;
        }

        struct aws_json_value *login_value_string = aws_json_value_new_string(allocator, login.identity_provider_token);
        if (login_value_string == NULL) {
            return AWS_OP_ERR;
        }

        if (aws_json_value_add_to_object(json_value, login.identity_provider_name, login_value_string)) {
            aws_json_value_destroy(login_value_string);
            return AWS_OP_ERR;
        }
    }

    return AWS_OP_SUCCESS;
}

AWS_STATIC_STRING_FROM_LITERAL(s_identity_id_key, "IdentityId");
AWS_STATIC_STRING_FROM_LITERAL(s_custom_role_arn_key, "CustomRoleArn");
AWS_STATIC_STRING_FROM_LITERAL(s_logins_key, "Logins");

int s_create_get_credentials_for_identity_body_buffer(
    struct aws_byte_buf *buffer,
    struct cognito_user_data *provider_user_data) {
    struct aws_allocator *allocator = provider_user_data->allocator;
    struct aws_credentials_provider_cognito_impl *impl = provider_user_data->provider->impl;

    int result = AWS_OP_ERR;

    struct aws_json_value *json_body = aws_json_value_new_object(allocator);
    if (json_body == NULL) {
        return AWS_OP_ERR;
    }

    struct aws_json_value *identity_string =
        aws_json_value_new_string(allocator, aws_byte_cursor_from_string(impl->identity));
    if (identity_string == NULL) {
        goto done;
    }

    if (aws_json_value_add_to_object(json_body, aws_byte_cursor_from_string(s_identity_id_key), identity_string)) {
        aws_json_value_destroy(identity_string);
        goto done;
    }

    if (impl->custom_role_arn != NULL) {
        struct aws_json_value *custom_role_arn_string =
            aws_json_value_new_string(allocator, aws_byte_cursor_from_string(impl->custom_role_arn));
        if (custom_role_arn_string == NULL) {
            goto done;
        }

        if (aws_json_value_add_to_object(
                json_body, aws_byte_cursor_from_string(s_custom_role_arn_key), custom_role_arn_string)) {
            aws_json_value_destroy(custom_role_arn_string);
            goto done;
        }
    }

    size_t static_login_count = aws_array_list_length(&impl->static_logins);
    size_t dynamic_login_count = aws_array_list_length(&provider_user_data->dynamic_logins);
    if (static_login_count + dynamic_login_count > 0) {
        struct aws_json_value *logins = aws_json_value_new_object(allocator);
        if (logins == NULL) {
            goto done;
        }

        if (aws_json_value_add_to_object(json_body, aws_byte_cursor_from_string(s_logins_key), logins)) {
            aws_json_value_destroy(logins);
            goto done;
        }

        if (s_add_login_tokens_to_json(logins, &impl->static_logins, allocator)) {
            goto done;
        }

        if (s_add_login_tokens_to_json(logins, &provider_user_data->dynamic_logins, allocator)) {
            goto done;
        }
    }

    if (aws_byte_buf_append_json_string(json_body, buffer)) {
        goto done;
    }

    result = AWS_OP_SUCCESS;

done:

    aws_json_value_destroy(json_body);

    return result;
}

static struct aws_http_header s_content_type_header = {
    .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("content-type"),
    .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("application/x-amz-json-1.1"),
};

AWS_STATIC_STRING_FROM_LITERAL(s_get_credentials_for_identity_path, "/");

static struct aws_http_header s_x_amz_target_header = {
    .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("X-Amz-Target"),
    .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("AWSCognitoIdentityService.GetCredentialsForIdentity"),
};

static int s_create_get_credentials_for_identity_request(struct cognito_user_data *provider_user_data) {
    struct aws_credentials_provider_cognito_impl *impl = provider_user_data->provider->impl;

    struct aws_byte_buf body_buffer;
    AWS_ZERO_STRUCT(body_buffer);

    struct aws_input_stream *body_stream = NULL;
    struct aws_http_message *request = aws_http_message_new_request(provider_user_data->allocator);
    if (request == NULL) {
        return AWS_OP_ERR;
    }

    if (aws_http_message_set_request_method(request, aws_http_method_post)) {
        goto on_error;
    }

    if (aws_http_message_set_request_path(request, aws_byte_cursor_from_string(s_get_credentials_for_identity_path))) {
        goto on_error;
    }

    struct aws_http_header host_header = {
        .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("host"),
        .value = aws_byte_cursor_from_string(impl->endpoint),
    };

    if (aws_http_message_add_header(request, host_header)) {
        goto on_error;
    }

    if (aws_http_message_add_header(request, s_content_type_header)) {
        goto on_error;
    }

    if (aws_http_message_add_header(request, s_x_amz_target_header)) {
        goto on_error;
    }

    if (aws_byte_buf_init(&body_buffer, provider_user_data->allocator, HTTP_REQUEST_BODY_INITIAL_SIZE)) {
        goto on_error;
    }

    if (s_create_get_credentials_for_identity_body_buffer(&body_buffer, provider_user_data)) {
        goto on_error;
    }

    char content_length[21];
    AWS_ZERO_ARRAY(content_length);
    snprintf(content_length, sizeof(content_length), "%" PRIu64, (uint64_t)body_buffer.len);

    struct aws_http_header content_length_header = {
        .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("Content-Length"),
        .value = aws_byte_cursor_from_c_str(content_length),
    };

    if (aws_http_message_add_header(request, content_length_header)) {
        goto on_error;
    }

    struct aws_byte_cursor payload_cur = aws_byte_cursor_from_buf(&body_buffer);
    body_stream = aws_input_stream_new_from_cursor(provider_user_data->allocator, &payload_cur);
    if (body_stream == NULL) {
        goto on_error;
    }

    aws_http_message_set_body_stream(request, body_stream);

    provider_user_data->get_credentials_request = request;
    provider_user_data->request_body_buffer = body_buffer;
    provider_user_data->request_body_stream = body_stream;

    return AWS_OP_SUCCESS;

on_error:

    aws_byte_buf_clean_up(&body_buffer);
    aws_input_stream_release(body_stream);
    aws_http_message_release(request);

    return AWS_OP_ERR;
}

static void s_on_connection_setup_fn(struct aws_http_connection *connection, int error_code, void *user_data) {
    struct cognito_user_data *wrapped_user_data = user_data;
    struct aws_http_stream *stream = NULL;
    struct aws_credentials_provider_cognito_impl *impl = wrapped_user_data->provider->impl;

    if (connection == NULL) {
        AWS_LOGF_ERROR(
            AWS_LS_AUTH_CREDENTIALS_PROVIDER,
            "(id=%p): Cognito credentials provider failed to acquire http connection: %s",
            (void *)wrapped_user_data->provider,
            aws_error_debug_str(error_code));
        goto on_error;
    }

    wrapped_user_data->connection = connection;
    if (s_create_get_credentials_for_identity_request(wrapped_user_data)) {
        error_code = aws_last_error();
        AWS_LOGF_ERROR(
            AWS_LS_AUTH_CREDENTIALS_PROVIDER,
            "(id=%p): Cognito credentials provider failed to create http request: %s",
            (void *)wrapped_user_data->provider,
            aws_error_debug_str(error_code));
        goto on_error;
    }

    struct aws_http_make_request_options options = {
        .user_data = user_data,
        .request = wrapped_user_data->get_credentials_request,
        .self_size = sizeof(struct aws_http_make_request_options),
        .on_response_headers = NULL,
        .on_response_header_block_done = NULL,
        .on_response_body = s_on_incoming_body_fn,
        .on_complete = s_on_stream_complete_fn,
    };

    stream = impl->function_table->aws_http_connection_make_request(connection, &options);
    if (!stream) {
        error_code = aws_last_error();
        AWS_LOGF_ERROR(
            AWS_LS_AUTH_CREDENTIALS_PROVIDER,
            "(id=%p): Cognito credentials provider failed to create http stream: %s",
            (void *)wrapped_user_data->provider,
            aws_error_debug_str(error_code));
        goto on_error;
    }

    if (impl->function_table->aws_http_stream_activate(stream)) {
        error_code = aws_last_error();
        AWS_LOGF_ERROR(
            AWS_LS_AUTH_CREDENTIALS_PROVIDER,
            "(id=%p): Cognito credentials provider failed to activate http stream: %s",
            (void *)wrapped_user_data->provider,
            aws_error_debug_str(error_code));
        goto on_error;
    }

    return;

on_error:

    impl->function_table->aws_http_stream_release(stream);
    s_finalize_credentials_query(wrapped_user_data, error_code);
}

static void s_on_get_token_pairs_completion(
    struct aws_cognito_identity_provider_token_pair *logins,
    size_t login_count,
    int error_code,
    void *completion_user_data) {

    struct cognito_user_data *wrapped_user_data = completion_user_data;
    struct aws_credentials_provider_cognito_impl *impl = wrapped_user_data->provider->impl;

    if (error_code == AWS_ERROR_SUCCESS) {
        for (size_t i = 0; i < login_count; i++) {
            struct aws_cognito_identity_provider_token_pair *login_pair = &logins[i];

            struct aws_cognito_login login;
            AWS_ZERO_STRUCT(login);

            s_aws_cognito_login_init(
                &login,
                wrapped_user_data->allocator,
                login_pair->identity_provider_name,
                login_pair->identity_provider_token);
            aws_array_list_push_back(&wrapped_user_data->dynamic_logins, &login);
        }

        impl->function_table->aws_http_connection_manager_acquire_connection(
            impl->connection_manager, s_on_connection_setup_fn, wrapped_user_data);
    } else {
        AWS_LOGF_ERROR(
            AWS_LS_AUTH_CREDENTIALS_PROVIDER,
            "(id=%p): Cognito credentials provider failed to dynamically append token pairs on get credentials "
            "invocation: %s",
            (void *)wrapped_user_data->provider,
            aws_error_debug_str(error_code));
        s_finalize_credentials_query(wrapped_user_data, error_code);
    }
}

static void s_on_retry_token_acquired(
    struct aws_retry_strategy *strategy,
    int error_code,
    struct aws_retry_token *token,
    void *user_data) {
    (void)strategy;
    struct cognito_user_data *wrapped_user_data = user_data;

    if (token == NULL) {
        AWS_LOGF_ERROR(
            AWS_LS_AUTH_CREDENTIALS_PROVIDER,
            "(id=%p): Cognito credentials provider failed to acquire retry token: %s",
            (void *)wrapped_user_data->provider,
            aws_error_debug_str(error_code));
        s_finalize_credentials_query(wrapped_user_data, error_code);
        return;
    }

    wrapped_user_data->retry_token = token;

    struct aws_credentials_provider_cognito_impl *impl = wrapped_user_data->provider->impl;

    if (impl->get_token_pairs != NULL) {
        if ((*impl->get_token_pairs)(
                impl->get_token_pairs_user_data, s_on_get_token_pairs_completion, wrapped_user_data)) {
            s_finalize_credentials_query(wrapped_user_data, aws_last_error());
        }
    } else {
        s_on_get_token_pairs_completion(NULL, 0, AWS_ERROR_SUCCESS, wrapped_user_data);
    }
}

static int s_credentials_provider_cognito_get_credentials_async(
    struct aws_credentials_provider *provider,
    aws_on_get_credentials_callback_fn callback,
    void *user_data) {

    struct aws_credentials_provider_cognito_impl *impl = provider->impl;

    struct cognito_user_data *wrapped_user_data = s_user_data_new(provider, callback, user_data);
    if (wrapped_user_data == NULL) {
        goto on_error;
    }

    if (aws_retry_strategy_acquire_retry_token(
            impl->retry_strategy, NULL, s_on_retry_token_acquired, wrapped_user_data, 100)) {
        AWS_LOGF_ERROR(
            AWS_LS_AUTH_CREDENTIALS_PROVIDER,
            "(id=%p): Cognito credentials provider failed to acquire retry token with error %s",
            (void *)provider,
            aws_error_debug_str(aws_last_error()));
        goto on_error;
    }

    return AWS_OP_SUCCESS;

on_error:

    s_user_data_destroy(wrapped_user_data);

    return AWS_OP_ERR;
}

static void s_credentials_provider_cognito_destroy(struct aws_credentials_provider *provider) {
    struct aws_credentials_provider_cognito_impl *impl = provider->impl;
    if (impl == NULL) {
        return;
    }

    /* aws_http_connection_manager_release will eventually leads to call of s_on_connection_manager_shutdown,
     * which will do memory release for provider and impl.
     */
    if (impl->connection_manager) {
        impl->function_table->aws_http_connection_manager_release(impl->connection_manager);
    } else {
        /* If provider setup failed halfway through, connection_manager might not exist.
         * In this case invoke shutdown completion callback directly to finish cleanup */
        s_on_connection_manager_shutdown(provider);
    }

    /* freeing the provider takes place in the shutdown callback below */
}

static struct aws_credentials_provider_vtable s_aws_credentials_provider_cognito_vtable = {
    .get_credentials = s_credentials_provider_cognito_get_credentials_async,
    .destroy = s_credentials_provider_cognito_destroy,
};

static void s_on_connection_manager_shutdown(void *user_data) {
    struct aws_credentials_provider *provider = user_data;

    aws_credentials_provider_invoke_shutdown_callback(provider);

    struct aws_credentials_provider_cognito_impl *impl = provider->impl;

    aws_retry_strategy_release(impl->retry_strategy);

    aws_string_destroy(impl->endpoint);
    aws_string_destroy(impl->identity);
    aws_string_destroy(impl->custom_role_arn);

    for (size_t i = 0; i < aws_array_list_length(&impl->static_logins); ++i) {
        struct aws_cognito_login login;
        if (aws_array_list_get_at(&impl->static_logins, &login, i)) {
            continue;
        }

        s_aws_cognito_login_clean_up(&login);
    }

    aws_array_list_clean_up(&impl->static_logins);

    aws_mem_release(provider->allocator, provider);
}

static int s_validate_options(const struct aws_credentials_provider_cognito_options *options) {
    if (options == NULL) {
        return AWS_OP_ERR;
    }

    if (options->tls_ctx == NULL) {
        AWS_LOGF_ERROR(
            AWS_LS_AUTH_CREDENTIALS_PROVIDER,
            "(static) Cognito credentials provider options must include a TLS context");
        return AWS_OP_ERR;
    }

    if (options->bootstrap == NULL) {
        AWS_LOGF_ERROR(
            AWS_LS_AUTH_CREDENTIALS_PROVIDER,
            "(static) Cognito credentials provider options must include a client bootstrap");
        return AWS_OP_ERR;
    }

    if (options->endpoint.len == 0) {
        AWS_LOGF_ERROR(
            AWS_LS_AUTH_CREDENTIALS_PROVIDER,
            "(static) Cognito credentials provider options must have a non-empty endpoint");
        return AWS_OP_ERR;
    }

    if (options->identity.len == 0) {
        AWS_LOGF_ERROR(
            AWS_LS_AUTH_CREDENTIALS_PROVIDER,
            "(static) Cognito credentials provider options must have a non-empty identity");
        return AWS_OP_ERR;
    }

    return AWS_OP_SUCCESS;
}

struct aws_credentials_provider *aws_credentials_provider_new_cognito(
    struct aws_allocator *allocator,
    const struct aws_credentials_provider_cognito_options *options) {

    struct aws_credentials_provider *provider = NULL;
    struct aws_credentials_provider_cognito_impl *impl = NULL;

    if (s_validate_options(options)) {
        aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
        return NULL;
    }

    aws_mem_acquire_many(
        allocator,
        2,
        &provider,
        sizeof(struct aws_credentials_provider),
        &impl,
        sizeof(struct aws_credentials_provider_cognito_impl));

    if (!provider) {
        return NULL;
    }

    AWS_ZERO_STRUCT(*provider);
    AWS_ZERO_STRUCT(*impl);

    aws_credentials_provider_init_base(provider, allocator, &s_aws_credentials_provider_cognito_vtable, impl);

    struct aws_tls_connection_options tls_connection_options;
    AWS_ZERO_STRUCT(tls_connection_options);
    aws_tls_connection_options_init_from_ctx(&tls_connection_options, options->tls_ctx);
    struct aws_byte_cursor host = options->endpoint;
    if (aws_tls_connection_options_set_server_name(&tls_connection_options, allocator, &host)) {
        AWS_LOGF_ERROR(
            AWS_LS_AUTH_CREDENTIALS_PROVIDER,
            "(id=%p): Cognito credentials provider failed to create tls connection options with error %s",
            (void *)provider,
            aws_error_debug_str(aws_last_error()));
        goto on_error;
    }

    struct aws_socket_options socket_options;
    AWS_ZERO_STRUCT(socket_options);
    socket_options.type = AWS_SOCKET_STREAM;
    socket_options.domain = AWS_SOCKET_IPV4;
    socket_options.connect_timeout_ms = (uint32_t)aws_timestamp_convert(
        COGNITO_CONNECT_TIMEOUT_DEFAULT_IN_SECONDS, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_MILLIS, NULL);

    struct aws_http_connection_manager_options manager_options;
    AWS_ZERO_STRUCT(manager_options);
    manager_options.bootstrap = options->bootstrap;
    manager_options.initial_window_size = SIZE_MAX;
    manager_options.socket_options = &socket_options;
    manager_options.host = options->endpoint;
    manager_options.port = 443;
    manager_options.max_connections = 2;
    manager_options.shutdown_complete_callback = s_on_connection_manager_shutdown;
    manager_options.shutdown_complete_user_data = provider;
    manager_options.tls_connection_options = &tls_connection_options;
    manager_options.proxy_options = options->http_proxy_options;
    manager_options.proxy_ev_settings = options->proxy_ev_settings;

    impl->function_table = options->function_table;
    if (impl->function_table == NULL) {
        impl->function_table = g_aws_credentials_provider_http_function_table;
    }

    impl->connection_manager = impl->function_table->aws_http_connection_manager_new(allocator, &manager_options);
    if (impl->connection_manager == NULL) {
        AWS_LOGF_ERROR(
            AWS_LS_AUTH_CREDENTIALS_PROVIDER,
            "(id=%p): Cognito credentials provider failed to create http connection manager with error %s",
            (void *)provider,
            aws_error_debug_str(aws_last_error()));
        goto on_error;
    }

    impl->endpoint = aws_string_new_from_cursor(allocator, &options->endpoint);
    impl->identity = aws_string_new_from_cursor(allocator, &options->identity);

    if (options->custom_role_arn != NULL) {
        impl->custom_role_arn = aws_string_new_from_cursor(allocator, options->custom_role_arn);
    }

    aws_array_list_init_dynamic(
        &impl->static_logins, allocator, options->login_count, sizeof(struct aws_cognito_login));

    for (size_t i = 0; i < options->login_count; ++i) {
        struct aws_cognito_identity_provider_token_pair *login_token_pair = &options->logins[i];

        struct aws_cognito_login login;
        if (s_aws_cognito_login_init(
                &login,
                allocator,
                login_token_pair->identity_provider_name,
                login_token_pair->identity_provider_token)) {
            AWS_LOGF_ERROR(
                AWS_LS_AUTH_CREDENTIALS_PROVIDER,
                "(id=%p): Cognito credentials provider failed to initialize login entry with error %s",
                (void *)provider,
                aws_error_debug_str(aws_last_error()));
            goto on_error;
        }

        aws_array_list_push_back(&impl->static_logins, &login);
    }

    struct aws_standard_retry_options retry_options = {
        .backoff_retry_options =
            {
                .el_group = options->bootstrap->event_loop_group,
                .max_retries = COGNITO_MAX_RETRIES,
            },
    };

    impl->retry_strategy = aws_retry_strategy_new_standard(allocator, &retry_options);
    if (!impl->retry_strategy) {
        AWS_LOGF_ERROR(
            AWS_LS_AUTH_CREDENTIALS_PROVIDER,
            "(id=%p): Cognito credentials provider failed to create a retry strategy with error %s",
            (void *)provider,
            aws_error_debug_str(aws_last_error()));
        goto on_error;
    }

    provider->shutdown_options = options->shutdown_options;

    aws_tls_connection_options_clean_up(&tls_connection_options);

    impl->get_token_pairs = options->get_token_pairs;
    impl->get_token_pairs_user_data = options->get_token_pairs_user_data;

    return provider;

on_error:

    aws_tls_connection_options_clean_up(&tls_connection_options);
    aws_credentials_provider_destroy(provider);

    return NULL;
}

/*************************************************************************/

#define DEFAULT_CREDENTIAL_PROVIDER_REFRESH_MS (15 * 60 * 1000)

/*
 * Cognito provider with caching implementation
 */
struct aws_credentials_provider *aws_credentials_provider_new_cognito_caching(
    struct aws_allocator *allocator,
    const struct aws_credentials_provider_cognito_options *options) {

    struct aws_credentials_provider *cognito_provider = NULL;
    struct aws_credentials_provider *caching_provider = NULL;

    cognito_provider = aws_credentials_provider_new_cognito(allocator, options);
    if (cognito_provider == NULL) {
        goto on_error;
    }

    struct aws_credentials_provider_cached_options cached_options = {
        .source = cognito_provider,
        .refresh_time_in_milliseconds = DEFAULT_CREDENTIAL_PROVIDER_REFRESH_MS,
    };

    caching_provider = aws_credentials_provider_new_cached(allocator, &cached_options);
    if (caching_provider == NULL) {
        goto on_error;
    }

    aws_credentials_provider_release(cognito_provider);

    return caching_provider;

on_error:

    aws_credentials_provider_release(caching_provider);
    aws_credentials_provider_release(cognito_provider);

    return NULL;
}
