Skip to content

OAuth redirect URI port mismatch breaks login to most MCP servers #4968

Closed

Description

@cmp0xff

OAuth redirect URI port mismatch breaks login to most MCP servers

Summary

copilot CLI publishes a CIMD (Client ID Metadata Document) that declares a
single loopback redirect URI on a fixed port, but at runtime the CLI binds an
ephemeral loopback port and sends that port in the OAuth redirect_uri
parameter. GitHub's own OAuth server accepts the mismatch (per RFC 8252 §7.3
loopback matching), but many third-party OAuth servers — including
Entra/Azure-fronted MCP servers — do strict string matching against the
registered redirect_uris and reject the request.

The result is that authentication fails against almost every non-GitHub MCP
server
users try to connect. Claude Code and Codex CLI are not affected in
the same environments.

Environment

  • CLI version: GitHub Copilot CLI 1.0.88
  • OS: macOS (arm64)
  • Install: Homebrew cask copilot-cli (SEA binary)

Reproducer

  1. Configure any MCP server whose OAuth authorize endpoint validates
    redirect_uri by exact match against the CIMD's redirect_uris list.
    (Entra/Azure-fronted services typically behave this way.)
  2. Trigger the OAuth flow from Copilot CLI:
    /mcp add <server-url> (or however the server is registered), then log in.
  3. The browser lands on the authorize endpoint, which returns:
{
  "error": "invalid_request",
  "error_description": "Redirect URI 'http://127.0.0.1:52752/' does not match CIMD redirect_uris.",
  "state": "..."
}

The port (52752 above) changes on every attempt.

Expected behavior

The redirect_uri sent to the authorize endpoint should either:

  • (a) match the port published in the CIMD, i.e. http://127.0.0.1:33418/, so
    strict-matching servers accept it; or
  • (b) rely on RFC 8252 §7.3 loopback matching and have the CIMD advertise
    http://127.0.0.1/ (no port), making clear to server implementers that
    port-agnostic matching is expected.

Today it does neither: it publishes a specific port and then uses a different
one.

Actual behavior / root cause

The CLI's published metadata at
https://github.com/copilot/cli/client-metadata.json:

{
  "client_id": "https://github.com/copilot/cli/client-metadata.json",
  "client_name": "GitHub Copilot CLI",
  "application_type": "native",
  "redirect_uris": ["http://127.0.0.1:33418/"],
  ...
}

But the CLI's OAuth loopback listener is bound with port 0 (verifiable by
inspecting the SEA blob inside the shipped binary):

// Port 0 = let the OS pick a free ephemeral port. Bind to loopback only.
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
const port = typeof address === "object" && address ? address.port : 0;

The dynamically-assigned port then flows into the OAuth redirect_uri, which
never equals 33418.

Impact

  • Blocks Copilot CLI from authenticating against most third-party MCP servers,
    which is a large and growing set as MCP adoption widens.
  • Internal reports at multiple organisations indicate the same failure pattern
    across every non-GitHub MCP they've tried, with Claude Code / Codex CLI
    unaffected in the same environments.
  • Users have no clean local workaround. The one lever we found —
    COPILOT_MCP_CIMD_CLIENT_ID_URL overriding the CIMD document URL — requires
    self-hosting a metadata file that enumerates every ephemeral loopback port,
    which is fragile and requires the MCP server to dynamically trust arbitrary
    CIMDs.

Suggested fixes (either is sufficient)

  1. Bind the published port. Change the listener to
    server.listen(33418, "127.0.0.1", …) with a fallback strategy (retry with
    an ephemeral port only if 33418 is in use, and clearly document the
    fallback), so the common case matches the CIMD exactly.
  2. Publish port-agnostic CIMD. Change redirect_uris in
    client-metadata.json to ["http://127.0.0.1/"] and rely on the RFC 8252
    §7.3 loopback rule. This shifts the responsibility to non-conformant OAuth
    servers, which is arguably correct, but will not fix real-world usage until
    those servers are updated.

Option 1 is the most defensive; option 2 is the most spec-pure. A combination
(bind 33418 first, publish 127.0.0.1/) would maximise interoperability.

References

  • RFC 8252 §7.3 (Loopback Interface Redirection): loopback redirects SHOULD be
    matched ignoring the port component.
  • OAuth 2.0 for Native Apps client identity metadata document (CIMD) spec.

Drafted with GitHub Copilot CLI.

Activity

  1. kvnloo commented on Sep 27, 2026

    @kvnloo

    I would add an interoperability test with a deliberately strict OAuth server here. GitHub accepting RFC 8252 loopback-port variance can hide a client metadata mismatch that other providers reject.

    The invariant should be simple:

    the redirect URI advertised by the client metadata and the redirect URI used in authorization must satisfy the same registration rule.

    If Copilot keeps a fixed 33418 URI, bind that exact port in the normal path. If ephemeral ports are required, the metadata/registration strategy needs to explicitly support them rather than publishing a different fixed URI.

    A test matrix with GitHub-style port-tolerant validation and strict exact-match validation would keep this from regressing.

  2. SamMorrowDrums commented on Sep 28, 2026

    @SamMorrowDrums

    I already have a fix: https://github.com/github/github/pull/457462

    I also sent the following to an engineer at Atlassian that asked about this.

    After double checking, I think you might actually being strict in a way that is not explicitly required in the spec however, so I’ll flag up the relevant parts:
    “The authorization server MUST allow any port to be specified at the time of the request for loopback IP redirect URIs, to accommodate clients that obtain an available ephemeral port from the operating system at the time of the request." RFC 8252 7.3 https://www.rfc-editor.org/rfc/rfc8252#section-7.3 - note the absence of any mention of validation where a port is specified. It simply doesn’t explicitly cover that case.

    Oauth 2.1 explicitly calls out an “except for port” exemption https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1#section-2.3.1 which should apply here I believe.

    And in RFC 9700 2.1 for Oauth 2.0 security best practices “authorization servers MUST utilize exact string matching except for port numbers in localhost redirection URIs of native apps” https://www.rfc-editor.org/rfc/rfc9700#section-2.1

    My read of the above plus my own understanding of the risks/security model suggest there is essentially no value to the auth server explicitly rejecting on port matching for loopback address validation (if the client wants or needs to pin a port it will provide that port but if it does give a different one that’s arguably better as it helps prevent port squatting and port conflicts),
    so regardless of what we change for our specific CIMD documents, I would recommend that you avoid being strict on port for loopback addresses regardless.

    After double checking, I think you might actually being strict in a way that is not explicitly required in the spec however, so I’ll flag up the relevant parts:
    “The authorization server MUST allow any port to be specified at the time of the request for loopback IP redirect URIs, to accommodate clients that obtain an available ephemeral port from the operating system at the time of the request." RFC 8252 7.3 https://www.rfc-editor.org/rfc/rfc8252#section-7.3 - note the absence of any mention of validation where a port is specified. It simply doesn’t explicitly cover that case.

    Oauth 2.1 explicitly calls out an “except for port” exemption https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1#section-2.3.1 which should apply here I believe.

    And in RFC 9700 2.1 for Oauth 2.0 security best practices “authorization servers MUST utilize exact string matching except for port numbers in localhost redirection URIs of native apps” https://www.rfc-editor.org/rfc/rfc9700#section-2.1

    My read of the above plus my own understanding of the risks/security model suggest there is essentially no value to the auth server explicitly rejecting on port matching for loopback address validation (if the client wants or needs to pin a port it will provide that port but if it does give a different one that’s arguably better as it helps prevent port squatting and port conflicts),
    so regardless of what we change for our specific CIMD documents, I would recommend that you avoid being strict on port for loopback addresses regardless.

    I believe the strict port validation is a misunderstanding/overreach on implementing the spec, and adds nothing for loopback addresses, but at the same time I am dropping it because I agree that it's easier to fix us than them, so once my GitHub PR lands it should fix the issues.

  3. SamMorrowDrums commented on Sep 29, 2026

    @SamMorrowDrums

    I merged PR so I'm closing this.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions