Skip to content

Enterprise managed authorization#770

Open
radar07 wants to merge 19 commits intomodelcontextprotocol:mainfrom
radar07:enterprise-managed-authorization
Open

Enterprise managed authorization#770
radar07 wants to merge 19 commits intomodelcontextprotocol:mainfrom
radar07:enterprise-managed-authorization

Conversation

@radar07
Copy link

@radar07 radar07 commented Jan 30, 2026

auth, oauthex: implement Enterprise Managed Authorization (SEP-990)
This PR implements Enterprise Managed Authorization (SEP-990) for the Go MCP SDK, enabling MCP Clients and Servers to leverage enterprise Identity Providers for seamless authorization without requiring users to authenticate separately to each MCP Server.
Overview
Enterprise Managed Authorization follows the Identity Assertion Authorization Grant specification (draft-ietf-oauth-identity-assertion-authz-grant), implementing a three-step flow:

  1. Single Sign-On (SSO): User authenticates to the MCP Client via enterprise IdP (Okta, Auth0, Azure AD, etc.)
  2. Token Exchange (RFC 8693): Client exchanges ID Token for Identity Assertion JWT Authorization Grant (ID-JAG) at the IdP
  3. JWT Bearer Grant (RFC 7523): Client exchanges ID-JAG for Access Token at the MCP Server
    This enables:
  • For end users: Single sign-on across MCP Clients and Servers—no manual connection/authorization per server
  • For enterprise admins: Centralized visibility and control over which MCP Servers can be used within the organization
  • For MCP clients: Automatic token acquisition without user interaction for each server

Closes: #628

@maciej-kisiel
Copy link
Contributor

Hi @radar07, thanks for submitting this PR. Could you link the issue that it is addressing?

Also, as a heads-up: it will likely take some time to review your proposal. Both because it's quite large, but more importantly I'm also working on a proposal how to structure the client-side OAuth implementation and this change will need to be aligned with it.

@radar07
Copy link
Author

radar07 commented Jan 31, 2026

Thanks @maciej-kisiel. I updated the description with the SEP that this PR solves.

@radar07
Copy link
Author

radar07 commented Feb 3, 2026

@maciej-kisiel I'd be happy to contribute to OAuth implementation. Let me know if I can help with anything. Just want to know if I should add conformance tests to this because I can see that there are PRs related to conformance tests.

Copy link
Contributor

@maciej-kisiel maciej-kisiel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took a brief look at your PR and I believe we could utilize the oauth2 library more aggressively.

Please also take a look at my PR/proposal for client-side OAuth at #785. I think this PR could be expressed with the proposed abstractions, but your OAuth expertise would make the feedback valuable.

// "mcp-client-secret",
// nil,
// )
func ExchangeJWTBearer(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like https://pkg.go.dev/golang.org/x/oauth2#Config.Exchange could be used to replicate the behavior of the function. Is there any reason not to use it?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oauth2#Config.Exchange always expect a code which we don't have in our Token Ecxhange. IdPs can reject the request if we don't pass code or pass it as an empty string.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IdPs can reject the request if we don't pass code or pass it as an empty string.

I don't think that should happen. Based on RFC 6749, Section 3.2:

Parameters sent without a value MUST be treated as if they were
omitted from the request. The authorization server MUST ignore
unrecognized request parameters.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a sample test and you're right. Changed to use Config.Exchange

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! At this point, I think I would remove this file (and the relevant test file) and just implement it as a private helper function in auth/extauth/enterprise_handler.go. It should allow you to skip some validations (relying on validation already present there).

@maciej-kisiel
Copy link
Contributor

Hi @radar07! I have recently merged #785 which I wrote about a few comments above. I think it's a good time to see if you could fit your desired auth flow into the OAuthHandler interface. I believe the Enterprise Managed authorization, as defined by the ext-auth repository should just expose a new implementation of this interface, just as AuthorizationCodeHandler implements the main MCP flow.

I would also ask you to implement this handler and put all the related helper files in a new sub-package of the auth package called extauth to make it clear that the functionality comes from an extension and is subject to different freshness guarantees.

If there is any logic from authorization_code.go that you would like to reuse, feel free to do so. We can move it to a more generic file in the future.

Thanks for working on this project!

@radar07 radar07 force-pushed the enterprise-managed-authorization branch from b3cba20 to f94e462 Compare March 4, 2026 10:48
@radar07
Copy link
Author

radar07 commented Mar 9, 2026

@maciej-kisiel Thanks for the feedback. I made the changes as you suggested. Could you please take a look again?

Copy link
Contributor

@maciej-kisiel maciej-kisiel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't manage to look at all files, I'll continue tomorrow.

In general I still believe we can use the oauth2 package more to reduce the amount of code to maintain. This should be an implementation detail, so we can always go back to custom logic if oauth2 stops being sufficient.

I think I would also simplify the API for OIDC login, to be similar to AuthorizationCodeHandler. Consistency would be an additional plus.

// }
//
// // Use accessToken to call MCP Server APIs
func EnterpriseAuthFlow(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need this if we have the EnterpriseHandler?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This uses OAuthHandler and allows users to use the Enterprise Auth Flow without the MCP imports. However, if we prefer keeping only the handler, we can remove this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would start with just the handler. It's always possible to add more helpers if there is a need, the other way round (removing from the API) is not possible.


// GetAuthServerMetadatForIssuer fetches authorization server metadata for the given issuer URL.
// It tries standard well-known endpoints (OAuth 2.0 and OIDC) and returns the first successful result.
func GetAuthServerMetadatForIssuer(ctx context.Context, IssuerURL string, httpClient *httpClient) (*oauthex.AuthServerMeta, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to align it with authorization_code.go's getAuthServerMetadata. It can be generalized to not be a method on AuthorizationCodeHandler, but receive the URL as parameter instead of PRM and the client as a parameter instead of using the receiver. It should probably be moved to a different file, maybe something like shared.go?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the code to shared.go (which can be renamed if needed). Also, the getAuthServerMetadata method has a different fallback behavior. I want to know your thoughts.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the fallback handling at the end is problematic, you can move it directly to the Authorize function, just after the getAuthServerMetadata call and modify the latter to use the new function to avoid code repetition.

ExpiresAt int64
}

// InitiateOIDCLogin initiates an OIDC Authorization Code flow with PKCE.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you see a situation occurring of InitiateOICDLogin not being used with CompleteOIDCLogin? If not, maybe we could be more opinionated and implement the whole flow as a single function that just takes the "direct user to authorization URL and return the authorization result" as a functional argument, similar to how AuthorizationCodeHandlerConfig takes AuthorizationCodeFetcher?

In general this flow looks very similar to the normal authorization code flow, so maybe we could even reuse some of the data structures? Do you see a risk of the OAuth flow and the OIDC flow diverging in the future?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that you have introduced PerformOIDCLogin that does it. I would go one step further and not export InitiateOIDCLogin and CompleteOIDCLogin. It would have the following benefits:

  1. You would need to verify the OIDCLoginConfig only once.
  2. You could reuse the oauth2.Config between the functions.
  3. You would need only one call for Authorization Metadata of the IdP.
  4. You would ensure state equality is verified in the flow.
  5. You could reuse auth.AuthorizationResult and auth.AuthorizationArgs instead of introducing new structs.
  6. Consistency with auth/authorization_code.go.

To achieve those in a readable way, you will likely need to slice the logic into functions slightly differently than currently. I would encourage you to follow auth/authorization_code.go example for added consistency.

Copy link
Contributor

@maciej-kisiel maciej-kisiel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few more comments. I looked at all Go files, excluding the tests. I will take a look at them when we align on the main logic.

// "mcp-client-secret",
// nil,
// )
func ExchangeJWTBearer(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IdPs can reject the request if we don't pass code or pass it as an empty string.

I don't think that should happen. Based on RFC 6749, Section 3.2:

Parameters sent without a value MUST be treated as if they were
omitted from the request. The authorization server MUST ignore
unrecognized request parameters.

@radar07 radar07 force-pushed the enterprise-managed-authorization branch 4 times, most recently from 24931e7 to a4fd173 Compare March 15, 2026 17:36
@radar07 radar07 force-pushed the enterprise-managed-authorization branch from a4fd173 to d5b2a5d Compare March 15, 2026 17:46
@radar07
Copy link
Author

radar07 commented Mar 16, 2026

@maciej-kisiel Thank you so much for taking the time to review this PR and your valuable feedback. I have made the changes as you suggested and need your suggestions on the changes.

@maciej-kisiel
Copy link
Contributor

Hi @radar07, thanks for applying the suggestions! I think we're pretty close now with the main logic. I have added a few more comments.

One bigger question that I would have is: would it make sense for the token returning methods (OIDC login, Token Exchange) to use oauth2.Token instead of custom types? Just like ExchangeJWTBearer currently does. I understand that they have some meaningful fields that need to be passed via oauth2.Token.Extra, but maybe it's fine? I would be curious to hear your thoughts.

One more idea: instead of adding extensive examples to doc comments of particular functions, what do you think about creating a single, end-to-end example in the examples/ directory?

@radar07
Copy link
Author

radar07 commented Mar 17, 2026

Hi @radar07, thanks for applying the suggestions! I think we're pretty close now with the main logic. I have added a few more comments.

One bigger question that I would have is: would it make sense for the token returning methods (OIDC login, Token Exchange) to use oauth2.Token instead of custom types? Just like ExchangeJWTBearer currently does. I understand that they have some meaningful fields that need to be passed via oauth2.Token.Extra, but maybe it's fine? I would be curious to hear your thoughts.

One more idea: instead of adding extensive examples to doc comments of particular functions, what do you think about creating a single, end-to-end example in the examples/ directory?

@maciej-kisiel Thanks again for your time in reviewing this PR!

Moving the examples from doc comments to their own directory makes more sense. I'll make the changes.

For using oauth2.Token, I agree with your opinion on making it consistent and use oauth2 for all tokens in the methods. However, issued_token_type is a required field and must be validated as per the spec. So, making this a custom explicit field makes this validation obvious to the callers. The tradeoff will be these fields will be hidden in the Token.Extra and users have to go through the code or comments which is not a huge deal (I think). I'd love to hear your opinion.

Copy link
Contributor

@maciej-kisiel maciej-kisiel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, I forgot to publish the comments yesterday.

ExpiresAt int64
}

// InitiateOIDCLogin initiates an OIDC Authorization Code flow with PKCE.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that you have introduced PerformOIDCLogin that does it. I would go one step further and not export InitiateOIDCLogin and CompleteOIDCLogin. It would have the following benefits:

  1. You would need to verify the OIDCLoginConfig only once.
  2. You could reuse the oauth2.Config between the functions.
  3. You would need only one call for Authorization Metadata of the IdP.
  4. You would ensure state equality is verified in the flow.
  5. You could reuse auth.AuthorizationResult and auth.AuthorizationArgs instead of introducing new structs.
  6. Consistency with auth/authorization_code.go.

To achieve those in a readable way, you will likely need to slice the logic into functions slightly differently than currently. I would encourage you to follow auth/authorization_code.go example for added consistency.

// IDTokenFetcher is called to obtain an ID Token from the enterprise IdP.
// This is typically done via OIDC login flow where the user authenticates
// with their enterprise identity provider.
type IDTokenFetcher func(ctx context.Context) (string, error)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just curious about future extensibility, I'm not a domain expert. As an example, in

we have introduced a struct to contain the URL, even though it's the only required data currently. However, if we have set it to string, we would be in a difficult place if there appeared a new use case to carry additional data, since a string is not extensible. This is something to consider here as well.

// }
//
// // Use accessToken to call MCP Server APIs
func EnterpriseAuthFlow(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would start with just the handler. It's always possible to add more helpers if there is a need, the other way round (removing from the API) is not possible.

auth/shared.go Outdated

// GetAuthServerMetadataForIssuer fetches authorization server metadata for the given issuer URL.
// It tries standard well-known endpoints (OAuth 2.0 and OIDC) and returns the first successful result.
func GetAuthServerMetadataForIssuer(ctx context.Context, issuerURL string, httpClient *http.Client) (*oauthex.AuthServerMeta, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I would keep it at GetAuthServerMetadata, the "for issuer" part should be pretty evident from the parameter list and the doc comment.

// "mcp-client-secret",
// nil,
// )
func ExchangeJWTBearer(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! At this point, I think I would remove this file (and the relevant test file) and just implement it as a private helper function in auth/extauth/enterprise_handler.go. It should allow you to skip some validations (relying on validation already present there).

// CheckURLScheme validates a URL scheme for security.
// This is exported for use by the auth package.
func CheckURLScheme(u string) error {
return checkURLScheme(u)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's needed from another package, let's just export it instead of adding a wrapper.

ClientSecret: clientSecret,
Endpoint: oauth2.Endpoint{
TokenURL: tokenEndpoint,
AuthStyle: oauth2.AuthStyleInParams, // Use POST body auth per SEP-990
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you point me where this is defined? I failed to find it.

Comment on lines +122 to +123
clientID string,
clientSecret string,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any other client authentication methods that could be used for token exchange? I think we should be more flexible here and define a struct that could be extended with JWT-based methods in the future.


// GetAuthServerMetadatForIssuer fetches authorization server metadata for the given issuer URL.
// It tries standard well-known endpoints (OAuth 2.0 and OIDC) and returns the first successful result.
func GetAuthServerMetadatForIssuer(ctx context.Context, IssuerURL string, httpClient *httpClient) (*oauthex.AuthServerMeta, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the fallback handling at the end is problematic, you can move it directly to the Authorize function, just after the getAuthServerMetadata call and modify the latter to use the new function to avoid code repetition.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ext-auth: Enterprise Managed Authorization

2 participants