Skip to content

Commit db67570

Browse files
committed
refactor: rename to CrossApplicationAccess*, shrink public surface, fold IdP config into options
1 parent 6390f2a commit db67570

18 files changed

Lines changed: 666 additions & 597 deletions

README.md

Lines changed: 9 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -31,59 +31,30 @@ For more information about MCP:
3131
- [Protocol Specification](https://modelcontextprotocol.io/specification/)
3232
- [GitHub Organization](https://github.com/modelcontextprotocol)
3333

34-
## Enterprise Auth / Enterprise Managed Authorization (SEP-990)
34+
## Enterprise Auth / Enterprise Managed Authorization
3535

36-
The SDK provides Enterprise Auth utilities for the Identity Assertion Authorization Grant flow (SEP-990),
36+
The SDK provides Cross-Application Access support for the [Identity Assertion Authorization Grant flow](https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/draft/enterprise-managed-authorization.mdx),
3737
enabling enterprise SSO scenarios where users authenticate once via their enterprise Identity Provider and
3838
access MCP servers without per-server authorization prompts.
3939

4040
The flow consists of two token operations:
4141
1. **RFC 8693 Token Exchange** at the IdP: ID Token → JWT Authorization Grant (JAG)
4242
2. **RFC 7523 JWT Bearer Grant** at the MCP Server: JAG → Access Token
4343

44-
### Using the Layer 2 utilities directly
44+
### Using CrossApplicationAccessProvider
4545

4646
```csharp
4747
using ModelContextProtocol.Authentication;
4848

49-
// Step 1: Exchange ID token for a JAG at the enterprise IdP
50-
var jag = await EnterpriseAuth.DiscoverAndRequestJwtAuthorizationGrantAsync(
51-
new DiscoverAndRequestJwtAuthGrantOptions
52-
{
53-
IdpUrl = "https://company.okta.com",
54-
Audience = "https://auth.mcp-server.example.com",
55-
Resource = "https://mcp-server.example.com",
56-
IdToken = myIdToken, // obtained via SSO/OIDC login
57-
ClientId = "idp-client-id",
58-
});
59-
60-
// Step 2: Exchange JAG for an access token at the MCP authorization server
61-
var tokens = await EnterpriseAuth.ExchangeJwtBearerGrantAsync(
62-
new ExchangeJwtBearerGrantOptions
63-
{
64-
TokenEndpoint = "https://auth.mcp-server.example.com/token",
65-
Assertion = jag,
66-
ClientId = "mcp-client-id",
67-
});
68-
```
69-
70-
### Using the EnterpriseAuthProvider (Layer 3)
71-
72-
```csharp
73-
var provider = new EnterpriseAuthProvider(new EnterpriseAuthProviderOptions
49+
var provider = new CrossApplicationAccessProvider(new CrossApplicationAccessProviderOptions
7450
{
7551
ClientId = "mcp-client-id",
76-
AssertionCallback = async (context, ct) =>
52+
IdpTokenEndpoint = "https://company.okta.com/oauth2/token",
53+
IdpClientId = "idp-client-id",
54+
IdTokenCallback = async (context, ct) =>
7755
{
78-
return await EnterpriseAuth.DiscoverAndRequestJwtAuthorizationGrantAsync(
79-
new DiscoverAndRequestJwtAuthGrantOptions
80-
{
81-
IdpUrl = "https://company.okta.com",
82-
Audience = context.AuthorizationServerUrl.ToString(),
83-
Resource = context.ResourceUrl.ToString(),
84-
IdToken = myIdToken,
85-
ClientId = "idp-client-id",
86-
}, ct);
56+
// Return the OIDC ID token from your SSO session
57+
return myIdToken;
8758
}
8859
});
8960

src/ModelContextProtocol.Core/Authentication/EnterpriseAuth.cs renamed to src/ModelContextProtocol.Core/Authentication/CrossApplicationAccess.cs

Lines changed: 19 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,13 @@
44
namespace ModelContextProtocol.Authentication;
55

66
/// <summary>
7-
/// Provides Enterprise Managed Authorization utilities for the Identity Assertion Authorization Grant flow.
7+
/// Provides internal utilities for the Cross-Application Access authorization flow.
88
/// </summary>
99
/// <remarks>
10-
/// <para>
1110
/// Implements the Enterprise Managed Authorization flow as specified at
1211
/// <see href="https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/draft/enterprise-managed-authorization.mdx"/>.
13-
/// </para>
14-
/// <para>
15-
/// This class provides standalone functions for:
16-
/// </para>
17-
/// <list type="bullet">
18-
/// <item><description>RFC 8693 Token Exchange: Exchange an ID token for a JWT Authorization Grant (JAG) at an Identity Provider</description></item>
19-
/// <item><description>RFC 7523 JWT Bearer Grant: Exchange a JAG for an access token at an MCP Server's authorization server</description></item>
20-
/// </list>
21-
/// <para>
22-
/// These utilities can be used directly or through the <see cref="EnterpriseAuthProvider"/> for full integration
23-
/// with the MCP client's OAuth infrastructure.
24-
/// </para>
2512
/// </remarks>
26-
public static class EnterpriseAuth
13+
internal static class CrossApplicationAccess
2714
{
2815
#region Constants
2916

@@ -48,7 +35,9 @@ public static class EnterpriseAuth
4835
public const string TokenTypeSaml2 = "urn:ietf:params:oauth:token-type:saml2";
4936

5037
/// <summary>
51-
/// Token type URN for Identity Assertion JWT Authorization Grants (SEP-990).
38+
/// Token type URN for Identity Assertion JWT Authorization Grants.
39+
/// As specified at
40+
/// <see href="https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/draft/enterprise-managed-authorization.mdx"/>.
5241
/// </summary>
5342
public const string TokenTypeIdJag = "urn:ietf:params:oauth:token-type:id-jag";
5443

@@ -60,18 +49,12 @@ public static class EnterpriseAuth
6049

6150
#endregion
6251

63-
#region Layer 2: Token Exchange (RFC 8693)
52+
#region Token Exchange (RFC 8693)
6453

6554
/// <summary>
6655
/// Requests a JWT Authorization Grant (JAG) from an Identity Provider via RFC 8693 Token Exchange.
6756
/// Returns the JAG string to be used as a JWT Bearer assertion (RFC 7523) against the MCP authorization server.
6857
/// </summary>
69-
/// <param name="options">Options for the token exchange request.</param>
70-
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
71-
/// <returns>The JAG JWT string.</returns>
72-
/// <exception cref="ArgumentNullException">Thrown when <paramref name="options"/> is null.</exception>
73-
/// <exception cref="ArgumentException">Thrown when required option values are missing.</exception>
74-
/// <exception cref="EnterpriseAuthException">Thrown when the token exchange request fails.</exception>
7558
public static async Task<string> RequestJwtAuthorizationGrantAsync(
7659
RequestJwtAuthGrantOptions options,
7760
CancellationToken cancellationToken = default)
@@ -129,7 +112,7 @@ public static async Task<string> RequestJwtAuthorizationGrantAsync(
129112
// Could not parse error response
130113
}
131114

132-
throw new EnterpriseAuthException(
115+
throw new CrossApplicationAccessException(
133116
$"Token exchange failed with status {(int)httpResponse.StatusCode}.",
134117
errorResponse?.Error,
135118
errorResponse?.ErrorDescription,
@@ -140,23 +123,23 @@ public static async Task<string> RequestJwtAuthorizationGrantAsync(
140123

141124
if (response is null)
142125
{
143-
throw new EnterpriseAuthException($"Failed to parse token exchange response: {responseBody}");
126+
throw new CrossApplicationAccessException($"Failed to parse token exchange response: {responseBody}");
144127
}
145128

146129
if (string.IsNullOrEmpty(response.AccessToken))
147130
{
148-
throw new EnterpriseAuthException("Token exchange response missing required field: access_token");
131+
throw new CrossApplicationAccessException("Token exchange response missing required field: access_token");
149132
}
150133

151134
if (!string.Equals(response.IssuedTokenType, TokenTypeIdJag, StringComparison.Ordinal))
152135
{
153-
throw new EnterpriseAuthException(
136+
throw new CrossApplicationAccessException(
154137
$"Token exchange response issued_token_type must be '{TokenTypeIdJag}', got '{response.IssuedTokenType}'.");
155138
}
156139

157140
if (!string.Equals(response.TokenType, TokenTypeNotApplicable, StringComparison.OrdinalIgnoreCase))
158141
{
159-
throw new EnterpriseAuthException(
142+
throw new CrossApplicationAccessException(
160143
$"Token exchange response token_type must be '{TokenTypeNotApplicable}' per RFC 8693 §2.2.1, got '{response.TokenType}'.");
161144
}
162145

@@ -167,11 +150,6 @@ public static async Task<string> RequestJwtAuthorizationGrantAsync(
167150
/// Discovers the IDP's token endpoint via OAuth/OIDC metadata, then requests a JWT Authorization Grant.
168151
/// Convenience wrapper over <see cref="RequestJwtAuthorizationGrantAsync"/>.
169152
/// </summary>
170-
/// <param name="options">Options for discovery and token exchange. Provides <c>IdpUrl</c> instead of <c>TokenEndpoint</c>.</param>
171-
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
172-
/// <returns>The JAG JWT string.</returns>
173-
/// <exception cref="ArgumentNullException">Thrown when <paramref name="options"/> is null.</exception>
174-
/// <exception cref="EnterpriseAuthException">Thrown when IDP discovery or token exchange fails.</exception>
175153
public static async Task<string> DiscoverAndRequestJwtAuthorizationGrantAsync(
176154
DiscoverAndRequestJwtAuthGrantOptions options,
177155
CancellationToken cancellationToken = default)
@@ -189,7 +167,7 @@ public static async Task<string> DiscoverAndRequestJwtAuthorizationGrantAsync(
189167
new Uri(options.IdpUrl!), httpClient, cancellationToken).ConfigureAwait(false);
190168

191169
tokenEndpoint = idpMetadata.TokenEndpoint?.ToString()
192-
?? throw new EnterpriseAuthException($"IDP metadata discovery for {options.IdpUrl} did not return a token_endpoint.");
170+
?? throw new CrossApplicationAccessException($"IDP metadata discovery for {options.IdpUrl} did not return a token_endpoint.");
193171
}
194172

195173
return await RequestJwtAuthorizationGrantAsync(new RequestJwtAuthGrantOptions
@@ -207,17 +185,12 @@ public static async Task<string> DiscoverAndRequestJwtAuthorizationGrantAsync(
207185

208186
#endregion
209187

210-
#region Layer 2: JWT Bearer Grant (RFC 7523)
188+
#region JWT Bearer Grant (RFC 7523)
211189

212190
/// <summary>
213191
/// Exchanges a JWT Authorization Grant (JAG) for an access token at an MCP Server's authorization server
214192
/// using the JWT Bearer grant (RFC 7523).
215193
/// </summary>
216-
/// <param name="options">Options for the JWT bearer grant request.</param>
217-
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
218-
/// <returns>A <see cref="TokenContainer"/> containing the access token.</returns>
219-
/// <exception cref="ArgumentNullException">Thrown when <paramref name="options"/> is null.</exception>
220-
/// <exception cref="EnterpriseAuthException">Thrown when the JWT bearer grant fails.</exception>
221194
public static async Task<TokenContainer> ExchangeJwtBearerGrantAsync(
222195
ExchangeJwtBearerGrantOptions options,
223196
CancellationToken cancellationToken = default)
@@ -269,7 +242,7 @@ public static async Task<TokenContainer> ExchangeJwtBearerGrantAsync(
269242
// Could not parse error response
270243
}
271244

272-
throw new EnterpriseAuthException(
245+
throw new CrossApplicationAccessException(
273246
$"JWT bearer grant failed with status {(int)httpResponse.StatusCode}.",
274247
errorResponse?.Error,
275248
errorResponse?.ErrorDescription,
@@ -280,22 +253,22 @@ public static async Task<TokenContainer> ExchangeJwtBearerGrantAsync(
280253

281254
if (response is null)
282255
{
283-
throw new EnterpriseAuthException($"Failed to parse JWT bearer grant response: {responseBody}");
256+
throw new CrossApplicationAccessException($"Failed to parse JWT bearer grant response: {responseBody}");
284257
}
285258

286259
if (string.IsNullOrEmpty(response.AccessToken))
287260
{
288-
throw new EnterpriseAuthException("JWT bearer grant response missing required field: access_token");
261+
throw new CrossApplicationAccessException("JWT bearer grant response missing required field: access_token");
289262
}
290263

291264
if (string.IsNullOrEmpty(response.TokenType))
292265
{
293-
throw new EnterpriseAuthException("JWT bearer grant response missing required field: token_type");
266+
throw new CrossApplicationAccessException("JWT bearer grant response missing required field: token_type");
294267
}
295268

296269
if (!string.Equals(response.TokenType, "bearer", StringComparison.OrdinalIgnoreCase))
297270
{
298-
throw new EnterpriseAuthException(
271+
throw new CrossApplicationAccessException(
299272
$"JWT bearer grant response token_type must be 'bearer' per RFC 7523, got '{response.TokenType}'.");
300273
}
301274

@@ -359,7 +332,7 @@ internal static async Task<AuthorizationServerMetadata> DiscoverAuthServerMetada
359332
}
360333
}
361334

362-
throw new EnterpriseAuthException($"Failed to discover authorization server metadata for: {issuerUrl}");
335+
throw new CrossApplicationAccessException($"Failed to discover authorization server metadata for: {issuerUrl}");
363336
}
364337

365338
#endregion
@@ -387,107 +360,3 @@ public static void IfNullOrEmpty(string? value, string message)
387360

388361
#endregion
389362
}
390-
391-
#region Response Types
392-
393-
/// <summary>
394-
/// Represents the response from an RFC 8693 Token Exchange for the JAG flow.
395-
/// Contains the JWT Authorization Grant in the <see cref="AccessToken"/> field.
396-
/// </summary>
397-
internal sealed class JagTokenExchangeResponse
398-
{
399-
/// <summary>
400-
/// Gets or sets the issued JAG. Despite the name "access_token" (required by RFC 8693),
401-
/// this contains a JAG JWT, not an OAuth access token.
402-
/// </summary>
403-
[System.Text.Json.Serialization.JsonPropertyName("access_token")]
404-
public string AccessToken { get; set; } = null!;
405-
406-
/// <summary>
407-
/// Gets or sets the type of the security token issued.
408-
/// This MUST be <see cref="EnterpriseAuth.TokenTypeIdJag"/>.
409-
/// </summary>
410-
[System.Text.Json.Serialization.JsonPropertyName("issued_token_type")]
411-
public string IssuedTokenType { get; set; } = null!;
412-
413-
/// <summary>
414-
/// Gets or sets the token type. This MUST be "N_A" per RFC 8693 §2.2.1.
415-
/// </summary>
416-
[System.Text.Json.Serialization.JsonPropertyName("token_type")]
417-
public string TokenType { get; set; } = null!;
418-
419-
/// <summary>
420-
/// Gets or sets the scope of the issued token, if different from the request.
421-
/// </summary>
422-
[System.Text.Json.Serialization.JsonPropertyName("scope")]
423-
public string? Scope { get; set; }
424-
425-
/// <summary>
426-
/// Gets or sets the lifetime in seconds of the issued token.
427-
/// </summary>
428-
[System.Text.Json.Serialization.JsonPropertyName("expires_in")]
429-
public int? ExpiresIn { get; set; }
430-
}
431-
432-
/// <summary>
433-
/// Represents the response from a JWT Bearer grant (RFC 7523) access token request.
434-
/// </summary>
435-
internal sealed class JwtBearerAccessTokenResponse
436-
{
437-
/// <summary>
438-
/// Gets or sets the OAuth access token.
439-
/// </summary>
440-
[System.Text.Json.Serialization.JsonPropertyName("access_token")]
441-
public string AccessToken { get; set; } = null!;
442-
443-
/// <summary>
444-
/// Gets or sets the token type. This should be "Bearer".
445-
/// </summary>
446-
[System.Text.Json.Serialization.JsonPropertyName("token_type")]
447-
public string TokenType { get; set; } = null!;
448-
449-
/// <summary>
450-
/// Gets or sets the lifetime in seconds of the access token.
451-
/// </summary>
452-
[System.Text.Json.Serialization.JsonPropertyName("expires_in")]
453-
public int? ExpiresIn { get; set; }
454-
455-
/// <summary>
456-
/// Gets or sets the refresh token.
457-
/// </summary>
458-
[System.Text.Json.Serialization.JsonPropertyName("refresh_token")]
459-
public string? RefreshToken { get; set; }
460-
461-
/// <summary>
462-
/// Gets or sets the scope of the access token.
463-
/// </summary>
464-
[System.Text.Json.Serialization.JsonPropertyName("scope")]
465-
public string? Scope { get; set; }
466-
}
467-
468-
/// <summary>
469-
/// Represents an OAuth error response per RFC 6749 Section 5.2.
470-
/// Used for both token exchange and JWT bearer grant error responses.
471-
/// </summary>
472-
internal sealed class OAuthErrorResponse
473-
{
474-
/// <summary>
475-
/// Gets or sets the error code.
476-
/// </summary>
477-
[System.Text.Json.Serialization.JsonPropertyName("error")]
478-
public string? Error { get; set; }
479-
480-
/// <summary>
481-
/// Gets or sets the human-readable error description.
482-
/// </summary>
483-
[System.Text.Json.Serialization.JsonPropertyName("error_description")]
484-
public string? ErrorDescription { get; set; }
485-
486-
/// <summary>
487-
/// Gets or sets the URI identifying a human-readable web page with error information.
488-
/// </summary>
489-
[System.Text.Json.Serialization.JsonPropertyName("error_uri")]
490-
public string? ErrorUri { get; set; }
491-
}
492-
493-
#endregion
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace ModelContextProtocol.Authentication;
2+
3+
/// <summary>
4+
/// Context provided to the <see cref="CrossApplicationAccessIdTokenCallback"/> for a Cross-Application Access
5+
/// authorization flow. Contains the URLs discovered during the OAuth flow needed for the token exchange step.
6+
/// </summary>
7+
public sealed class CrossApplicationAccessContext
8+
{
9+
/// <summary>
10+
/// Gets the MCP resource server URL (i.e., the <c>resource</c> parameter for token exchange).
11+
/// This is the URL of the MCP server being accessed.
12+
/// </summary>
13+
public required Uri ResourceUrl { get; init; }
14+
15+
/// <summary>
16+
/// Gets the MCP authorization server URL (i.e., the <c>audience</c> parameter for token exchange).
17+
/// This is the URL of the authorization server protecting the MCP resource.
18+
/// </summary>
19+
public required Uri AuthorizationServerUrl { get; init; }
20+
}

0 commit comments

Comments
 (0)