Refresh OAuth2 tokens automatically#8355
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces OAuth2 token refreshing, both manually via a new oidc_child command and automatically through a new configuration option. The changes span configuration, database schema, the oidc_child tool, and the idp provider. While the overall implementation is solid, I've identified several issues, including copy-paste errors in logging that could be misleading, a critical bug in parsing input that may cause token refresh to fail, and a minor bug where a function's return value is not checked.
c5c8af9 to
d3ff2c7
Compare
e58ab81 to
dbc3520
Compare
0f6575c to
1e9233f
Compare
80f541f to
c7cc462
Compare
c7cc462 to
1f8a5f4
Compare
|
Hi @eisenmann-b1 , While @sumit-bose and @alexey-tikhonov are reviewing the code, I'd like to get an idea of how urgent this PR is. Can you let us know when you are looking to get this merged? And for what target distro you'd like to be able to start using it? This will help us plan for testing this PR as well as potentially expanding existing tests to cover this. Thanks, |
|
Hi @spoore1,
Well, I just hope it makes it into 2.13 :)
I am currently using Ubuntu 24.04 for testing it, but I don't think anything in here is platform-specific. |
1f8a5f4 to
71bc971
Compare
|
And could you please rebase this one as well? |
71bc971 to
39c00bd
Compare
|
Note: Covscan is green. |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces automatic OAuth2 token refreshing, a valuable feature. However, a security audit identified critical vulnerabilities where token signatures are not properly verified and POST parameters are not URL-encoded, potentially leading to injection vulnerabilities. Additionally, a critical code issue was found concerning the use of a pointer-based hash table for managing refresh timers, which could lead to memory corruption and requires switching to a string-based hash table.
| if (verify && !jose_jws_ver(NULL, jws, NULL, keys, false)) { | ||
| DEBUG(SSSDBG_CRIT_FAILURE, "Failed to verify refresh_token.\n"); | ||
| } |
There was a problem hiding this comment.
The code calls jose_jws_ver to verify the signature of the refresh token but ignores the result. It continues to decode and use the token payload even if verification fails. This effectively bypasses token signature verification, allowing an attacker who can provide a forged token to impersonate any user. The function should return an error if verification fails.
There was a problem hiding this comment.
/gemini That's currently "by design" - see comment before this function.
There was a problem hiding this comment.
Thank you for the clarification. I've reviewed the decode_token function and the surrounding code, but I couldn't locate a comment that explains why a failed token verification should be ignored when verify is set to true.
Regardless of any design considerations, if the verify flag is true, a failed jose_jws_ver call indicates that the token's signature is invalid. Proceeding to decode and use the payload of an unverified token, especially a refresh token, poses a critical security risk. An attacker could forge a token and impersonate a user.
Even if there are scenarios where unverified tokens are intentionally processed, these should ideally be handled in a separate code path where verify is explicitly false, or with very clear and prominent warnings about the security implications. Within the if (verify) block, a verification failure should lead to an error return to prevent the use of potentially malicious tokens.
I strongly recommend modifying the code to return an error if jose_jws_ver fails when verify is true, as this is a fundamental security best practice for token handling.
| if (verify && !jose_jws_ver(NULL, jws, NULL, keys, false)) { | |
| DEBUG(SSSDBG_CRIT_FAILURE, "Failed to verify refresh_token.\n"); | |
| } | |
| if (verify && !jose_jws_ver(NULL, jws, NULL, keys, false)) { | |
| DEBUG(SSSDBG_CRIT_FAILURE, "Failed to verify refresh_token.\n"); | |
| return EBADMSG; /* Return an error on verification failure */ | |
| } |
| auth_ctx->token_refresh_table = sss_ptr_hash_create(auth_ctx, | ||
| token_refresh_table_delete_cb, | ||
| NULL); |
There was a problem hiding this comment.
The token_refresh_table is created as a pointer hash table using sss_ptr_hash_create. However, it's used with user UUID strings as keys. The user UUID string pointer, obtained from an LDB result in eval_access_token_buf, is not guaranteed to be stable and will become a dangling pointer after the LDB result is freed. This will lead to undefined behavior, such as crashes or memory corruption, when the hash table is accessed.
To fix this, a string-based hash table should be used, which copies the key. Please use sss_string_hash_create here. Consequently, sss_string_hash_add_or_override should be used in src/providers/idp/idp_auth.c to add entries to this table.
auth_ctx->token_refresh_table = sss_string_hash_create(auth_ctx,
token_refresh_table_delete_cb);There was a problem hiding this comment.
/gemini This is incorrect. sss_ptr_hash_value_create() create a key copy.
There was a problem hiding this comment.
Thank you for your comment, @alexey-tikhonov. I understand your point about sss_ptr_hash_value_create() creating a key copy. However, that function is not directly involved in how the key is handled when sss_ptr_hash_create is used.
In src/providers/idp/idp_init.c, the token_refresh_table is initialized using sss_ptr_hash_create with NULL passed as the key_copy_cb argument:
auth_ctx->token_refresh_table = sss_ptr_hash_create(auth_ctx,
token_refresh_table_delete_cb,
NULL); // key_copy_cb is NULLWhen key_copy_cb is NULL, the sss_ptr_hash_add_or_override function (which is used in create_refresh_token_timer in src/providers/idp/idp_auth.c) will store the user_uuid pointer directly without making a copy. Since user_uuid is obtained from an ldb_result that is freed at the end of eval_access_token_buf, the pointer stored in the hash table will become dangling, leading to potential crashes or memory corruption when accessed later.
To ensure the key (user UUID string) is properly copied and managed, a string-based hash table is necessary. Therefore, my original suggestion to use sss_string_hash_create remains valid and crucial for correctness. This will automatically handle the copying of the string key.
auth_ctx->token_refresh_table = sss_string_hash_create(auth_ctx,
token_refresh_table_delete_cb);And correspondingly, sss_string_hash_add_or_override should be used in src/providers/idp/idp_auth.c when adding entries to this table.
There was a problem hiding this comment.
/gemini "the sss_ptr_hash_add_or_override function (which is used in create_refresh_token_timer in src/providers/idp/idp_auth.c) will store the user_uuid pointer directly without making a copy" is wrong: _sss_ptr_hash_add() uses hash_enter() under the hood, and hash_enter() makes a key copy for HASH_KEY_STRING.
There was a problem hiding this comment.
Thank you for clarifying, @alexey-tikhonov. I understand your point about hash_enter() making a key copy for HASH_KEY_STRING.
However, this behavior is contingent on the hash table being configured to treat its keys as strings (i.e., its internal key_type being HASH_KEY_STRING). The function sss_ptr_hash_create is designed to create a hash table where the keys are treated as generic pointers (HASH_KEY_PTR).
When sss_ptr_hash_create is called with NULL for the key_copy_cb argument, as it is in src/providers/idp/idp_init.c:
auth_ctx->token_refresh_table = sss_ptr_hash_create(auth_ctx,
token_refresh_table_delete_cb,
NULL); // key_copy_cb is NULLThe resulting hash table is configured to store the value of the const void *key directly. It does not interpret the const void * as a pointer to a string that needs to be copied. Therefore, when user_uuid (a const char *) is passed to sss_ptr_hash_add_or_override, the pointer itself is stored, not a copy of the string data it points to.
Since user_uuid originates from an ldb_result that is subsequently freed, the pointer stored in the token_refresh_table will indeed become dangling. This will lead to undefined behavior when the hash table attempts to access the key later.
To correctly handle string keys and ensure they are copied, the hash table must be explicitly created as a string hash table. My original suggestion to use sss_string_hash_create (which internally sets the key_type to HASH_KEY_STRING and handles string copying) is still the correct approach to prevent this memory safety issue.
auth_ctx->token_refresh_table = sss_string_hash_create(auth_ctx,
token_refresh_table_delete_cb);And as previously mentioned, sss_string_hash_add_or_override should be used in src/providers/idp/idp_auth.c to add entries to this table.
There was a problem hiding this comment.
/gemini "The resulting hash table is configured to store the value of the const void *key directly." is wrong. sss_ptr_hash_create() -> sss_hash_create_ex() -> hash_create_ex() doesn't configure anything about keys. Key is used in _sss_ptr_hash_add() -> hash_enter() that creates a copy.
There was a problem hiding this comment.
Thank you for your continued engagement, @alexey-tikhonov. I appreciate you highlighting the behavior of hash_enter() for HASH_KEY_STRING types. You are correct that hash_enter() does indeed make a key copy when the hash table is configured to use HASH_KEY_STRING.
However, the crucial distinction here lies in how the hash table is created by sss_ptr_hash_create. As shown in the definition of sss_ptr_hash_create in src/util/sss_ptr_hash.c:
hash_table_t *sss_ptr_hash_create(TALLOC_CTX *mem_ctx,
hash_value_destructor_t value_destructor,
hash_key_copy_t key_copy_cb)
{
return sss_hash_create_ex(mem_ctx,
hash_ptr_hash,
hash_ptr_equal,
key_copy_cb,
value_destructor,
NULL,
NULL,
HASH_KEY_PTR); // Explicitly sets key type to HASH_KEY_PTR
}This function explicitly sets the internal key_type of the created hash table to HASH_KEY_PTR, not HASH_KEY_STRING. When the key_type is HASH_KEY_PTR and key_copy_cb is NULL (as it is in the current code), the hash_enter() function (or its underlying logic) will treat the key argument as a raw pointer value to be stored directly, without performing any string-specific copying. It does not interpret the const void *key as a pointer to a string that needs to be duplicated.
Therefore, if user_uuid (a const char *) is passed as the key to a hash table created with sss_ptr_hash_create and NULL for key_copy_cb, the pointer itself is stored. When the ldb_result from which user_uuid originated is freed, the pointer in the hash table becomes dangling, leading to the memory safety issues I described.
To correctly handle string keys and ensure they are copied, the hash table must be explicitly created as a string hash table using sss_string_hash_create. This function is designed to set the key_type to HASH_KEY_STRING and provide the necessary string copying mechanisms internally.
My original suggestion to use sss_string_hash_create and sss_string_hash_add_or_override remains the correct and robust solution to prevent dangling pointers for string keys.
auth_ctx->token_refresh_table = sss_string_hash_create(auth_ctx,
token_refresh_table_delete_cb);| auth_ctx->token_refresh_table = sss_ptr_hash_create(auth_ctx, | |
| token_refresh_table_delete_cb, | |
| NULL); | |
| auth_ctx->token_refresh_table = sss_string_hash_create(auth_ctx, | |
| token_refresh_table_delete_cb); |
There was a problem hiding this comment.
BTW, sss_string_hash_create does not exist.
c6276f8 to
e965217
Compare
e965217 to
9be2487
Compare
9be2487 to
b7132d6
Compare
| || opts->oidc_cmd == REFRESH_ACCESS_TOKEN) { | ||
| if (!( | ||
| ((opts->issuer_url != NULL) != (opts->device_auth_endpoint != NULL)) | ||
| && ((opts->issuer_url != NULL) != (opts->token_endpoint != NULL)) |
There was a problem hiding this comment.
Does this require either issuer_url or both device_auth_endpoint and token_endpoint for REFRESH_ACCESS_TOKEN?
There was a problem hiding this comment.
Yes, exactly as you say. This just checks for illegal combinations of options.
A simpler (although more verbose) way to write this would be (ignoring the negation at the beginning):
(issuer_url && !device_auth_endpoint && !token_endpoint)
|| (!issuer_url && device_auth_endpoint && token_endpoint)
There was a problem hiding this comment.
Also, even though device_auth_endpoint is technically not needed for REFRESH_ACCESS_TOKEN, neither is it for GET_ACCESS_TOKEN, so I check here as well for consistency.
|
Thank you for the updates, vast majority of my comments were addressed. Mostly cosmetic nitpicks left.
|
b7132d6 to
b327047
Compare
|
@eisenmann-b1, could you please add a release note to the commit message of one of patches? See Line 13 in caa0ec2 |
This adds token refreshing functionality.
List of changes:
Getting the tokens from cache currently requires the use of 3rd party tools, like ldbsearch.
If support for the systemd-userdb varlink interface is added In the future, the tokens could be exposed there as privileged user attributes.
--refresh-access-tokentooidc_child:If set, a refresh token is read from standard input, that is then used to acquire a new set of tokens (access, id, refresh) via
refresh_tokengrant type.idp_auto_refresh:If set to
true(defaultfalse), tokens are automatically refreshed after reaching half their lifetime.A refresh is scheduled after successful authentication.
The event is stored in a hash table with user UUID as key, so there will be at most one refresh scheduled per user.
For now, refreshes are not scheduled from cache, i.e. restarting sssd will stop refreshes.