feat: Add Enterprise Managed Authorization (SEP-990) support (#1305)

This commit is contained in:
Aniket Anil Kumar 2026-06-03 20:08:08 +05:30 • committed by GitHub
commit 8202bcc932
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1695 additions and 2 deletions

View file

@ -0,0 +1,32 @@
namespace ModelContextProtocol.Authentication;
/// <summary>
/// Options for exchanging a JWT Authorization Grant for an access token via RFC 7523.
/// </summary>
internal sealed class ExchangeJwtBearerGrantOptions
{
/// <summary>
/// Gets or sets the MCP Server's authorization server token endpoint URL.
/// </summary>
public required string TokenEndpoint { get; set; }
/// <summary>
/// Gets or sets the JWT Authorization Grant (JAG) assertion obtained from token exchange.
/// </summary>
public required string Assertion { get; set; }
/// <summary>
/// Gets or sets the client ID for authentication with the MCP authorization server.
/// </summary>
public required string ClientId { get; set; }
/// <summary>
/// Gets or sets the client secret for authentication with the MCP authorization server. Optional.
/// </summary>
public string? ClientSecret { get; set; }
/// <summary>
/// Gets or sets the scopes to request (space-separated). Optional.
/// </summary>
public string? Scope { get; set; }
}

View file

@ -0,0 +1,296 @@
using System.Net.Http.Headers;
using System.Text.Json;
namespace ModelContextProtocol.Authentication;
/// <summary>
/// Provides internal utilities for the Cross-Application Access authorization flow.
/// </summary>
/// <remarks>
/// Implements the Enterprise Managed Authorization flow as specified at
/// <see href="https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/draft/enterprise-managed-authorization.mdx"/>.
/// </remarks>
internal static class IdentityAssertionGrant
{
#region Constants
/// <summary>Grant type URN for RFC 8693 token exchange.</summary>
public const string GrantTypeTokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange";
/// <summary>Grant type URN for RFC 7523 JWT Bearer authorization grant.</summary>
public const string GrantTypeJwtBearer = "urn:ietf:params:oauth:grant-type:jwt-bearer";
/// <summary>Token type URN for OpenID Connect ID Tokens (RFC 8693).</summary>
public const string TokenTypeIdToken = "urn:ietf:params:oauth:token-type:id_token";
/// <summary>Token type URN for SAML 2.0 assertions (RFC 8693).</summary>
public const string TokenTypeSaml2 = "urn:ietf:params:oauth:token-type:saml2";
/// <summary>
/// Token type URN for Identity Assertion JWT Authorization Grants.
/// As specified at
/// <see href="https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/draft/enterprise-managed-authorization.mdx"/>.
/// </summary>
public const string TokenTypeIdJag = "urn:ietf:params:oauth:token-type:id-jag";
/// <summary>
/// The expected value for <c>token_type</c> in a JAG token exchange response per RFC 8693 §2.2.1.
/// The issued token is not an OAuth access token, so its type is "N_A".
/// </summary>
public const string TokenTypeNotApplicable = "N_A";
#endregion
#region Token Exchange (RFC 8693)
/// <summary>
/// Requests a JWT Authorization Grant (JAG) from an Identity Provider via RFC 8693 Token Exchange.
/// Returns the JAG string to be used as a JWT Bearer assertion (RFC 7523) against the MCP authorization server.
/// </summary>
public static async Task<string> RequestJwtAuthorizationGrantAsync(
RequestJwtAuthGrantOptions options,
HttpClient httpClient,
CancellationToken cancellationToken = default)
{
Throw.IfNull(options);
Throw.IfNullOrWhiteSpace(options.TokenEndpoint);
Throw.IfNullOrWhiteSpace(options.Audience);
Throw.IfNullOrWhiteSpace(options.Resource);
Throw.IfNullOrWhiteSpace(options.IdToken);
Throw.IfNullOrWhiteSpace(options.ClientId);
var formData = new Dictionary<string, string>
{
["grant_type"] = GrantTypeTokenExchange,
["requested_token_type"] = TokenTypeIdJag,
["subject_token"] = options.IdToken,
["subject_token_type"] = TokenTypeIdToken,
["audience"] = options.Audience,
["resource"] = options.Resource,
["client_id"] = options.ClientId,
};
if (!string.IsNullOrEmpty(options.ClientSecret))
{
formData["client_secret"] = options.ClientSecret!;
}
if (!string.IsNullOrEmpty(options.Scope))
{
formData["scope"] = options.Scope!;
}
using var requestContent = new FormUrlEncodedContent(formData);
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, options.TokenEndpoint)
{
Content = requestContent
};
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
using var httpResponse = await httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
var responseBody = await httpResponse.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
if (!httpResponse.IsSuccessStatusCode)
{
OAuthErrorResponse? errorResponse = null;
try
{
errorResponse = JsonSerializer.Deserialize(responseBody, McpJsonUtilities.JsonContext.Default.OAuthErrorResponse);
}
catch
{
// Could not parse error response
}
throw new IdentityAssertionGrantException(
$"Token exchange failed with status {(int)httpResponse.StatusCode}.",
errorResponse?.Error,
errorResponse?.ErrorDescription,
errorResponse?.ErrorUri);
}
var response = JsonSerializer.Deserialize(responseBody, McpJsonUtilities.JsonContext.Default.JagTokenExchangeResponse);
if (response is null)
{
var ex = new IdentityAssertionGrantException("Failed to parse token exchange response.");
ex.Data["ResponseBody"] = responseBody;
throw ex;
}
if (string.IsNullOrEmpty(response.AccessToken))
{
throw new IdentityAssertionGrantException("Token exchange response missing required field: access_token");
}
if (!string.Equals(response.IssuedTokenType, TokenTypeIdJag, StringComparison.Ordinal))
{
throw new IdentityAssertionGrantException(
$"Token exchange response issued_token_type must be '{TokenTypeIdJag}', got '{response.IssuedTokenType}'.");
}
if (!string.Equals(response.TokenType, TokenTypeNotApplicable, StringComparison.Ordinal))
{
throw new IdentityAssertionGrantException(
$"Token exchange response token_type must be '{TokenTypeNotApplicable}' per RFC 8693 §2.2.1, got '{response.TokenType}'.");
}
return response.AccessToken;
}
#endregion
#region JWT Bearer Grant (RFC 7523)
/// <summary>
/// Exchanges a JWT Authorization Grant (JAG) for an access token at an MCP Server's authorization server
/// using the JWT Bearer grant (RFC 7523).
/// </summary>
public static async Task<TokenContainer> ExchangeJwtBearerGrantAsync(
ExchangeJwtBearerGrantOptions options,
HttpClient httpClient,
CancellationToken cancellationToken = default)
{
Throw.IfNull(options);
Throw.IfNullOrWhiteSpace(options.TokenEndpoint);
Throw.IfNullOrWhiteSpace(options.Assertion);
Throw.IfNullOrWhiteSpace(options.ClientId);
var formData = new Dictionary<string, string>
{
["grant_type"] = GrantTypeJwtBearer,
["assertion"] = options.Assertion,
["client_id"] = options.ClientId,
};
if (!string.IsNullOrEmpty(options.ClientSecret))
{
formData["client_secret"] = options.ClientSecret!;
}
if (!string.IsNullOrEmpty(options.Scope))
{
formData["scope"] = options.Scope!;
}
using var requestContent = new FormUrlEncodedContent(formData);
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, options.TokenEndpoint)
{
Content = requestContent
};
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
using var httpResponse = await httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
var responseBody = await httpResponse.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
if (!httpResponse.IsSuccessStatusCode)
{
OAuthErrorResponse? errorResponse = null;
try
{
errorResponse = JsonSerializer.Deserialize(responseBody, McpJsonUtilities.JsonContext.Default.OAuthErrorResponse);
}
catch
{
// Could not parse error response
}
throw new IdentityAssertionGrantException(
$"JWT bearer grant failed with status {(int)httpResponse.StatusCode}.",
errorResponse?.Error,
errorResponse?.ErrorDescription,
errorResponse?.ErrorUri);
}
var response = JsonSerializer.Deserialize(responseBody, McpJsonUtilities.JsonContext.Default.JwtBearerAccessTokenResponse);
if (response is null)
{
var ex = new IdentityAssertionGrantException("Failed to parse JWT bearer grant response.");
ex.Data["ResponseBody"] = responseBody;
throw ex;
}
if (string.IsNullOrEmpty(response.AccessToken))
{
throw new IdentityAssertionGrantException("JWT bearer grant response missing required field: access_token");
}
if (string.IsNullOrEmpty(response.TokenType))
{
throw new IdentityAssertionGrantException("JWT bearer grant response missing required field: token_type");
}
if (!string.Equals(response.TokenType, "bearer", StringComparison.OrdinalIgnoreCase))
{
throw new IdentityAssertionGrantException(
$"JWT bearer grant response token_type must be 'bearer' per RFC 7523, got '{response.TokenType}'.");
}
return new TokenContainer
{
AccessToken = response.AccessToken,
TokenType = response.TokenType,
RefreshToken = response.RefreshToken,
ExpiresIn = response.ExpiresIn,
Scope = response.Scope,
ObtainedAt = DateTimeOffset.UtcNow,
};
}
#endregion
#region Helper: Auth Server Metadata Discovery
private static readonly string[] s_wellKnownPaths = [".well-known/openid-configuration", ".well-known/oauth-authorization-server"];
/// <summary>
/// Discovers authorization server metadata from the well-known endpoints.
/// </summary>
internal static async Task<AuthorizationServerMetadata> DiscoverAuthServerMetadataAsync(
Uri issuerUrl,
HttpClient httpClient,
CancellationToken cancellationToken)
{
var baseUrl = issuerUrl.ToString();
if (!baseUrl.EndsWith("/", StringComparison.Ordinal))
{
issuerUrl = new Uri($"{baseUrl}/");
}
foreach (var path in s_wellKnownPaths)
{
try
{
var wellKnownEndpoint = new Uri(issuerUrl, path);
var response = await httpClient.GetAsync(wellKnownEndpoint, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
continue;
}
using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var metadata = await JsonSerializer.DeserializeAsync(
stream,
McpJsonUtilities.JsonContext.Default.AuthorizationServerMetadata,
cancellationToken).ConfigureAwait(false);
if (metadata is not null)
{
return metadata;
}
}
catch
{
continue;
}
}
throw new IdentityAssertionGrantException($"Failed to discover authorization server metadata for: {issuerUrl}");
}
#endregion
}

View file

@ -0,0 +1,20 @@
namespace ModelContextProtocol.Authentication;
/// <summary>
/// Context provided to the <see cref="IdentityAssertionGrantIdTokenCallback"/> for a Cross-Application Access
/// authorization flow. Contains the URLs discovered during the OAuth flow needed for the token exchange step.
/// </summary>
public sealed class IdentityAssertionGrantContext
{
/// <summary>
/// Gets the MCP resource server URL (i.e., the <c>resource</c> parameter for token exchange).
/// This is the URL of the MCP server being accessed.
/// </summary>
public required Uri ResourceUrl { get; init; }
/// <summary>
/// Gets the MCP authorization server URL (i.e., the <c>audience</c> parameter for token exchange).
/// This is the URL of the authorization server protecting the MCP resource.
/// </summary>
public required Uri AuthorizationServerUrl { get; init; }
}

View file

@ -0,0 +1,51 @@
namespace ModelContextProtocol.Authentication;
/// <summary>
/// Represents an error that occurred during a Cross-Application Access authorization operation
/// (token exchange per RFC 8693, and JWT bearer grant per RFC 7523).
/// </summary>
public sealed class IdentityAssertionGrantException : Exception
{
/// <summary>
/// Gets the OAuth error code, if available (e.g., "invalid_request", "invalid_grant").
/// </summary>
public string? ErrorCode { get; }
/// <summary>
/// Gets the human-readable error description from the OAuth error response.
/// </summary>
public string? ErrorDescription { get; }
/// <summary>
/// Gets the URI identifying a human-readable web page with error information.
/// </summary>
public string? ErrorUri { get; }
/// <summary>
/// Initializes a new instance of the <see cref="IdentityAssertionGrantException"/> class.
/// </summary>
/// <param name="message">The error message.</param>
/// <param name="errorCode">The OAuth error code.</param>
/// <param name="errorDescription">The human-readable error description.</param>
/// <param name="errorUri">The error URI.</param>
public IdentityAssertionGrantException(string message, string? errorCode = null, string? errorDescription = null, string? errorUri = null)
: base(FormatMessage(message, errorCode, errorDescription))
{
ErrorCode = errorCode;
ErrorDescription = errorDescription;
ErrorUri = errorUri;
}
private static string FormatMessage(string message, string? errorCode, string? errorDescription)
{
if (!string.IsNullOrEmpty(errorCode))
{
message = $"{message} Error: {errorCode}";
if (!string.IsNullOrEmpty(errorDescription))
{
message = $"{message} ({errorDescription})";
}
}
return message;
}
}

View file

@ -0,0 +1,17 @@
namespace ModelContextProtocol.Authentication;
/// <summary>
/// Represents a method that returns an OIDC ID token for use in a Cross-Application Access authorization flow.
/// </summary>
/// <param name="context">
/// Context containing the MCP resource and authorization server URLs discovered during the OAuth flow.
/// </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>
/// 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.
/// </returns>
public delegate Task<string> IdentityAssertionGrantIdTokenCallback(
IdentityAssertionGrantContext context,
CancellationToken cancellationToken);

View file

@ -0,0 +1,207 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace ModelContextProtocol.Authentication;
/// <summary>
/// Provides Cross-Application Access authorization as a standalone, non-interactive provider
/// that can be used alongside the MCP client's OAuth infrastructure.
/// </summary>
/// <remarks>
/// <para>
/// This provider implements the full Identity Assertion Authorization Grant flow as specified at
/// <see href="https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/draft/enterprise-managed-authorization.mdx"/>:
/// </para>
/// <list type="number">
/// <item><description>
/// The <see cref="IdentityAssertionGrantProviderOptions.IdTokenCallback"/> is called to obtain an OIDC ID token.
/// It receives a <see cref="IdentityAssertionGrantContext"/> with the discovered resource and authorization
/// server URLs.
/// </description></item>
/// <item><description>
/// The provider performs the RFC 8693 token exchange at the enterprise Identity Provider
/// (using the configured <c>IdpTokenEndpoint</c> or discovered from <c>IdpUrl</c>),
/// exchanging the ID token for a JWT Authorization Grant (JAG).
/// </description></item>
/// <item><description>
/// The JAG is then exchanged for an access token at the MCP Server's authorization server
/// via the RFC 7523 JWT Bearer grant.
/// </description></item>
/// </list>
/// </remarks>
/// <example>
/// <code>
/// var provider = new IdentityAssertionGrantProvider(
/// new IdentityAssertionGrantProviderOptions
/// {
/// ClientId = "mcp-client-id",
/// IdpTokenEndpoint = "https://company.okta.com/oauth2/token",
/// IdpClientId = "idp-client-id",
/// IdTokenCallback = (context, ct) =>
/// mySsoClient.GetIdTokenAsync(ct)
/// },
/// httpClient: myHttpClient);
///
/// var tokens = await provider.GetAccessTokenAsync(
/// resourceUrl: new Uri("https://mcp-server.example.com"),
/// authorizationServerUrl: new Uri("https://auth.example.com"),
/// cancellationToken: ct);
/// </code>
/// </example>
public sealed class IdentityAssertionGrantProvider
{
private readonly IdentityAssertionGrantProviderOptions _options;
private readonly HttpClient _httpClient;
private readonly ILogger _logger;
private TokenContainer? _cachedTokens;
/// <summary>
/// Initializes a new instance of the <see cref="IdentityAssertionGrantProvider"/> class.
/// </summary>
/// <param name="options">Configuration for the Cross-Application Access provider.</param>
/// <param name="httpClient">
/// The HTTP client to use for token exchange requests. The caller is responsible for the lifetime of this instance.
/// </param>
/// <param name="loggerFactory">Optional logger factory.</param>
/// <exception cref="ArgumentNullException"><paramref name="options"/> or <paramref name="httpClient"/> is null.</exception>
/// <exception cref="ArgumentException">Required option values are missing.</exception>
public IdentityAssertionGrantProvider(
IdentityAssertionGrantProviderOptions options,
HttpClient httpClient,
ILoggerFactory? loggerFactory = null)
{
Throw.IfNull(options);
Throw.IfNull(httpClient);
Throw.IfNullOrWhiteSpace(options.ClientId);
Throw.IfNullOrWhiteSpace(options.IdpClientId);
if (string.IsNullOrEmpty(options.IdpUrl) && string.IsNullOrEmpty(options.IdpTokenEndpoint))
{
throw new ArgumentException("Either IdpUrl or IdpTokenEndpoint is required.", $"{nameof(options)}.{nameof(options.IdpUrl)}");
}
if (options.IdTokenCallback is null)
{
throw new ArgumentNullException($"{nameof(options)}.{nameof(options.IdTokenCallback)}");
}
_options = options;
_httpClient = httpClient;
_logger = (ILogger?)loggerFactory?.CreateLogger<IdentityAssertionGrantProvider>() ?? NullLogger.Instance;
}
/// <summary>
/// Performs the full Cross-Application Access flow to obtain an access token for the given MCP resource.
/// </summary>
/// <param name="resourceUrl">The MCP resource server URL.</param>
/// <param name="authorizationServerUrl">The MCP authorization server URL.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>A <see cref="TokenContainer"/> containing the access token.</returns>
/// <exception cref="IdentityAssertionGrantException">Thrown when any step of the flow fails.</exception>
public async Task<TokenContainer> GetAccessTokenAsync(
Uri resourceUrl,
Uri authorizationServerUrl,
CancellationToken cancellationToken = default)
{
// Return cached token if still valid
if (_cachedTokens is not null && !_cachedTokens.IsExpired)
{
return _cachedTokens;
}
_logger.LogDebug("Starting Cross-Application Access flow for resource {ResourceUrl}", resourceUrl);
// Step 1: Discover MCP authorization server metadata to find the token endpoint
var mcpAuthMetadata = await IdentityAssertionGrant.DiscoverAuthServerMetadataAsync(
authorizationServerUrl, _httpClient, cancellationToken).ConfigureAwait(false);
var mcpTokenEndpoint = mcpAuthMetadata.TokenEndpoint?.ToString()
?? 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
var context = new IdentityAssertionGrantContext
{
ResourceUrl = resourceUrl,
AuthorizationServerUrl = authorizationServerUrl,
};
_logger.LogDebug("Requesting ID token via callback");
var idToken = await _options.IdTokenCallback(context, cancellationToken).ConfigureAwait(false);
if (string.IsNullOrEmpty(idToken))
{
throw new IdentityAssertionGrantException("ID token callback returned a null or empty token.");
}
// Step 3: RFC 8693 token exchange — ID 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);
var jag = await IdentityAssertionGrant.RequestJwtAuthorizationGrantAsync(
new RequestJwtAuthGrantOptions
{
TokenEndpoint = idpTokenEndpoint,
Audience = authorizationServerUrl.ToString(),
Resource = resourceUrl.ToString(),
IdToken = idToken,
ClientId = _options.IdpClientId,
ClientSecret = _options.IdpClientSecret,
Scope = _options.IdpScope,
}, _httpClient, cancellationToken).ConfigureAwait(false);
// Step 4: RFC 7523 JWT bearer grant — JAG → access token at the MCP authorization server
_logger.LogDebug("Exchanging JAG for access token at {McpTokenEndpoint}", mcpTokenEndpoint);
var tokens = await IdentityAssertionGrant.ExchangeJwtBearerGrantAsync(
new ExchangeJwtBearerGrantOptions
{
TokenEndpoint = mcpTokenEndpoint,
Assertion = jag,
ClientId = _options.ClientId,
ClientSecret = _options.ClientSecret,
Scope = _options.Scope,
}, _httpClient, cancellationToken).ConfigureAwait(false);
_cachedTokens = tokens;
_logger.LogDebug("Cross-Application Access flow completed successfully");
return tokens;
}
/// <summary>
/// Clears any cached tokens, forcing a fresh token exchange on the next call to <see cref="GetAccessTokenAsync"/>.
/// </summary>
public void InvalidateCache()
{
_cachedTokens = null;
}
private string? _resolvedIdpTokenEndpoint;
private async Task<string> ResolveIdpTokenEndpointAsync(CancellationToken cancellationToken)
{
if (_resolvedIdpTokenEndpoint is not null)
{
return _resolvedIdpTokenEndpoint;
}
if (!string.IsNullOrEmpty(_options.IdpTokenEndpoint))
{
_resolvedIdpTokenEndpoint = _options.IdpTokenEndpoint!;
return _resolvedIdpTokenEndpoint;
}
// Discover from IdpUrl
var idpMetadata = await IdentityAssertionGrant.DiscoverAuthServerMetadataAsync(
new Uri(_options.IdpUrl!), _httpClient, cancellationToken).ConfigureAwait(false);
var resolved = idpMetadata.TokenEndpoint?.ToString()
?? throw new IdentityAssertionGrantException(
$"IdP metadata discovery for {_options.IdpUrl} did not return a token_endpoint.");
_resolvedIdpTokenEndpoint = resolved;
return resolved;
}
}

View file

@ -0,0 +1,68 @@
namespace ModelContextProtocol.Authentication;
/// <summary>
/// Configuration options for the <see cref="IdentityAssertionGrantProvider"/>.
/// </summary>
public sealed class IdentityAssertionGrantProviderOptions
{
/// <summary>
/// Gets or sets the MCP client ID used for the JWT Bearer grant (RFC 7523) at the MCP authorization server.
/// </summary>
public required string ClientId { get; set; }
/// <summary>
/// Gets or sets the MCP client secret used for the JWT Bearer grant at the MCP authorization server.
/// Optional; only required if the MCP authorization server requires client authentication.
/// </summary>
public string? ClientSecret { get; set; }
/// <summary>
/// Gets or sets the scopes to request from the MCP authorization server (space-separated). Optional.
/// </summary>
public string? Scope { get; set; }
/// <summary>
/// Gets or sets the enterprise Identity Provider base URL for OAuth/OIDC metadata discovery.
/// Used to discover <c>IdpTokenEndpoint</c> automatically when <see cref="IdpTokenEndpoint"/> is not set.
/// Either this or <see cref="IdpTokenEndpoint"/> must be provided.
/// </summary>
public string? IdpUrl { get; set; }
/// <summary>
/// Gets or sets the enterprise Identity Provider token endpoint URL for RFC 8693 token exchange.
/// When provided, skips IdP metadata discovery. Either this or <see cref="IdpUrl"/> must be provided.
/// </summary>
public string? IdpTokenEndpoint { get; set; }
/// <summary>
/// Gets or sets the client ID for authentication with the enterprise Identity Provider (RFC 8693 token exchange).
/// </summary>
public required string IdpClientId { get; set; }
/// <summary>
/// Gets or sets the client secret for authentication with the enterprise Identity Provider. Optional.
/// </summary>
public string? IdpClientSecret { get; set; }
/// <summary>
/// Gets or sets the scopes to request from the enterprise Identity Provider (space-separated). Optional.
/// </summary>
public string? IdpScope { get; set; }
/// <summary>
/// Gets or sets the callback that supplies the OIDC ID token for the Cross-Application Access flow.
/// </summary>
/// <remarks>
/// <para>
/// This callback is invoked after the MCP resource and authorization server URLs have been discovered.
/// It receives a <see cref="IdentityAssertionGrantContext"/> with these URLs and should return the
/// OIDC ID token string obtained from the enterprise Identity Provider (e.g., from an SSO login session).
/// </para>
/// <para>
/// The provider will use the returned ID 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.
/// </para>
/// </remarks>
public required IdentityAssertionGrantIdTokenCallback IdTokenCallback { get; set; }
}

View file

@ -0,0 +1,40 @@
namespace ModelContextProtocol.Authentication;
/// <summary>
/// Represents the response from an RFC 8693 Token Exchange for the JAG flow.
/// Contains the JWT Authorization Grant in the <see cref="AccessToken"/> field.
/// </summary>
internal sealed class JagTokenExchangeResponse
{
/// <summary>
/// Gets or sets the issued JAG. Despite the name "access_token" (required by RFC 8693),
/// this contains a JAG JWT, not an OAuth access token.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("access_token")]
public string AccessToken { get; set; } = null!;
/// <summary>
/// Gets or sets the type of the security token issued.
/// This MUST be <see cref="IdentityAssertionGrant.TokenTypeIdJag"/>.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("issued_token_type")]
public string IssuedTokenType { get; set; } = null!;
/// <summary>
/// Gets or sets the token type. This MUST be "N_A" per RFC 8693 §2.2.1.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("token_type")]
public string TokenType { get; set; } = null!;
/// <summary>
/// Gets or sets the scope of the issued token, if different from the request.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("scope")]
public string? Scope { get; set; }
/// <summary>
/// Gets or sets the lifetime in seconds of the issued token.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("expires_in")]
public int? ExpiresIn { get; set; }
}

View file

@ -0,0 +1,37 @@
namespace ModelContextProtocol.Authentication;
/// <summary>
/// Represents the response from a JWT Bearer grant (RFC 7523) access token request.
/// </summary>
internal sealed class JwtBearerAccessTokenResponse
{
/// <summary>
/// Gets or sets the OAuth access token.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("access_token")]
public string AccessToken { get; set; } = null!;
/// <summary>
/// Gets or sets the token type. This should be "Bearer".
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("token_type")]
public string TokenType { get; set; } = null!;
/// <summary>
/// Gets or sets the lifetime in seconds of the access token.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("expires_in")]
public int? ExpiresIn { get; set; }
/// <summary>
/// Gets or sets the refresh token.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("refresh_token")]
public string? RefreshToken { get; set; }
/// <summary>
/// Gets or sets the scope of the access token.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("scope")]
public string? Scope { get; set; }
}

View file

@ -0,0 +1,26 @@
namespace ModelContextProtocol.Authentication;
/// <summary>
/// Represents an OAuth error response per RFC 6749 Section 5.2.
/// Used for both token exchange and JWT bearer grant error responses.
/// </summary>
internal sealed class OAuthErrorResponse
{
/// <summary>
/// Gets or sets the error code.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("error")]
public string? Error { get; set; }
/// <summary>
/// Gets or sets the human-readable error description.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("error_description")]
public string? ErrorDescription { get; set; }
/// <summary>
/// Gets or sets the URI identifying a human-readable web page with error information.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("error_uri")]
public string? ErrorUri { get; set; }
}

View file

@ -0,0 +1,42 @@
namespace ModelContextProtocol.Authentication;
/// <summary>
/// Options for requesting a JWT Authorization Grant from an Identity Provider via RFC 8693 Token Exchange.
/// </summary>
internal sealed class RequestJwtAuthGrantOptions
{
/// <summary>
/// Gets or sets the IDP's token endpoint URL.
/// </summary>
public required string TokenEndpoint { get; set; }
/// <summary>
/// Gets or sets the MCP authorization server URL (used as the <c>audience</c> parameter).
/// </summary>
public required string Audience { get; set; }
/// <summary>
/// Gets or sets the MCP resource server URL (used as the <c>resource</c> parameter).
/// </summary>
public required string Resource { get; set; }
/// <summary>
/// Gets or sets the OIDC ID token to exchange.
/// </summary>
public required string IdToken { get; set; }
/// <summary>
/// Gets or sets the client ID for authentication with the IDP.
/// </summary>
public required string ClientId { get; set; }
/// <summary>
/// Gets or sets the client secret for authentication with the IDP. Optional.
/// </summary>
public string? ClientSecret { get; set; }
/// <summary>
/// Gets or sets the scopes to request (space-separated). Optional.
/// </summary>
public string? Scope { get; set; }
}

View file

@ -187,6 +187,12 @@ public static partial class McpJsonUtilities
[JsonSerializable(typeof(DynamicClientRegistrationRequest))]
[JsonSerializable(typeof(DynamicClientRegistrationResponse))]
// For Enterprise Managed Authorization flow as specified at
// https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/draft/enterprise-managed-authorization.mdx
[JsonSerializable(typeof(JagTokenExchangeResponse))]
[JsonSerializable(typeof(JwtBearerAccessTokenResponse))]
[JsonSerializable(typeof(OAuthErrorResponse))]
// Primitive types for use in consuming AIFunctions
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(byte))]