Enterprise managed authorization#770
Enterprise managed authorization#770radar07 wants to merge 19 commits intomodelcontextprotocol:mainfrom
Conversation
|
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. |
|
Thanks @maciej-kisiel. I updated the description with the SEP that this PR solves. |
|
@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. |
maciej-kisiel
left a comment
There was a problem hiding this comment.
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.
oauthex/jwt_bearer.go
Outdated
| // "mcp-client-secret", | ||
| // nil, | ||
| // ) | ||
| func ExchangeJWTBearer( |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
IdPs can reject the request if we don't pass
codeor 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.
There was a problem hiding this comment.
I did a sample test and you're right. Changed to use Config.Exchange
There was a problem hiding this comment.
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).
|
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 I would also ask you to implement this handler and put all the related helper files in a new sub-package of the If there is any logic from Thanks for working on this project! |
b3cba20 to
f94e462
Compare
|
@maciej-kisiel Thanks for the feedback. I made the changes as you suggested. Could you please take a look again? |
maciej-kisiel
left a comment
There was a problem hiding this comment.
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.
auth/enterprise_auth.go
Outdated
| // } | ||
| // | ||
| // // Use accessToken to call MCP Server APIs | ||
| func EnterpriseAuthFlow( |
There was a problem hiding this comment.
Do we still need this if we have the EnterpriseHandler?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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/enterprise_auth.go
Outdated
|
|
||
| // 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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
auth/extauth/oidc_login.go
Outdated
| ExpiresAt int64 | ||
| } | ||
|
|
||
| // InitiateOIDCLogin initiates an OIDC Authorization Code flow with PKCE. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
- You would need to verify the
OIDCLoginConfigonly once. - You could reuse the
oauth2.Configbetween the functions. - You would need only one call for Authorization Metadata of the IdP.
- You would ensure
stateequality is verified in the flow. - You could reuse
auth.AuthorizationResultandauth.AuthorizationArgsinstead of introducing new structs. - 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.
maciej-kisiel
left a comment
There was a problem hiding this comment.
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.
oauthex/jwt_bearer.go
Outdated
| // "mcp-client-secret", | ||
| // nil, | ||
| // ) | ||
| func ExchangeJWTBearer( |
There was a problem hiding this comment.
IdPs can reject the request if we don't pass
codeor 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.
- Adds the Token Exchange (RFC 8693) for Enterprise-Managed Authorization
# Conflicts: # oauthex/oauth2.go
24931e7 to
a4fd173
Compare
a4fd173 to
d5b2a5d
Compare
|
@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. |
|
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 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 |
@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 |
maciej-kisiel
left a comment
There was a problem hiding this comment.
Oops, I forgot to publish the comments yesterday.
auth/extauth/oidc_login.go
Outdated
| ExpiresAt int64 | ||
| } | ||
|
|
||
| // InitiateOIDCLogin initiates an OIDC Authorization Code flow with PKCE. |
There was a problem hiding this comment.
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:
- You would need to verify the
OIDCLoginConfigonly once. - You could reuse the
oauth2.Configbetween the functions. - You would need only one call for Authorization Metadata of the IdP.
- You would ensure
stateequality is verified in the flow. - You could reuse
auth.AuthorizationResultandauth.AuthorizationArgsinstead of introducing new structs. - 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.
auth/extauth/enterprise_handler.go
Outdated
| // 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) |
There was a problem hiding this comment.
I'm just curious about future extensibility, I'm not a domain expert. As an example, in
go-sdk/auth/authorization_code.go
Line 73 in a433a83
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.
auth/enterprise_auth.go
Outdated
| // } | ||
| // | ||
| // // Use accessToken to call MCP Server APIs | ||
| func EnterpriseAuthFlow( |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Nit: I would keep it at GetAuthServerMetadata, the "for issuer" part should be pretty evident from the parameter list and the doc comment.
oauthex/jwt_bearer.go
Outdated
| // "mcp-client-secret", | ||
| // nil, | ||
| // ) | ||
| func ExchangeJWTBearer( |
There was a problem hiding this comment.
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).
oauthex/oauth2.go
Outdated
| // CheckURLScheme validates a URL scheme for security. | ||
| // This is exported for use by the auth package. | ||
| func CheckURLScheme(u string) error { | ||
| return checkURLScheme(u) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Could you point me where this is defined? I failed to find it.
oauthex/token_exchange.go
Outdated
| clientID string, | ||
| clientSecret string, |
There was a problem hiding this comment.
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.
auth/enterprise_auth.go
Outdated
|
|
||
| // 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) { |
There was a problem hiding this comment.
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.
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:
This enables:
Closes: #628