Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Session ID is lost during Token Exchange #1312

Open
tos-ilex opened this issue Jul 2, 2024 · 5 comments
Open

Session ID is lost during Token Exchange #1312

tos-ilex opened this issue Jul 2, 2024 · 5 comments

Comments

@tos-ilex
Copy link

tos-ilex commented Jul 2, 2024

Which version of Duende IdentityServer are you using?
7.0.5

Which version of .NET are you using?
8

Describe the bug
When calling /connect/token with grant_type urn:ietf:params:oauth:grant-type:token-exchange and an existing access token with a sid claim, the resulting access token does not have a sid claim. A resulting refresh token is not correlated with the session, and an entry with SessionId=null is added to the persisted grant store for each exchange.

This is causing issues for us because we exchange tokens quite frequently and each one creates a fairly long-lived, orphaned refresh token entry in the persisted grant database that remains after logout because its session id is null.

To Reproduce

  1. get access token with a session id in the sid claim and offline_access in its scopes.
  2. make token exchange.
  3. observe that resulting token has no sid claim.
  4. observe that persisted grant store has new refresh_token entry with SessionId=NULL.

Expected behavior

sid of given access token should be put into exchanged token. No extra refresh token should go into the persisted grant store, rather the existing one should be renewed or replaced.

Additional context
Our TokenExchangeGrantValidator:

public async Task ValidateAsync(ExtensionGrantValidationContext context)
    {
        context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest);

        ... //some validation that does nothing or lets the request fail

        string? subjectTokenType = context.Request.Raw.Get(OidcConstants.TokenRequest.SubjectTokenType);
        TokenValidationResult? validationResult;
        switch (subjectTokenType)
        {
            case OidcConstants.TokenTypeIdentifiers.AccessToken:
                validationResult = await _validator.ValidateAccessTokenAsync(subjectToken); // Default ITokenValidator
                break;
            default:
                context.Result.ErrorDescription = TokenConstants.Errors.UnsupportedTokenType;
                return;
        }

        string sub = validationResult.Claims.First(c => c.Type == JwtClaimTypes.Subject).Value;

        var customClaims = new List<Claim>();

        string? redacted = context.Request.Raw.Get("redacted");
        if (redacted == null) // issue happens in both cases.
        {
            context.Result = new GrantValidationResult(sub, GrantType, customClaims);
            return;
        }

        customClaims.Add(new Claim("redacted"));

        context.Result = new GrantValidationResult(sub, GrantType, customClaims);
    }
@saithis
Copy link

saithis commented Jul 2, 2024

And this together with the slow token cleanup query is a bad combination #1304

@josephdecock
Copy link
Member

Have you tried copying the session id into the custom claims? We don't automatically copy the sid forward into an exchanged token because depending on the semantics of the exchange that may or may not be appropriate. But I can't think of anything that would prevent that from working.

@josephdecock josephdecock self-assigned this Jul 11, 2024
@tos-ilex
Copy link
Author

I have added this to our IProfileService implementation:

if (context.ValidatedRequest.SessionId != null)
    claims.Add(new Claim("sid", context.ValidatedRequest.SessionId));

context.AddRequestedClaims(claims);

This does result in the exchanged token having a correct sid claim. However, a new refresh token is issued anyway, and its session id in the Persisted Grant store is null:
image
Screenshot from my local test db after I

  • Deleted all persisted grants
  • Went through an auth code flow to get tokens including a refresh token
  • Exchanged the access token via token exchange

Let me clarify that our main problem isn't the missing sid claim (I just thought it was a clear sign that something is wrong). It's that new, "orphaned" refresh tokens are written to the PG store, making our DB fill up rapidly because we do frequent token exchanges. This is causing performance issues.

@AndersAbel
Copy link
Member

I'm revisiting this. I think that you correctly identified the issue in the original post:

No extra refresh token should go into the persisted grant store, rather the existing one should be renewed or replaced.

Token exchange swaps one access token for another. It should not involve any refresh token at all. The request does not include any refresh token from the original session, so (if such a refresh token exists) it shouldn't be updated. The result of the token exchange is another access token - it should not create any new refresh token.

Do you have custom code that is used to create the token that is the result of the token exchange?

@tos-ilex
Copy link
Author

Do you have custom code that is used to create the token that is the result of the token exchange?

We use custom implementations for IProfileService and ITokenExchangeGrantValidator. My original post has the relevant parts of the latter. Our IProfileService implementation only adds some custom claims. Otherwise, no.

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

No branches or pull requests

4 participants