[go: up one dir, main page]

Skip to content

Draft: Fix PKCE validation blocking MCP token refresh

What does this MR do and why?

Fixes token refresh for dynamic OAuth applications by skipping refresh token grants from PKCE validation.

PKCE is only required for authorization code exchange per RFC 7636, not for refresh token grants. This allows MCP clients to automatically refresh expired tokens without re-authentication.

When MCP clients (like Cursor) try to refresh expired access tokens, the request fails with:

PKCE code_verifier is required for dynamic OAuth applications

This prevents automatic token refresh, breaking the UX when tokens expire.

Note: I still have issues connecting with Claude Desktop and Claude Code, but that's probably because of some specific configuration options. Looking into it.

References

https://datatracker.ietf.org/doc/html/rfc8252#section-8.1

Related to #577575

How to set up and validate locally

Prerequisites

  • GDK running locally
  • Cursor installed
  • MCP feature flag enabled

Steps

  1. Clean up existing OAuth apps:

    gdk rails c
    # Delete all dynamic MCP applications and tokens
    apps = Doorkeeper::Application.where(dynamic: true, scopes: 'mcp')
    apps.each do |app|
      OauthAccessToken.where(application_id: app.id).each(&:revoke)
      app.destroy
    end
  2. Clean up Cursor's OAuth cache:

    # Close Cursor first
    # For macOS
    sqlite3 ~/Library/Application\ Support/Cursor/User/globalStorage/state.vscdb \
      "DELETE FROM ItemTable WHERE key LIKE '%mcp_client_information%' OR key LIKE '%mcp_tokens%';"
  3. Configure Cursor with MCP server:

    Add to Cursor settings (JSON):

    {
      "mcpServers": {
        "GitLab-GDK-http": {
          "type": "http",
          "url": "https://gdk.test:3443/api/v4/mcp"
        }
      }
    }
  4. Initial authentication (should work):

    • Open Cursor
    • Go to MCP settings
    • Click "Connect" on GitLab-GDK-http
    • Browser opens for OAuth authorization
    • Approve the application
    • Connection successful
  5. Expire the token to trigger refresh:

    # In Rails console
    app = Doorkeeper::Application.where(dynamic: true).order(created_at: :desc).first
    token = OauthAccessToken.where(application_id: app.id).last
    
    # Make token expire 3 hours ago (the time actually doesn't really matter as long as it's in the past)
    token.update(created_at: 3.hours.ago)
  6. Trigger token refresh (bug appears here):

    • Restart Cursor, the connection won't be successful
    • Without the fix logs show: Fails with "PKCE code_verifier is required"
    • With the fix: Token automatically refreshes, connection works

Expected Logs (Bug)

Without fix:

Started POST "/oauth/token" for 127.0.0.1
Parameters: {"grant_type"=>"refresh_token", "refresh_token"=>"[FILTERED]", "client_id"=>"..."}
Completed 400 Bad Request
{"error":"invalid_request","error_description":"PKCE code_verifier is required for dynamic OAuth applications"}

With fix:

Started POST "/oauth/token" for 127.0.0.1
Parameters: {"grant_type"=>"refresh_token", "refresh_token"=>"[FILTERED]", "client_id"=>"..."}
Completed 200 OK

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Merge request reports

Loading