Skip to content

Commit 3abde0b

Browse files
mauri0686claude
andcommitted
fix(auth): validate the discovered issuer before using it as the JAG audience
RFC 8414 Section 3.3 requires the issuer in the metadata to be identical to the issuer identifier used for discovery. ClientOAuthProvider already checks this; the Cross-Application Access flow now does too, before the issuer is sent as the token-exchange audience. A mismatched issuer fails instead of becoming the audience. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent d580aef commit 3abde0b

2 files changed

Lines changed: 51 additions & 1 deletion

File tree

‎src/ModelContextProtocol.Core/Authentication/IdentityAssertionGrantProvider.cs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,16 @@ private async Task<TokenContainer> AcquireAccessTokenAsync(
169169
var mcpAuthMetadata = await IdentityAssertionGrant.DiscoverAuthServerMetadataAsync(
170170
authorizationServerUrl, _httpClient, cancellationToken).ConfigureAwait(false);
171171

172+
// The issuer becomes the JAG audience below, so validate it first (RFC 8414 Section 3.3): it MUST be
173+
// identical to the issuer identifier used for discovery. Like ClientOAuthProvider, compare the exact
174+
// strings without normalizing case, trailing slashes or percent-encoding.
175+
if (mcpAuthMetadata.Issuer is not null &&
176+
!string.Equals(mcpAuthMetadata.Issuer.OriginalString, authorizationServerUrl.OriginalString, StringComparison.Ordinal))
177+
{
178+
throw new IdentityAssertionGrantException(
179+
$"Authorization server metadata issuer '{mcpAuthMetadata.Issuer.OriginalString}' does not match the expected issuer '{authorizationServerUrl.OriginalString}' (RFC 8414 Section 3.3).");
180+
}
181+
172182
var mcpTokenEndpoint = mcpAuthMetadata.TokenEndpoint?.ToString()
173183
?? throw new IdentityAssertionGrantException(
174184
$"MCP authorization server metadata at {authorizationServerUrl} missing token_endpoint.");

‎tests/ModelContextProtocol.Tests/IdentityAssertionGrantTests.cs‎

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public async Task IdentityAssertionGrantProvider_FullFlow_ReturnsAccessToken()
9797

9898
[Theory]
9999
[InlineData("https://auth.example.com", "https://auth.example.com", "https://auth.example.com")]
100-
[InlineData("https://auth.example.com/", "https://auth.example.com/tenant", "https://auth.example.com/tenant")]
100+
[InlineData("https://auth.example.com/tenant", "https://auth.example.com/tenant", "https://auth.example.com/tenant")]
101101
[InlineData("https://auth.example.com", null, "https://auth.example.com/")]
102102
public async Task IdentityAssertionGrantProvider_UsesDiscoveredIssuerAsJagAudience(
103103
string authorizationServerUrl, string? advertisedIssuer, string expectedAudience)
@@ -157,6 +157,46 @@ await provider.GetAccessTokenAsync(
157157
Assert.Equal(expectedAudience, audience);
158158
}
159159

160+
[Theory]
161+
[InlineData("https://auth.example.com/", "https://auth.example.com/tenant")]
162+
[InlineData("https://auth.example.com/", "https://auth.example.com")]
163+
public async Task IdentityAssertionGrantProvider_RejectsMismatchedIssuer(
164+
string authorizationServerUrl, string advertisedIssuer)
165+
{
166+
var idpCalled = false;
167+
_mockHandler.Handler = request =>
168+
{
169+
if (request.RequestUri!.ToString().Contains("idp.example.com"))
170+
{
171+
idpCalled = true;
172+
}
173+
174+
return JsonResponse(HttpStatusCode.OK, new JsonObject
175+
{
176+
["issuer"] = advertisedIssuer,
177+
["token_endpoint"] = "https://auth.example.com/token",
178+
});
179+
};
180+
181+
var provider = new IdentityAssertionGrantProvider(
182+
new IdentityAssertionGrantProviderOptions
183+
{
184+
ClientId = "mcp-client-id",
185+
IdpTokenEndpoint = "https://idp.example.com/token",
186+
IdpClientId = "idp-client-id",
187+
IdTokenCallback = (_, _) => Task.FromResult("mock-id-token"),
188+
},
189+
_httpClient);
190+
191+
var ex = await Assert.ThrowsAsync<IdentityAssertionGrantException>(() => provider.GetAccessTokenAsync(
192+
new Uri("https://resource.example.com"),
193+
new Uri(authorizationServerUrl),
194+
TestContext.Current.CancellationToken));
195+
196+
Assert.Contains("RFC 8414 Section 3.3", ex.Message);
197+
Assert.False(idpCalled);
198+
}
199+
160200
[Fact]
161201
public Task IdentityAssertionGrantProvider_DefaultsToPostRegardlessOfMetadataOrder() =>
162202
AssertMcpTokenEndpointAuthenticationAsync(

0 commit comments

Comments
 (0)