Skip to content

Commit d580aef

Browse files
mauri0686claude
andcommitted
fix(auth): use the discovered issuer as the ID-JAG token-exchange audience
IdentityAssertionGrantProvider passed authorizationServerUrl.ToString() as the RFC 8693 audience. Uri.ToString() appends a trailing slash to an empty path, so the common case (https://auth.example.com, whose metadata advertises the issuer without a slash) sent an audience that did not match the issuer. Use the issuer from the authorization server metadata already fetched in step 1, falling back to the configured URL when the metadata omits it. Fixes #1617 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c40ee04 commit d580aef

2 files changed

Lines changed: 66 additions & 1 deletion

File tree

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,10 @@ private async Task<TokenContainer> AcquireAccessTokenAsync(
196196
new RequestJwtAuthGrantOptions
197197
{
198198
TokenEndpoint = idpTokenEndpoint,
199-
Audience = authorizationServerUrl.ToString(),
199+
// The JAG audience is the MCP authorization server's issuer identifier (RFC 8414), not the
200+
// caller-supplied URL, which can differ from it (e.g. Uri.ToString() appends a trailing slash).
201+
// OriginalString preserves the advertised issuer exactly as published.
202+
Audience = mcpAuthMetadata.Issuer?.OriginalString ?? authorizationServerUrl.ToString(),
200203
Resource = resourceUrl.ToString(),
201204
IdToken = idToken,
202205
ClientId = _options.IdpClientId,

‎tests/ModelContextProtocol.Tests/IdentityAssertionGrantTests.cs‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,68 @@ public async Task IdentityAssertionGrantProvider_FullFlow_ReturnsAccessToken()
9595
Assert.Equal(3600, tokens.ExpiresIn);
9696
}
9797

98+
[Theory]
99+
[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")]
101+
[InlineData("https://auth.example.com", null, "https://auth.example.com/")]
102+
public async Task IdentityAssertionGrantProvider_UsesDiscoveredIssuerAsJagAudience(
103+
string authorizationServerUrl, string? advertisedIssuer, string expectedAudience)
104+
{
105+
string? audience = null;
106+
_mockHandler.AsyncHandler = async request =>
107+
{
108+
var url = request.RequestUri!.ToString();
109+
if (url.Contains(".well-known"))
110+
{
111+
return JsonResponse(HttpStatusCode.OK, new JsonObject
112+
{
113+
["issuer"] = advertisedIssuer,
114+
["token_endpoint"] = "https://auth.example.com/token",
115+
});
116+
}
117+
118+
if (url.Contains("idp.example.com"))
119+
{
120+
var body = await request.Content!.ReadAsStringAsync(TestContext.Current.CancellationToken);
121+
audience = body.Split('&')
122+
.Select(pair => pair.Split('='))
123+
.Where(kv => kv[0] == "audience")
124+
.Select(kv => WebUtility.UrlDecode(kv[1]))
125+
.Single();
126+
127+
return JsonResponse(HttpStatusCode.OK, new JsonObject
128+
{
129+
["access_token"] = "mock-jag",
130+
["issued_token_type"] = "urn:ietf:params:oauth:token-type:id-jag",
131+
["token_type"] = "N_A",
132+
});
133+
}
134+
135+
return JsonResponse(HttpStatusCode.OK, new JsonObject
136+
{
137+
["access_token"] = "final-access-token",
138+
["token_type"] = "Bearer",
139+
});
140+
};
141+
142+
var provider = new IdentityAssertionGrantProvider(
143+
new IdentityAssertionGrantProviderOptions
144+
{
145+
ClientId = "mcp-client-id",
146+
IdpTokenEndpoint = "https://idp.example.com/token",
147+
IdpClientId = "idp-client-id",
148+
IdTokenCallback = (_, _) => Task.FromResult("mock-id-token"),
149+
},
150+
_httpClient);
151+
152+
await provider.GetAccessTokenAsync(
153+
new Uri("https://resource.example.com"),
154+
new Uri(authorizationServerUrl),
155+
TestContext.Current.CancellationToken);
156+
157+
Assert.Equal(expectedAudience, audience);
158+
}
159+
98160
[Fact]
99161
public Task IdentityAssertionGrantProvider_DefaultsToPostRegardlessOfMetadataOrder() =>
100162
AssertMcpTokenEndpointAuthenticationAsync(

0 commit comments

Comments
 (0)