44namespace 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
0 commit comments