Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server implementation of MCP auth #151

Merged
merged 45 commits into from
Feb 21, 2025
Merged

Server implementation of MCP auth #151

merged 45 commits into from
Feb 21, 2025

Conversation

jspahrsummers
Copy link
Member

@jspahrsummers jspahrsummers commented Feb 16, 2025

This implements the server side only of the MCP auth draft spec.

To do

  • Authorization server metadata
  • Dynamic client registration
  • Use 405s for Method Not Allowed everywhere, including Allow header
  • Update /authorize endpoint to check body for parameters in POST requests
  • Authorization codes
  • Strongly-typed error handling (e.g., to indicate that a scope is invalid)
  • Token revocation API
  • Catch exceptions from user code
  • Access tokens
  • Refresh tokens
  • PKCE
  • Direct integration with Express?
  • Add auth requirement to MCP server endpoint(s)
  • Add WWW-Authenticate header when bearer token auth fails
  • Cache-Control header
  • Tests
  • Remove X-Powered-By header (this is done at the application level)
  • Add rate limiting (h/t CodeQL)

Follow-ups

  • Prevent refresh token replay
  • Revoke tokens if authorization code presented a second time
  • Make token revocation API a requirement?
  • High-level convenience APIs for:
    • Becoming an authorization server directly (with pluggable storage)
    • Creating proxy tokens for a separate authorization server (with pluggable storage)
    • Passthrough proxying a separate authorization server
  • Security considerations
    • Authorization servers MUST prevent clickjacking attacks. Multiple countermeasures are described in [RFC6819], including the use of the X-Frame-Options HTTP response header field and frame-busting JavaScript. In addition to those, authorization servers SHOULD also use Content Security Policy (CSP) level 2 [CSP-2] or greater.
    • Based on its risk assessment, the AS needs to decide whether it can trust the redirect URI or not. It could take into account URI analytics done internally or through some external service to evaluate the credibility and trustworthiness content behind the URI, and the source of the redirect URI and other client data.
    • The AS SHOULD only automatically redirect the user agent if it trusts the redirect URI. If the URI is not trusted, the AS MAY inform the user and rely on the user to make the correct decision.
    • Add iss parameter, per https://www.rfc-editor.org/rfc/rfc9207.html
    • Require 127.0.0.1 instead of localhost?
  • Documentation & examples
  • Rename authenticateClient to requireAuthedClient or similar

Base automatically changed from justin/client-auth to main February 17, 2025 10:46
Copy link
Contributor

@jerome3o-anthropic jerome3o-anthropic left a comment

Choose a reason for hiding this comment

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

This looks really good - I can't wait to cook with it.

One question regarding expired servers vs servers without secrets, the rest are non-blocking qns/comments

"eventsource": "^3.0.2",
"express": "^5.0.1",
Copy link
Contributor

Choose a reason for hiding this comment

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

[non-blocking] do we want this to be optional

Copy link
Member Author

Choose a reason for hiding this comment

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

Possibly… my main concern is really browser-based apps, but I think tree-shaking will ensure that none of Express makes it into those (it wouldn't be compatible anyway). I think it's pretty low-concern to have here, but willing to change it later if it becomes a problem.

Choose a reason for hiding this comment

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

We should probably include CORS, or browser based clients won't be able to connect

standardHeaders: true,
legacyHeaders: false,
message: new TooManyRequestsError('You have exceeded the rate limit for authorization requests').toResponseObject(),
...rateLimitConfig
Copy link
Contributor

Choose a reason for hiding this comment

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

[non-blocking qn] explicitly disabled by setting configuration opens here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sorry, I don't understand the question. Can you rephrase?


// Validate scopes
let requestedScopes: string[] = [];
if (scope !== undefined && client.scope !== undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

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

[non-blocking] if scopes are requested, but the client doesn't have any scopes (i.e. it's undefined), should we also throw an error? ig the client isn't concerned with scopes so it doesn't really matter what the client provides?

Copy link
Contributor

Choose a reason for hiding this comment

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

oh also is "Invalid client_secret" correct for this error?

Copy link
Member Author

@jspahrsummers jspahrsummers Feb 21, 2025

Choose a reason for hiding this comment

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

Yes, we should throw an error. Good catch.

Where do you see "invalid client_secret?"

*
* This will validate the token with the auth provider and add the resulting auth info to the request object.
*/
export function requireBearerAuth({ provider, requiredScopes = [] }: BearerAuthMiddlewareOptions): RequestHandler {
Copy link
Contributor

Choose a reason for hiding this comment

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

[non-blocking] is there not something pre-built for this?

Copy link
Member Author

Choose a reason for hiding this comment

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

Most of the logic is custom here. Only the header parsing bit conceivably might exist already, but it's so simple that it's probably not worth it.

/**
* Verifies an access token and returns information about it.
*/
verifyAccessToken(token: string): Promise<AuthInfo>;
Copy link
Contributor

Choose a reason for hiding this comment

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

[non-blocking thought] is there room to do more for the server creator here? i.e. can we just have them make the "getToken"/"setToken" and we do the verification/expiry checks here?

Copy link
Contributor

Choose a reason for hiding this comment

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

Claude thinks this could work:

interface SimplifiedOAuthProvider {
  // Token storage operations
  storeAuthorizationCode(userId: string, clientId: string, code: string, metadata: CodeMetadata): Promise<void>;
  fetchAuthorizationCode(code: string): Promise<CodeRecord | null>;
  deleteAuthorizationCode(code: string): Promise<void>;
  
  // Token operations
  storeToken(userId: string, clientId: string, tokenType: 'access'|'refresh', token: string, metadata: TokenMetadata): Promise<void>;
  fetchToken(token: string, tokenType: 'access'|'refresh'): Promise<TokenRecord | null>;
  deleteToken(token: string, tokenType: 'access'|'refresh'): Promise<void>;
  
  // User operations
  authenticateUser(req: Request, res: Response): Promise<string | null>; // Returns userId on success
  
  // Optional: Permission checking
  checkUserPermissions?(userId: string, clientId: string, scopes: string[]): Promise<boolean>;
}

But tbh we can add this later if needed

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, let's tackle this later. I'm worried that this will actually be quite complex to make fully pluggable (e.g., for the auth proxying case too).

@jspahrsummers jspahrsummers marked this pull request as ready for review February 21, 2025 12:12
@jspahrsummers
Copy link
Member Author

Thanks for the review! Will tackle the follow-ups in one or more other PRs, as this one has gotten big enough as it is.

@jspahrsummers jspahrsummers merged commit 80e1484 into main Feb 21, 2025
4 checks passed
@jspahrsummers jspahrsummers deleted the justin/server-auth branch February 21, 2025 15:47
MediaInfluences pushed a commit to MediaInfluences/typescript-sdk that referenced this pull request Apr 3, 2025
…/justin/server-auth

Server implementation of MCP auth
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Development

Successfully merging this pull request may close these issues.

3 participants