-
Notifications
You must be signed in to change notification settings - Fork 545
Tokens can be cached beyond the lifetime of the (http) transport. #834
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
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add tests for supplying a custom ITokenCache that verifies it gets used?
@localden @DavidParks8 Do you have any thoughts on this?
I added tests that verify a custom token cache is used to look up cached tokens and to store new ones. Since I wanted to test the public API, I had to use What do you think? |
…oken, until we get the TokenCache (modelcontextprotocol/csharp-sdk#834).
Until this is approved and merged, I use reflection magic to extract and inject the oauth token like this: var httpClientTransport = new HttpClientTransport(...)
var token = File.Exists("token.json") ? File.ReadAllText("token.json") : null;
httpClientTransport.InjectOAuthToken(token);
await using var mcpClient = await McpClient.CreateAsync(httpClientTransport);
File.WriteAllText("token.json", httpClientTransport.ExtractOAuthToken()); |
/// <summary> | ||
/// Represents a cacheable token representation. | ||
/// </summary> | ||
public class TokenContainerCacheable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should use the better name for the public API.
public class TokenContainerCacheable | |
public class TokenContainer |
Then, we could call the internal type we deserialize the /token response with TokenResponse
, and this wouldn't require an ObtainedAt property since that's already provided by the public type.
|
||
internal static class TokenContainerConvert | ||
{ | ||
internal static TokenContainer ForUse(this TokenContainerCacheable token) => new() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add a reflection-based test for this and ForCache
that's similar to AuthTests.CloneResourceMetadataClonesAllProperties. I don't want to accidently add a property to one and not the other or not clone it.
Instructions = "This server provides weather information and file system access." | ||
}) | ||
}), | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than having to mock all of these responses, which seems to be over half this test class, you could use a real call to MapMcp().RequireAuthentication()
like CanAuthenticate_WithTokenRefresh does. Then you only have to mock ITokenCache and verify gets called in the expected way.
_testOAuthServer
is accessible, so you can like make the last-issued access token a property similar to HasIssuedRefreshToken
, but I don't think you even need to do anything like that. Simply verifying that ITokenCache.GetTokenAsync gets called and AuthorizationRedirectDelegate doesn't should be sufficient.
/// <summary> | ||
/// Get the cached token. This method is invoked for every request. | ||
/// </summary> | ||
ValueTask<TokenContainerCacheable?> GetTokenAsync(CancellationToken cancellationToken); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we make the methods name plural? I think StoreTokensAsync and GetTokensAsync makes more sense considering we're storing and retrieving both the access token and refresh token if available.
I added the possibility to plugin a token cache, so that the client can be made to reuse its access token beyond the lifetime of the transport. By default tokens are only cached for the lifetime of the transport.
Fixes #749
Fixes #597
Motivation and Context
When the client acquires an access token and a refresh token, it should be able to utilze them to make authenticated requests to the server even after it was restarted. The client should not need to go through the OAuth dance again and again every time it is initialised (unless the access token is no longer valid and no refresh token was acquired or the refresh token was revoked).
How Has This Been Tested?
I tested it via a proof of concept application without a token cache (using the default
InMemoryTokenCache
) and with a custom implementation of a file-based token cache.Breaking Changes
The introduction of the
ITokenCache
is backwards compatible. If no custom token cache is provided, it falls back to anInMemoryTokenCache
with the same lifetime as the (oauth enabled http) transport (like before).Types of changes
Checklist
Additional context
TokenContainer
public, so that it can be passed into custom token caches.ObtainedAt
property serialisable, because it needs to remember when it was obtained after serialization to calculate the expiration.These two changes seemed more reasonable than introducing another token type for cache serialization only. I didn't find usages that could be disrupted by this.