Skip to content

Commit bd4f0ff

Browse files
committed
Type rename; alignment test
1 parent 26f80ef commit bd4f0ff

File tree

8 files changed

+68
-78
lines changed

8 files changed

+68
-78
lines changed

src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -139,23 +139,22 @@ public ClientOAuthProvider(
139139
{
140140
ThrowIfNotBearerScheme(scheme);
141141

142-
var cachedToken = await _tokenCache.GetTokenAsync(cancellationToken).ConfigureAwait(false);
143-
var token = cachedToken?.ForUse();
144-
142+
var tokens = await _tokenCache.GetTokensAsync(cancellationToken).ConfigureAwait(false);
143+
145144
// Return the token if it's valid
146-
if (token != null && token.ExpiresAt > DateTimeOffset.UtcNow.AddMinutes(5))
145+
if (tokens != null && tokens.ExpiresAt > DateTimeOffset.UtcNow.AddMinutes(5))
147146
{
148-
return token.AccessToken;
147+
return tokens.AccessToken;
149148
}
150149

151150
// Try to refresh the token if we have a refresh token
152-
if (token?.RefreshToken != null && _authServerMetadata != null)
151+
if (tokens?.RefreshToken != null && _authServerMetadata != null)
153152
{
154-
var newToken = await RefreshTokenAsync(token.RefreshToken, resourceUri, _authServerMetadata, cancellationToken).ConfigureAwait(false);
155-
if (newToken != null)
153+
var newTokens = await RefreshTokenAsync(tokens.RefreshToken, resourceUri, _authServerMetadata, cancellationToken).ConfigureAwait(false);
154+
if (newTokens != null)
156155
{
157-
await _tokenCache.StoreTokenAsync(newToken.ForCache(), cancellationToken).ConfigureAwait(false);
158-
return newToken.AccessToken;
156+
await _tokenCache.StoreTokensAsync(newTokens, cancellationToken).ConfigureAwait(false);
157+
return newTokens.AccessToken;
159158
}
160159
}
161160

@@ -234,14 +233,14 @@ private async Task PerformOAuthAuthorizationAsync(
234233
}
235234

236235
// Perform the OAuth flow
237-
var token = await InitiateAuthorizationCodeFlowAsync(protectedResourceMetadata, authServerMetadata, cancellationToken).ConfigureAwait(false);
236+
var tokens = await InitiateAuthorizationCodeFlowAsync(protectedResourceMetadata, authServerMetadata, cancellationToken).ConfigureAwait(false);
238237

239-
if (token is null)
238+
if (tokens is null)
240239
{
241240
ThrowFailedToHandleUnauthorizedResponse($"The {nameof(AuthorizationRedirectDelegate)} returned a null or empty token.");
242241
}
243242

244-
await _tokenCache.StoreTokenAsync(token.ForCache(), cancellationToken).ConfigureAwait(false);
243+
await _tokenCache.StoreTokensAsync(tokens, cancellationToken).ConfigureAwait(false);
245244
LogOAuthAuthorizationCompleted();
246245
}
247246

@@ -413,15 +412,23 @@ private async Task<TokenContainer> FetchTokenAsync(HttpRequestMessage request, C
413412
httpResponse.EnsureSuccessStatusCode();
414413

415414
using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
416-
var tokenResponse = await JsonSerializer.DeserializeAsync(stream, McpJsonUtilities.JsonContext.Default.TokenContainer, cancellationToken).ConfigureAwait(false);
415+
var tokenResponse = await JsonSerializer.DeserializeAsync(stream, McpJsonUtilities.JsonContext.Default.TokenResponse, cancellationToken).ConfigureAwait(false);
417416

418417
if (tokenResponse is null)
419418
{
420419
ThrowFailedToHandleUnauthorizedResponse($"The token endpoint '{request.RequestUri}' returned an empty response.");
421420
}
422421

423-
tokenResponse.ObtainedAt = DateTimeOffset.UtcNow;
424-
return tokenResponse;
422+
return new()
423+
{
424+
AccessToken = tokenResponse.AccessToken,
425+
RefreshToken = tokenResponse.RefreshToken,
426+
ExpiresIn = tokenResponse.ExpiresIn,
427+
ExtExpiresIn = tokenResponse.ExtExpiresIn,
428+
TokenType = tokenResponse.TokenType,
429+
Scope = tokenResponse.Scope,
430+
ObtainedAt = DateTimeOffset.UtcNow,
431+
};
425432
}
426433

427434
/// <summary>

src/ModelContextProtocol.Core/Authentication/ITokenCache.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ public interface ITokenCache
88
/// <summary>
99
/// Cache the token. After a new access token is acquired, this method is invoked to store it.
1010
/// </summary>
11-
ValueTask StoreTokenAsync(TokenContainerCacheable token, CancellationToken cancellationToken);
11+
ValueTask StoreTokensAsync(TokenContainer tokens, CancellationToken cancellationToken);
1212

1313
/// <summary>
1414
/// Get the cached token. This method is invoked for every request.
1515
/// </summary>
16-
ValueTask<TokenContainerCacheable?> GetTokenAsync(CancellationToken cancellationToken);
16+
ValueTask<TokenContainer?> GetTokensAsync(CancellationToken cancellationToken);
1717
}

src/ModelContextProtocol.Core/Authentication/InMemoryTokenCache.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,22 @@ namespace ModelContextProtocol.Authentication;
66
/// </summary>
77
internal class InMemoryTokenCache : ITokenCache
88
{
9-
private TokenContainerCacheable? _token;
9+
private TokenContainer? _tokens;
1010

1111
/// <summary>
1212
/// Cache the token.
1313
/// </summary>
14-
public ValueTask StoreTokenAsync(TokenContainerCacheable token, CancellationToken cancellationToken)
14+
public ValueTask StoreTokensAsync(TokenContainer tokens, CancellationToken cancellationToken)
1515
{
16-
_token = token;
16+
_tokens = tokens;
1717
return default;
1818
}
1919

2020
/// <summary>
2121
/// Get the cached token.
2222
/// </summary>
23-
public ValueTask<TokenContainerCacheable?> GetTokenAsync(CancellationToken cancellationToken)
23+
public ValueTask<TokenContainer?> GetTokensAsync(CancellationToken cancellationToken)
2424
{
25-
return new ValueTask<TokenContainerCacheable?>(_token);
25+
return new ValueTask<TokenContainer?>(_tokens);
2626
}
2727
}
Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,47 @@
1-
using System.Text.Json.Serialization;
2-
31
namespace ModelContextProtocol.Authentication;
42

53
/// <summary>
6-
/// Represents a token response from the OAuth server.
4+
/// Represents a cacheable combination of tokens ready to be used for authentication.
75
/// </summary>
8-
internal sealed class TokenContainer
6+
public class TokenContainer
97
{
108
/// <summary>
119
/// Gets or sets the access token.
1210
/// </summary>
13-
[JsonPropertyName("access_token")]
1411
public string AccessToken { get; set; } = string.Empty;
1512

1613
/// <summary>
1714
/// Gets or sets the refresh token.
1815
/// </summary>
19-
[JsonPropertyName("refresh_token")]
2016
public string? RefreshToken { get; set; }
2117

2218
/// <summary>
2319
/// Gets or sets the number of seconds until the access token expires.
2420
/// </summary>
25-
[JsonPropertyName("expires_in")]
2621
public int ExpiresIn { get; set; }
2722

2823
/// <summary>
2924
/// Gets or sets the extended expiration time in seconds.
3025
/// </summary>
31-
[JsonPropertyName("ext_expires_in")]
3226
public int ExtExpiresIn { get; set; }
3327

3428
/// <summary>
3529
/// Gets or sets the token type (typically "Bearer").
3630
/// </summary>
37-
[JsonPropertyName("token_type")]
3831
public string TokenType { get; set; } = string.Empty;
3932

4033
/// <summary>
4134
/// Gets or sets the scope of the access token.
4235
/// </summary>
43-
[JsonPropertyName("scope")]
4436
public string Scope { get; set; } = string.Empty;
4537

4638
/// <summary>
4739
/// Gets or sets the timestamp when the token was obtained.
4840
/// </summary>
49-
[JsonIgnore]
5041
public DateTimeOffset ObtainedAt { get; set; }
5142

5243
/// <summary>
5344
/// Gets the timestamp when the token expires, calculated from ObtainedAt and ExpiresIn.
5445
/// </summary>
55-
[JsonIgnore]
5646
public DateTimeOffset ExpiresAt => ObtainedAt.AddSeconds(ExpiresIn);
5747
}

src/ModelContextProtocol.Core/Authentication/TokenContainerConvert.cs

Lines changed: 0 additions & 26 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,45 @@
1+
using System.Text.Json.Serialization;
2+
13
namespace ModelContextProtocol.Authentication;
24

35
/// <summary>
4-
/// Represents a cacheable token representation.
6+
/// Represents a token response from the OAuth server.
57
/// </summary>
6-
public class TokenContainerCacheable
8+
internal sealed class TokenResponse
79
{
810
/// <summary>
911
/// Gets or sets the access token.
1012
/// </summary>
13+
[JsonPropertyName("access_token")]
1114
public string AccessToken { get; set; } = string.Empty;
1215

1316
/// <summary>
1417
/// Gets or sets the refresh token.
1518
/// </summary>
19+
[JsonPropertyName("refresh_token")]
1620
public string? RefreshToken { get; set; }
1721

1822
/// <summary>
1923
/// Gets or sets the number of seconds until the access token expires.
2024
/// </summary>
25+
[JsonPropertyName("expires_in")]
2126
public int ExpiresIn { get; set; }
2227

2328
/// <summary>
2429
/// Gets or sets the extended expiration time in seconds.
2530
/// </summary>
31+
[JsonPropertyName("ext_expires_in")]
2632
public int ExtExpiresIn { get; set; }
2733

2834
/// <summary>
2935
/// Gets or sets the token type (typically "Bearer").
3036
/// </summary>
37+
[JsonPropertyName("token_type")]
3138
public string TokenType { get; set; } = string.Empty;
3239

3340
/// <summary>
3441
/// Gets or sets the scope of the access token.
3542
/// </summary>
43+
[JsonPropertyName("scope")]
3644
public string Scope { get; set; } = string.Empty;
37-
38-
/// <summary>
39-
/// Gets or sets the timestamp when the token was obtained.
40-
/// </summary>
41-
public DateTimeOffset ObtainedAt { get; set; }
4245
}

src/ModelContextProtocol.Core/McpJsonUtilities.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ internal static bool IsValidMcpToolSchema(JsonElement element)
158158

159159
[JsonSerializable(typeof(ProtectedResourceMetadata))]
160160
[JsonSerializable(typeof(AuthorizationServerMetadata))]
161-
[JsonSerializable(typeof(TokenContainer))]
161+
[JsonSerializable(typeof(TokenResponse))]
162162
[JsonSerializable(typeof(DynamicClientRegistrationRequest))]
163163
[JsonSerializable(typeof(DynamicClientRegistrationResponse))]
164164

tests/ModelContextProtocol.Tests/Client/CustomTokenCacheTests.cs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using ModelContextProtocol.Protocol;
33
using ModelContextProtocol.Authentication;
44
using System.Text.Json;
5+
using System.Text.Json.Serialization.Metadata;
56
using Moq;
67
using Moq.Protected;
78
using System.Net;
@@ -12,6 +13,16 @@ namespace ModelContextProtocol.Tests.Client;
1213

1314
public class CustomTokenCacheTests
1415
{
16+
[Fact]
17+
public void TokenContainerIsAlignedWithTokenResponse()
18+
{
19+
var tokenResponseType = Type.GetType("ModelContextProtocol.Authentication.TokenResponse, ModelContextProtocol.Core");
20+
Assert.NotNull(tokenResponseType);
21+
var tokenResponseProperties = tokenResponseType.GetProperties().Select(p => p.Name);
22+
var tokenContainerProperties = typeof(TokenContainer).GetProperties().Select(p => p.Name);
23+
Assert.Equivalent(tokenResponseProperties, tokenContainerProperties);
24+
}
25+
1526
[Fact]
1627
public async Task GetTokenAsync_CachedAccessTokenIsUsedForOutgoingRequests()
1728
{
@@ -80,8 +91,8 @@ public async Task StoreTokenAsync_NewlyAcquiredAccessTokenIsCached()
8091

8192
// Assert
8293
tokenCacheMock
83-
.Verify(tc => tc.StoreTokenAsync(
84-
It.Is<TokenContainerCacheable>(token => token.AccessToken == newAccessToken),
94+
.Verify(tc => tc.StoreTokensAsync(
95+
It.Is<TokenContainer>(token => token.AccessToken == newAccessToken),
8596
It.IsAny<CancellationToken>()), Times.Once);
8697
}
8798

@@ -103,8 +114,8 @@ public async Task StoreTokenAsync_NewlyAcquiredAccessTokenIsCached()
103114
static void MockCachedAccessToken(Mock<ITokenCache> tokenCache, string cachedAccessToken)
104115
{
105116
tokenCache
106-
.Setup(tc => tc.GetTokenAsync(It.IsAny<CancellationToken>()))
107-
.ReturnsAsync(new TokenContainerCacheable
117+
.Setup(tc => tc.GetTokensAsync(It.IsAny<CancellationToken>()))
118+
.ReturnsAsync(new TokenContainer
108119
{
109120
AccessToken = cachedAccessToken,
110121
ObtainedAt = DateTimeOffset.UtcNow,
@@ -115,8 +126,8 @@ static void MockCachedAccessToken(Mock<ITokenCache> tokenCache, string cachedAcc
115126
static void MockNoAccessTokenUntilStored(Mock<ITokenCache> tokenCache)
116127
{
117128
tokenCache
118-
.Setup(tc => tc.StoreTokenAsync(It.IsAny<TokenContainerCacheable>(), It.IsAny<CancellationToken>()))
119-
.Callback<TokenContainerCacheable, CancellationToken>((token, ct) =>
129+
.Setup(tc => tc.StoreTokensAsync(It.IsAny<TokenContainer>(), It.IsAny<CancellationToken>()))
130+
.Callback<TokenContainer, CancellationToken>((token, ct) =>
120131
{
121132
// Simulate that the token is now cached
122133
MockCachedAccessToken(tokenCache, token.AccessToken);
@@ -216,18 +227,23 @@ static void MockInitializeResponse(Mock<HttpMessageHandler> httpMessageHandler)
216227

217228
static void MockHttpResponse(Mock<HttpMessageHandler> httpMessageHandler, Expression<Func<HttpRequestMessage, bool>>? request = null, HttpResponseMessage? response = null)
218229
{
219-
httpMessageHandler
230+
_ = httpMessageHandler
220231
.Protected()
221232
.Setup<Task<HttpResponseMessage>>("SendAsync", request != null ? ItExpr.Is(request) : ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
222233
.ReturnsAsync(response ?? new HttpResponseMessage());
223234
}
224235

225236
static StringContent ToJsonContent<T>(T content) => new(
226-
content: JsonSerializer.Serialize(content, McpJsonUtilities.DefaultOptions),
237+
content: JsonSerializer.Serialize(content, GetReflectionCapableJsonOptions()),
227238
encoding: System.Text.Encoding.UTF8,
228239
mediaType: "application/json");
229240

230241
static JsonNode? ToJson<T>(T content) => JsonSerializer.SerializeToNode(
231242
value: content,
232-
options: McpJsonUtilities.DefaultOptions);
243+
options: GetReflectionCapableJsonOptions());
244+
245+
static JsonSerializerOptions GetReflectionCapableJsonOptions() => new(JsonSerializerDefaults.Web)
246+
{
247+
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
248+
};
233249
}

0 commit comments

Comments
 (0)