diff --git a/include/aws/auth/auth.h b/include/aws/auth/auth.h index 4411d8ff..b0092837 100644 --- a/include/aws/auth/auth.h +++ b/include/aws/auth/auth.h @@ -43,6 +43,10 @@ enum aws_auth_errors { AWS_AUTH_SIGV4A_SIGNATURE_VALIDATION_FAILURE, AWS_AUTH_CREDENTIALS_PROVIDER_COGNITO_SOURCE_FAILURE, AWS_AUTH_CREDENTIALS_PROVIDER_DELEGATE_FAILURE, + AWS_AUTH_SSO_TOKEN_PROVIDER_SOURCE_FAILURE, + AWS_AUTH_SSO_TOKEN_INVALID, + AWS_AUTH_SSO_TOKEN_EXPIRED, + AWS_AUTH_CREDENTIALS_PROVIDER_SSO_SOURCE_FAILURE, AWS_AUTH_ERROR_END_RANGE = AWS_ERROR_ENUM_END_RANGE(AWS_C_AUTH_PACKAGE_ID) }; diff --git a/include/aws/auth/credentials.h b/include/aws/auth/credentials.h index 93a4e9cb..2a92f42c 100644 --- a/include/aws/auth/credentials.h +++ b/include/aws/auth/credentials.h @@ -90,8 +90,8 @@ struct aws_credentials_provider_environment_options { }; /** - * Configuration options for a provider that sources credentials from the aws profile and credentials files - * (by default ~/.aws/profile and ~/.aws/credentials) + * Configuration options for a provider that sources credentials from the aws config and credentials files + * (by default ~/.aws/config and ~/.aws/credentials) */ struct aws_credentials_provider_profile_options { struct aws_credentials_provider_shutdown_options shutdown_options; @@ -114,7 +114,7 @@ struct aws_credentials_provider_profile_options { /** * (Optional) * Use a cached merged profile collection. A merge collection has both config file - * (~/.aws/profile) and credentials file based profile collection (~/.aws/credentials) using + * (~/.aws/config) and credentials file based profile collection (~/.aws/credentials) using * `aws_profile_collection_new_from_merge`. * If this option is provided, `config_file_name_override` and `credentials_file_name_override` will be ignored. */ @@ -355,6 +355,49 @@ struct aws_credentials_provider_sts_web_identity_options { struct aws_auth_http_system_vtable *function_table; }; +/* + * Configuration for the SSOCredentialsProvider that sends a GetRoleCredentialsRequest to the AWS Single + * Sign-On Service to maintain short-lived sessions to use for authentication. + * + * https://docs.aws.amazon.com/sdkref/latest/guide/feature-sso-credentials.html + */ +struct aws_credentials_provider_sso_options { + struct aws_credentials_provider_shutdown_options shutdown_options; + + /* + * Override of what profile to use to source credentials from ('default' by default) + */ + struct aws_byte_cursor profile_name_override; + + /* + * Override path to the profile config file (~/.aws/config by default) + */ + struct aws_byte_cursor config_file_name_override; + + /** + * (Optional) + * Use a cached config profile collection. You can also pass a merged collection. + * config_file_name_override will be ignored if this option is provided. + */ + struct aws_profile_collection *config_file_cached; + + /* + * Connection bootstrap to use for any network connections made while sourcing credentials + * Required. + */ + struct aws_client_bootstrap *bootstrap; + + /* + * Client TLS context to use when querying SSO provider. + * Required. + */ + struct aws_tls_ctx *tls_ctx; + + /* For mocking, leave NULL otherwise */ + struct aws_auth_http_system_vtable *function_table; + aws_io_clock_fn *system_clock_fn; +}; + /** * Configuration options for the STS credentials provider */ @@ -457,7 +500,7 @@ struct aws_credentials_provider_chain_default_options { /** * (Optional) * Use a cached merged profile collection. A merge collection has both config file - * (~/.aws/profile) and credentials file based profile collection (~/.aws/credentials) using + * (~/.aws/config) and credentials file based profile collection (~/.aws/credentials) using * `aws_profile_collection_new_from_merge`. * If this option is provided, `config_file_name_override` and `credentials_file_name_override` will be ignored. */ @@ -927,6 +970,19 @@ struct aws_credentials_provider *aws_credentials_provider_new_sts_web_identity( struct aws_allocator *allocator, const struct aws_credentials_provider_sts_web_identity_options *options); +/** + * Creates a provider that sources credentials from SSO using a SSOToken. + * + * @param allocator memory allocator to use for all memory allocation + * @param options provider-specific configuration options + * + * @return the newly-constructed credentials provider, or NULL if an error occurred. + */ +AWS_AUTH_API +struct aws_credentials_provider *aws_credentials_provider_new_sso( + struct aws_allocator *allocator, + const struct aws_credentials_provider_sso_options *options); + /* * Creates a provider that sources credentials from running an external command or process * diff --git a/include/aws/auth/private/credentials_utils.h b/include/aws/auth/private/credentials_utils.h index 5c041a9b..598c3ba0 100644 --- a/include/aws/auth/private/credentials_utils.h +++ b/include/aws/auth/private/credentials_utils.h @@ -73,6 +73,7 @@ struct aws_auth_http_system_vtable { enum aws_parse_credentials_expiration_format { AWS_PCEF_STRING_ISO_8601_DATE, AWS_PCEF_NUMBER_UNIX_EPOCH, + AWS_PCEF_NUMBER_UNIX_EPOCH_MS, }; struct aws_parse_credentials_from_json_doc_options { @@ -80,6 +81,7 @@ struct aws_parse_credentials_from_json_doc_options { const char *secret_access_key_name; const char *token_name; const char *expiration_name; + const char *top_level_object_name; enum aws_parse_credentials_expiration_format expiration_format; bool token_required; bool expiration_required; @@ -160,6 +162,14 @@ struct aws_credentials *aws_parse_credentials_from_json_document( AWS_AUTH_API enum aws_retry_error_type aws_credentials_provider_compute_retry_error_type(int response_code, int error_code); +/* + * Loads an aws config profile collection + */ +AWS_AUTH_API +struct aws_profile_collection *aws_load_profile_collection_from_config_file( + struct aws_allocator *allocator, + struct aws_byte_cursor config_file_name_override); + AWS_EXTERN_C_END #endif /* AWS_AUTH_CREDENTIALS_PRIVATE_H */ diff --git a/include/aws/auth/private/sso_token_providers.h b/include/aws/auth/private/sso_token_providers.h new file mode 100644 index 00000000..a9f93079 --- /dev/null +++ b/include/aws/auth/private/sso_token_providers.h @@ -0,0 +1,112 @@ +#ifndef AWS_AUTH_TOKEN_PROVIDERS_PRIVATE_H +#define AWS_AUTH_TOKEN_PROVIDERS_PRIVATE_H + +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include +#include + +/** + * Configuration options for a provider that sources sso token information from the aws profile (by default + * ~/.aws/config) and token from ~/.aws/sso/cache/.json. + */ +struct aws_token_provider_sso_profile_options { + struct aws_credentials_provider_shutdown_options shutdown_options; + + /* + * Override of what profile to use to source credentials from ('default' by default) + */ + struct aws_byte_cursor profile_name_override; + + /* + * Override path to the profile config file (~/.aws/config by default) + */ + struct aws_byte_cursor config_file_name_override; + + /** + * (Optional) + * Use a cached config profile collection. You can also pass a merged collection. + * config_file_name_override will be ignored if this option is provided. + */ + struct aws_profile_collection *config_file_cached; + + /* For mocking, leave NULL otherwise */ + aws_io_clock_fn *system_clock_fn; +}; + +/** + * Configuration options for a provider that sources sso token information from the aws profile (by default + * ~/.aws/config) and token from ~/.aws/sso/cache/.json. + */ +struct aws_token_provider_sso_session_options { + struct aws_credentials_provider_shutdown_options shutdown_options; + + /* + * Override of what profile to use to source credentials from ('default' by default) + */ + struct aws_byte_cursor profile_name_override; + + /* + * Override path to the profile config file (~/.aws/config by default) + */ + struct aws_byte_cursor config_file_name_override; + + /** + * (Optional) + * Use a cached config profile collection. You can also pass a merged collection. + * config_file_name_override will be ignored if this option is provided. + */ + struct aws_profile_collection *config_file_cached; + + /* + * Connection bootstrap to use for any network connections made + */ + struct aws_client_bootstrap *bootstrap; + + /* + * Client TLS context to use for any network connections made. + */ + struct aws_tls_ctx *tls_ctx; + + /* For mocking, leave NULL otherwise */ + aws_io_clock_fn *system_clock_fn; +}; + +AWS_EXTERN_C_BEGIN + +/** + * Creates a provider that sources sso token based credentials from key-value profiles loaded from the aws + * config("~/.aws/config" by default) and ~/.aws/sso/cache/.json + * This is the legacy way which doesn't support refreshing credentials. + * + * @param allocator memory allocator to use for all memory allocation + * @param options provider-specific configuration options + * + * @return the newly-constructed credentials provider, or NULL if an error occurred. + */ +AWS_AUTH_API +struct aws_credentials_provider *aws_token_provider_new_sso_profile( + struct aws_allocator *allocator, + const struct aws_token_provider_sso_profile_options *options); + +/** + * Creates a provider that sources sso token based credentials from key-value profiles loaded from the aws + * config("~/.aws/config" by default) and ~/.aws/sso/cache/.json + * Note: Token refresh is not currently supported + * + * @param allocator memory allocator to use for all memory allocation + * @param options provider-specific configuration options + * + * @return the newly-constructed credentials provider, or NULL if an error occurred. + */ +AWS_AUTH_API +struct aws_credentials_provider *aws_token_provider_new_sso_session( + struct aws_allocator *allocator, + const struct aws_token_provider_sso_session_options *options); + +AWS_EXTERN_C_END + +#endif /* AWS_AUTH_TOKEN_PROVIDERS_PRIVATE_H */ diff --git a/include/aws/auth/private/sso_token_utils.h b/include/aws/auth/private/sso_token_utils.h new file mode 100644 index 00000000..42c5c2bf --- /dev/null +++ b/include/aws/auth/private/sso_token_utils.h @@ -0,0 +1,58 @@ +#ifndef AWS_AUTH_TOKEN_PRIVATE_H +#define AWS_AUTH_TOKEN_PRIVATE_H + +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include +#include + +/* structure to represent a parsed sso token */ +struct aws_sso_token { + struct aws_allocator *allocator; + + struct aws_string *access_token; + struct aws_date_time expiration; +}; + +AWS_EXTERN_C_BEGIN + +/* Construct token path which is ~/.aws/sso/cache/.json */ +AWS_AUTH_API +struct aws_string *aws_construct_sso_token_path(struct aws_allocator *allocator, const struct aws_string *input); + +AWS_AUTH_API +void aws_sso_token_destroy(struct aws_sso_token *token); + +/* Parse `aws_sso_token` from the give file path */ +AWS_AUTH_API +struct aws_sso_token *aws_sso_token_new_from_file(struct aws_allocator *allocator, const struct aws_string *file_path); + +/** + * Creates a set of AWS credentials based on a token with expiration. + * + * @param allocator memory allocator to use for all memory allocation + * @param token token for the credentials + * @param expiration_timepoint_in_seconds time at which these credentials expire + * @return a new pair of AWS credentials, or NULL + */ +AWS_AUTH_API +struct aws_credentials *aws_credentials_new_token( + struct aws_allocator *allocator, + struct aws_byte_cursor token, + uint64_t expiration_timepoint_in_seconds); + +/** + * Get the token from a set of AWS credentials + * + * @param credentials credentials to get the token from + * @return a byte cursor to the token or an empty byte cursor if there is no token + */ +AWS_AUTH_API +struct aws_byte_cursor aws_credentials_get_token(const struct aws_credentials *credentials); + +AWS_EXTERN_C_END + +#endif /* AWS_AUTH_TOKEN_PRIVATE_H */ diff --git a/source/auth.c b/source/auth.c index b3fb4d3f..5a0fbc8c 100644 --- a/source/auth.c +++ b/source/auth.c @@ -91,6 +91,18 @@ static struct aws_error_info s_errors[] = { AWS_DEFINE_ERROR_INFO_AUTH( AWS_AUTH_CREDENTIALS_PROVIDER_DELEGATE_FAILURE, "Valid credentials could not be sourced by the delegate provider"), + AWS_DEFINE_ERROR_INFO_AUTH( + AWS_AUTH_SSO_TOKEN_PROVIDER_SOURCE_FAILURE, + "Valid token could not be sourced by the sso token provider"), + AWS_DEFINE_ERROR_INFO_AUTH( + AWS_AUTH_SSO_TOKEN_INVALID, + "Token sourced by the sso token provider is invalid."), + AWS_DEFINE_ERROR_INFO_AUTH( + AWS_AUTH_SSO_TOKEN_EXPIRED, + "Token sourced by the sso token provider is expired."), + AWS_DEFINE_ERROR_INFO_AUTH( + AWS_AUTH_CREDENTIALS_PROVIDER_SSO_SOURCE_FAILURE, + "Valid credentials could not be sourced by the sso credentials provider"), }; /* clang-format on */ diff --git a/source/credentials.c b/source/credentials.c index f838c3e1..3340663d 100644 --- a/source/credentials.c +++ b/source/credentials.c @@ -5,22 +5,45 @@ #include +#include #include #include #include +/* aws ecc identity which contains the data needed to sign a Sigv4a AWS request */ +struct aws_ecc_identity { + struct aws_string *access_key_id; + struct aws_string *session_token; + struct aws_ecc_key_pair *ecc_key; +}; + +/* aws credentials identity which contains the data needed to sign an authenticated AWS request */ +struct aws_credentials_identity { + struct aws_string *access_key_id; + struct aws_string *secret_access_key; + struct aws_string *session_token; +}; + +/* aws_token identity contains only a token to represent token only identities like a bearer token. */ +struct aws_token_identity { + struct aws_string *token; +}; + +enum aws_identity_type { + AWS_CREDENTIALS_IDENTITY, + TOKEN_IDENTITY, + ANONYMOUS_IDENTITY, + ECC_IDENTITY, +}; + /* - * A structure that wraps the public/private data needed to sign an authenticated AWS request + * A structure that wraps the different types of credentials that the customer can provider to establish their + * identity. */ struct aws_credentials { struct aws_allocator *allocator; struct aws_atomic_var ref_count; - - struct aws_string *access_key_id; - struct aws_string *secret_access_key; - struct aws_string *session_token; - /* * A timepoint, in seconds since epoch, at which the credentials should no longer be used because they * will have expired. @@ -51,7 +74,12 @@ struct aws_credentials { */ uint64_t expiration_timepoint_seconds; - struct aws_ecc_key_pair *ecc_key; + enum aws_identity_type identity_type; + union { + struct aws_credentials_identity credentials_identity; + struct aws_token_identity token_identity; + struct aws_ecc_identity ecc_identity; + } identity; }; /* @@ -83,23 +111,24 @@ struct aws_credentials *aws_credentials_new( credentials->allocator = allocator; aws_atomic_init_int(&credentials->ref_count, 1); - - credentials->access_key_id = + credentials->identity_type = AWS_CREDENTIALS_IDENTITY; + struct aws_credentials_identity *credentials_identity = &credentials->identity.credentials_identity; + credentials_identity->access_key_id = aws_string_new_from_array(allocator, access_key_id_cursor.ptr, access_key_id_cursor.len); - if (credentials->access_key_id == NULL) { + if (credentials_identity->access_key_id == NULL) { goto error; } - credentials->secret_access_key = + credentials_identity->secret_access_key = aws_string_new_from_array(allocator, secret_access_key_cursor.ptr, secret_access_key_cursor.len); - if (credentials->secret_access_key == NULL) { + if (credentials_identity->secret_access_key == NULL) { goto error; } if (session_token_cursor.ptr != NULL && session_token_cursor.len > 0) { - credentials->session_token = + credentials_identity->session_token = aws_string_new_from_array(allocator, session_token_cursor.ptr, session_token_cursor.len); - if (credentials->session_token == NULL) { + if (credentials_identity->session_token == NULL) { goto error; } } @@ -120,6 +149,7 @@ struct aws_credentials *aws_credentials_new_anonymous(struct aws_allocator *allo struct aws_credentials *credentials = aws_mem_calloc(allocator, 1, sizeof(struct aws_credentials)); credentials->allocator = allocator; + credentials->identity_type = ANONYMOUS_IDENTITY; aws_atomic_init_int(&credentials->ref_count, 1); credentials->expiration_timepoint_seconds = UINT64_MAX; @@ -131,21 +161,24 @@ static void s_aws_credentials_destroy(struct aws_credentials *credentials) { if (credentials == NULL) { return; } - - if (credentials->access_key_id != NULL) { - aws_string_destroy(credentials->access_key_id); - } - - if (credentials->secret_access_key != NULL) { - aws_string_destroy_secure(credentials->secret_access_key); + switch (credentials->identity_type) { + case AWS_CREDENTIALS_IDENTITY: + aws_string_destroy(credentials->identity.credentials_identity.access_key_id); + aws_string_destroy_secure(credentials->identity.credentials_identity.secret_access_key); + aws_string_destroy_secure(credentials->identity.credentials_identity.session_token); + break; + case ECC_IDENTITY: + aws_string_destroy(credentials->identity.ecc_identity.access_key_id); + aws_string_destroy_secure(credentials->identity.ecc_identity.session_token); + aws_ecc_key_pair_release(credentials->identity.ecc_identity.ecc_key); + break; + case TOKEN_IDENTITY: + aws_string_destroy_secure(credentials->identity.token_identity.token); + break; + case ANONYMOUS_IDENTITY: + break; } - if (credentials->session_token != NULL) { - aws_string_destroy_secure(credentials->session_token); - } - - aws_ecc_key_pair_release(credentials->ecc_key); - aws_mem_release(credentials->allocator, credentials); } @@ -174,26 +207,64 @@ static struct aws_byte_cursor s_empty_token_cursor = { }; struct aws_byte_cursor aws_credentials_get_access_key_id(const struct aws_credentials *credentials) { - if (credentials->access_key_id == NULL) { - return s_empty_token_cursor; + switch (credentials->identity_type) { + case AWS_CREDENTIALS_IDENTITY: + if (credentials->identity.credentials_identity.access_key_id != NULL) { + return aws_byte_cursor_from_string(credentials->identity.credentials_identity.access_key_id); + } + break; + case ECC_IDENTITY: + if (credentials->identity.ecc_identity.access_key_id != NULL) { + return aws_byte_cursor_from_string(credentials->identity.ecc_identity.access_key_id); + } + break; + default: + break; } - - return aws_byte_cursor_from_string(credentials->access_key_id); + return s_empty_token_cursor; } struct aws_byte_cursor aws_credentials_get_secret_access_key(const struct aws_credentials *credentials) { - if (credentials->secret_access_key == NULL) { - return s_empty_token_cursor; + switch (credentials->identity_type) { + case AWS_CREDENTIALS_IDENTITY: + if (credentials->identity.credentials_identity.secret_access_key != NULL) { + return aws_byte_cursor_from_string(credentials->identity.credentials_identity.secret_access_key); + } + break; + default: + break; } - - return aws_byte_cursor_from_string(credentials->secret_access_key); + return s_empty_token_cursor; } struct aws_byte_cursor aws_credentials_get_session_token(const struct aws_credentials *credentials) { - if (credentials->session_token != NULL) { - return aws_byte_cursor_from_string(credentials->session_token); + switch (credentials->identity_type) { + case AWS_CREDENTIALS_IDENTITY: + if (credentials->identity.credentials_identity.session_token != NULL) { + return aws_byte_cursor_from_string(credentials->identity.credentials_identity.session_token); + } + break; + case ECC_IDENTITY: + if (credentials->identity.ecc_identity.session_token != NULL) { + return aws_byte_cursor_from_string(credentials->identity.ecc_identity.session_token); + } + break; + default: + break; } + return s_empty_token_cursor; +} +struct aws_byte_cursor aws_credentials_get_token(const struct aws_credentials *credentials) { + switch (credentials->identity_type) { + case TOKEN_IDENTITY: + if (credentials->identity.token_identity.token != NULL) { + return aws_byte_cursor_from_string(credentials->identity.token_identity.token); + } + break; + default: + break; + } return s_empty_token_cursor; } @@ -202,12 +273,15 @@ uint64_t aws_credentials_get_expiration_timepoint_seconds(const struct aws_crede } struct aws_ecc_key_pair *aws_credentials_get_ecc_key_pair(const struct aws_credentials *credentials) { - return credentials->ecc_key; + if (credentials->identity_type == ECC_IDENTITY) { + return credentials->identity.ecc_identity.ecc_key; + } + return NULL; } bool aws_credentials_is_anonymous(const struct aws_credentials *credentials) { AWS_PRECONDITION(credentials); - return credentials->access_key_id == NULL && credentials->secret_access_key == NULL; + return credentials->identity_type == ANONYMOUS_IDENTITY; } struct aws_credentials *aws_credentials_new_from_string( @@ -250,16 +324,19 @@ struct aws_credentials *aws_credentials_new_ecc( credentials->expiration_timepoint_seconds = expiration_timepoint_in_seconds; aws_atomic_init_int(&credentials->ref_count, 1); aws_ecc_key_pair_acquire(ecc_key); - credentials->ecc_key = ecc_key; + credentials->identity_type = ECC_IDENTITY; + credentials->identity.ecc_identity.ecc_key = ecc_key; - credentials->access_key_id = aws_string_new_from_array(allocator, access_key_id.ptr, access_key_id.len); - if (credentials->access_key_id == NULL) { + credentials->identity.ecc_identity.access_key_id = + aws_string_new_from_array(allocator, access_key_id.ptr, access_key_id.len); + if (credentials->identity.ecc_identity.access_key_id == NULL) { goto on_error; } if (session_token.ptr != NULL && session_token.len > 0) { - credentials->session_token = aws_string_new_from_array(allocator, session_token.ptr, session_token.len); - if (credentials->session_token == NULL) { + credentials->identity.ecc_identity.session_token = + aws_string_new_from_array(allocator, session_token.ptr, session_token.len); + if (credentials->identity.ecc_identity.session_token == NULL) { goto on_error; } } @@ -295,6 +372,25 @@ struct aws_credentials *aws_credentials_new_ecc_from_aws_credentials( return ecc_credentials; } +struct aws_credentials *aws_credentials_new_token( + struct aws_allocator *allocator, + struct aws_byte_cursor token, + uint64_t expiration_timepoint_in_seconds) { + if (token.ptr == NULL || token.len == 0) { + aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + return NULL; + } + struct aws_credentials *credentials = aws_mem_calloc(allocator, 1, sizeof(struct aws_credentials)); + + credentials->allocator = allocator; + aws_atomic_init_int(&credentials->ref_count, 1); + credentials->identity_type = TOKEN_IDENTITY; + struct aws_token_identity *token_identity = &credentials->identity.token_identity; + token_identity->token = aws_string_new_from_array(allocator, token.ptr, token.len); + credentials->expiration_timepoint_seconds = expiration_timepoint_in_seconds; + return credentials; +} + /* * global credentials provider APIs */ diff --git a/source/credentials_provider_sso.c b/source/credentials_provider_sso.c new file mode 100644 index 00000000..e7c39e75 --- /dev/null +++ b/source/credentials_provider_sso.c @@ -0,0 +1,851 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_MSC_VER) +# pragma warning(disable : 4204) +#endif /* _MSC_VER */ + +#define SSO_RESPONSE_SIZE_INITIAL 2048 +#define SSO_RESPONSE_SIZE_LIMIT 10000 +#define SSO_CONNECT_TIMEOUT_DEFAULT_IN_SECONDS 2 +#define SSO_MAX_ATTEMPTS 3 +#define SSO_RETRY_TIMEOUT_MS 100 + +struct aws_credentials_provider_sso_impl { + struct aws_http_connection_manager *connection_manager; + const struct aws_auth_http_system_vtable *function_table; + struct aws_string *endpoint; + struct aws_string *sso_account_id; + struct aws_string *sso_role_name; + struct aws_credentials_provider *token_provider; + struct aws_retry_strategy *retry_strategy; +}; + +/** + * aws_sso_query_context - context for each outstanding SSO query. + */ +struct aws_sso_query_context { + /* immutable post-creation */ + struct aws_allocator *allocator; + struct aws_credentials_provider *provider; + aws_on_get_credentials_callback_fn *original_callback; + void *original_user_data; + + /* mutable */ + struct aws_http_connection *connection; + struct aws_http_message *request; + struct aws_byte_buf payload; + struct aws_retry_token *retry_token; + struct aws_byte_buf path_and_query; + struct aws_string *token; + + int status_code; + int error_code; +}; + +/* called in between retries. */ +static void s_sso_query_context_reset_request_specific_data(struct aws_sso_query_context *sso_query_context) { + if (sso_query_context->request) { + aws_http_message_release(sso_query_context->request); + sso_query_context->request = NULL; + } + if (sso_query_context->connection) { + struct aws_credentials_provider_sso_impl *provider_impl = sso_query_context->provider->impl; + int result = provider_impl->function_table->aws_http_connection_manager_release_connection( + provider_impl->connection_manager, sso_query_context->connection); + (void)result; + AWS_ASSERT(result == AWS_OP_SUCCESS); + sso_query_context->connection = NULL; + } + if (sso_query_context->token) { + aws_string_destroy_secure(sso_query_context->token); + sso_query_context->token = NULL; + } + sso_query_context->status_code = 0; + sso_query_context->error_code = 0; +} + +static void s_sso_query_context_destroy(struct aws_sso_query_context *sso_query_context) { + if (sso_query_context == NULL) { + return; + } + + s_sso_query_context_reset_request_specific_data(sso_query_context); + aws_byte_buf_clean_up(&sso_query_context->payload); + aws_byte_buf_clean_up(&sso_query_context->path_and_query); + aws_credentials_provider_release(sso_query_context->provider); + aws_retry_token_release(sso_query_context->retry_token); + aws_mem_release(sso_query_context->allocator, sso_query_context); +} + +static struct aws_sso_query_context *s_sso_query_context_new( + struct aws_credentials_provider *provider, + aws_on_get_credentials_callback_fn callback, + void *user_data) { + struct aws_credentials_provider_sso_impl *impl = provider->impl; + + struct aws_sso_query_context *sso_query_context = + aws_mem_calloc(provider->allocator, 1, sizeof(struct aws_sso_query_context)); + sso_query_context->allocator = provider->allocator; + sso_query_context->provider = aws_credentials_provider_acquire(provider); + sso_query_context->original_user_data = user_data; + sso_query_context->original_callback = callback; + + /* construct path and query */ + struct aws_byte_cursor account_id_cursor = aws_byte_cursor_from_string(impl->sso_account_id); + struct aws_byte_cursor role_name_cursor = aws_byte_cursor_from_string(impl->sso_role_name); + struct aws_byte_cursor path_cursor = aws_byte_cursor_from_c_str("/federation/credentials?account_id="); + struct aws_byte_cursor role_name_param_cursor = aws_byte_cursor_from_c_str("&role_name="); + + if (aws_byte_buf_init_copy_from_cursor(&sso_query_context->path_and_query, provider->allocator, path_cursor) || + aws_byte_buf_append_encoding_uri_param(&sso_query_context->path_and_query, &account_id_cursor) || + aws_byte_buf_append_dynamic(&sso_query_context->path_and_query, &role_name_param_cursor) || + aws_byte_buf_append_encoding_uri_param(&sso_query_context->path_and_query, &role_name_cursor)) { + goto on_error; + } + + if (aws_byte_buf_init(&sso_query_context->payload, provider->allocator, SSO_RESPONSE_SIZE_INITIAL)) { + goto on_error; + } + + return sso_query_context; + +on_error: + s_sso_query_context_destroy(sso_query_context); + + return NULL; +} + +/* + * No matter the result, this always gets called assuming that sso_query_context is successfully allocated + */ +static void s_finalize_get_credentials_query(struct aws_sso_query_context *sso_query_context) { + struct aws_credentials *credentials = NULL; + + if (sso_query_context->error_code == AWS_ERROR_SUCCESS) { + /* parse credentials */ + struct aws_parse_credentials_from_json_doc_options parse_options = { + .access_key_id_name = "accessKeyId", + .secret_access_key_name = "secretAccessKey", + .token_name = "sessionToken", + .expiration_name = "expiration", + .top_level_object_name = "roleCredentials", + .token_required = true, + .expiration_required = true, + .expiration_format = AWS_PCEF_NUMBER_UNIX_EPOCH_MS, + }; + + credentials = aws_parse_credentials_from_json_document( + sso_query_context->allocator, aws_byte_cursor_from_buf(&sso_query_context->payload), &parse_options); + } + + if (credentials) { + AWS_LOGF_INFO( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "(id=%p) successfully queried credentials", + (void *)sso_query_context->provider); + } else { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "(id=%p) failed to query credentials", + (void *)sso_query_context->provider); + + if (sso_query_context->error_code == AWS_ERROR_SUCCESS) { + sso_query_context->error_code = AWS_AUTH_CREDENTIALS_PROVIDER_SSO_SOURCE_FAILURE; + } + } + + /* pass the credentials back */ + sso_query_context->original_callback( + credentials, sso_query_context->error_code, sso_query_context->original_user_data); + + /* clean up */ + s_sso_query_context_destroy(sso_query_context); + aws_credentials_release(credentials); +} +static void s_on_retry_ready(struct aws_retry_token *token, int error_code, void *user_data); + +static void s_on_stream_complete_fn(struct aws_http_stream *stream, int error_code, void *user_data) { + struct aws_sso_query_context *sso_query_context = user_data; + + struct aws_credentials_provider_sso_impl *impl = sso_query_context->provider->impl; + impl->function_table->aws_http_stream_release(stream); + + /* set error code */ + sso_query_context->error_code = error_code; + impl->function_table->aws_http_stream_get_incoming_response_status(stream, &sso_query_context->status_code); + if (error_code == AWS_OP_SUCCESS && sso_query_context->status_code != AWS_HTTP_STATUS_CODE_200_OK) { + sso_query_context->error_code = AWS_AUTH_CREDENTIALS_PROVIDER_HTTP_STATUS_FAILURE; + } + + /* + * If we can retry the request based on error response or http status code failure, retry it, otherwise, call the + * finalize function. + */ + if (error_code || sso_query_context->status_code != AWS_HTTP_STATUS_CODE_200_OK) { + enum aws_retry_error_type error_type = + aws_credentials_provider_compute_retry_error_type(sso_query_context->status_code, error_code); + + /* don't retry client errors at all. */ + if (error_type != AWS_RETRY_ERROR_TYPE_CLIENT_ERROR) { + if (aws_retry_strategy_schedule_retry( + sso_query_context->retry_token, error_type, s_on_retry_ready, sso_query_context) == + AWS_OP_SUCCESS) { + AWS_LOGF_INFO( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "(id=%p): successfully scheduled a retry", + (void *)sso_query_context->provider); + return; + } + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "(id=%p): failed to schedule retry: %s", + (void *)sso_query_context->provider, + aws_error_str(aws_last_error())); + sso_query_context->error_code = aws_last_error(); + } + } else { + int result = aws_retry_token_record_success(sso_query_context->retry_token); + (void)result; + AWS_ASSERT(result == AWS_ERROR_SUCCESS); + } + + s_finalize_get_credentials_query(sso_query_context); +} + +static int s_on_incoming_body_fn(struct aws_http_stream *stream, const struct aws_byte_cursor *body, void *user_data) { + + (void)stream; + + struct aws_sso_query_context *sso_query_context = user_data; + + AWS_LOGF_TRACE( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "(id=%p) received %zu response bytes", + (void *)sso_query_context->provider, + body->len); + + if (body->len + sso_query_context->payload.len > SSO_RESPONSE_SIZE_LIMIT) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "(id=%p) response exceeded maximum allowed length", + (void *)sso_query_context->provider); + + return aws_raise_error(AWS_ERROR_SHORT_BUFFER); + } + + if (aws_byte_buf_append_dynamic(&sso_query_context->payload, body)) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "(id=%p) error appending response payload: %s", + (void *)sso_query_context->provider, + aws_error_str(aws_last_error())); + + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +/* Request headers. */ +AWS_STATIC_STRING_FROM_LITERAL(s_sso_token_header, "x-amz-sso_bearer_token"); +AWS_STATIC_STRING_FROM_LITERAL(s_sso_user_agent_header, "User-Agent"); +AWS_STATIC_STRING_FROM_LITERAL(s_sso_user_agent_header_value, "aws-sdk-crt/sso-credentials-provider"); + +static void s_query_credentials(struct aws_sso_query_context *sso_query_context) { + AWS_FATAL_ASSERT(sso_query_context->connection); + struct aws_http_stream *stream = NULL; + struct aws_credentials_provider_sso_impl *impl = sso_query_context->provider->impl; + + sso_query_context->request = aws_http_message_new_request(sso_query_context->allocator); + if (sso_query_context->request == NULL) { + goto on_error; + } + + struct aws_http_header auth_header = { + .name = aws_byte_cursor_from_string(s_sso_token_header), + .value = aws_byte_cursor_from_string(sso_query_context->token), + }; + struct aws_http_header host_header = { + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("Host"), + .value = aws_byte_cursor_from_string(impl->endpoint), + }; + struct aws_http_header user_agent_header = { + .name = aws_byte_cursor_from_string(s_sso_user_agent_header), + .value = aws_byte_cursor_from_string(s_sso_user_agent_header_value), + }; + + if (aws_http_message_add_header(sso_query_context->request, auth_header) || + aws_http_message_add_header(sso_query_context->request, host_header) || + aws_http_message_add_header(sso_query_context->request, user_agent_header)) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "(id=%p) failed to add http header with error: %s", + (void *)sso_query_context->provider, + aws_error_debug_str(aws_last_error())); + goto on_error; + } + + if (aws_http_message_set_request_method(sso_query_context->request, aws_http_method_get)) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "(id=%p) failed to set request method with error: %s", + (void *)sso_query_context->provider, + aws_error_debug_str(aws_last_error())); + goto on_error; + } + + if (aws_http_message_set_request_path( + sso_query_context->request, aws_byte_cursor_from_buf(&sso_query_context->path_and_query))) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "(id=%p) failed to set request path with error: %s", + (void *)sso_query_context->provider, + aws_error_debug_str(aws_last_error())); + goto on_error; + } + + struct aws_http_make_request_options request_options = { + .self_size = sizeof(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, + .user_data = sso_query_context, + .request = sso_query_context->request, + }; + + stream = impl->function_table->aws_http_connection_make_request(sso_query_context->connection, &request_options); + if (!stream) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "(id=%p) failed to make request with error: %s", + (void *)sso_query_context->provider, + aws_error_debug_str(aws_last_error())); + goto on_error; + } + + if (impl->function_table->aws_http_stream_activate(stream)) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "(id=%p) failed to activate the stream with error: %s", + (void *)sso_query_context->provider, + aws_error_debug_str(aws_last_error())); + goto on_error; + } + + return; + +on_error: + sso_query_context->error_code = aws_last_error(); + impl->function_table->aws_http_stream_release(stream); + s_finalize_get_credentials_query(sso_query_context); +} + +static void s_on_get_token_callback(struct aws_credentials *credentials, int error_code, void *user_data) { + struct aws_sso_query_context *sso_query_context = user_data; + + if (error_code) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "id=%p: failed to acquire a token, error code %d(%s)", + (void *)sso_query_context->provider, + error_code, + aws_error_str(error_code)); + sso_query_context->error_code = error_code; + s_finalize_get_credentials_query(sso_query_context); + return; + } + + struct aws_byte_cursor token = aws_credentials_get_token(credentials); + AWS_LOGF_INFO( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "(id=%p): successfully accquired a token", + (void *)sso_query_context->provider); + + sso_query_context->token = aws_string_new_from_cursor(sso_query_context->allocator, &token); + s_query_credentials(sso_query_context); +} + +static void s_on_acquire_connection(struct aws_http_connection *connection, int error_code, void *user_data) { + struct aws_sso_query_context *sso_query_context = user_data; + + if (error_code) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "id=%p: failed to acquire a connection, error code %d(%s)", + (void *)sso_query_context->provider, + error_code, + aws_error_str(error_code)); + sso_query_context->error_code = error_code; + s_finalize_get_credentials_query(sso_query_context); + return; + } + AWS_LOGF_INFO( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "(id=%p): successfully accquired a connection", + (void *)sso_query_context->provider); + sso_query_context->connection = connection; + + struct aws_credentials_provider_sso_impl *impl = sso_query_context->provider->impl; + if (aws_credentials_provider_get_credentials(impl->token_provider, s_on_get_token_callback, user_data)) { + int last_error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "id=%p: failed to get a token, error code %d(%s)", + (void *)sso_query_context->provider, + last_error_code, + aws_error_str(last_error_code)); + + sso_query_context->error_code = last_error_code; + s_finalize_get_credentials_query(sso_query_context); + } +} + +/* called for each retry. */ +static void s_on_retry_ready(struct aws_retry_token *token, int error_code, void *user_data) { + (void)token; + struct aws_sso_query_context *sso_query_context = user_data; + struct aws_credentials_provider_sso_impl *impl = sso_query_context->provider->impl; + + if (error_code) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "(id=%p): failed to schedule retry with error: %s", + (void *)sso_query_context->provider, + aws_error_debug_str(error_code)); + sso_query_context->error_code = error_code; + s_finalize_get_credentials_query(sso_query_context); + return; + } + + /* clear the result from previous attempt */ + s_sso_query_context_reset_request_specific_data(sso_query_context); + + impl->function_table->aws_http_connection_manager_acquire_connection( + impl->connection_manager, s_on_acquire_connection, sso_query_context); +} + +static void s_on_retry_token_acquired( + struct aws_retry_strategy *strategy, + int error_code, + struct aws_retry_token *token, + void *user_data) { + struct aws_sso_query_context *sso_query_context = user_data; + (void)strategy; + + if (error_code) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "(id=%p): failed to acquire retry token: %s", + (void *)sso_query_context->provider, + aws_error_debug_str(error_code)); + sso_query_context->error_code = error_code; + s_finalize_get_credentials_query(sso_query_context); + return; + } + + sso_query_context->retry_token = token; + struct aws_credentials_provider_sso_impl *impl = sso_query_context->provider->impl; + impl->function_table->aws_http_connection_manager_acquire_connection( + impl->connection_manager, s_on_acquire_connection, user_data); +} + +static int s_credentials_provider_sso_get_credentials( + struct aws_credentials_provider *provider, + aws_on_get_credentials_callback_fn callback, + void *user_data) { + + struct aws_credentials_provider_sso_impl *impl = provider->impl; + + struct aws_sso_query_context *sso_query_context = s_sso_query_context_new(provider, callback, user_data); + if (sso_query_context == NULL) { + return AWS_OP_ERR; + } + + if (aws_retry_strategy_acquire_retry_token( + impl->retry_strategy, NULL, s_on_retry_token_acquired, sso_query_context, SSO_RETRY_TIMEOUT_MS)) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "(id=%p): failed to acquire retry token: %s", + (void *)provider, + aws_error_debug_str(aws_last_error())); + goto on_error; + } + + return AWS_OP_SUCCESS; + +on_error: + s_sso_query_context_destroy(sso_query_context); + return AWS_OP_ERR; +} + +static void s_on_connection_manager_shutdown(void *user_data) { + struct aws_credentials_provider *provider = user_data; + + aws_credentials_provider_invoke_shutdown_callback(provider); + aws_mem_release(provider->allocator, provider); +} + +static void s_credentials_provider_sso_destroy(struct aws_credentials_provider *provider) { + + struct aws_credentials_provider_sso_impl *impl = provider->impl; + if (impl == NULL) { + return; + } + aws_string_destroy(impl->endpoint); + aws_string_destroy(impl->sso_account_id); + aws_string_destroy(impl->sso_role_name); + aws_retry_strategy_release(impl->retry_strategy); + aws_credentials_provider_release(impl->token_provider); + + /* 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. So We should be freeing impl + * related memory first, then call aws_http_connection_manager_release. + */ + 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); + } +} + +static struct aws_credentials_provider_vtable s_aws_credentials_provider_sso_vtable = { + .get_credentials = s_credentials_provider_sso_get_credentials, + .destroy = s_credentials_provider_sso_destroy, +}; + +static int s_construct_sso_portal_endpoint( + struct aws_allocator *allocator, + struct aws_byte_buf *out_endpoint, + const struct aws_string *region) { + AWS_PRECONDITION(allocator); + AWS_PRECONDITION(out_endpoint); + + if (!region) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + aws_byte_buf_clean_up(out_endpoint); + struct aws_byte_cursor sso_prefix = aws_byte_cursor_from_c_str("portal.sso."); + struct aws_byte_cursor region_cursor = aws_byte_cursor_from_string(region); + struct aws_byte_cursor amazonaws_cursor = aws_byte_cursor_from_c_str(".amazonaws.com"); + struct aws_byte_cursor cn_cursor = aws_byte_cursor_from_c_str(".cn"); + + if (aws_byte_buf_init_copy_from_cursor(out_endpoint, allocator, sso_prefix) || + aws_byte_buf_append_dynamic(out_endpoint, ®ion_cursor) || + aws_byte_buf_append_dynamic(out_endpoint, &amazonaws_cursor)) { + goto on_error; + } + + if (aws_string_eq_c_str_ignore_case(region, "cn-north-1") || + aws_string_eq_c_str_ignore_case(region, "cn-northwest-1")) { + if (aws_byte_buf_append_dynamic(out_endpoint, &cn_cursor)) { + goto on_error; + } + } + return AWS_OP_SUCCESS; + +on_error: + aws_byte_buf_clean_up(out_endpoint); + return AWS_OP_ERR; +} + +AWS_STATIC_STRING_FROM_LITERAL(s_sso_account_id, "sso_account_id"); +AWS_STATIC_STRING_FROM_LITERAL(s_sso_region, "sso_region"); +AWS_STATIC_STRING_FROM_LITERAL(s_sso_role_name, "sso_role_name"); +AWS_STATIC_STRING_FROM_LITERAL(s_sso_session, "sso_session"); + +struct sso_parameters { + struct aws_allocator *allocator; + struct aws_byte_buf endpoint; + struct aws_string *sso_account_id; + struct aws_string *sso_role_name; + struct aws_credentials_provider *token_provider; +}; + +static void s_parameters_destroy(struct sso_parameters *parameters) { + if (!parameters) { + return; + } + aws_byte_buf_clean_up(¶meters->endpoint); + aws_string_destroy(parameters->sso_account_id); + aws_string_destroy(parameters->sso_role_name); + aws_credentials_provider_release(parameters->token_provider); + aws_mem_release(parameters->allocator, parameters); +} + +/** + * Read the config file and construct profile or sso_session token provider based on sso_session property. + * + * If the profile contains sso_session property, a valid config example is as follow. + * [profile sso-profile] + * sso_session = dev + * sso_account_id = 012345678901 + * sso_role_name = SampleRole + * + * [sso-session dev] + * sso_region = us-east-1 + * sso_start_url = https://d-abc123.awsapps.com/start + * + * If the profile does't contains sso_session, the legacy valid config example is as follow. + * [profile sso-profile] + * sso_account_id = 012345678901 + * sso_region = us-east-1 + * sso_role_name = SampleRole + * sso_start_url = https://d-abc123.awsapps.com/start-beta + */ +static struct sso_parameters *s_parameters_new( + struct aws_allocator *allocator, + const struct aws_credentials_provider_sso_options *options) { + + struct sso_parameters *parameters = aws_mem_calloc(allocator, 1, sizeof(struct sso_parameters)); + parameters->allocator = allocator; + + struct aws_profile_collection *config_profile_collection = NULL; + struct aws_string *profile_name = NULL; + bool success = false; + + profile_name = aws_get_profile_name(allocator, &options->profile_name_override); + if (!profile_name) { + AWS_LOGF_ERROR(AWS_LS_AUTH_CREDENTIALS_PROVIDER, "sso: failed to resolve profile name"); + goto on_finish; + } + if (options->config_file_cached) { + /* Use cached config file */ + config_profile_collection = aws_profile_collection_acquire(options->config_file_cached); + } else { + /* load config file */ + config_profile_collection = + aws_load_profile_collection_from_config_file(allocator, options->config_file_name_override); + } + + if (!config_profile_collection) { + goto on_finish; + } + + const struct aws_profile *profile = aws_profile_collection_get_profile(config_profile_collection, profile_name); + if (!profile) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, "sso: failed to load \"%s\" profile", aws_string_c_str(profile_name)); + goto on_finish; + } + + const struct aws_profile_property *sso_account_id = aws_profile_get_property(profile, s_sso_account_id); + const struct aws_profile_property *sso_role_name = aws_profile_get_property(profile, s_sso_role_name); + const struct aws_profile_property *sso_region = NULL; + + if (!sso_account_id) { + AWS_LOGF_ERROR(AWS_LS_AUTH_CREDENTIALS_PROVIDER, "sso: sso_account_id is missing"); + aws_raise_error(AWS_AUTH_CREDENTIALS_PROVIDER_SSO_SOURCE_FAILURE); + goto on_finish; + } + if (!sso_role_name) { + AWS_LOGF_ERROR(AWS_LS_AUTH_CREDENTIALS_PROVIDER, "sso: sso_role_name is missing"); + aws_raise_error(AWS_AUTH_CREDENTIALS_PROVIDER_SSO_SOURCE_FAILURE); + goto on_finish; + } + + const struct aws_profile_property *sso_session_property = aws_profile_get_property(profile, s_sso_session); + /* create the appropriate token provider based on sso_session property is available or not */ + if (sso_session_property) { + /* construct sso_session token provider */ + struct aws_token_provider_sso_session_options token_provider_options = { + .config_file_name_override = options->config_file_name_override, + .config_file_cached = config_profile_collection, + .profile_name_override = options->profile_name_override, + .bootstrap = options->bootstrap, + .tls_ctx = options->tls_ctx, + .system_clock_fn = options->system_clock_fn, + }; + parameters->token_provider = aws_token_provider_new_sso_session(allocator, &token_provider_options); + if (!parameters->token_provider) { + AWS_LOGF_ERROR(AWS_LS_AUTH_CREDENTIALS_PROVIDER, "sso: unable to create a sso token provider"); + aws_raise_error(AWS_AUTH_CREDENTIALS_PROVIDER_SSO_SOURCE_FAILURE); + goto on_finish; + } + sso_region = aws_profile_get_property( + aws_profile_collection_get_section( + config_profile_collection, + AWS_PROFILE_SECTION_TYPE_SSO_SESSION, + aws_profile_property_get_value(sso_session_property)), + s_sso_region); + } else { + /* construct profile token provider */ + struct aws_token_provider_sso_profile_options token_provider_options = { + .config_file_name_override = options->config_file_name_override, + .config_file_cached = config_profile_collection, + .profile_name_override = options->profile_name_override, + .system_clock_fn = options->system_clock_fn, + }; + + parameters->token_provider = aws_token_provider_new_sso_profile(allocator, &token_provider_options); + if (!parameters->token_provider) { + AWS_LOGF_ERROR(AWS_LS_AUTH_CREDENTIALS_PROVIDER, "sso: unable to create a profile token provider"); + aws_raise_error(AWS_AUTH_CREDENTIALS_PROVIDER_SSO_SOURCE_FAILURE); + goto on_finish; + } + sso_region = aws_profile_get_property(profile, s_sso_region); + } + + if (!sso_region) { + AWS_LOGF_ERROR(AWS_LS_AUTH_CREDENTIALS_PROVIDER, "sso: sso_region is missing"); + aws_raise_error(AWS_AUTH_CREDENTIALS_PROVIDER_SSO_SOURCE_FAILURE); + goto on_finish; + } + + parameters->sso_account_id = aws_string_new_from_string(allocator, aws_profile_property_get_value(sso_account_id)); + parameters->sso_role_name = aws_string_new_from_string(allocator, aws_profile_property_get_value(sso_role_name)); + /* determine endpoint */ + if (s_construct_sso_portal_endpoint(allocator, ¶meters->endpoint, aws_profile_property_get_value(sso_region))) { + AWS_LOGF_ERROR(AWS_LS_AUTH_CREDENTIALS_PROVIDER, "Failed to construct sso endpoint"); + goto on_finish; + } + AWS_LOGF_DEBUG( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, "Successfully loaded all required parameters for sso credentials provider."); + success = true; + +on_finish: + if (!success) { + s_parameters_destroy(parameters); + parameters = NULL; + } + aws_string_destroy(profile_name); + aws_profile_collection_release(config_profile_collection); + + return parameters; +} + +struct aws_credentials_provider *aws_credentials_provider_new_sso( + struct aws_allocator *allocator, + const struct aws_credentials_provider_sso_options *options) { + + struct sso_parameters *parameters = s_parameters_new(allocator, options); + if (!parameters) { + return NULL; + } + + struct aws_credentials_provider *provider = NULL; + struct aws_credentials_provider_sso_impl *impl = NULL; + struct aws_tls_connection_options tls_connection_options; + + aws_mem_acquire_many( + allocator, + 2, + &provider, + sizeof(struct aws_credentials_provider), + &impl, + sizeof(struct aws_credentials_provider_sso_impl)); + + AWS_ZERO_STRUCT(*provider); + AWS_ZERO_STRUCT(*impl); + AWS_ZERO_STRUCT(tls_connection_options); + + aws_credentials_provider_init_base(provider, allocator, &s_aws_credentials_provider_sso_vtable, impl); + + if (!options->tls_ctx) { + AWS_LOGF_ERROR(AWS_LS_AUTH_CREDENTIALS_PROVIDER, "(id=%p): a TLS context must be provided", (void *)provider); + aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + goto on_error; + } + + if (!options->bootstrap) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, "(id=%p): a bootstrap instance must be provided", (void *)provider); + aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + goto on_error; + } + + aws_tls_connection_options_init_from_ctx(&tls_connection_options, options->tls_ctx); + struct aws_byte_cursor host = aws_byte_cursor_from_buf(¶meters->endpoint); + if (aws_tls_connection_options_set_server_name(&tls_connection_options, allocator, &host)) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "(id=%p): failed to create a tls connection options with error %s", + (void *)provider, + aws_error_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( + SSO_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 = SSO_RESPONSE_SIZE_LIMIT; + manager_options.socket_options = &socket_options; + manager_options.host = host; + 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; + + 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) { + goto on_error; + } + + impl->token_provider = aws_credentials_provider_acquire(parameters->token_provider); + impl->endpoint = aws_string_new_from_buf(allocator, ¶meters->endpoint); + impl->sso_account_id = aws_string_new_from_string(allocator, parameters->sso_account_id); + impl->sso_role_name = aws_string_new_from_string(allocator, parameters->sso_role_name); + + provider->shutdown_options = options->shutdown_options; + + struct aws_standard_retry_options retry_options = { + .backoff_retry_options = + { + .el_group = options->bootstrap->event_loop_group, + .max_retries = SSO_MAX_ATTEMPTS, + }, + }; + 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): failed to create a retry strategy with error %s", + (void *)provider, + aws_error_debug_str(aws_last_error())); + goto on_error; + } + + s_parameters_destroy(parameters); + aws_tls_connection_options_clean_up(&tls_connection_options); + return provider; + +on_error: + aws_credentials_provider_destroy(provider); + s_parameters_destroy(parameters); + aws_tls_connection_options_clean_up(&tls_connection_options); + return NULL; +} diff --git a/source/credentials_utils.c b/source/credentials_utils.c index 2b86869f..a1ee268c 100644 --- a/source/credentials_utils.c +++ b/source/credentials_utils.c @@ -5,6 +5,7 @@ #include +#include #include #include #include @@ -12,6 +13,7 @@ #include #include #include +#include #if defined(_MSC_VER) # pragma warning(disable : 4232) @@ -127,6 +129,20 @@ static bool s_parse_expiration_value_from_json_object( return true; } + case AWS_PCEF_NUMBER_UNIX_EPOCH_MS: { + double expiration_value_ms = 0; + if (aws_json_value_get_number(value, &expiration_value_ms)) { + AWS_LOGF_INFO( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "Unabled to extract credentials Expiration field from Json document."); + return false; + } + + *expiration_timepoint_in_seconds = + aws_timestamp_convert((uint64_t)expiration_value_ms, AWS_TIMESTAMP_MILLIS, AWS_TIMESTAMP_SECS, NULL); + return true; + } + default: return false; } @@ -253,13 +269,27 @@ struct aws_credentials *aws_parse_credentials_from_json_document( struct aws_allocator *allocator, struct aws_byte_cursor document, const struct aws_parse_credentials_from_json_doc_options *options) { + struct aws_credentials *credentials = NULL; struct aws_json_value *document_root = aws_json_value_new_from_string(allocator, document); if (document_root == NULL) { AWS_LOGF_ERROR(AWS_LS_AUTH_CREDENTIALS_PROVIDER, "Failed to parse document as Json document."); return NULL; } - struct aws_credentials *credentials = aws_parse_credentials_from_aws_json_object(allocator, document_root, options); + + struct aws_json_value *top_level_object = NULL; + if (options->top_level_object_name) { + top_level_object = + aws_json_value_get_from_object(document_root, aws_byte_cursor_from_c_str(options->top_level_object_name)); + if (!top_level_object) { + AWS_LOGF_ERROR(AWS_LS_AUTH_CREDENTIALS_PROVIDER, "failed to parse top level object in json document."); + goto done; + } + } + + credentials = aws_parse_credentials_from_aws_json_object( + allocator, top_level_object ? top_level_object : document_root, options); +done: aws_json_value_destroy(document_root); return credentials; } @@ -291,3 +321,37 @@ enum aws_retry_error_type aws_credentials_provider_compute_retry_error_type(int return error_type; } + +struct aws_profile_collection *aws_load_profile_collection_from_config_file( + struct aws_allocator *allocator, + struct aws_byte_cursor config_file_name_override) { + + struct aws_profile_collection *config_profiles = NULL; + struct aws_string *config_file_path = NULL; + + config_file_path = aws_get_config_file_path(allocator, &config_file_name_override); + if (!config_file_path) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "Failed to resolve config file path: %s", + aws_error_str(aws_last_error())); + return NULL; + } + + config_profiles = aws_profile_collection_new_from_file(allocator, config_file_path, AWS_PST_CONFIG); + if (config_profiles != NULL) { + AWS_LOGF_DEBUG( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "Successfully built config profile collection from file at (%s)", + aws_string_c_str(config_file_path)); + } else { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "Failed to build config profile collection from file at (%s) : %s", + aws_string_c_str(config_file_path), + aws_error_str(aws_last_error())); + } + + aws_string_destroy(config_file_path); + return config_profiles; +} diff --git a/source/sso_token_utils.c b/source/sso_token_utils.c new file mode 100644 index 00000000..7b90ef43 --- /dev/null +++ b/source/sso_token_utils.c @@ -0,0 +1,160 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include +#include +#include +#include +#include + +#if defined(_MSC_VER) +# pragma warning(disable : 4232) +#endif /* _MSC_VER */ + +struct aws_string *aws_construct_sso_token_path(struct aws_allocator *allocator, const struct aws_string *input) { + AWS_PRECONDITION(input); + + struct aws_string *sso_token_path_str = NULL; + + struct aws_string *home_directory = aws_get_home_directory(allocator); + if (!home_directory) { + return NULL; + } + + struct aws_byte_cursor home_dir_cursor = aws_byte_cursor_from_string(home_directory); + struct aws_byte_cursor input_cursor = aws_byte_cursor_from_string(input); + struct aws_byte_cursor json_cursor = aws_byte_cursor_from_c_str(".json"); + + struct aws_byte_buf sso_token_path_buf; + AWS_ZERO_STRUCT(sso_token_path_buf); + struct aws_byte_buf sha1_buf; + AWS_ZERO_STRUCT(sha1_buf); + + /* append home directory */ + if (aws_byte_buf_init_copy_from_cursor(&sso_token_path_buf, allocator, home_dir_cursor)) { + goto cleanup; + } + + /* append sso cache directory */ + struct aws_byte_cursor sso_cache_dir_cursor = aws_byte_cursor_from_c_str("/.aws/sso/cache/"); + if (aws_byte_buf_append_dynamic(&sso_token_path_buf, &sso_cache_dir_cursor)) { + goto cleanup; + } + + /* append hex encoded sha1 of input */ + if (aws_byte_buf_init(&sha1_buf, allocator, AWS_SHA1_LEN) || + aws_sha1_compute(allocator, &input_cursor, &sha1_buf, 0)) { + goto cleanup; + } + struct aws_byte_cursor sha1_cursor = aws_byte_cursor_from_buf(&sha1_buf); + if (aws_hex_encode_append_dynamic(&sha1_cursor, &sso_token_path_buf)) { + goto cleanup; + } + + /* append .json */ + if (aws_byte_buf_append_dynamic(&sso_token_path_buf, &json_cursor)) { + goto cleanup; + } + + /* use platform-specific directory separator. */ + aws_normalize_directory_separator(&sso_token_path_buf); + + sso_token_path_str = aws_string_new_from_buf(allocator, &sso_token_path_buf); + AWS_LOGF_INFO( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "successfully constructed token path: %s", + aws_string_c_str(sso_token_path_str)); +cleanup: + aws_byte_buf_clean_up(&sso_token_path_buf); + aws_byte_buf_clean_up(&sha1_buf); + aws_string_destroy(home_directory); + return sso_token_path_str; +} + +void aws_sso_token_destroy(struct aws_sso_token *sso_token) { + if (sso_token == NULL) { + return; + } + + aws_string_destroy(sso_token->access_token); + aws_mem_release(sso_token->allocator, sso_token); +} + +struct aws_sso_token *aws_sso_token_new_from_file(struct aws_allocator *allocator, const struct aws_string *file_path) { + AWS_PRECONDITION(allocator); + AWS_PRECONDITION(file_path); + + bool success = false; + + struct aws_sso_token *token = aws_mem_calloc(allocator, 1, sizeof(struct aws_sso_token)); + token->allocator = allocator; + struct aws_byte_buf file_contents_buf; + AWS_ZERO_STRUCT(file_contents_buf); + struct aws_json_value *document_root = NULL; + + if (aws_byte_buf_init_from_file(&file_contents_buf, allocator, aws_string_c_str(file_path))) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, "sso token: failed to load token file %s", aws_string_c_str(file_path)); + goto cleanup; + } + + struct aws_byte_cursor document_cursor = aws_byte_cursor_from_buf(&file_contents_buf); + document_root = aws_json_value_new_from_string(allocator, document_cursor); + if (document_root == NULL) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "sso token: failed to parse sso token file %s", + aws_string_c_str(file_path)); + aws_raise_error(AWS_AUTH_SSO_TOKEN_INVALID); + goto cleanup; + } + + struct aws_byte_cursor access_token_cursor; + struct aws_json_value *access_token = + aws_json_value_get_from_object(document_root, aws_byte_cursor_from_c_str("accessToken")); + if (!aws_json_value_is_string(access_token) || aws_json_value_get_string(access_token, &access_token_cursor)) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "sso token: failed to parse accessToken from %s", + aws_string_c_str(file_path)); + aws_raise_error(AWS_AUTH_SSO_TOKEN_INVALID); + goto cleanup; + } + + struct aws_byte_cursor expires_at_cursor; + struct aws_json_value *expires_at = + aws_json_value_get_from_object(document_root, aws_byte_cursor_from_c_str("expiresAt")); + if (!aws_json_value_is_string(expires_at) || aws_json_value_get_string(expires_at, &expires_at_cursor)) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "sso token: failed to parse expiresAt from %s", + aws_string_c_str(file_path)); + aws_raise_error(AWS_AUTH_SSO_TOKEN_INVALID); + goto cleanup; + } + struct aws_date_time expiration; + if (aws_date_time_init_from_str_cursor(&expiration, &expires_at_cursor, AWS_DATE_FORMAT_ISO_8601)) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "sso token: expiresAt '" PRInSTR "' in %s is not a valid ISO-8601 date string", + AWS_BYTE_CURSOR_PRI(expires_at_cursor), + aws_string_c_str(file_path)); + aws_raise_error(AWS_AUTH_SSO_TOKEN_INVALID); + goto cleanup; + } + token->access_token = aws_string_new_from_cursor(allocator, &access_token_cursor); + token->expiration = expiration; + + success = true; + +cleanup: + aws_json_value_destroy(document_root); + aws_byte_buf_clean_up(&file_contents_buf); + if (!success) { + aws_sso_token_destroy(token); + token = NULL; + } + return token; +} diff --git a/source/token_provider_sso_profile.c b/source/token_provider_sso_profile.c new file mode 100644 index 00000000..6e2a3521 --- /dev/null +++ b/source/token_provider_sso_profile.c @@ -0,0 +1,186 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include +#include +#include +#include + +#ifdef _MSC_VER +/* allow non-constant declared initializers. */ +# pragma warning(disable : 4204) +#endif + +/* + * sso-token profile provider implementation + */ +struct aws_token_provider_profile_impl { + struct aws_string *sso_token_file_path; + + aws_io_clock_fn *system_clock_fn; +}; + +static int s_token_provider_profile_get_token( + struct aws_credentials_provider *provider, + aws_on_get_credentials_callback_fn callback, + void *user_data) { + struct aws_token_provider_profile_impl *impl = provider->impl; + + struct aws_sso_token *sso_token = NULL; + struct aws_credentials *credentials = NULL; + int result = AWS_OP_ERR; + sso_token = aws_sso_token_new_from_file(provider->allocator, impl->sso_token_file_path); + if (!sso_token) { + AWS_LOGF_ERROR(AWS_LS_AUTH_CREDENTIALS_PROVIDER, "(id=%p) failed to get sso token from file", (void *)provider); + goto done; + } + + /* check token expiration. */ + uint64_t now_ns = UINT64_MAX; + if (impl->system_clock_fn(&now_ns) != AWS_OP_SUCCESS) { + goto done; + } + + if (aws_date_time_as_nanos(&sso_token->expiration) <= now_ns) { + AWS_LOGF_ERROR(AWS_LS_AUTH_CREDENTIALS_PROVIDER, "(id=%p) cached sso token is expired.", (void *)provider); + aws_raise_error(AWS_AUTH_SSO_TOKEN_EXPIRED); + goto done; + } + + credentials = aws_credentials_new_token( + provider->allocator, + aws_byte_cursor_from_string(sso_token->access_token), + (uint64_t)aws_date_time_as_epoch_secs(&sso_token->expiration)); + if (!credentials) { + AWS_LOGF_ERROR(AWS_LS_AUTH_CREDENTIALS_PROVIDER, "(id=%p) Unable to construct credentials.", (void *)provider); + goto done; + } + + callback(credentials, AWS_OP_SUCCESS, user_data); + result = AWS_OP_SUCCESS; + +done: + aws_sso_token_destroy(sso_token); + aws_credentials_release(credentials); + return result; +} + +static void s_token_provider_profile_destroy(struct aws_credentials_provider *provider) { + struct aws_token_provider_profile_impl *impl = provider->impl; + if (impl == NULL) { + return; + } + + aws_string_destroy(impl->sso_token_file_path); + + aws_credentials_provider_invoke_shutdown_callback(provider); + aws_mem_release(provider->allocator, provider); +} + +static struct aws_credentials_provider_vtable s_aws_token_provider_profile_vtable = { + .get_credentials = s_token_provider_profile_get_token, + .destroy = s_token_provider_profile_destroy, +}; + +AWS_STRING_FROM_LITERAL(s_profile_sso_start_url_name, "sso_start_url"); + +static struct aws_string *s_construct_profile_sso_token_path( + struct aws_allocator *allocator, + const struct aws_token_provider_sso_profile_options *options) { + + struct aws_profile_collection *config_collection = NULL; + struct aws_string *profile_name = NULL; + struct aws_string *sso_token_path = NULL; + + profile_name = aws_get_profile_name(allocator, &options->profile_name_override); + if (!profile_name) { + AWS_LOGF_ERROR(AWS_LS_AUTH_CREDENTIALS_PROVIDER, "token-provider-sso-profile: failed to resolve profile name"); + aws_raise_error(AWS_AUTH_SSO_TOKEN_PROVIDER_SOURCE_FAILURE); + goto cleanup; + } + if (options->config_file_cached) { + /* Use cached config file */ + config_collection = aws_profile_collection_acquire(options->config_file_cached); + } else { + /* load config file */ + config_collection = aws_load_profile_collection_from_config_file(allocator, options->config_file_name_override); + } + + if (!config_collection) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "token-provider-sso-profile: could not load or parse" + " a config file."); + aws_raise_error(AWS_AUTH_SSO_TOKEN_PROVIDER_SOURCE_FAILURE); + goto cleanup; + } + + const struct aws_profile *profile = aws_profile_collection_get_profile(config_collection, profile_name); + + if (!profile) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "token-provider-sso-profile: could not load" + " a profile at %s.", + aws_string_c_str(profile_name)); + aws_raise_error(AWS_AUTH_SSO_TOKEN_PROVIDER_SOURCE_FAILURE); + goto cleanup; + } + + const struct aws_profile_property *sso_start_url_property = + aws_profile_get_property(profile, s_profile_sso_start_url_name); + + if (!sso_start_url_property) { + AWS_LOGF_ERROR(AWS_LS_AUTH_CREDENTIALS_PROVIDER, "token-provider-sso-profile: failed to find sso_start_url"); + aws_raise_error(AWS_AUTH_SSO_TOKEN_PROVIDER_SOURCE_FAILURE); + goto cleanup; + } + + sso_token_path = aws_construct_sso_token_path(allocator, aws_profile_property_get_value(sso_start_url_property)); + if (!sso_token_path) { + AWS_LOGF_ERROR(AWS_LS_AUTH_CREDENTIALS_PROVIDER, "token-provider-sso-profile: failed to construct token path"); + goto cleanup; + } + +cleanup: + aws_string_destroy(profile_name); + aws_profile_collection_release(config_collection); + return sso_token_path; +} + +struct aws_credentials_provider *aws_token_provider_new_sso_profile( + struct aws_allocator *allocator, + const struct aws_token_provider_sso_profile_options *options) { + struct aws_string *token_path = s_construct_profile_sso_token_path(allocator, options); + if (!token_path) { + return NULL; + } + struct aws_credentials_provider *provider = NULL; + struct aws_token_provider_profile_impl *impl = NULL; + + aws_mem_acquire_many( + allocator, + 2, + &provider, + sizeof(struct aws_credentials_provider), + &impl, + sizeof(struct aws_token_provider_profile_impl)); + AWS_ZERO_STRUCT(*provider); + AWS_ZERO_STRUCT(*impl); + aws_credentials_provider_init_base(provider, allocator, &s_aws_token_provider_profile_vtable, impl); + impl->sso_token_file_path = aws_string_new_from_string(allocator, token_path); + provider->shutdown_options = options->shutdown_options; + if (options->system_clock_fn) { + impl->system_clock_fn = options->system_clock_fn; + } else { + impl->system_clock_fn = aws_sys_clock_get_ticks; + } + + aws_string_destroy(token_path); + return provider; +} diff --git a/source/token_provider_sso_session.c b/source/token_provider_sso_session.c new file mode 100644 index 00000000..e3a17b7f --- /dev/null +++ b/source/token_provider_sso_session.c @@ -0,0 +1,251 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include +#include +#include +#include + +#ifdef _MSC_VER +/* allow non-constant declared initializers. */ +# pragma warning(disable : 4204) +#endif + +/* + * sso-session token provider implementation + */ +struct aws_token_provider_sso_session_impl { + struct aws_string *sso_token_file_path; + aws_io_clock_fn *system_clock_fn; +}; + +static int s_token_provider_sso_session_get_token( + struct aws_credentials_provider *provider, + aws_on_get_credentials_callback_fn callback, + void *user_data) { + + struct aws_token_provider_sso_session_impl *impl = provider->impl; + struct aws_sso_token *sso_token = NULL; + struct aws_credentials *credentials = NULL; + int result = AWS_OP_ERR; + + sso_token = aws_sso_token_new_from_file(provider->allocator, impl->sso_token_file_path); + if (!sso_token) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, "(id=%p) failed to get sso token from file.", (void *)provider); + goto done; + } + + /* check token expiration. */ + uint64_t now_ns = UINT64_MAX; + if (impl->system_clock_fn(&now_ns) != AWS_OP_SUCCESS) { + goto done; + } + + if (aws_date_time_as_nanos(&sso_token->expiration) <= now_ns) { + AWS_LOGF_ERROR(AWS_LS_AUTH_CREDENTIALS_PROVIDER, "(id=%p) cached sso token is expired.", (void *)provider); + aws_raise_error(AWS_AUTH_SSO_TOKEN_EXPIRED); + goto done; + } + + /* TODO: Refresh token if it is within refresh window and refreshable */ + + credentials = aws_credentials_new_token( + provider->allocator, + aws_byte_cursor_from_string(sso_token->access_token), + (uint64_t)aws_date_time_as_epoch_secs(&sso_token->expiration)); + if (!credentials) { + AWS_LOGF_ERROR(AWS_LS_AUTH_CREDENTIALS_PROVIDER, "(id=%p) Unable to construct credentials.", (void *)provider); + goto done; + } + callback(credentials, AWS_OP_SUCCESS, user_data); + result = AWS_OP_SUCCESS; + +done: + aws_sso_token_destroy(sso_token); + aws_credentials_release(credentials); + return result; +} + +static void s_token_provider_sso_session_destroy(struct aws_credentials_provider *provider) { + struct aws_token_provider_sso_session_impl *impl = provider->impl; + if (impl == NULL) { + return; + } + + aws_string_destroy(impl->sso_token_file_path); + + aws_credentials_provider_invoke_shutdown_callback(provider); + aws_mem_release(provider->allocator, provider); +} + +static struct aws_credentials_provider_vtable s_aws_token_provider_sso_session_vtable = { + .get_credentials = s_token_provider_sso_session_get_token, + .destroy = s_token_provider_sso_session_destroy, +}; + +AWS_STRING_FROM_LITERAL(s_sso_session_name, "sso_session"); +AWS_STRING_FROM_LITERAL(s_sso_region_name, "sso_region"); +AWS_STRING_FROM_LITERAL(s_sso_start_url_name, "sso_start_url"); + +/** + * Parses the config file to validate and construct a token path. A valid profile with sso session is as follow + * [profile sso-profile] + * sso_session = dev + * sso_account_id = 012345678901 + * sso_role_name = SampleRole + * + * [sso-session dev] + * sso_region = us-east-1 + * sso_start_url = https://d-abc123.awsapps.com/start + */ +static struct aws_string *s_verify_config_and_construct_sso_token_path( + struct aws_allocator *allocator, + const struct aws_token_provider_sso_session_options *options) { + struct aws_profile_collection *config_collection = NULL; + struct aws_string *profile_name = NULL; + struct aws_string *sso_token_path = NULL; + + profile_name = aws_get_profile_name(allocator, &options->profile_name_override); + if (!profile_name) { + AWS_LOGF_ERROR(AWS_LS_AUTH_CREDENTIALS_PROVIDER, "sso-session: token provider failed to resolve profile name"); + goto cleanup; + } + if (options->config_file_cached) { + /* Use cached config file */ + config_collection = aws_profile_collection_acquire(options->config_file_cached); + } else { + /* load config file */ + config_collection = aws_load_profile_collection_from_config_file(allocator, options->config_file_name_override); + } + if (!config_collection) { + goto cleanup; + } + + const struct aws_profile *profile = aws_profile_collection_get_profile(config_collection, profile_name); + + if (!profile) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "sso-session: token provider could not load" + " a profile at %s.", + aws_string_c_str(profile_name)); + goto cleanup; + } + + const struct aws_profile_property *sso_session_property = aws_profile_get_property(profile, s_sso_session_name); + if (!sso_session_property) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "token-provider-sso-session: token provider could not find an sso-session at profile %s", + aws_string_c_str(profile_name)); + aws_raise_error(AWS_AUTH_SSO_TOKEN_PROVIDER_SOURCE_FAILURE); + goto cleanup; + } + const struct aws_string *sso_session_name = aws_profile_property_get_value(sso_session_property); + + /* parse sso_session */ + const struct aws_profile *session_profile = + aws_profile_collection_get_section(config_collection, AWS_PROFILE_SECTION_TYPE_SSO_SESSION, sso_session_name); + if (!session_profile) { + AWS_LOGF_ERROR(AWS_LS_AUTH_CREDENTIALS_PROVIDER, "token-provider-sso-session: failed to find an sso-session"); + aws_raise_error(AWS_AUTH_SSO_TOKEN_PROVIDER_SOURCE_FAILURE); + goto cleanup; + } + + const struct aws_profile_property *sso_region_property = + aws_profile_get_property(session_profile, s_sso_region_name); + const struct aws_profile_property *sso_start_url_property = + aws_profile_get_property(session_profile, s_sso_start_url_name); + + if (!sso_region_property) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, "token-provider-sso-session: failed to find sso_region in sso-session"); + aws_raise_error(AWS_AUTH_SSO_TOKEN_PROVIDER_SOURCE_FAILURE); + goto cleanup; + } + + if (!sso_start_url_property) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "token-provider-sso-session: failed to find sso_start_url in sso-session"); + aws_raise_error(AWS_AUTH_SSO_TOKEN_PROVIDER_SOURCE_FAILURE); + goto cleanup; + } + + /* Verify sso_region & start_url are the same in profile section if they exist */ + const struct aws_string *sso_region = aws_profile_property_get_value(sso_region_property); + const struct aws_string *sso_start_url = aws_profile_property_get_value(sso_start_url_property); + + const struct aws_profile_property *profile_sso_region_property = + aws_profile_get_property(profile, s_sso_region_name); + const struct aws_profile_property *profile_sso_start_url_property = + aws_profile_get_property(profile, s_sso_start_url_name); + + if (profile_sso_region_property && + !aws_string_eq(sso_region, aws_profile_property_get_value(profile_sso_region_property))) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "token-provider-sso-session: profile & sso-session have different value for sso_region"); + aws_raise_error(AWS_AUTH_SSO_TOKEN_PROVIDER_SOURCE_FAILURE); + goto cleanup; + } + + if (profile_sso_start_url_property && + !aws_string_eq(sso_start_url, aws_profile_property_get_value(profile_sso_start_url_property))) { + AWS_LOGF_ERROR( + AWS_LS_AUTH_CREDENTIALS_PROVIDER, + "token-provider-sso-session: profile & sso-session have different value for sso_start_url"); + aws_raise_error(AWS_AUTH_SSO_TOKEN_PROVIDER_SOURCE_FAILURE); + goto cleanup; + } + + sso_token_path = aws_construct_sso_token_path(allocator, sso_session_name); + +cleanup: + aws_string_destroy(profile_name); + aws_profile_collection_release(config_collection); + return sso_token_path; +} + +struct aws_credentials_provider *aws_token_provider_new_sso_session( + struct aws_allocator *allocator, + const struct aws_token_provider_sso_session_options *options) { + + /* Currently, they are not used but they will be required when we implement the refresh token functionality. */ + AWS_ASSERT(options->bootstrap); + AWS_ASSERT(options->tls_ctx); + + struct aws_string *token_path = s_verify_config_and_construct_sso_token_path(allocator, options); + if (!token_path) { + return NULL; + } + struct aws_credentials_provider *provider = NULL; + struct aws_token_provider_sso_session_impl *impl = NULL; + + aws_mem_acquire_many( + allocator, + 2, + &provider, + sizeof(struct aws_credentials_provider), + &impl, + sizeof(struct aws_token_provider_sso_session_impl)); + AWS_ZERO_STRUCT(*provider); + AWS_ZERO_STRUCT(*impl); + aws_credentials_provider_init_base(provider, allocator, &s_aws_token_provider_sso_session_vtable, impl); + impl->sso_token_file_path = aws_string_new_from_string(allocator, token_path); + provider->shutdown_options = options->shutdown_options; + if (options->system_clock_fn) { + impl->system_clock_fn = options->system_clock_fn; + } else { + impl->system_clock_fn = aws_sys_clock_get_ticks; + } + + aws_string_destroy(token_path); + return provider; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a4859f9f..943a12fe 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -103,6 +103,40 @@ if (AWS_HAS_CI_ENVIRONMENT) add_net_test_case(credentials_provider_cognito_success_unauthenticated) endif() +add_test_case(sso_token_provider_profile_invalid_profile_test) +add_test_case(sso_token_provider_profile_valid_profile_test) +add_net_test_case(sso_token_provider_sso_session_invalid_config_test) +add_net_test_case(sso_token_provider_sso_session_valid_config_test) +add_net_test_case(sso_token_provider_sso_session_basic_success) +add_net_test_case(sso_token_provider_sso_session_config_file_cached) +add_net_test_case(sso_token_provider_sso_session_expired_token) +add_test_case(sso_token_provider_profile_basic_success) +add_test_case(sso_token_provider_profile_cached_config_file) +add_test_case(sso_token_provider_profile_expired_token) + +add_test_case(parse_token_location_url_test) +add_test_case(parse_token_location_session_test) +add_test_case(parse_sso_token_valid) +add_test_case(parse_sso_token_invalid) +add_test_case(parse_sso_token_invalid_missing_access_token) +add_test_case(parse_sso_token_missing_expires_at) +add_test_case(parse_sso_token_invalid_expires_at) + +add_net_test_case(credentials_provider_sso_failed_invalid_config) +add_net_test_case(credentials_provider_sso_create_destroy_valid_config) +add_net_test_case(credentials_provider_sso_connect_failure) +add_net_test_case(credentials_provider_sso_failure_token_missing) +add_net_test_case(credentials_provider_sso_failure_token_expired) +add_net_test_case(credentials_provider_sso_failure_token_empty) +add_net_test_case(credentials_provider_sso_request_failure) +add_net_test_case(credentials_provider_sso_bad_response) +add_net_test_case(credentials_provider_sso_retryable_error) +add_net_test_case(credentials_provider_sso_basic_success) +add_net_test_case(credentials_provider_sso_basic_success_cached_config_file) +add_net_test_case(credentials_provider_sso_basic_success_profile) +add_net_test_case(credentials_provider_sso_basic_success_profile_cached_config_file) +add_net_test_case(credentials_provider_sso_basic_success_after_failure) + add_test_case(imds_client_new_release) add_test_case(imds_client_connect_failure) add_test_case(imds_client_token_request_failure) diff --git a/tests/credentials_provider_sso_tests.c b/tests/credentials_provider_sso_tests.c new file mode 100644 index 00000000..93ee775d --- /dev/null +++ b/tests/credentials_provider_sso_tests.c @@ -0,0 +1,937 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include "credentials_provider_utils.h" +#include "shared_credentials_test_definitions.h" + +#include +#include +#include +#include + +AWS_STATIC_STRING_FROM_LITERAL(s_sso_profile, "sso"); +static int s_aws_credentials_provider_sso_test_init_config_profile( + struct aws_allocator *allocator, + const struct aws_string *config_contents) { + + struct aws_string *config_file_path_str = aws_create_process_unique_file_name(allocator); + ASSERT_TRUE(config_file_path_str != NULL); + ASSERT_TRUE(aws_create_profile_file(config_file_path_str, config_contents) == AWS_OP_SUCCESS); + + ASSERT_TRUE( + aws_set_environment_value(s_default_config_path_env_variable_name, config_file_path_str) == AWS_OP_SUCCESS); + + ASSERT_TRUE(aws_set_environment_value(s_default_profile_env_variable_name, s_sso_profile) == AWS_OP_SUCCESS); + + aws_string_destroy(config_file_path_str); + return AWS_OP_SUCCESS; +} + +/* start_url should be same in `s_sso_profile_start_url` and `s_sso_profile_config_contents` */ +AWS_STATIC_STRING_FROM_LITERAL(s_sso_profile_start_url, "https://d-123.awsapps.com/start"); +AWS_STATIC_STRING_FROM_LITERAL( + s_sso_profile_config_contents, + "[profile sso]\n" + "sso_start_url = https://d-123.awsapps.com/start\n" + "sso_region = us-west-2\n" + "sso_account_id = 123\n" + "sso_role_name = roleName\n"); +/* session name should be same in both `s_sso_session_name` and `s_sso_session_config_contents`*/ +AWS_STATIC_STRING_FROM_LITERAL(s_sso_session_name, "session"); +AWS_STATIC_STRING_FROM_LITERAL( + s_sso_session_config_contents, + "[profile sso]\n" + "sso_start_url = https://d-123.awsapps.com/start\n" + "sso_region = us-west-2\n" + "sso_account_id = 123\n" + "sso_role_name = roleName\n" + "sso_session = session\n" + "[sso-session session]\n" + "sso_start_url = https://d-123.awsapps.com/start\n" + "sso_region = us-west-2\n"); +AWS_STATIC_STRING_FROM_LITERAL( + s_expected_sso_request_path, + "/federation/credentials?account_id=123&role_name=roleName"); + +static int s_credentials_provider_sso_failed_invalid_config(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + const struct { + const char *name; + const char *text; + } invalid_config_examples[] = { + {"empty", ""}, + + {"profile without any sso config", "[profile sso]\naccessKey=access"}, + + {"profile without role_name", + "[profile sso]\n" + "accessKey=access\n" + "sso_start_url=https://d-123.awsapps.com/start\n" + "sso_region=us-west-2\n"}, + + {"profile without account_id", + "[profile sso]\n" + "accessKey=access\n" + "sso_start_url=https://d-123.awsapps.com/start\n" + "sso_region=us-west-2\n" + "sso_role_name=roleName\n"}, + + {"profile without region", + "[profile sso]\n" + "accessKey=access\n" + "sso_start_url=https://d-123.awsapps.com/start\n" + "sso_account_id=123\n" + "sso_role_name=roleName\n"}, + + {"profile without start_url", + "[profile sso]\n" + "accessKey=access\n" + "sso_region=us-west-2\n" + "sso_account_id=123\n" + "sso_role_name=roleName\n"}, + + {"profile with invalid session", + "[profile sso]\n" + "accessKey=access\n" + "sso_start_url=https://d-123.awsapps.com/start\n" + "sso_region=us-west-2\n" + "sso_account_id=123\n" + "sso_role_name=roleName\n" + "sso_session = session\n" + "[sso-session session]\n"}, + + {"session without start_url", + "[profile sso]\n" + "accessKey=access\n" + "sso_start_url=https://d-123.awsapps.com/start\n" + "sso_region=us-west-2\n" + "sso_account_id=123\n" + "sso_role_name=roleName\n" + "sso_session = session\n" + "[sso-session session]\n" + "sso_region = us-west-2\n"}, + + {"session without region", + "[profile sso]\n" + "accessKey=access\n" + "sso_start_url=https://d-123.awsapps.com/start\n" + "sso_region=us-west-2\n" + "sso_account_id=123\n" + "sso_role_name=roleName\n" + "sso_session = session\n" + "[sso-session session]\n" + "sso_start_url = https://d-123.awsapps.com/start\n"}, + + {"session with different region", + "[profile sso]\n" + "accessKey=access\n" + "sso_start_url=https://d-123.awsapps.com/start\n" + "sso_region=us-east-1\n" + "sso_account_id=123\n" + "sso_role_name=roleName\n" + "sso_session = session\n" + "[sso-session session]\n" + "sso_start_url = https://d-123.awsapps.com/start\n" + "sso_region = us-west-2\n"}, + + {"session with different start-url", + "[profile sso]\n" + "accessKey=access\n" + "sso_start_url=https://d-123.awsapps.com/start\n" + "sso_region=us-west-2\n" + "sso_account_id=123\n" + "sso_role_name=roleName\n" + "sso_session = session\n" + "[sso-session session]\n" + "sso_start_url = https://d-321.awsapps.com/start\n" + "sso_region = us-west-2\n"}, + }; + + aws_credentials_provider_http_mock_tester_init(allocator); + + struct aws_credentials_provider_sso_options options = { + .bootstrap = credentials_provider_http_mock_tester.bootstrap, + .tls_ctx = credentials_provider_http_mock_tester.tls_ctx, + .function_table = &aws_credentials_provider_http_mock_function_table, + .shutdown_options = + { + .shutdown_callback = aws_credentials_provider_http_mock_on_shutdown_complete, + .shutdown_user_data = NULL, + }, + }; + for (int i = 0; i < AWS_ARRAY_SIZE(invalid_config_examples); i++) { + printf("invalid config example [%d]: %s\n", i, invalid_config_examples[i].name); + struct aws_string *content = aws_string_new_from_c_str(allocator, invalid_config_examples[i].text); + ASSERT_TRUE(content != NULL); + s_aws_credentials_provider_sso_test_init_config_profile(allocator, content); + aws_string_destroy(content); + struct aws_credentials_provider *provider = aws_credentials_provider_new_sso(allocator, &options); + ASSERT_NULL(provider); + } + + aws_credentials_provider_http_mock_tester_cleanup(); + + return 0; +} +AWS_TEST_CASE(credentials_provider_sso_failed_invalid_config, s_credentials_provider_sso_failed_invalid_config); + +static int s_credentials_provider_sso_create_destroy_valid_config(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + const struct { + const char *name; + const char *text; + } valid_config_examples[] = { + + {"profile", + "[profile sso]\n" + "accessKey=access\n" + "sso_start_url=https://d-123.awsapps.com/start\n" + "sso_account_id=123\n" + "sso_region=us-west-2\n" + "sso_role_name=roleName\n"}, + + {"session", + "[profile sso]\n" + "accessKey=access\n" + "sso_account_id=123\n" + "sso_role_name=roleName\n" + "sso_session = session\n" + "[sso-session session]\n" + "sso_start_url = https://d-123.awsapps.com/start\n" + "sso_region = us-west-2\n"}, + + {"session with profile", + "[profile sso]\n" + "accessKey=access\n" + "sso_start_url=https://d-123.awsapps.com/start\n" + "sso_region=us-west-2\n" + "sso_account_id=123\n" + "sso_role_name=roleName\n" + "sso_session = session\n" + "[sso-session session]\n" + "sso_start_url = https://d-123.awsapps.com/start\n" + "sso_region = us-west-2\n"}, + + }; + + aws_credentials_provider_http_mock_tester_init(allocator); + + struct aws_credentials_provider_sso_options options = { + .bootstrap = credentials_provider_http_mock_tester.bootstrap, + .tls_ctx = credentials_provider_http_mock_tester.tls_ctx, + .function_table = &aws_credentials_provider_http_mock_function_table, + .shutdown_options = + { + .shutdown_callback = aws_credentials_provider_http_mock_on_shutdown_complete, + .shutdown_user_data = NULL, + }, + }; + for (int i = 0; i < AWS_ARRAY_SIZE(valid_config_examples); i++) { + printf("valid config example [%d]: %s\n", i, valid_config_examples[i].name); + struct aws_string *content = aws_string_new_from_c_str(allocator, valid_config_examples[i].text); + ASSERT_TRUE(content != NULL); + s_aws_credentials_provider_sso_test_init_config_profile(allocator, content); + aws_string_destroy(content); + struct aws_credentials_provider *provider = aws_credentials_provider_new_sso(allocator, &options); + ASSERT_NOT_NULL(provider); + aws_credentials_provider_release(provider); + } + + aws_credentials_provider_http_mock_tester_cleanup(); + + return 0; +} +AWS_TEST_CASE( + credentials_provider_sso_create_destroy_valid_config, + s_credentials_provider_sso_create_destroy_valid_config); + +AWS_STATIC_STRING_FROM_LITERAL( + s_good_response, + "{\"roleCredentials\": {\"accessKeyId\": \"SuccessfulAccessKey\",\"secretAccessKey\": " + "\"SuccessfulSecret\",\"sessionToken\": \"SuccessfulToken\",\"expiration\": 1678574216000}}"); +AWS_STATIC_STRING_FROM_LITERAL(s_good_access_key_id, "SuccessfulAccessKey"); +AWS_STATIC_STRING_FROM_LITERAL(s_good_secret_access_key, "SuccessfulSecret"); +AWS_STATIC_STRING_FROM_LITERAL(s_good_session_token, "SuccessfulToken"); +static int s_good_response_expiration = 1678574216; +static int s_verify_credentials(bool request_made, bool got_credentials, int expected_attempts) { + ASSERT_TRUE(credentials_provider_http_mock_tester.has_received_credentials_callback); + + if (got_credentials) { + ASSERT_TRUE(credentials_provider_http_mock_tester.credentials != NULL); + ASSERT_CURSOR_VALUE_STRING_EQUALS( + aws_credentials_get_access_key_id(credentials_provider_http_mock_tester.credentials), s_good_access_key_id); + ASSERT_CURSOR_VALUE_STRING_EQUALS( + aws_credentials_get_secret_access_key(credentials_provider_http_mock_tester.credentials), + s_good_secret_access_key); + ASSERT_CURSOR_VALUE_STRING_EQUALS( + aws_credentials_get_session_token(credentials_provider_http_mock_tester.credentials), s_good_session_token); + ASSERT_INT_EQUALS( + aws_credentials_get_expiration_timepoint_seconds(credentials_provider_http_mock_tester.credentials), + s_good_response_expiration); + } else { + ASSERT_TRUE(credentials_provider_http_mock_tester.error_code); + ASSERT_TRUE(credentials_provider_http_mock_tester.credentials == NULL); + } + + if (request_made) { + ASSERT_CURSOR_VALUE_STRING_EQUALS( + aws_byte_cursor_from_buf(&credentials_provider_http_mock_tester.request_path), s_expected_sso_request_path); + } + ASSERT_INT_EQUALS(credentials_provider_http_mock_tester.attempts, expected_attempts); + + return AWS_OP_SUCCESS; +} + +static int s_credentials_provider_sso_connect_failure(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_credentials_provider_http_mock_tester_init(allocator); + credentials_provider_http_mock_tester.is_connection_acquire_successful = false; + + s_aws_credentials_provider_sso_test_init_config_profile(allocator, s_sso_session_config_contents); + + struct aws_credentials_provider_sso_options options = { + .bootstrap = credentials_provider_http_mock_tester.bootstrap, + .tls_ctx = credentials_provider_http_mock_tester.tls_ctx, + .function_table = &aws_credentials_provider_http_mock_function_table, + .shutdown_options = + { + .shutdown_callback = aws_credentials_provider_http_mock_on_shutdown_complete, + .shutdown_user_data = NULL, + }, + }; + + struct aws_credentials_provider *provider = aws_credentials_provider_new_sso(allocator, &options); + ASSERT_NOT_NULL(provider); + + aws_credentials_provider_get_credentials( + provider, aws_credentials_provider_http_mock_get_credentials_callback, NULL); + + aws_credentials_provider_http_mock_wait_for_credentials_result(); + ASSERT_SUCCESS(s_verify_credentials(false /*no request*/, false /*get creds*/, 0 /*expected attempts*/)); + + aws_credentials_provider_release(provider); + + aws_credentials_provider_http_mock_wait_for_shutdown_callback(); + + aws_credentials_provider_http_mock_tester_cleanup(); + + return 0; +} +AWS_TEST_CASE(credentials_provider_sso_connect_failure, s_credentials_provider_sso_connect_failure); + +AWS_STATIC_STRING_FROM_LITERAL(s_home_env_var, "HOME"); +AWS_STATIC_STRING_FROM_LITERAL(s_home_env_current_directory, "."); + +static int s_credentials_provider_sso_failure_token_missing(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_credentials_provider_http_mock_tester_init(allocator); + credentials_provider_http_mock_tester.is_request_successful = false; + + /* redirect $HOME */ + ASSERT_SUCCESS(aws_set_environment_value(s_home_env_var, s_home_env_current_directory)); + + s_aws_credentials_provider_sso_test_init_config_profile(allocator, s_sso_session_config_contents); + + struct aws_credentials_provider_sso_options options = { + .bootstrap = credentials_provider_http_mock_tester.bootstrap, + .tls_ctx = credentials_provider_http_mock_tester.tls_ctx, + .function_table = &aws_credentials_provider_http_mock_function_table, + .shutdown_options = + { + .shutdown_callback = aws_credentials_provider_http_mock_on_shutdown_complete, + .shutdown_user_data = NULL, + }, + }; + + struct aws_credentials_provider *provider = aws_credentials_provider_new_sso(allocator, &options); + ASSERT_NOT_NULL(provider); + + aws_credentials_provider_get_credentials( + provider, aws_credentials_provider_http_mock_get_credentials_callback, NULL); + + aws_credentials_provider_http_mock_wait_for_credentials_result(); + + ASSERT_SUCCESS(s_verify_credentials(false /*no request*/, false /*get creds*/, 0 /*expected attempts*/)); + + aws_credentials_provider_release(provider); + + aws_credentials_provider_http_mock_wait_for_shutdown_callback(); + + aws_credentials_provider_http_mock_tester_cleanup(); + + return 0; +} +AWS_TEST_CASE(credentials_provider_sso_failure_token_missing, s_credentials_provider_sso_failure_token_missing); + +AWS_STATIC_STRING_FROM_LITERAL( + s_sso_token, + "{\"accessToken\": \"ValidAccessToken\",\"expiresAt\": \"2015-03-12T05:35:19Z\"}"); +static uint64_t s_sso_token_expiration_s = 1426138519; + +static int s_credentials_provider_sso_failure_token_expired(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_credentials_provider_http_mock_tester_init(allocator); + credentials_provider_http_mock_tester.is_request_successful = false; + + /* redirect $HOME */ + ASSERT_SUCCESS(aws_set_environment_value(s_home_env_var, s_home_env_current_directory)); + + /* create token file */ + struct aws_string *token_path = aws_construct_sso_token_path(allocator, s_sso_session_name); + ASSERT_NOT_NULL(token_path); + ASSERT_SUCCESS(aws_create_directory_components(allocator, token_path)); + ASSERT_SUCCESS(aws_create_profile_file(token_path, s_sso_token)); + + s_aws_credentials_provider_sso_test_init_config_profile(allocator, s_sso_session_config_contents); + uint64_t nano_expiration = + aws_timestamp_convert(s_sso_token_expiration_s + 100, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + mock_aws_set_system_time(nano_expiration); + struct aws_credentials_provider_sso_options options = { + .bootstrap = credentials_provider_http_mock_tester.bootstrap, + .tls_ctx = credentials_provider_http_mock_tester.tls_ctx, + .function_table = &aws_credentials_provider_http_mock_function_table, + .shutdown_options = + { + .shutdown_callback = aws_credentials_provider_http_mock_on_shutdown_complete, + .shutdown_user_data = NULL, + }, + .system_clock_fn = mock_aws_get_system_time, + }; + + struct aws_credentials_provider *provider = aws_credentials_provider_new_sso(allocator, &options); + ASSERT_NOT_NULL(provider); + + aws_credentials_provider_get_credentials( + provider, aws_credentials_provider_http_mock_get_credentials_callback, NULL); + + aws_credentials_provider_http_mock_wait_for_credentials_result(); + + ASSERT_SUCCESS(s_verify_credentials(false /*no request*/, false /*get creds*/, 0 /*expected attempts*/)); + ASSERT_INT_EQUALS(credentials_provider_http_mock_tester.error_code, AWS_AUTH_SSO_TOKEN_EXPIRED); + + aws_credentials_provider_release(provider); + + aws_credentials_provider_http_mock_wait_for_shutdown_callback(); + + aws_credentials_provider_http_mock_tester_cleanup(); + aws_string_destroy(token_path); + return 0; +} +AWS_TEST_CASE(credentials_provider_sso_failure_token_expired, s_credentials_provider_sso_failure_token_expired); + +AWS_STATIC_STRING_FROM_LITERAL(s_sso_empty_token, "{\"accessToken\": \"\",\"expiresAt\": \"2015-03-12T05:35:19Z\"}"); +static int s_credentials_provider_sso_failure_token_empty(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_credentials_provider_http_mock_tester_init(allocator); + credentials_provider_http_mock_tester.is_request_successful = false; + + /* redirect $HOME */ + ASSERT_SUCCESS(aws_set_environment_value(s_home_env_var, s_home_env_current_directory)); + + /* create token file */ + struct aws_string *token_path = aws_construct_sso_token_path(allocator, s_sso_session_name); + ASSERT_NOT_NULL(token_path); + ASSERT_SUCCESS(aws_create_directory_components(allocator, token_path)); + ASSERT_SUCCESS(aws_create_profile_file(token_path, s_sso_empty_token)); + + s_aws_credentials_provider_sso_test_init_config_profile(allocator, s_sso_session_config_contents); + mock_aws_set_system_time(0); + struct aws_credentials_provider_sso_options options = { + .bootstrap = credentials_provider_http_mock_tester.bootstrap, + .tls_ctx = credentials_provider_http_mock_tester.tls_ctx, + .function_table = &aws_credentials_provider_http_mock_function_table, + .shutdown_options = + { + .shutdown_callback = aws_credentials_provider_http_mock_on_shutdown_complete, + .shutdown_user_data = NULL, + }, + .system_clock_fn = mock_aws_get_system_time, + }; + + struct aws_credentials_provider *provider = aws_credentials_provider_new_sso(allocator, &options); + ASSERT_NOT_NULL(provider); + + aws_credentials_provider_get_credentials( + provider, aws_credentials_provider_http_mock_get_credentials_callback, NULL); + + aws_credentials_provider_http_mock_wait_for_credentials_result(); + + ASSERT_SUCCESS(s_verify_credentials(false /*no request*/, false /*get creds*/, 0 /*expected attempts*/)); + ASSERT_INT_EQUALS(credentials_provider_http_mock_tester.error_code, AWS_ERROR_INVALID_ARGUMENT); + + aws_credentials_provider_release(provider); + + aws_credentials_provider_http_mock_wait_for_shutdown_callback(); + + aws_credentials_provider_http_mock_tester_cleanup(); + aws_string_destroy(token_path); + return 0; +} +AWS_TEST_CASE(credentials_provider_sso_failure_token_empty, s_credentials_provider_sso_failure_token_empty); + +static int s_credentials_provider_sso_request_failure(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_credentials_provider_http_mock_tester_init(allocator); + credentials_provider_http_mock_tester.is_request_successful = false; + credentials_provider_http_mock_tester.response_code = AWS_HTTP_STATUS_CODE_400_BAD_REQUEST; + + /* redirect $HOME */ + ASSERT_SUCCESS(aws_set_environment_value(s_home_env_var, s_home_env_current_directory)); + + /* create token file */ + struct aws_string *token_path = aws_construct_sso_token_path(allocator, s_sso_session_name); + ASSERT_NOT_NULL(token_path); + ASSERT_SUCCESS(aws_create_directory_components(allocator, token_path)); + ASSERT_SUCCESS(aws_create_profile_file(token_path, s_sso_token)); + + s_aws_credentials_provider_sso_test_init_config_profile(allocator, s_sso_session_config_contents); + + mock_aws_set_system_time(0); + struct aws_credentials_provider_sso_options options = { + .bootstrap = credentials_provider_http_mock_tester.bootstrap, + .tls_ctx = credentials_provider_http_mock_tester.tls_ctx, + .function_table = &aws_credentials_provider_http_mock_function_table, + .shutdown_options = + { + .shutdown_callback = aws_credentials_provider_http_mock_on_shutdown_complete, + .shutdown_user_data = NULL, + }, + .system_clock_fn = mock_aws_get_system_time, + }; + + struct aws_credentials_provider *provider = aws_credentials_provider_new_sso(allocator, &options); + ASSERT_NOT_NULL(provider); + + aws_credentials_provider_get_credentials( + provider, aws_credentials_provider_http_mock_get_credentials_callback, NULL); + + aws_credentials_provider_http_mock_wait_for_credentials_result(); + + ASSERT_SUCCESS(s_verify_credentials(true /*request made*/, false /*get creds*/, 1 /*expected attempts*/)); + + aws_credentials_provider_release(provider); + + aws_credentials_provider_http_mock_wait_for_shutdown_callback(); + + aws_credentials_provider_http_mock_tester_cleanup(); + + aws_string_destroy(token_path); + return 0; +} +AWS_TEST_CASE(credentials_provider_sso_request_failure, s_credentials_provider_sso_request_failure); + +AWS_STATIC_STRING_FROM_LITERAL(s_bad_json_response, "{ \"accessKey\": \"bad\"}"); +static int s_credentials_provider_sso_bad_response(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_credentials_provider_http_mock_tester_init(allocator); + + /* redirect $HOME */ + ASSERT_SUCCESS(aws_set_environment_value(s_home_env_var, s_home_env_current_directory)); + + /* create token file */ + struct aws_string *token_path = aws_construct_sso_token_path(allocator, s_sso_session_name); + ASSERT_NOT_NULL(token_path); + + ASSERT_SUCCESS(aws_create_directory_components(allocator, token_path)); + ASSERT_SUCCESS(aws_create_profile_file(token_path, s_sso_token)); + + s_aws_credentials_provider_sso_test_init_config_profile(allocator, s_sso_session_config_contents); + + struct aws_byte_cursor bad_json_cursor = aws_byte_cursor_from_string(s_bad_json_response); + aws_array_list_push_back(&credentials_provider_http_mock_tester.response_data_callbacks, &bad_json_cursor); + + mock_aws_set_system_time(0); + struct aws_credentials_provider_sso_options options = { + .bootstrap = credentials_provider_http_mock_tester.bootstrap, + .tls_ctx = credentials_provider_http_mock_tester.tls_ctx, + .function_table = &aws_credentials_provider_http_mock_function_table, + .shutdown_options = + { + .shutdown_callback = aws_credentials_provider_http_mock_on_shutdown_complete, + .shutdown_user_data = NULL, + }, + .system_clock_fn = mock_aws_get_system_time, + }; + + struct aws_credentials_provider *provider = aws_credentials_provider_new_sso(allocator, &options); + ASSERT_NOT_NULL(provider); + + aws_credentials_provider_get_credentials( + provider, aws_credentials_provider_http_mock_get_credentials_callback, NULL); + + aws_credentials_provider_http_mock_wait_for_credentials_result(); + + ASSERT_SUCCESS(s_verify_credentials(true /*request made*/, false /*get creds*/, 1 /*expected attempts*/)); + + aws_credentials_provider_release(provider); + + aws_credentials_provider_http_mock_wait_for_shutdown_callback(); + + aws_credentials_provider_http_mock_tester_cleanup(); + + aws_string_destroy(token_path); + return 0; +} +AWS_TEST_CASE(credentials_provider_sso_bad_response, s_credentials_provider_sso_bad_response); + +static int s_credentials_provider_sso_retryable_error(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_credentials_provider_http_mock_tester_init(allocator); + credentials_provider_http_mock_tester.response_code = AWS_HTTP_STATUS_CODE_500_INTERNAL_SERVER_ERROR; + + /* redirect $HOME */ + ASSERT_SUCCESS(aws_set_environment_value(s_home_env_var, s_home_env_current_directory)); + + /* create token file */ + struct aws_string *token_path = aws_construct_sso_token_path(allocator, s_sso_session_name); + ASSERT_NOT_NULL(token_path); + + ASSERT_SUCCESS(aws_create_directory_components(allocator, token_path)); + ASSERT_SUCCESS(aws_create_profile_file(token_path, s_sso_token)); + + s_aws_credentials_provider_sso_test_init_config_profile(allocator, s_sso_session_config_contents); + + struct aws_byte_cursor bad_json_cursor = aws_byte_cursor_from_string(s_bad_json_response); + aws_array_list_push_back(&credentials_provider_http_mock_tester.response_data_callbacks, &bad_json_cursor); + + mock_aws_set_system_time(0); + struct aws_credentials_provider_sso_options options = { + .bootstrap = credentials_provider_http_mock_tester.bootstrap, + .tls_ctx = credentials_provider_http_mock_tester.tls_ctx, + .function_table = &aws_credentials_provider_http_mock_function_table, + .shutdown_options = + { + .shutdown_callback = aws_credentials_provider_http_mock_on_shutdown_complete, + .shutdown_user_data = NULL, + }, + .system_clock_fn = mock_aws_get_system_time, + }; + + struct aws_credentials_provider *provider = aws_credentials_provider_new_sso(allocator, &options); + ASSERT_NOT_NULL(provider); + + aws_credentials_provider_get_credentials( + provider, aws_credentials_provider_http_mock_get_credentials_callback, NULL); + + aws_credentials_provider_http_mock_wait_for_credentials_result(); + + ASSERT_SUCCESS(s_verify_credentials(true /*request made*/, false /*get creds*/, 4 /*expected attempts*/)); + + aws_credentials_provider_release(provider); + + aws_credentials_provider_http_mock_wait_for_shutdown_callback(); + + aws_credentials_provider_http_mock_tester_cleanup(); + + aws_string_destroy(token_path); + return 0; +} +AWS_TEST_CASE(credentials_provider_sso_retryable_error, s_credentials_provider_sso_retryable_error); + +static int s_credentials_provider_sso_basic_success(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_credentials_provider_http_mock_tester_init(allocator); + + /* redirect $HOME */ + ASSERT_SUCCESS(aws_set_environment_value(s_home_env_var, s_home_env_current_directory)); + + /* create token file */ + struct aws_string *token_path = aws_construct_sso_token_path(allocator, s_sso_session_name); + ASSERT_NOT_NULL(token_path); + + ASSERT_SUCCESS(aws_create_directory_components(allocator, token_path)); + ASSERT_SUCCESS(aws_create_profile_file(token_path, s_sso_token)); + + s_aws_credentials_provider_sso_test_init_config_profile(allocator, s_sso_session_config_contents); + + /* set the response */ + struct aws_byte_cursor good_response_cursor = aws_byte_cursor_from_string(s_good_response); + aws_array_list_push_back(&credentials_provider_http_mock_tester.response_data_callbacks, &good_response_cursor); + + mock_aws_set_system_time(0); + struct aws_credentials_provider_sso_options options = { + .bootstrap = credentials_provider_http_mock_tester.bootstrap, + .tls_ctx = credentials_provider_http_mock_tester.tls_ctx, + .function_table = &aws_credentials_provider_http_mock_function_table, + .shutdown_options = + { + .shutdown_callback = aws_credentials_provider_http_mock_on_shutdown_complete, + .shutdown_user_data = NULL, + }, + .system_clock_fn = mock_aws_get_system_time, + }; + + struct aws_credentials_provider *provider = aws_credentials_provider_new_sso(allocator, &options); + ASSERT_NOT_NULL(provider); + + aws_credentials_provider_get_credentials( + provider, aws_credentials_provider_http_mock_get_credentials_callback, NULL); + + aws_credentials_provider_http_mock_wait_for_credentials_result(); + + ASSERT_SUCCESS(s_verify_credentials(true /*request made*/, true /*get creds*/, 1 /*expected attempts*/)); + + aws_credentials_provider_release(provider); + + aws_credentials_provider_http_mock_wait_for_shutdown_callback(); + + aws_credentials_provider_http_mock_tester_cleanup(); + + aws_string_destroy(token_path); + return 0; +} +AWS_TEST_CASE(credentials_provider_sso_basic_success, s_credentials_provider_sso_basic_success); +AWS_STATIC_STRING_FROM_LITERAL(s_invalid_config, "invalid config"); +static int s_credentials_provider_sso_basic_success_cached_config_file(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_credentials_provider_http_mock_tester_init(allocator); + + /* redirect $HOME */ + ASSERT_SUCCESS(aws_set_environment_value(s_home_env_var, s_home_env_current_directory)); + + /* create token file */ + struct aws_string *token_path = aws_construct_sso_token_path(allocator, s_sso_session_name); + ASSERT_NOT_NULL(token_path); + + ASSERT_SUCCESS(aws_create_directory_components(allocator, token_path)); + ASSERT_SUCCESS(aws_create_profile_file(token_path, s_sso_token)); + + s_aws_credentials_provider_sso_test_init_config_profile(allocator, s_invalid_config); + + struct aws_byte_buf profile_buffer = aws_byte_buf_from_c_str(aws_string_c_str(s_sso_session_config_contents)); + struct aws_profile_collection *config_collection = + aws_profile_collection_new_from_buffer(allocator, &profile_buffer, AWS_PST_CONFIG); + + /* set the response */ + struct aws_byte_cursor good_response_cursor = aws_byte_cursor_from_string(s_good_response); + aws_array_list_push_back(&credentials_provider_http_mock_tester.response_data_callbacks, &good_response_cursor); + + mock_aws_set_system_time(0); + struct aws_credentials_provider_sso_options options = { + .bootstrap = credentials_provider_http_mock_tester.bootstrap, + .tls_ctx = credentials_provider_http_mock_tester.tls_ctx, + .function_table = &aws_credentials_provider_http_mock_function_table, + .config_file_cached = config_collection, + .shutdown_options = + { + .shutdown_callback = aws_credentials_provider_http_mock_on_shutdown_complete, + .shutdown_user_data = NULL, + }, + .system_clock_fn = mock_aws_get_system_time, + }; + + struct aws_credentials_provider *provider = aws_credentials_provider_new_sso(allocator, &options); + ASSERT_NOT_NULL(provider); + + aws_credentials_provider_get_credentials( + provider, aws_credentials_provider_http_mock_get_credentials_callback, NULL); + + aws_credentials_provider_http_mock_wait_for_credentials_result(); + + ASSERT_SUCCESS(s_verify_credentials(true /*request made*/, true /*get creds*/, 1 /*expected attempts*/)); + + aws_credentials_provider_release(provider); + + aws_credentials_provider_http_mock_wait_for_shutdown_callback(); + + aws_credentials_provider_http_mock_tester_cleanup(); + + aws_string_destroy(token_path); + aws_profile_collection_release(config_collection); + + return 0; +} +AWS_TEST_CASE( + credentials_provider_sso_basic_success_cached_config_file, + s_credentials_provider_sso_basic_success_cached_config_file); + +static int s_credentials_provider_sso_basic_success_profile(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_credentials_provider_http_mock_tester_init(allocator); + + /* redirect $HOME */ + ASSERT_SUCCESS(aws_set_environment_value(s_home_env_var, s_home_env_current_directory)); + + /* create token file */ + struct aws_string *token_path = aws_construct_sso_token_path(allocator, s_sso_profile_start_url); + ASSERT_NOT_NULL(token_path); + + ASSERT_SUCCESS(aws_create_directory_components(allocator, token_path)); + ASSERT_SUCCESS(aws_create_profile_file(token_path, s_sso_token)); + + s_aws_credentials_provider_sso_test_init_config_profile(allocator, s_sso_profile_config_contents); + + /* set the response */ + struct aws_byte_cursor good_response_cursor = aws_byte_cursor_from_string(s_good_response); + aws_array_list_push_back(&credentials_provider_http_mock_tester.response_data_callbacks, &good_response_cursor); + + mock_aws_set_system_time(0); + struct aws_credentials_provider_sso_options options = { + .bootstrap = credentials_provider_http_mock_tester.bootstrap, + .tls_ctx = credentials_provider_http_mock_tester.tls_ctx, + .function_table = &aws_credentials_provider_http_mock_function_table, + .shutdown_options = + { + .shutdown_callback = aws_credentials_provider_http_mock_on_shutdown_complete, + .shutdown_user_data = NULL, + }, + .system_clock_fn = mock_aws_get_system_time, + }; + + struct aws_credentials_provider *provider = aws_credentials_provider_new_sso(allocator, &options); + ASSERT_NOT_NULL(provider); + + aws_credentials_provider_get_credentials( + provider, aws_credentials_provider_http_mock_get_credentials_callback, NULL); + + aws_credentials_provider_http_mock_wait_for_credentials_result(); + + ASSERT_SUCCESS(s_verify_credentials(true /*request made*/, true /*get creds*/, 1 /*expected attempts*/)); + + aws_credentials_provider_release(provider); + + aws_credentials_provider_http_mock_wait_for_shutdown_callback(); + + aws_credentials_provider_http_mock_tester_cleanup(); + + aws_string_destroy(token_path); + return 0; +} +AWS_TEST_CASE(credentials_provider_sso_basic_success_profile, s_credentials_provider_sso_basic_success_profile); + +static int s_credentials_provider_sso_basic_success_profile_cached_config_file( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_credentials_provider_http_mock_tester_init(allocator); + + /* redirect $HOME */ + ASSERT_SUCCESS(aws_set_environment_value(s_home_env_var, s_home_env_current_directory)); + + /* create token file */ + struct aws_string *token_path = aws_construct_sso_token_path(allocator, s_sso_profile_start_url); + ASSERT_NOT_NULL(token_path); + + ASSERT_SUCCESS(aws_create_directory_components(allocator, token_path)); + ASSERT_SUCCESS(aws_create_profile_file(token_path, s_sso_token)); + + s_aws_credentials_provider_sso_test_init_config_profile(allocator, s_invalid_config); + + struct aws_byte_buf profile_buffer = aws_byte_buf_from_c_str(aws_string_c_str(s_sso_profile_config_contents)); + struct aws_profile_collection *config_collection = + aws_profile_collection_new_from_buffer(allocator, &profile_buffer, AWS_PST_CONFIG); + + /* set the response */ + struct aws_byte_cursor good_response_cursor = aws_byte_cursor_from_string(s_good_response); + aws_array_list_push_back(&credentials_provider_http_mock_tester.response_data_callbacks, &good_response_cursor); + + mock_aws_set_system_time(0); + struct aws_credentials_provider_sso_options options = { + .bootstrap = credentials_provider_http_mock_tester.bootstrap, + .tls_ctx = credentials_provider_http_mock_tester.tls_ctx, + .config_file_cached = config_collection, + .function_table = &aws_credentials_provider_http_mock_function_table, + .shutdown_options = + { + .shutdown_callback = aws_credentials_provider_http_mock_on_shutdown_complete, + .shutdown_user_data = NULL, + }, + .system_clock_fn = mock_aws_get_system_time, + }; + + struct aws_credentials_provider *provider = aws_credentials_provider_new_sso(allocator, &options); + ASSERT_NOT_NULL(provider); + + aws_credentials_provider_get_credentials( + provider, aws_credentials_provider_http_mock_get_credentials_callback, NULL); + + aws_credentials_provider_http_mock_wait_for_credentials_result(); + + ASSERT_SUCCESS(s_verify_credentials(true /*request made*/, true /*get creds*/, 1 /*expected attempts*/)); + + aws_credentials_provider_release(provider); + aws_credentials_provider_http_mock_wait_for_shutdown_callback(); + aws_credentials_provider_http_mock_tester_cleanup(); + aws_profile_collection_release(config_collection); + aws_string_destroy(token_path); + + return 0; +} +AWS_TEST_CASE( + credentials_provider_sso_basic_success_profile_cached_config_file, + s_credentials_provider_sso_basic_success_profile_cached_config_file); + +static int s_credentials_provider_sso_basic_success_after_failure(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_credentials_provider_http_mock_tester_init(allocator); + credentials_provider_http_mock_tester.failure_count = 2; + credentials_provider_http_mock_tester.failure_response_code = AWS_HTTP_STATUS_CODE_500_INTERNAL_SERVER_ERROR; + /* redirect $HOME */ + ASSERT_SUCCESS(aws_set_environment_value(s_home_env_var, s_home_env_current_directory)); + + /* create token file */ + struct aws_string *token_path = aws_construct_sso_token_path(allocator, s_sso_session_name); + ASSERT_NOT_NULL(token_path); + + ASSERT_SUCCESS(aws_create_directory_components(allocator, token_path)); + ASSERT_SUCCESS(aws_create_profile_file(token_path, s_sso_token)); + + s_aws_credentials_provider_sso_test_init_config_profile(allocator, s_sso_session_config_contents); + + /* set the response */ + struct aws_byte_cursor good_response_cursor = aws_byte_cursor_from_string(s_good_response); + aws_array_list_push_back(&credentials_provider_http_mock_tester.response_data_callbacks, &good_response_cursor); + + mock_aws_set_system_time(0); + struct aws_credentials_provider_sso_options options = { + .bootstrap = credentials_provider_http_mock_tester.bootstrap, + .tls_ctx = credentials_provider_http_mock_tester.tls_ctx, + .function_table = &aws_credentials_provider_http_mock_function_table, + .shutdown_options = + { + .shutdown_callback = aws_credentials_provider_http_mock_on_shutdown_complete, + .shutdown_user_data = NULL, + }, + .system_clock_fn = mock_aws_get_system_time, + }; + + struct aws_credentials_provider *provider = aws_credentials_provider_new_sso(allocator, &options); + ASSERT_NOT_NULL(provider); + + aws_credentials_provider_get_credentials( + provider, aws_credentials_provider_http_mock_get_credentials_callback, NULL); + + aws_credentials_provider_http_mock_wait_for_credentials_result(); + + ASSERT_SUCCESS(s_verify_credentials(true /*request made*/, true /*get creds*/, 3 /*expected attempts*/)); + + aws_credentials_provider_release(provider); + + aws_credentials_provider_http_mock_wait_for_shutdown_callback(); + + aws_credentials_provider_http_mock_tester_cleanup(); + + aws_string_destroy(token_path); + return 0; +} +AWS_TEST_CASE( + credentials_provider_sso_basic_success_after_failure, + s_credentials_provider_sso_basic_success_after_failure); diff --git a/tests/credentials_provider_utils.c b/tests/credentials_provider_utils.c index b663477d..58051593 100644 --- a/tests/credentials_provider_utils.c +++ b/tests/credentials_provider_utils.c @@ -4,13 +4,18 @@ */ #include "credentials_provider_utils.h" +#include #include #include #include #include +#include +#include #include #include +#include +#include #include @@ -465,3 +470,324 @@ struct aws_credentials_provider *aws_credentials_provider_new_null( return provider; } + +int aws_create_directory_components(struct aws_allocator *allocator, const struct aws_string *path) { + const char local_platform_separator = aws_get_platform_directory_separator(); + + /* Create directory components and ensure use of platform separator at the same time. */ + for (size_t i = 0; i < path->len; ++i) { + if (aws_is_any_directory_separator((char)path->bytes[i])) { + ((char *)path->bytes)[i] = local_platform_separator; + + struct aws_string *segment = aws_string_new_from_array(allocator, path->bytes, i); + int rc = aws_directory_create(segment); + aws_string_destroy(segment); + + if (rc != AWS_OP_SUCCESS) { + return rc; + } + } + } + return AWS_OP_SUCCESS; +} + +/* + * Mocked HTTP connection manager for tests + */ + +struct aws_auth_http_system_vtable aws_credentials_provider_http_mock_function_table = { + .aws_http_connection_manager_new = aws_credentials_provider_http_mock_connection_manager_new, + .aws_http_connection_manager_release = aws_credentials_provider_http_mock_connection_manager_release, + .aws_http_connection_manager_acquire_connection = + aws_credentials_provider_http_mock_connection_manager_acquire_connection, + .aws_http_connection_manager_release_connection = + aws_credentials_provider_http_mock_connection_manager_release_connection, + .aws_http_connection_make_request = aws_credentials_provider_http_mock_make_request, + .aws_http_stream_activate = aws_credentials_provider_http_mock_stream_activate, + .aws_http_stream_get_connection = aws_credentials_provider_http_mock_stream_get_connection, + .aws_http_stream_get_incoming_response_status = + aws_credentials_provider_http_mock_stream_get_incoming_response_status, + .aws_http_stream_release = aws_credentials_provider_http_mock_stream_release, + .aws_http_connection_close = aws_credentials_provider_http_mock_connection_close}; + +struct aws_credentials_provider_http_mock_tester credentials_provider_http_mock_tester; + +int aws_credentials_provider_http_mock_tester_init(struct aws_allocator *allocator) { + aws_auth_library_init(allocator); + + AWS_ZERO_STRUCT(credentials_provider_http_mock_tester); + + struct aws_tls_ctx_options tls_ctx_options; + aws_tls_ctx_options_init_default_client(&tls_ctx_options, allocator); + credentials_provider_http_mock_tester.tls_ctx = aws_tls_client_ctx_new(allocator, &tls_ctx_options); + ASSERT_NOT_NULL(credentials_provider_http_mock_tester.tls_ctx); + + credentials_provider_http_mock_tester.el_group = aws_event_loop_group_new_default(allocator, 0, NULL); + + struct aws_host_resolver_default_options resolver_options = { + .el_group = credentials_provider_http_mock_tester.el_group, + .max_entries = 8, + }; + credentials_provider_http_mock_tester.resolver = aws_host_resolver_new_default(allocator, &resolver_options); + + struct aws_client_bootstrap_options bootstrap_options = { + .event_loop_group = credentials_provider_http_mock_tester.el_group, + .host_resolver = credentials_provider_http_mock_tester.resolver, + }; + credentials_provider_http_mock_tester.bootstrap = aws_client_bootstrap_new(allocator, &bootstrap_options); + + if (aws_array_list_init_dynamic( + &credentials_provider_http_mock_tester.response_data_callbacks, + allocator, + 10, + sizeof(struct aws_byte_cursor))) { + return AWS_OP_ERR; + } + + if (aws_byte_buf_init(&credentials_provider_http_mock_tester.request_path, allocator, 256)) { + return AWS_OP_ERR; + } + if (aws_byte_buf_init(&credentials_provider_http_mock_tester.request_body, allocator, 256)) { + return AWS_OP_ERR; + } + + if (aws_mutex_init(&credentials_provider_http_mock_tester.lock)) { + return AWS_OP_ERR; + } + + if (aws_condition_variable_init(&credentials_provider_http_mock_tester.signal)) { + return AWS_OP_ERR; + } + + /* default to everything successful */ + credentials_provider_http_mock_tester.is_connection_acquire_successful = true; + credentials_provider_http_mock_tester.is_request_successful = true; + + return AWS_OP_SUCCESS; +} + +void aws_credentials_provider_http_mock_tester_cleanup(void) { + aws_tls_ctx_release(credentials_provider_http_mock_tester.tls_ctx); + aws_client_bootstrap_release(credentials_provider_http_mock_tester.bootstrap); + aws_host_resolver_release(credentials_provider_http_mock_tester.resolver); + aws_event_loop_group_release(credentials_provider_http_mock_tester.el_group); + aws_array_list_clean_up(&credentials_provider_http_mock_tester.response_data_callbacks); + aws_byte_buf_clean_up(&credentials_provider_http_mock_tester.request_path); + aws_byte_buf_clean_up(&credentials_provider_http_mock_tester.request_body); + aws_condition_variable_clean_up(&credentials_provider_http_mock_tester.signal); + aws_mutex_clean_up(&credentials_provider_http_mock_tester.lock); + aws_credentials_release(credentials_provider_http_mock_tester.credentials); + aws_auth_library_clean_up(); +} + +void aws_credentials_provider_http_mock_on_shutdown_complete(void *user_data) { + (void)user_data; + aws_mutex_lock(&credentials_provider_http_mock_tester.lock); + credentials_provider_http_mock_tester.has_received_shutdown_callback = true; + aws_mutex_unlock(&credentials_provider_http_mock_tester.lock); + + aws_condition_variable_notify_one(&credentials_provider_http_mock_tester.signal); +} + +bool aws_credentials_provider_http_mock_has_received_shutdown_callback(void *user_data) { + (void)user_data; + + return credentials_provider_http_mock_tester.has_received_shutdown_callback; +} + +void aws_credentials_provider_http_mock_wait_for_shutdown_callback(void) { + aws_mutex_lock(&credentials_provider_http_mock_tester.lock); + aws_condition_variable_wait_pred( + &credentials_provider_http_mock_tester.signal, + &credentials_provider_http_mock_tester.lock, + aws_credentials_provider_http_mock_has_received_shutdown_callback, + NULL); + aws_mutex_unlock(&credentials_provider_http_mock_tester.lock); +} + +struct mock_connection_manager { + struct aws_allocator *allocator; + aws_http_connection_manager_shutdown_complete_fn *shutdown_complete_callback; + void *shutdown_complete_user_data; +}; + +struct aws_http_connection_manager *aws_credentials_provider_http_mock_connection_manager_new( + struct aws_allocator *allocator, + const struct aws_http_connection_manager_options *options) { + + struct mock_connection_manager *mock_manager = aws_mem_calloc(allocator, 1, sizeof(struct mock_connection_manager)); + mock_manager->allocator = allocator; + mock_manager->shutdown_complete_callback = options->shutdown_complete_callback; + mock_manager->shutdown_complete_user_data = options->shutdown_complete_user_data; + return (struct aws_http_connection_manager *)mock_manager; +} + +void aws_credentials_provider_http_mock_connection_manager_release(struct aws_http_connection_manager *manager) { + struct mock_connection_manager *mock_manager = (struct mock_connection_manager *)manager; + mock_manager->shutdown_complete_callback(mock_manager->shutdown_complete_user_data); + aws_mem_release(mock_manager->allocator, mock_manager); +} + +void aws_credentials_provider_http_mock_connection_manager_acquire_connection( + struct aws_http_connection_manager *manager, + aws_http_connection_manager_on_connection_setup_fn *callback, + void *user_data) { + + (void)manager; + (void)callback; + (void)user_data; + + if (credentials_provider_http_mock_tester.is_connection_acquire_successful) { + callback((struct aws_http_connection *)1, AWS_OP_SUCCESS, user_data); + } else { + aws_raise_error(AWS_ERROR_HTTP_UNKNOWN); + callback(NULL, AWS_OP_ERR, user_data); + } +} + +int aws_credentials_provider_http_mock_connection_manager_release_connection( + struct aws_http_connection_manager *manager, + struct aws_http_connection *connection) { + + (void)manager; + (void)connection; + + return AWS_OP_SUCCESS; +} + +void aws_credentials_provider_http_mock_invoke_request_callbacks( + const struct aws_http_make_request_options *options, + struct aws_array_list *data_callbacks, + bool is_request_successful) { + + size_t data_callback_count = aws_array_list_length(data_callbacks); + + struct aws_http_header headers[1]; + AWS_ZERO_ARRAY(headers); + + headers[0].name = aws_byte_cursor_from_c_str("some-header"); + headers[0].value = aws_byte_cursor_from_c_str("value"); + if (options->on_response_headers) { + options->on_response_headers( + (struct aws_http_stream *)1, AWS_HTTP_HEADER_BLOCK_MAIN, headers, 1, options->user_data); + } + if (options->on_response_header_block_done) { + options->on_response_header_block_done( + (struct aws_http_stream *)1, data_callback_count > 0, options->user_data); + } + + for (size_t i = 0; i < data_callback_count; ++i) { + struct aws_byte_cursor data_callback_cursor; + if (aws_array_list_get_at(data_callbacks, &data_callback_cursor, i)) { + continue; + } + + options->on_response_body((struct aws_http_stream *)1, &data_callback_cursor, options->user_data); + } + + options->on_complete( + (struct aws_http_stream *)1, + is_request_successful ? AWS_ERROR_SUCCESS : AWS_ERROR_HTTP_UNKNOWN, + options->user_data); +} + +struct aws_http_stream *aws_credentials_provider_http_mock_make_request( + struct aws_http_connection *client_connection, + const struct aws_http_make_request_options *options) { + + (void)client_connection; + (void)options; + + struct aws_byte_cursor path; + AWS_ZERO_STRUCT(path); + struct aws_input_stream *body_stream = aws_http_message_get_body_stream(options->request); + struct aws_allocator *allocator = credentials_provider_http_mock_tester.request_body.allocator; + aws_byte_buf_clean_up(&credentials_provider_http_mock_tester.request_body); + aws_byte_buf_init(&credentials_provider_http_mock_tester.request_body, allocator, 256); + if (body_stream) { + aws_input_stream_read(body_stream, &credentials_provider_http_mock_tester.request_body); + } + aws_byte_buf_clean_up(&credentials_provider_http_mock_tester.request_path); + + struct aws_byte_cursor request_path_cursor; + aws_http_message_get_request_path(options->request, &request_path_cursor); + aws_byte_buf_init_copy_from_cursor( + &credentials_provider_http_mock_tester.request_path, allocator, request_path_cursor); + credentials_provider_http_mock_tester.attempts++; + credentials_provider_http_mock_tester.request_options = *options; + + return (struct aws_http_stream *)1; +} + +int aws_credentials_provider_http_mock_stream_activate(struct aws_http_stream *stream) { + (void)stream; + aws_credentials_provider_http_mock_invoke_request_callbacks( + &credentials_provider_http_mock_tester.request_options, + &credentials_provider_http_mock_tester.response_data_callbacks, + credentials_provider_http_mock_tester.is_request_successful); + return AWS_OP_SUCCESS; +} + +int aws_credentials_provider_http_mock_stream_get_incoming_response_status( + const struct aws_http_stream *stream, + int *out_status_code) { + (void)stream; + + if (credentials_provider_http_mock_tester.failure_count) { + credentials_provider_http_mock_tester.failure_count--; + *out_status_code = credentials_provider_http_mock_tester.failure_response_code; + } else if (credentials_provider_http_mock_tester.response_code) { + *out_status_code = credentials_provider_http_mock_tester.response_code; + } else { + *out_status_code = AWS_HTTP_STATUS_CODE_200_OK; + } + + return AWS_OP_SUCCESS; +} + +void aws_credentials_provider_http_mock_stream_release(struct aws_http_stream *stream) { + (void)stream; +} + +void aws_credentials_provider_http_mock_connection_close(struct aws_http_connection *connection) { + (void)connection; +} + +struct aws_http_connection *aws_credentials_provider_http_mock_stream_get_connection( + const struct aws_http_stream *stream) { + (void)stream; + return (struct aws_http_connection *)1; +} + +bool aws_credentials_provider_http_mock_has_received_credentials_callback(void *user_data) { + (void)user_data; + + return credentials_provider_http_mock_tester.has_received_credentials_callback; +} + +void aws_credentials_provider_http_mock_wait_for_credentials_result(void) { + aws_mutex_lock(&credentials_provider_http_mock_tester.lock); + aws_condition_variable_wait_pred( + &credentials_provider_http_mock_tester.signal, + &credentials_provider_http_mock_tester.lock, + aws_credentials_provider_http_mock_has_received_credentials_callback, + NULL); + aws_mutex_unlock(&credentials_provider_http_mock_tester.lock); +} + +void aws_credentials_provider_http_mock_get_credentials_callback( + struct aws_credentials *credentials, + int error_code, + void *user_data) { + (void)user_data; + + aws_mutex_lock(&credentials_provider_http_mock_tester.lock); + credentials_provider_http_mock_tester.has_received_credentials_callback = true; + credentials_provider_http_mock_tester.credentials = credentials; + credentials_provider_http_mock_tester.error_code = error_code; + if (credentials != NULL) { + aws_credentials_acquire(credentials); + } + aws_condition_variable_notify_one(&credentials_provider_http_mock_tester.signal); + aws_mutex_unlock(&credentials_provider_http_mock_tester.lock); +} diff --git a/tests/credentials_provider_utils.h b/tests/credentials_provider_utils.h index 960e858e..5f752d91 100644 --- a/tests/credentials_provider_utils.h +++ b/tests/credentials_provider_utils.h @@ -7,9 +7,12 @@ */ #include +#include #include #include +#include +#include struct aws_credentials; struct aws_credentials_provider; @@ -91,4 +94,82 @@ struct aws_credentials_provider *aws_credentials_provider_new_null( struct aws_allocator *allocator, struct aws_credentials_provider_shutdown_options *shutdown_options); +/** + * Create the directory components of @path: + * - if @path ends in a path separator, create every directory component; + * - else, stop at the last path separator (parent directory of @path). + */ +int aws_create_directory_components(struct aws_allocator *allocator, const struct aws_string *path); + +/** + * Mocked HTTP connection manager for tests + */ +struct aws_credentials_provider_http_mock_tester { + struct aws_tls_ctx *tls_ctx; + struct aws_event_loop_group *el_group; + struct aws_host_resolver *resolver; + struct aws_client_bootstrap *bootstrap; + + struct aws_byte_buf request_path; + struct aws_byte_buf request_body; + struct aws_http_make_request_options request_options; + + struct aws_array_list response_data_callbacks; + bool is_connection_acquire_successful; + bool is_request_successful; + + struct aws_mutex lock; + struct aws_condition_variable signal; + + struct aws_credentials *credentials; + bool has_received_credentials_callback; + bool has_received_shutdown_callback; + + int attempts; + int response_code; + int error_code; + int failure_response_code; + int failure_count; +}; + +extern struct aws_credentials_provider_http_mock_tester credentials_provider_http_mock_tester; +int aws_credentials_provider_http_mock_tester_init(struct aws_allocator *allocator); +void aws_credentials_provider_http_mock_tester_cleanup(void); +void aws_credentials_provider_http_mock_on_shutdown_complete(void *user_data); +bool aws_credentials_provider_http_mock_has_received_shutdown_callback(void *user_data); +void aws_credentials_provider_http_mock_wait_for_shutdown_callback(void); +struct aws_http_connection_manager *aws_credentials_provider_http_mock_connection_manager_new( + struct aws_allocator *allocator, + const struct aws_http_connection_manager_options *options); +void aws_credentials_provider_http_mock_connection_manager_release(struct aws_http_connection_manager *manager); +void aws_credentials_provider_http_mock_connection_manager_acquire_connection( + struct aws_http_connection_manager *manager, + aws_http_connection_manager_on_connection_setup_fn *callback, + void *user_data); +int aws_credentials_provider_http_mock_connection_manager_release_connection( + struct aws_http_connection_manager *manager, + struct aws_http_connection *connection); +void aws_credentials_provider_http_mock_invoke_request_callbacks( + const struct aws_http_make_request_options *options, + struct aws_array_list *data_callbacks, + bool is_request_successful); +struct aws_http_stream *aws_credentials_provider_http_mock_make_request( + struct aws_http_connection *client_connection, + const struct aws_http_make_request_options *options); +int aws_credentials_provider_http_mock_stream_activate(struct aws_http_stream *stream); +int aws_credentials_provider_http_mock_stream_get_incoming_response_status( + const struct aws_http_stream *stream, + int *out_status_code); +void aws_credentials_provider_http_mock_stream_release(struct aws_http_stream *stream); +void aws_credentials_provider_http_mock_connection_close(struct aws_http_connection *connection); +struct aws_http_connection *aws_credentials_provider_http_mock_stream_get_connection( + const struct aws_http_stream *stream); +bool aws_credentials_provider_http_mock_has_received_credentials_callback(void *user_data); +void aws_credentials_provider_http_mock_wait_for_credentials_result(void); +void aws_credentials_provider_http_mock_get_credentials_callback( + struct aws_credentials *credentials, + int error_code, + void *user_data); +extern struct aws_auth_http_system_vtable aws_credentials_provider_http_mock_function_table; + #endif /* AWS_AUTH_CREDENTIALS_PROVIDER_MOCK_H */ diff --git a/tests/sso_token_util_tests.c b/tests/sso_token_util_tests.c new file mode 100644 index 00000000..b9688bd4 --- /dev/null +++ b/tests/sso_token_util_tests.c @@ -0,0 +1,134 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include "shared_credentials_test_definitions.h" +#include + +static int s_parse_token_location_url_test(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + aws_auth_library_init(allocator); + struct aws_string *start_url = aws_string_new_from_c_str(allocator, "https://d-92671207e4.awsapps.com/start"); + struct aws_string *token_path = aws_construct_sso_token_path(allocator, start_url); + + struct aws_byte_cursor token_cursor = aws_byte_cursor_from_string(token_path); + struct aws_byte_cursor expected_token_cursor = + aws_byte_cursor_from_c_str("13f9d35043871d073ab260e020f0ffde092cb14b.json"); + struct aws_byte_cursor find_cursor; + ASSERT_SUCCESS(aws_byte_cursor_find_exact(&token_cursor, &expected_token_cursor, &find_cursor)); + + aws_string_destroy(start_url); + aws_string_destroy(token_path); + aws_auth_library_clean_up(); + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE(parse_token_location_url_test, s_parse_token_location_url_test); + +static int s_parse_token_location_session_test(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + aws_auth_library_init(allocator); + struct aws_string *session = aws_string_new_from_c_str(allocator, "admin"); + struct aws_string *token_path = aws_construct_sso_token_path(allocator, session); + struct aws_byte_cursor token_cursor = aws_byte_cursor_from_string(token_path); + struct aws_byte_cursor expected_token_cursor = + aws_byte_cursor_from_c_str("d033e22ae348aeb5660fc2140aec35850c4da997.json"); + struct aws_byte_cursor find_cursor; + ASSERT_SUCCESS(aws_byte_cursor_find_exact(&token_cursor, &expected_token_cursor, &find_cursor)); + + aws_string_destroy(session); + aws_string_destroy(token_path); + aws_auth_library_clean_up(); + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE(parse_token_location_session_test, s_parse_token_location_session_test); + +AWS_STATIC_STRING_FROM_LITERAL( + s_valid_token_json, + "{\"accessToken\": \"string\",\"expiresAt\": \"2019-11-14T04:05:45Z\",\"refreshToken\": \"string\",\"clientId\": " + "\"123321\",\"clientSecret\": \"ABCDE123\",\"registrationExpiresAt\": " + "\"2022-03-06T19:53:17Z\",\"region\": \"us-west-2\",\"startUrl\": \"https://d-abc123.awsapps.com/start\"}"); +static int s_parse_sso_token_valid(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + aws_auth_library_init(allocator); + struct aws_string *file_path = aws_create_process_unique_file_name(allocator); + ASSERT_SUCCESS(aws_create_profile_file(file_path, s_valid_token_json)); + struct aws_sso_token *sso_token = aws_sso_token_new_from_file(allocator, file_path); + ASSERT_TRUE(aws_string_eq_c_str(sso_token->access_token, "string")); + ASSERT_INT_EQUALS((uint64_t)aws_date_time_as_epoch_secs(&sso_token->expiration), 1573704345); + aws_string_destroy(file_path); + aws_sso_token_destroy(sso_token); + aws_auth_library_clean_up(); + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE(parse_sso_token_valid, s_parse_sso_token_valid); + +AWS_STATIC_STRING_FROM_LITERAL(s_invalid_token_json, "invalid json"); +static int s_parse_sso_token_invalid(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + aws_auth_library_init(allocator); + struct aws_string *file_path = aws_create_process_unique_file_name(allocator); + ASSERT_SUCCESS(aws_create_profile_file(file_path, s_invalid_token_json)); + ASSERT_NULL(aws_sso_token_new_from_file(allocator, file_path)); + ASSERT_INT_EQUALS(AWS_AUTH_SSO_TOKEN_INVALID, aws_last_error()); + aws_string_destroy(file_path); + aws_auth_library_clean_up(); + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE(parse_sso_token_invalid, s_parse_sso_token_invalid); + +AWS_STATIC_STRING_FROM_LITERAL( + s_missing_access_token_json, + "{\"expiresAt\": \"2019-11-14T04:05:45Z\",\"refreshToken\": \"string\",\"clientId\": " + "\"123321\",\"clientSecret\": \"ABCDE123\",\"registrationExpiresAt\": " + "\"2022-03-06T19:53:17Z\",\"region\": \"us-west-2\",\"startUrl\": \"https://d-abc123.awsapps.com/start\"}"); +static int s_parse_sso_token_invalid_missing_access_token(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + aws_auth_library_init(allocator); + struct aws_string *file_path = aws_create_process_unique_file_name(allocator); + ASSERT_SUCCESS(aws_create_profile_file(file_path, s_missing_access_token_json)); + ASSERT_NULL(aws_sso_token_new_from_file(allocator, file_path)); + ASSERT_INT_EQUALS(AWS_AUTH_SSO_TOKEN_INVALID, aws_last_error()); + aws_string_destroy(file_path); + aws_auth_library_clean_up(); + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE(parse_sso_token_invalid_missing_access_token, s_parse_sso_token_invalid_missing_access_token); + +AWS_STATIC_STRING_FROM_LITERAL( + s_missing_expires_at_json, + "{\"accessToken\": \"string\",\"refreshToken\": \"string\",\"clientId\": " + "\"123321\",\"clientSecret\": \"ABCDE123\",\"registrationExpiresAt\": " + "\"2022-03-06T19:53:17Z\",\"region\": \"us-west-2\",\"startUrl\": \"https://d-abc123.awsapps.com/start\"}"); +static int s_parse_sso_token_missing_expires_at(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + aws_auth_library_init(allocator); + struct aws_string *file_path = aws_create_process_unique_file_name(allocator); + ASSERT_SUCCESS(aws_create_profile_file(file_path, s_missing_expires_at_json)); + ASSERT_NULL(aws_sso_token_new_from_file(allocator, file_path)); + ASSERT_INT_EQUALS(AWS_AUTH_SSO_TOKEN_INVALID, aws_last_error()); + aws_string_destroy(file_path); + aws_auth_library_clean_up(); + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE(parse_sso_token_missing_expires_at, s_parse_sso_token_missing_expires_at); + +AWS_STATIC_STRING_FROM_LITERAL( + s_invalid_expires_at_json, + "{\"accessToken\": \"string\",\"expiresAt\": \"1234567\",\"refreshToken\": \"string\",\"clientId\": " + "\"123321\",\"clientSecret\": \"ABCDE123\",\"registrationExpiresAt\": " + "\"2022-03-06T19:53:17Z\",\"region\": \"us-west-2\",\"startUrl\": \"https://d-abc123.awsapps.com/start\"}"); +static int s_parse_sso_token_invalid_expires_at(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + aws_auth_library_init(allocator); + struct aws_string *file_path = aws_create_process_unique_file_name(allocator); + ASSERT_SUCCESS(aws_create_profile_file(file_path, s_invalid_expires_at_json)); + ASSERT_NULL(aws_sso_token_new_from_file(allocator, file_path)); + ASSERT_INT_EQUALS(AWS_AUTH_SSO_TOKEN_INVALID, aws_last_error()); + aws_string_destroy(file_path); + aws_auth_library_clean_up(); + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE(parse_sso_token_invalid_expires_at, s_parse_sso_token_invalid_expires_at); diff --git a/tests/token_provider_sso_tests.c b/tests/token_provider_sso_tests.c new file mode 100644 index 00000000..f61dec0e --- /dev/null +++ b/tests/token_provider_sso_tests.c @@ -0,0 +1,605 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "shared_credentials_test_definitions.h" + +struct sso_session_profile_example { + const char *name; + struct aws_byte_cursor text; +}; + +static int s_sso_token_provider_profile_invalid_profile_test(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + aws_auth_library_init(allocator); + const struct sso_session_profile_example invalid_profile_examples[] = { + { + .name = "No config", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL( + "[profile default]\naws_access_key_id=fake_access_key\naws_secret_access_key=fake_secret_key\n"), + }, + { + .name = "No sso_start_url", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("[profile " + "default]\naws_access_key_id=fake_access_key\naws_secret_" + "access_key=fake_secret_key\nsso_region=us-east-1\n"), + }, + { + .name = "only sso_session", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("[profile " + "default]\naws_access_key_id=fake_access_key\naws_secret_" + "access_key=fake_secret_key\nsso_session=dev\n[sso-session " + "dev]\nsso_start_url=url\nsso_region=us-east-1"), + }, + }; + + struct aws_string *config_file_str = aws_create_process_unique_file_name(allocator); + struct aws_token_provider_sso_profile_options options = { + .config_file_name_override = aws_byte_cursor_from_string(config_file_str), + }; + + for (size_t i = 0; i < AWS_ARRAY_SIZE(invalid_profile_examples); ++i) { + printf("invalid example [%zu]: %s\n", i, invalid_profile_examples[i].name); + struct aws_string *config_contents = aws_string_new_from_cursor(allocator, &invalid_profile_examples[i].text); + ASSERT_SUCCESS(aws_create_profile_file(config_file_str, config_contents)); + ASSERT_NULL(aws_token_provider_new_sso_profile(allocator, &options)); + aws_string_destroy(config_contents); + } + + aws_string_destroy(config_file_str); + aws_auth_library_clean_up(); + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(sso_token_provider_profile_invalid_profile_test, s_sso_token_provider_profile_invalid_profile_test); + +static int s_sso_token_provider_profile_valid_profile_test(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + aws_auth_library_init(allocator); + static struct sso_session_profile_example s_valid_profile_examples[] = { + { + .name = "profile", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("[default]\naws_access_key_id=fake_access_key\naws_secret_" + "access_key=fake_secret_key\nsso_region=us-east-" + "1\nsso_start_url=url"), + }, + { + .name = "with sso_session", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL( + "[default]\naws_access_key_id=fake_access_key\naws_secret_" + "access_key=fake_secret_key\nsso_region=us-east-1\nsso_start_url=url\nsso_" + "session=dev\n[sso-session dev]\nsso_region=us-east-" + "1\nsso_start_url=url2"), + }, + }; + + struct aws_string *config_file_str = aws_create_process_unique_file_name(allocator); + struct aws_token_provider_sso_profile_options options = { + .config_file_name_override = aws_byte_cursor_from_string(config_file_str), + }; + + for (size_t i = 0; i < AWS_ARRAY_SIZE(s_valid_profile_examples); ++i) { + printf("valid example [%zu]: %s\n", i, s_valid_profile_examples[i].name); + struct aws_string *config_contents = aws_string_new_from_cursor(allocator, &s_valid_profile_examples[i].text); + ASSERT_SUCCESS(aws_create_profile_file(config_file_str, config_contents)); + struct aws_credentials_provider *provider = aws_token_provider_new_sso_profile(allocator, &options); + ASSERT_NOT_NULL(provider); + aws_credentials_provider_release(provider); + aws_string_destroy(config_contents); + } + + aws_string_destroy(config_file_str); + aws_auth_library_clean_up(); + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(sso_token_provider_profile_valid_profile_test, s_sso_token_provider_profile_valid_profile_test); + +static struct aws_mock_token_provider_sso_tester { + struct aws_tls_ctx *tls_ctx; + struct aws_event_loop_group *el_group; + struct aws_host_resolver *resolver; + struct aws_client_bootstrap *bootstrap; + + struct aws_mutex lock; + struct aws_condition_variable signal; + struct aws_credentials *credentials; + bool has_received_credentials_callback; + bool has_received_shutdown_callback; + int error_code; + +} s_tester; + +static int s_aws_mock_token_provider_sso_tester_init(struct aws_allocator *allocator) { + aws_auth_library_init(allocator); + + AWS_ZERO_STRUCT(s_tester); + + struct aws_tls_ctx_options tls_ctx_options; + aws_tls_ctx_options_init_default_client(&tls_ctx_options, allocator); + s_tester.tls_ctx = aws_tls_client_ctx_new(allocator, &tls_ctx_options); + ASSERT_NOT_NULL(s_tester.tls_ctx); + + s_tester.el_group = aws_event_loop_group_new_default(allocator, 0, NULL); + + struct aws_host_resolver_default_options resolver_options = { + .el_group = s_tester.el_group, + .max_entries = 8, + }; + s_tester.resolver = aws_host_resolver_new_default(allocator, &resolver_options); + + struct aws_client_bootstrap_options bootstrap_options = { + .event_loop_group = s_tester.el_group, + .host_resolver = s_tester.resolver, + }; + s_tester.bootstrap = aws_client_bootstrap_new(allocator, &bootstrap_options); + + return AWS_OP_SUCCESS; +} + +void s_aws_mock_token_provider_sso_tester_cleanup(void) { + aws_tls_ctx_release(s_tester.tls_ctx); + aws_client_bootstrap_release(s_tester.bootstrap); + aws_host_resolver_release(s_tester.resolver); + aws_event_loop_group_release(s_tester.el_group); + + aws_condition_variable_clean_up(&s_tester.signal); + aws_mutex_clean_up(&s_tester.lock); + aws_credentials_release(s_tester.credentials); + aws_auth_library_clean_up(); +} + +static int s_sso_token_provider_sso_session_invalid_config_test(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + s_aws_mock_token_provider_sso_tester_init(allocator); + const struct sso_session_profile_example invalid_config_examples[] = { + { + .name = "no sso-session", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("[default]\naws_access_key_id=fake_access_key\naws_secret_" + "access_key=fake_secret_key\nsso_region=us-east-" + "1\nsso_start_url=url"), + }, + { + .name = "sso_session with without sso_region", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("[profile " + "default]\naws_access_key_id=fake_access_key\naws_secret_" + "access_key=fake_secret_key\nsso_session=dev\n[sso-session " + "dev]\nsso_start_url=url"), + }, + { + .name = "sso_session with without sso_start_url", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("[profile " + "default]\naws_access_key_id=fake_access_key\naws_secret_" + "access_key=fake_secret_key\nsso_session=dev\n[sso-session " + "dev]\nsso_region=us-east-1"), + }, + { + .name = "sso_session with different profile region", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL( + "[profile " + "default]\naws_access_key_id=fake_access_key\naws_secret_" + "access_key=fake_secret_key\nsso_session=dev\nsso_region=us-west-" + "1\nsso_start_url=url\n[sso-session dev]\nsso_region=us-east-1\nsso_start_url=url"), + }, + { + .name = "sso_session with different profile start url", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL( + "[profile " + "default]\naws_access_key_id=fake_access_key\naws_secret_" + "access_key=fake_secret_key\nsso_session=dev\nsso_region=us-east-" + "1\nsso_start_url=url\n[sso-session dev]\nsso_region=us-east-1\nsso_start_url=url2"), + }, + { + .name = "different sso_session name", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL( + "[default]\naws_access_key_id=fake_access_key\naws_secret_" + "access_key=fake_secret_key\nsso_region=us-east-1\nsso_start_url=url\nsso_" + "session=dev\n[sso-session dev2]\nsso_region=us-east-" + "1\nsso_start_url=url"), + }, + }; + + struct aws_string *config_file_str = aws_create_process_unique_file_name(allocator); + struct aws_token_provider_sso_session_options options = { + .config_file_name_override = aws_byte_cursor_from_string(config_file_str), + .tls_ctx = s_tester.tls_ctx, + .bootstrap = s_tester.bootstrap, + }; + + for (size_t i = 0; i < AWS_ARRAY_SIZE(invalid_config_examples); ++i) { + printf("invalid example [%zu]: %s\n", i, invalid_config_examples[i].name); + struct aws_string *config_contents = aws_string_new_from_cursor(allocator, &invalid_config_examples[i].text); + ASSERT_SUCCESS(aws_create_profile_file(config_file_str, config_contents)); + ASSERT_NULL(aws_token_provider_new_sso_session(allocator, &options)); + aws_string_destroy(config_contents); + } + + aws_string_destroy(config_file_str); + s_aws_mock_token_provider_sso_tester_cleanup(); + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(sso_token_provider_sso_session_invalid_config_test, s_sso_token_provider_sso_session_invalid_config_test); + +static int s_sso_token_provider_sso_session_valid_config_test(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + s_aws_mock_token_provider_sso_tester_init(allocator); + + static struct sso_session_profile_example s_valid_profile_examples[] = { + { + .name = "sso-session", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("[default]\naws_access_key_id=fake_access_key\naws_secret_" + "access_key=fake_secret_key\nsso_" + "session=dev\n[sso-session dev]\nsso_region=us-east-" + "1\nsso_start_url=url"), + }, + { + .name = "with profile sso_region", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("[default]\naws_access_key_id=fake_access_key\naws_secret_" + "access_key=fake_secret_key\nsso_region=us-east-1\nsso_" + "session=dev\n[sso-session dev]\nsso_region=us-east-" + "1\nsso_start_url=url"), + }, + { + .name = "with profile sso_start_url", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("[default]\naws_access_key_id=fake_access_key\naws_secret_" + "access_key=fake_secret_key\nsso_start_url=url\nsso_" + "session=dev\n[sso-session dev]\nsso_region=us-east-" + "1\nsso_start_url=url"), + }, + { + .name = "with profile sso_region and sso_start_url", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL( + "[default]\naws_access_key_id=fake_access_key\naws_secret_" + "access_key=fake_secret_key\nsso_region=us-east-1\nsso_start_url=url\nsso_" + "session=dev\n[sso-session dev]\nsso_region=us-east-" + "1\nsso_start_url=url"), + }, + }; + + struct aws_string *config_file_str = aws_create_process_unique_file_name(allocator); + struct aws_token_provider_sso_session_options options = { + .config_file_name_override = aws_byte_cursor_from_string(config_file_str), + .tls_ctx = s_tester.tls_ctx, + .bootstrap = s_tester.bootstrap, + }; + + for (size_t i = 0; i < AWS_ARRAY_SIZE(s_valid_profile_examples); ++i) { + printf("valid example [%zu]: %s\n", i, s_valid_profile_examples[i].name); + struct aws_string *config_contents = aws_string_new_from_cursor(allocator, &s_valid_profile_examples[i].text); + ASSERT_SUCCESS(aws_create_profile_file(config_file_str, config_contents)); + struct aws_credentials_provider *provider = aws_token_provider_new_sso_session(allocator, &options); + ASSERT_NOT_NULL(provider); + aws_credentials_provider_release(provider); + aws_string_destroy(config_contents); + } + + aws_string_destroy(config_file_str); + s_aws_mock_token_provider_sso_tester_cleanup(); + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(sso_token_provider_sso_session_valid_config_test, s_sso_token_provider_sso_session_valid_config_test); + +/* start_url should be same in `s_sso_profile_start_url` and `s_sso_profile_config_contents` */ +AWS_STATIC_STRING_FROM_LITERAL(s_sso_profile_start_url, "https://d-123.awsapps.com/start"); +AWS_STATIC_STRING_FROM_LITERAL( + s_sso_profile_config_contents, + "[default]\n" + "sso_start_url = https://d-123.awsapps.com/start\n"); +/* session name should be same in both `s_sso_session_name` and `s_sso_session_config_contents`*/ +AWS_STATIC_STRING_FROM_LITERAL(s_sso_session_name, "session"); +AWS_STATIC_STRING_FROM_LITERAL( + s_sso_session_config_contents, + "[default]\n" + "sso_session = session\n" + "[sso-session session]\n" + "sso_start_url = https://d-123.awsapps.com/start\n" + "sso_region = us-west-2\n"); +AWS_STATIC_STRING_FROM_LITERAL( + s_sso_token, + "{\"accessToken\": \"ValidAccessToken\",\"expiresAt\": \"2015-03-12T05:35:19Z\"}"); +AWS_STATIC_STRING_FROM_LITERAL(s_invalid_config, "invalid config"); + +AWS_STATIC_STRING_FROM_LITERAL(s_home_env_var, "HOME"); +AWS_STATIC_STRING_FROM_LITERAL(s_home_env_current_directory, "."); +AWS_STATIC_STRING_FROM_LITERAL(s_good_token, "ValidAccessToken"); +static uint64_t s_token_expiration_s = 1426138519; +static int s_sso_token_provider_sso_session_basic_success(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + s_aws_mock_token_provider_sso_tester_init(allocator); + + /* redirect $HOME */ + ASSERT_SUCCESS(aws_set_environment_value(s_home_env_var, s_home_env_current_directory)); + + /* create token file */ + struct aws_string *token_path = aws_construct_sso_token_path(allocator, s_sso_session_name); + ASSERT_NOT_NULL(token_path); + ASSERT_SUCCESS(aws_create_directory_components(allocator, token_path)); + ASSERT_SUCCESS(aws_create_profile_file(token_path, s_sso_token)); + + struct aws_string *config_file_str = aws_create_process_unique_file_name(allocator); + ASSERT_SUCCESS(aws_create_profile_file(config_file_str, s_sso_session_config_contents)); + mock_aws_set_system_time(0); + struct aws_token_provider_sso_session_options options = { + .config_file_name_override = aws_byte_cursor_from_string(config_file_str), + .tls_ctx = s_tester.tls_ctx, + .bootstrap = s_tester.bootstrap, + .system_clock_fn = mock_aws_get_system_time, + }; + + struct aws_credentials_provider *provider = aws_token_provider_new_sso_session(allocator, &options); + ASSERT_NOT_NULL(provider); + + struct aws_get_credentials_test_callback_result callback_results; + aws_get_credentials_test_callback_result_init(&callback_results, 1); + ASSERT_SUCCESS( + aws_credentials_provider_get_credentials(provider, aws_test_get_credentials_async_callback, &callback_results)); + aws_wait_on_credentials_callback(&callback_results); + + ASSERT_CURSOR_VALUE_STRING_EQUALS(aws_credentials_get_token(callback_results.credentials), s_good_token); + ASSERT_INT_EQUALS( + aws_credentials_get_expiration_timepoint_seconds(callback_results.credentials), s_token_expiration_s); + + aws_get_credentials_test_callback_result_clean_up(&callback_results); + aws_credentials_provider_release(provider); + + aws_string_destroy(token_path); + aws_string_destroy(config_file_str); + s_aws_mock_token_provider_sso_tester_cleanup(); + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(sso_token_provider_sso_session_basic_success, s_sso_token_provider_sso_session_basic_success); + +static int s_sso_token_provider_sso_session_config_file_cached(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + s_aws_mock_token_provider_sso_tester_init(allocator); + + /* redirect $HOME */ + ASSERT_SUCCESS(aws_set_environment_value(s_home_env_var, s_home_env_current_directory)); + + /* create token file */ + struct aws_string *token_path = aws_construct_sso_token_path(allocator, s_sso_session_name); + ASSERT_NOT_NULL(token_path); + ASSERT_SUCCESS(aws_create_directory_components(allocator, token_path)); + ASSERT_SUCCESS(aws_create_profile_file(token_path, s_sso_token)); + + struct aws_string *config_file_str = aws_create_process_unique_file_name(allocator); + ASSERT_SUCCESS(aws_create_profile_file(config_file_str, s_invalid_config)); + + struct aws_byte_buf profile_buffer = aws_byte_buf_from_c_str(aws_string_c_str(s_sso_session_config_contents)); + struct aws_profile_collection *config_collection = + aws_profile_collection_new_from_buffer(allocator, &profile_buffer, AWS_PST_CONFIG); + + mock_aws_set_system_time(0); + struct aws_token_provider_sso_session_options options = { + .config_file_name_override = aws_byte_cursor_from_string(config_file_str), + .config_file_cached = config_collection, + .tls_ctx = s_tester.tls_ctx, + .bootstrap = s_tester.bootstrap, + .system_clock_fn = mock_aws_get_system_time, + }; + + struct aws_credentials_provider *provider = aws_token_provider_new_sso_session(allocator, &options); + ASSERT_NOT_NULL(provider); + + struct aws_get_credentials_test_callback_result callback_results; + aws_get_credentials_test_callback_result_init(&callback_results, 1); + ASSERT_SUCCESS( + aws_credentials_provider_get_credentials(provider, aws_test_get_credentials_async_callback, &callback_results)); + aws_wait_on_credentials_callback(&callback_results); + + ASSERT_CURSOR_VALUE_STRING_EQUALS(aws_credentials_get_token(callback_results.credentials), s_good_token); + ASSERT_INT_EQUALS( + aws_credentials_get_expiration_timepoint_seconds(callback_results.credentials), s_token_expiration_s); + + aws_get_credentials_test_callback_result_clean_up(&callback_results); + aws_credentials_provider_release(provider); + + aws_string_destroy(token_path); + aws_string_destroy(config_file_str); + s_aws_mock_token_provider_sso_tester_cleanup(); + aws_profile_collection_release(config_collection); + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(sso_token_provider_sso_session_config_file_cached, s_sso_token_provider_sso_session_config_file_cached); + +static int s_sso_token_provider_sso_session_expired_token(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + s_aws_mock_token_provider_sso_tester_init(allocator); + + /* redirect $HOME */ + ASSERT_SUCCESS(aws_set_environment_value(s_home_env_var, s_home_env_current_directory)); + + /* create token file */ + struct aws_string *token_path = aws_construct_sso_token_path(allocator, s_sso_session_name); + ASSERT_NOT_NULL(token_path); + ASSERT_SUCCESS(aws_create_directory_components(allocator, token_path)); + ASSERT_SUCCESS(aws_create_profile_file(token_path, s_sso_token)); + + struct aws_string *config_file_str = aws_create_process_unique_file_name(allocator); + ASSERT_SUCCESS(aws_create_profile_file(config_file_str, s_sso_session_config_contents)); + uint64_t nano_expiration = + aws_timestamp_convert(s_token_expiration_s + 1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + mock_aws_set_system_time(nano_expiration); + struct aws_token_provider_sso_session_options options = { + .config_file_name_override = aws_byte_cursor_from_string(config_file_str), + .tls_ctx = s_tester.tls_ctx, + .bootstrap = s_tester.bootstrap, + .system_clock_fn = mock_aws_get_system_time, + }; + + struct aws_credentials_provider *provider = aws_token_provider_new_sso_session(allocator, &options); + ASSERT_NOT_NULL(provider); + + struct aws_get_credentials_test_callback_result callback_results; + aws_get_credentials_test_callback_result_init(&callback_results, 1); + ASSERT_ERROR( + AWS_AUTH_SSO_TOKEN_EXPIRED, + aws_credentials_provider_get_credentials(provider, aws_test_get_credentials_async_callback, &callback_results)); + aws_credentials_provider_release(provider); + + aws_string_destroy(token_path); + aws_string_destroy(config_file_str); + s_aws_mock_token_provider_sso_tester_cleanup(); + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(sso_token_provider_sso_session_expired_token, s_sso_token_provider_sso_session_expired_token); + +static int s_sso_token_provider_profile_basic_success(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + s_aws_mock_token_provider_sso_tester_init(allocator); + + /* redirect $HOME */ + ASSERT_SUCCESS(aws_set_environment_value(s_home_env_var, s_home_env_current_directory)); + + /* create token file */ + struct aws_string *token_path = aws_construct_sso_token_path(allocator, s_sso_profile_start_url); + ASSERT_NOT_NULL(token_path); + ASSERT_SUCCESS(aws_create_directory_components(allocator, token_path)); + ASSERT_SUCCESS(aws_create_profile_file(token_path, s_sso_token)); + + struct aws_string *config_file_str = aws_create_process_unique_file_name(allocator); + ASSERT_SUCCESS(aws_create_profile_file(config_file_str, s_sso_profile_config_contents)); + + mock_aws_set_system_time(0); + struct aws_token_provider_sso_profile_options options = { + .config_file_name_override = aws_byte_cursor_from_string(config_file_str), + .system_clock_fn = mock_aws_get_system_time, + }; + + struct aws_credentials_provider *provider = aws_token_provider_new_sso_profile(allocator, &options); + ASSERT_NOT_NULL(provider); + + struct aws_get_credentials_test_callback_result callback_results; + aws_get_credentials_test_callback_result_init(&callback_results, 1); + ASSERT_SUCCESS( + aws_credentials_provider_get_credentials(provider, aws_test_get_credentials_async_callback, &callback_results)); + aws_wait_on_credentials_callback(&callback_results); + + ASSERT_CURSOR_VALUE_STRING_EQUALS(aws_credentials_get_token(callback_results.credentials), s_good_token); + ASSERT_INT_EQUALS( + aws_credentials_get_expiration_timepoint_seconds(callback_results.credentials), s_token_expiration_s); + + aws_get_credentials_test_callback_result_clean_up(&callback_results); + aws_credentials_provider_release(provider); + + aws_string_destroy(token_path); + aws_string_destroy(config_file_str); + s_aws_mock_token_provider_sso_tester_cleanup(); + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(sso_token_provider_profile_basic_success, s_sso_token_provider_profile_basic_success); +static int s_sso_token_provider_profile_cached_config_file(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + s_aws_mock_token_provider_sso_tester_init(allocator); + + /* redirect $HOME */ + ASSERT_SUCCESS(aws_set_environment_value(s_home_env_var, s_home_env_current_directory)); + + /* create token file */ + struct aws_string *token_path = aws_construct_sso_token_path(allocator, s_sso_profile_start_url); + ASSERT_NOT_NULL(token_path); + ASSERT_SUCCESS(aws_create_directory_components(allocator, token_path)); + ASSERT_SUCCESS(aws_create_profile_file(token_path, s_sso_token)); + + struct aws_string *config_file_str = aws_create_process_unique_file_name(allocator); + ASSERT_SUCCESS(aws_create_profile_file(config_file_str, s_invalid_config)); + + struct aws_byte_buf profile_buffer = aws_byte_buf_from_c_str(aws_string_c_str(s_sso_profile_config_contents)); + + struct aws_profile_collection *config_collection = + aws_profile_collection_new_from_buffer(allocator, &profile_buffer, AWS_PST_CONFIG); + + mock_aws_set_system_time(0); + struct aws_token_provider_sso_profile_options options = { + .config_file_name_override = aws_byte_cursor_from_string(config_file_str), + .config_file_cached = config_collection, + .system_clock_fn = mock_aws_get_system_time, + }; + + struct aws_credentials_provider *provider = aws_token_provider_new_sso_profile(allocator, &options); + ASSERT_NOT_NULL(provider); + + struct aws_get_credentials_test_callback_result callback_results; + aws_get_credentials_test_callback_result_init(&callback_results, 1); + ASSERT_SUCCESS( + aws_credentials_provider_get_credentials(provider, aws_test_get_credentials_async_callback, &callback_results)); + aws_wait_on_credentials_callback(&callback_results); + + ASSERT_CURSOR_VALUE_STRING_EQUALS(aws_credentials_get_token(callback_results.credentials), s_good_token); + ASSERT_INT_EQUALS( + aws_credentials_get_expiration_timepoint_seconds(callback_results.credentials), s_token_expiration_s); + + aws_get_credentials_test_callback_result_clean_up(&callback_results); + aws_credentials_provider_release(provider); + + aws_string_destroy(token_path); + aws_string_destroy(config_file_str); + s_aws_mock_token_provider_sso_tester_cleanup(); + aws_profile_collection_release(config_collection); + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(sso_token_provider_profile_cached_config_file, s_sso_token_provider_profile_cached_config_file); + +static int s_sso_token_provider_profile_expired_token(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + s_aws_mock_token_provider_sso_tester_init(allocator); + + /* redirect $HOME */ + ASSERT_SUCCESS(aws_set_environment_value(s_home_env_var, s_home_env_current_directory)); + + /* create token file */ + struct aws_string *token_path = aws_construct_sso_token_path(allocator, s_sso_profile_start_url); + ASSERT_NOT_NULL(token_path); + ASSERT_SUCCESS(aws_create_directory_components(allocator, token_path)); + ASSERT_SUCCESS(aws_create_profile_file(token_path, s_sso_token)); + + struct aws_string *config_file_str = aws_create_process_unique_file_name(allocator); + ASSERT_SUCCESS(aws_create_profile_file(config_file_str, s_sso_profile_config_contents)); + + uint64_t nano_expiration = + aws_timestamp_convert(s_token_expiration_s + 100, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + mock_aws_set_system_time(nano_expiration); + struct aws_token_provider_sso_profile_options options = { + .config_file_name_override = aws_byte_cursor_from_string(config_file_str), + .system_clock_fn = mock_aws_get_system_time, + }; + + struct aws_credentials_provider *provider = aws_token_provider_new_sso_profile(allocator, &options); + ASSERT_NOT_NULL(provider); + + struct aws_get_credentials_test_callback_result callback_results; + aws_get_credentials_test_callback_result_init(&callback_results, 1); + ASSERT_ERROR( + AWS_AUTH_SSO_TOKEN_EXPIRED, + aws_credentials_provider_get_credentials(provider, aws_test_get_credentials_async_callback, &callback_results)); + + aws_credentials_provider_release(provider); + + aws_string_destroy(token_path); + aws_string_destroy(config_file_str); + s_aws_mock_token_provider_sso_tester_cleanup(); + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(sso_token_provider_profile_expired_token, s_sso_token_provider_profile_expired_token);