diff --git a/src/ModelContextProtocol.Core/Authentication/IdentityAssertionGrant.cs b/src/ModelContextProtocol.Core/Authentication/IdentityAssertionGrant.cs
index ecc5eb35d..692b731d4 100644
--- a/src/ModelContextProtocol.Core/Authentication/IdentityAssertionGrant.cs
+++ b/src/ModelContextProtocol.Core/Authentication/IdentityAssertionGrant.cs
@@ -21,10 +21,10 @@ internal static class IdentityAssertionGrant
public const string GrantTypeJwtBearer = "urn:ietf:params:oauth:grant-type:jwt-bearer";
/// Token type URN for OpenID Connect ID Tokens (RFC 8693).
- public const string TokenTypeIdToken = "urn:ietf:params:oauth:token-type:id_token";
+ public const string TokenTypeIdToken = IdentityAssertionGrantSubjectTokenTypes.IdToken;
/// Token type URN for SAML 2.0 assertions (RFC 8693).
- public const string TokenTypeSaml2 = "urn:ietf:params:oauth:token-type:saml2";
+ public const string TokenTypeSaml2 = IdentityAssertionGrantSubjectTokenTypes.Saml2;
///
/// Token type URN for Identity Assertion JWT Authorization Grants.
@@ -56,15 +56,16 @@ public static async Task RequestJwtAuthorizationGrantAsync(
Throw.IfNullOrWhiteSpace(options.TokenEndpoint);
Throw.IfNullOrWhiteSpace(options.Audience);
Throw.IfNullOrWhiteSpace(options.Resource);
- Throw.IfNullOrWhiteSpace(options.IdToken);
+ Throw.IfNullOrWhiteSpace(options.SubjectToken);
+ Throw.IfNullOrWhiteSpace(options.SubjectTokenType);
Throw.IfNullOrWhiteSpace(options.ClientId);
var formData = new Dictionary
{
["grant_type"] = GrantTypeTokenExchange,
["requested_token_type"] = TokenTypeIdJag,
- ["subject_token"] = options.IdToken,
- ["subject_token_type"] = TokenTypeIdToken,
+ ["subject_token"] = options.SubjectToken,
+ ["subject_token_type"] = options.SubjectTokenType,
["audience"] = options.Audience,
["resource"] = options.Resource,
["client_id"] = options.ClientId,
diff --git a/src/ModelContextProtocol.Core/Authentication/IdentityAssertionGrantIdTokenCallback.cs b/src/ModelContextProtocol.Core/Authentication/IdentityAssertionGrantIdTokenCallback.cs
index 2951d1e8b..73c884a81 100644
--- a/src/ModelContextProtocol.Core/Authentication/IdentityAssertionGrantIdTokenCallback.cs
+++ b/src/ModelContextProtocol.Core/Authentication/IdentityAssertionGrantIdTokenCallback.cs
@@ -1,16 +1,17 @@
namespace ModelContextProtocol.Authentication;
///
-/// Represents a method that returns an OIDC ID token for use in a Cross-Application Access authorization flow.
+/// Represents a method that returns a subject token for use in a Cross-Application Access authorization flow.
///
///
/// Context containing the MCP resource and authorization server URLs discovered during the OAuth flow.
///
/// The to monitor for cancellation requests.
///
-/// A task that represents the asynchronous operation. The task result contains the OIDC ID token string
-/// obtained from the enterprise Identity Provider (e.g., via SSO login). The provider will then use this
-/// ID token to perform the RFC 8693 token exchange to obtain a JWT Authorization Grant.
+/// A task that represents the asynchronous operation. The task result contains the subject token obtained
+/// from the enterprise Identity Provider (e.g., via SSO login). This is an OIDC ID token by default, or a
+/// SAML 2.0 assertion when configured through .
+/// The provider uses the token to perform the RFC 8693 token exchange and obtain a JWT Authorization Grant.
///
public delegate Task IdentityAssertionGrantIdTokenCallback(
IdentityAssertionGrantContext context,
diff --git a/src/ModelContextProtocol.Core/Authentication/IdentityAssertionGrantProvider.cs b/src/ModelContextProtocol.Core/Authentication/IdentityAssertionGrantProvider.cs
index 41d2ea36d..d98c94ad5 100644
--- a/src/ModelContextProtocol.Core/Authentication/IdentityAssertionGrantProvider.cs
+++ b/src/ModelContextProtocol.Core/Authentication/IdentityAssertionGrantProvider.cs
@@ -14,14 +14,14 @@ namespace ModelContextProtocol.Authentication;
///
///
/// -
-/// The is called to obtain an OIDC ID token.
+/// The is called to obtain a subject token.
/// It receives a with the discovered resource and authorization
/// server URLs.
///
/// -
/// The provider performs the RFC 8693 token exchange at the enterprise Identity Provider
/// (using the configured IdpTokenEndpoint or discovered from IdpUrl),
-/// exchanging the ID token for a JWT Authorization Grant (JAG).
+/// exchanging the subject token for a JWT Authorization Grant (JAG).
///
/// -
/// The JAG is then exchanged for an access token at the MCP Server's authorization server
@@ -91,6 +91,7 @@ public IdentityAssertionGrantProvider(
Throw.IfNullOrWhiteSpace(options.ClientId);
Throw.IfNullOrWhiteSpace(options.IdpClientId);
+ Throw.IfNullOrWhiteSpace(options.SubjectTokenType);
if (string.IsNullOrEmpty(options.IdpUrl) && string.IsNullOrEmpty(options.IdpTokenEndpoint))
{
@@ -173,22 +174,22 @@ private async Task AcquireAccessTokenAsync(
?? throw new IdentityAssertionGrantException(
$"MCP authorization server metadata at {authorizationServerUrl} missing token_endpoint.");
- // Step 2: Call the ID token callback to get the caller's OIDC ID token
+ // Step 2: Call the callback to get the caller's subject token
var context = new IdentityAssertionGrantContext
{
ResourceUrl = resourceUrl,
AuthorizationServerUrl = authorizationServerUrl,
};
- _logger.LogDebug("Requesting ID token via callback");
- var idToken = await _options.IdTokenCallback(context, cancellationToken).ConfigureAwait(false);
+ _logger.LogDebug("Requesting subject token via callback");
+ var subjectToken = await _options.IdTokenCallback(context, cancellationToken).ConfigureAwait(false);
- if (string.IsNullOrEmpty(idToken))
+ if (string.IsNullOrEmpty(subjectToken))
{
- throw new IdentityAssertionGrantException("ID token callback returned a null or empty token.");
+ throw new IdentityAssertionGrantException("Subject token callback returned a null or empty token.");
}
- // Step 3: RFC 8693 token exchange — ID token → JWT Authorization Grant (JAG) at the enterprise IdP
+ // Step 3: RFC 8693 token exchange — subject token → JWT Authorization Grant (JAG) at the enterprise IdP
_logger.LogDebug("Performing RFC 8693 token exchange at IdP");
var idpTokenEndpoint = await ResolveIdpTokenEndpointAsync(cancellationToken).ConfigureAwait(false);
@@ -198,7 +199,8 @@ private async Task AcquireAccessTokenAsync(
TokenEndpoint = idpTokenEndpoint,
Audience = authorizationServerUrl.ToString(),
Resource = resourceUrl.ToString(),
- IdToken = idToken,
+ SubjectToken = subjectToken,
+ SubjectTokenType = _options.SubjectTokenType,
ClientId = _options.IdpClientId,
ClientSecret = _options.IdpClientSecret,
Scope = _options.IdpScope,
diff --git a/src/ModelContextProtocol.Core/Authentication/IdentityAssertionGrantProviderOptions.cs b/src/ModelContextProtocol.Core/Authentication/IdentityAssertionGrantProviderOptions.cs
index ecbd0551e..8064131cc 100644
--- a/src/ModelContextProtocol.Core/Authentication/IdentityAssertionGrantProviderOptions.cs
+++ b/src/ModelContextProtocol.Core/Authentication/IdentityAssertionGrantProviderOptions.cs
@@ -61,16 +61,29 @@ public sealed class IdentityAssertionGrantProviderOptions
public string? IdpScope { get; set; }
///
- /// Gets or sets the callback that supplies the OIDC ID token for the Cross-Application Access flow.
+ /// Gets or sets the RFC 8693 token type identifier for the subject token returned by
+ /// .
+ ///
+ ///
+ /// The default is . Set this to
+ /// when the callback returns a SAML 2.0 assertion.
+ /// Custom RFC 8693 subject token type identifiers are also supported.
+ ///
+ public string SubjectTokenType { get; set; } = IdentityAssertionGrantSubjectTokenTypes.IdToken;
+
+ ///
+ /// Gets or sets the callback that supplies the subject token for the Cross-Application Access flow.
///
///
///
/// This callback is invoked after the MCP resource and authorization server URLs have been discovered.
/// It receives a with these URLs and should return the
- /// OIDC ID token string obtained from the enterprise Identity Provider (e.g., from an SSO login session).
+ /// subject token obtained from the enterprise Identity Provider (e.g., from an SSO login session).
+ /// By default this is an OIDC ID token. When is
+ /// , return a SAML 2.0 assertion instead.
///
///
- /// The provider will use the returned ID token to internally perform the RFC 8693 token exchange at the
+ /// The provider will use the returned subject token to internally perform the RFC 8693 token exchange at the
/// configured IdP, obtaining a JWT Authorization Grant, which is then exchanged for an access token at
/// the MCP authorization server via RFC 7523.
///
diff --git a/src/ModelContextProtocol.Core/Authentication/IdentityAssertionGrantSubjectTokenTypes.cs b/src/ModelContextProtocol.Core/Authentication/IdentityAssertionGrantSubjectTokenTypes.cs
new file mode 100644
index 000000000..ed8ba2c55
--- /dev/null
+++ b/src/ModelContextProtocol.Core/Authentication/IdentityAssertionGrantSubjectTokenTypes.cs
@@ -0,0 +1,17 @@
+namespace ModelContextProtocol.Authentication;
+
+///
+/// Provides subject token type identifiers supported by the Identity Assertion Authorization Grant flow.
+///
+public static class IdentityAssertionGrantSubjectTokenTypes
+{
+ ///
+ /// The RFC 8693 token type identifier for an OpenID Connect ID token.
+ ///
+ public const string IdToken = "urn:ietf:params:oauth:token-type:id_token";
+
+ ///
+ /// The RFC 8693 token type identifier for a SAML 2.0 assertion.
+ ///
+ public const string Saml2 = "urn:ietf:params:oauth:token-type:saml2";
+}
diff --git a/src/ModelContextProtocol.Core/Authentication/RequestJwtAuthGrantOptions.cs b/src/ModelContextProtocol.Core/Authentication/RequestJwtAuthGrantOptions.cs
index 7e83b198d..adbf82f5d 100644
--- a/src/ModelContextProtocol.Core/Authentication/RequestJwtAuthGrantOptions.cs
+++ b/src/ModelContextProtocol.Core/Authentication/RequestJwtAuthGrantOptions.cs
@@ -21,9 +21,14 @@ internal sealed class RequestJwtAuthGrantOptions
public required string Resource { get; set; }
///
- /// Gets or sets the OIDC ID token to exchange.
+ /// Gets or sets the subject token to exchange.
///
- public required string IdToken { get; set; }
+ public required string SubjectToken { get; set; }
+
+ ///
+ /// Gets or sets the RFC 8693 subject token type identifier.
+ ///
+ public required string SubjectTokenType { get; set; }
///
/// Gets or sets the client ID for authentication with the IDP.
diff --git a/tests/ModelContextProtocol.Tests/IdentityAssertionGrantTests.cs b/tests/ModelContextProtocol.Tests/IdentityAssertionGrantTests.cs
index 92e81c93c..5632e5bae 100644
--- a/tests/ModelContextProtocol.Tests/IdentityAssertionGrantTests.cs
+++ b/tests/ModelContextProtocol.Tests/IdentityAssertionGrantTests.cs
@@ -95,6 +95,79 @@ public async Task IdentityAssertionGrantProvider_FullFlow_ReturnsAccessToken()
Assert.Equal(3600, tokens.ExpiresIn);
}
+ [Theory]
+ [InlineData(null, IdentityAssertionGrantSubjectTokenTypes.IdToken)]
+ [InlineData(IdentityAssertionGrantSubjectTokenTypes.Saml2, IdentityAssertionGrantSubjectTokenTypes.Saml2)]
+ public async Task IdentityAssertionGrantProvider_SendsConfiguredSubjectTokenType(
+ string? configuredSubjectTokenType,
+ string expectedSubjectTokenType)
+ {
+ string? tokenExchangeBody = null;
+ _mockHandler.AsyncHandler = async request =>
+ {
+ var url = request.RequestUri!.ToString();
+
+ if (url.Contains(".well-known/openid-configuration"))
+ {
+ return JsonResponse(HttpStatusCode.OK, new JsonObject
+ {
+ ["issuer"] = "https://auth.mcp-server.example.com",
+ ["authorization_endpoint"] = "https://auth.mcp-server.example.com/authorize",
+ ["token_endpoint"] = "https://auth.mcp-server.example.com/token",
+ });
+ }
+
+ if (url.Contains("idp.example.com/token"))
+ {
+ tokenExchangeBody = await request.Content!.ReadAsStringAsync(TestContext.Current.CancellationToken);
+ return JsonResponse(HttpStatusCode.OK, new JsonObject
+ {
+ ["access_token"] = "mock-jag-assertion",
+ ["issued_token_type"] = "urn:ietf:params:oauth:token-type:id-jag",
+ ["token_type"] = "N_A",
+ });
+ }
+
+ if (url.Contains("auth.mcp-server.example.com/token"))
+ {
+ return JsonResponse(HttpStatusCode.OK, new JsonObject
+ {
+ ["access_token"] = "final-access-token",
+ ["token_type"] = "Bearer",
+ });
+ }
+
+ return new HttpResponseMessage(HttpStatusCode.NotFound);
+ };
+
+ var options = new IdentityAssertionGrantProviderOptions
+ {
+ ClientId = "mcp-client-id",
+ IdpTokenEndpoint = "https://idp.example.com/token",
+ IdpClientId = "idp-client-id",
+ IdTokenCallback = (_, _) => Task.FromResult("mock-subject-token"),
+ };
+
+ if (configuredSubjectTokenType is not null)
+ {
+ options.SubjectTokenType = configuredSubjectTokenType;
+ }
+
+ var provider = new IdentityAssertionGrantProvider(options, _httpClient);
+
+ await provider.GetAccessTokenAsync(
+ resourceUrl: new Uri("https://mcp-server.example.com"),
+ authorizationServerUrl: new Uri("https://auth.mcp-server.example.com"),
+ TestContext.Current.CancellationToken);
+
+ Assert.NotNull(tokenExchangeBody);
+ Assert.Contains("subject_token=mock-subject-token", tokenExchangeBody, StringComparison.Ordinal);
+ Assert.Contains(
+ $"subject_token_type={Uri.EscapeDataString(expectedSubjectTokenType)}",
+ tokenExchangeBody,
+ StringComparison.Ordinal);
+ }
+
[Fact]
public Task IdentityAssertionGrantProvider_DefaultsToPostRegardlessOfMetadataOrder() =>
AssertMcpTokenEndpointAuthenticationAsync(
@@ -330,6 +403,24 @@ public void IdentityAssertionGrantProvider_MissingIdTokenCallback_ThrowsArgument
_httpClient));
}
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void IdentityAssertionGrantProvider_MissingSubjectTokenType_ThrowsArgumentException(string? subjectTokenType)
+ {
+ Assert.Throws(() => new IdentityAssertionGrantProvider(
+ new IdentityAssertionGrantProviderOptions
+ {
+ ClientId = "client-id",
+ IdpTokenEndpoint = "https://idp.example.com/token",
+ IdpClientId = "idp-client-id",
+ IdTokenCallback = (_, _) => Task.FromResult("test"),
+ SubjectTokenType = subjectTokenType!,
+ },
+ _httpClient));
+ }
+
[Fact]
public void IdentityAssertionGrantProvider_MissingIdpConfig_ThrowsArgumentException()
{