Skip to content

Refresh OAuth2 tokens automatically#8355

Open
eisenmann-b1 wants to merge 13 commits intoSSSD:masterfrom
eisenmann-b1:oauth2-cache-and-refresh-tokens
Open

Refresh OAuth2 tokens automatically#8355
eisenmann-b1 wants to merge 13 commits intoSSSD:masterfrom
eisenmann-b1:oauth2-cache-and-refresh-tokens

Conversation

@eisenmann-b1
Copy link
Contributor

@eisenmann-b1 eisenmann-b1 commented Jan 15, 2026

This adds token refreshing functionality.

List of changes:

  • Tokens are saved as user attrs in the cache.
    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.
  • Add flag --refresh-access-token to oidc_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_token grant type.
  • Add option idp_auto_refresh:
    If set to true (default false), 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.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@eisenmann-b1 eisenmann-b1 force-pushed the oauth2-cache-and-refresh-tokens branch 2 times, most recently from c5c8af9 to d3ff2c7 Compare January 15, 2026 09:36
@eisenmann-b1 eisenmann-b1 force-pushed the oauth2-cache-and-refresh-tokens branch 2 times, most recently from e58ab81 to dbc3520 Compare January 21, 2026 16:25
@eisenmann-b1 eisenmann-b1 force-pushed the oauth2-cache-and-refresh-tokens branch 5 times, most recently from 0f6575c to 1e9233f Compare February 4, 2026 08:42
@eisenmann-b1 eisenmann-b1 force-pushed the oauth2-cache-and-refresh-tokens branch 2 times, most recently from 80f541f to c7cc462 Compare February 12, 2026 10:18
@eisenmann-b1 eisenmann-b1 marked this pull request as ready for review February 12, 2026 11:14
@alexey-tikhonov alexey-tikhonov self-requested a review February 12, 2026 13:38
@alexey-tikhonov alexey-tikhonov self-assigned this Feb 12, 2026
@alexey-tikhonov alexey-tikhonov added the no-backport This should go to target branch only. label Feb 12, 2026
@eisenmann-b1 eisenmann-b1 force-pushed the oauth2-cache-and-refresh-tokens branch from c7cc462 to 1f8a5f4 Compare February 12, 2026 14:01
@spoore1
Copy link
Contributor

spoore1 commented Feb 12, 2026

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,
Scott

@eisenmann-b1
Copy link
Contributor Author

Hi @spoore1,

Can you let us know when you are looking to get this merged?

Well, I just hope it makes it into 2.13 :)

And for what target distro you'd like to be able to start using it?

I am currently using Ubuntu 24.04 for testing it, but I don't think anything in here is platform-specific.

@eisenmann-b1 eisenmann-b1 force-pushed the oauth2-cache-and-refresh-tokens branch from 1f8a5f4 to 71bc971 Compare February 17, 2026 16:13
@alexey-tikhonov
Copy link
Member

And could you please rebase this one as well?

@eisenmann-b1 eisenmann-b1 force-pushed the oauth2-cache-and-refresh-tokens branch from 71bc971 to 39c00bd Compare February 26, 2026 09:15
@alexey-tikhonov alexey-tikhonov added the coverity Trigger a coverity scan label Feb 26, 2026
@alexey-tikhonov
Copy link
Member

Note: Covscan is green.

@alexey-tikhonov alexey-tikhonov removed the coverity Trigger a coverity scan label Feb 26, 2026
@alexey-tikhonov
Copy link
Member

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +312 to +314
if (verify && !jose_jws_ver(NULL, jws, NULL, keys, false)) {
DEBUG(SSSDBG_CRIT_FAILURE, "Failed to verify refresh_token.\n");
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/gemini That's currently "by design" - see comment before this function.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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 */
}

Comment on lines +343 to +345
auth_ctx->token_refresh_table = sss_ptr_hash_create(auth_ctx,
token_refresh_table_delete_cb,
NULL);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

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);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/gemini This is incorrect. sss_ptr_hash_value_create() create a key copy.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 NULL

When 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 NULL

The 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Suggested change
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);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, sss_string_hash_create does not exist.

@eisenmann-b1 eisenmann-b1 force-pushed the oauth2-cache-and-refresh-tokens branch 4 times, most recently from c6276f8 to e965217 Compare March 14, 2026 21:08
@eisenmann-b1 eisenmann-b1 force-pushed the oauth2-cache-and-refresh-tokens branch from e965217 to 9be2487 Compare March 16, 2026 14:30
@eisenmann-b1 eisenmann-b1 force-pushed the oauth2-cache-and-refresh-tokens branch from 9be2487 to b7132d6 Compare March 16, 2026 19:33
|| opts->oidc_cmd == REFRESH_ACCESS_TOKEN) {
if (!(
((opts->issuer_url != NULL) != (opts->device_auth_endpoint != NULL))
&& ((opts->issuer_url != NULL) != (opts->token_endpoint != NULL))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this require either issuer_url or both device_auth_endpoint and token_endpoint for REFRESH_ACCESS_TOKEN?

Copy link
Contributor Author

@eisenmann-b1 eisenmann-b1 Mar 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@alexey-tikhonov
Copy link
Member

Thank you for the updates, vast majority of my comments were addressed. Mostly cosmetic nitpicks left.
And:

@eisenmann-b1 eisenmann-b1 force-pushed the oauth2-cache-and-refresh-tokens branch from b7132d6 to b327047 Compare March 16, 2026 21:36
@alexey-tikhonov
Copy link
Member

@eisenmann-b1, could you please add a release note to the commit message of one of patches?

See

# :feature: New feature description.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-backport This should go to target branch only.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants