Skip to content

Support multiple authentication flows for enterprise SSO compatibility #5665

Closed as not planned

Description

@huchenlei

Problem

I can successfully sign in to GitHub Copilot in VSCode using enterprise SSO, but cannot sign in to opencode using the device code flow. The current authentication implementation only supports a single authentication method, which fails for certain enterprise configurations.

Current Implementation

opencode currently only implements the device code flow (/login/device/code) via the opencode-github-copilot package, which may not work for:

  • Enterprise SSO configurations
  • Self-hosted GitHub Enterprise Server (GHES) instances
  • Environments where device code flow is blocked or unsupported
  • Web worker contexts (CORS issues)

Proposed Solution

Implement multiple authentication flows with automatic fallback, matching VSCode's production implementation:

1. OAuth Authorization Code Flow with Local Server

  • Opens a local loopback server to handle OAuth redirects
  • Endpoint: /login/oauth/authorize
  • Best for: Desktop environments with enterprise SSO
  • Supports: PKCE for enhanced security

2. OAuth Authorization Code Flow with URI Handler

  • Uses vscode:// URI handler for redirects
  • Endpoint: /login/oauth/authorize
  • Best for: Web and remote environments
  • Supports: Enterprise SSO, PKCE

3. Device Code Flow (current)

  • Keep existing implementation as fallback
  • Endpoint: /login/device/code
  • Best for: Headless/remote environments

4. Personal Access Token (PAT) Flow

  • Manual token entry as last resort
  • Endpoint: /settings/tokens/new
  • Best for: All environments including old GHES versions

Technical Details

VSCode's implementation (in extensions/github-authentication/src/flows.ts) includes:

Flow Selection Logic:

// Intelligently selects flows based on:
- Extension host type (local/remote/web worker)
- GitHub target (GitHub.com/Enterprise Server/Hosted Enterprise)
- Client capabilities and configuration
- User preferences

Security Features:

  • PKCE (Proof Key for Code Exchange) for OAuth flows
  • Secure token storage via system keychain
  • Proper state/nonce validation

Error Handling:

  • Automatic fallback to next available flow
  • Network error recovery
  • User cancellation handling
  • Timeout management (5min for device code)

Benefits

  1. Broader Compatibility: Works with enterprise SSO, GHES, and restricted environments
  2. Better UX: Automatic fallback prevents authentication failures
  3. Production-Grade: Matches VSCode's battle-tested implementation
  4. Security: PKCE support for OAuth flows

Enterprise Use Case

Many organizations use:

  • GitHub Enterprise with SAML SSO
  • Custom OAuth apps with restricted flows
  • Network policies that block device code endpoints
  • Self-hosted GHES instances (device code requires GHES 3.1+)

Without multiple auth flows, these users cannot use opencode even though they can use VSCode's Copilot.

Reference Implementation

VSCode's GitHub authentication extension:

Minimal Implementation

At minimum, adding OAuth authorization code flow would significantly improve enterprise compatibility:

  1. Supports enterprise SSO configurations
  2. Works where device code is blocked
  3. Provides better UX for desktop users

Related Issue

See detailed implementation discussion in the auth repo: anomalyco/opencode-github-copilot#1

Would appreciate consideration for implementing multiple auth flows to improve enterprise user experience!

Activity

  1. github-actions commented on Dec 17, 2025

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    Feel free to ignore if none of these address your specific case.

  2. huchenlei commented on Dec 17, 2025

    @huchenlei
    Author

    Extra note:

    I step down to the auth execution and found https://${domain}/login/device/code returns 404. The error is not properly handled which causes the app to do nothing after clicking "Github Copilot" option in connect dialog.

    The code in https://github.com/sst/opencode-github-copilot/ seems to be out-dated.

    Runtime code ref from ~/.bun/install/cache/opencode-copilot-auth@0.0.9@@@1/index.mjs:

    /**
     * @type {import('@opencode-ai/plugin').Plugin}
     */
    export async function CopilotAuthPlugin({ client }) {
      const CLIENT_ID = "Iv1.b507a08c87ecfe98";
      const HEADERS = {
        "User-Agent": "GitHubCopilotChat/0.32.4",
        "Editor-Version": "vscode/1.105.1",
        "Editor-Plugin-Version": "copilot-chat/0.32.4",
        "Copilot-Integration-Id": "vscode-chat",
      };
      const RESPONSES_API_ALTERNATE_INPUT_TYPES = [
        "file_search_call",
        "computer_call",
        "computer_call_output",
        "web_search_call",
        "function_call",
        "function_call_output",
        "image_generation_call",
        "code_interpreter_call",
        "local_shell_call",
        "local_shell_call_output",
        "mcp_list_tools",
        "mcp_approval_request",
        "mcp_approval_response",
        "mcp_call",
        "reasoning",
      ];
    
      function normalizeDomain(url) {
        return url.replace(/^https?:\/\//, "").replace(/\/$/, "");
      }
    
      function getUrls(domain) {
        return {
          DEVICE_CODE_URL: `https://${domain}/login/device/code`,
          ACCESS_TOKEN_URL: `https://${domain}/login/oauth/access_token`,
          COPILOT_API_KEY_URL: `https://api.${domain}/copilot_internal/v2/token`,
        };
      }
    
      return {
        auth: {
          provider: "github-copilot",
          loader: async (getAuth, provider) => {
            let info = await getAuth();
            if (!info || info.type !== "oauth") return {};
    
            if (provider && provider.models) {
              for (const model of Object.values(provider.models)) {
                model.cost = {
                  input: 0,
                  output: 0,
                  cache: {
                    read: 0,
                    write: 0,
                  },
                };
              }
            }
    
            // Set baseURL based on deployment type
            const enterpriseUrl = info.enterpriseUrl;
            const baseURL = enterpriseUrl
              ? `https://copilot-api.${normalizeDomain(enterpriseUrl)}`
              : "https://api.githubcopilot.com";
    
            return {
              baseURL,
              apiKey: "",
              async fetch(input, init) {
                const info = await getAuth();
                if (info.type !== "oauth") return {};
                if (!info.access || info.expires < Date.now()) {
                  const domain = info.enterpriseUrl
                    ? normalizeDomain(info.enterpriseUrl)
                    : "github.com";
                  const urls = getUrls(domain);
    
                  const response = await fetch(urls.COPILOT_API_KEY_URL, {
                    headers: {
                      Accept: "application/json",
                      Authorization: `Bearer ${info.refresh}`,
                      ...HEADERS,
                    },
                  });
    
                  if (!response.ok) {
                    throw new Error(`Token refresh failed: ${response.status}`);
                  }
    
                  const tokenData = await response.json();
    
                  const saveProviderID = info.enterpriseUrl
                    ? "github-copilot-enterprise"
                    : "github-copilot";
                  await client.auth.set({
                    path: {
                      id: saveProviderID,
                    },
                    body: {
                      type: "oauth",
                      refresh: info.refresh,
                      access: tokenData.token,
                      expires: tokenData.expires_at * 1000,
                      ...(info.enterpriseUrl && {
                        enterpriseUrl: info.enterpriseUrl,
                      }),
                    },
                  });
                  info.access = tokenData.token;
                }
                let isAgentCall = false;
                let isVisionRequest = false;
                try {
                  const body =
                    typeof init.body === "string"
                      ? JSON.parse(init.body)
                      : init.body;
                  if (body?.messages) {
                    isAgentCall = body.messages.some(
                      (msg) => msg.role && ["tool", "assistant"].includes(msg.role),
                    );
                    isVisionRequest = body.messages.some(
                      (msg) =>
                        Array.isArray(msg.content) &&
                        msg.content.some((part) => part.type === "image_url"),
                    );
                  }
    
                  if (body?.input) {
                    const lastInput = body.input[body.input.length - 1];
    
                    const isAssistant = lastInput?.role === "assistant";
                    const hasAgentType = lastInput?.type
                      ? RESPONSES_API_ALTERNATE_INPUT_TYPES.includes(lastInput.type)
                      : false;
                    isAgentCall = isAssistant || hasAgentType;
    
                    isVisionRequest =
                      Array.isArray(lastInput?.content) &&
                      lastInput.content.some((part) => part.type === "input_image");
                  }
                } catch {}
                const headers = {
                  ...init.headers,
                  ...HEADERS,
                  Authorization: `Bearer ${info.access}`,
                  "Openai-Intent": "conversation-edits",
                  "X-Initiator": isAgentCall ? "agent" : "user",
                };
                if (isVisionRequest) {
                  headers["Copilot-Vision-Request"] = "true";
                }
    
                delete headers["x-api-key"];
                delete headers["authorization"];
    
                return fetch(input, {
                  ...init,
                  headers,
                });
              },
            };
          },
          methods: [
            {
              type: "oauth",
              label: "Login with GitHub Copilot",
              prompts: [
                {
                  type: "select",
                  key: "deploymentType",
                  message: "Select GitHub deployment type",
                  options: [
                    {
                      label: "GitHub.com",
                      value: "github.com",
                      hint: "Public",
                    },
                    {
                      label: "GitHub Enterprise",
                      value: "enterprise",
                      hint: "Data residency or self-hosted",
                    },
                  ],
                },
                {
                  type: "text",
                  key: "enterpriseUrl",
                  message: "Enter your GitHub Enterprise URL or domain",
                  placeholder: "company.ghe.com or https://company.ghe.com",
                  condition: (inputs) => inputs.deploymentType === "enterprise",
                  validate: (value) => {
                    if (!value) return "URL or domain is required";
                    try {
                      const url = value.includes("://")
                        ? new URL(value)
                        : new URL(`https://${value}`);
                      if (!url.hostname)
                        return "Please enter a valid URL or domain";
                      return undefined;
                    } catch {
                      return "Please enter a valid URL (e.g., company.ghe.com or https://company.ghe.com)";
                    }
                  },
                },
              ],
              async authorize(inputs = {}) {
                const deploymentType = inputs.deploymentType || "github.com";
    
                let domain = "github.com";
                let actualProvider = "github-copilot";
    
                if (deploymentType === "enterprise") {
                  const enterpriseUrl = inputs.enterpriseUrl;
                  domain = normalizeDomain(enterpriseUrl);
                  actualProvider = "github-copilot-enterprise";
                }
    
                const urls = getUrls(domain);
    
                const deviceResponse = await fetch(urls.DEVICE_CODE_URL, {
                  method: "POST",
                  headers: {
                    Accept: "application/json",
                    "Content-Type": "application/json",
                    "User-Agent": "GitHubCopilotChat/0.35.0",
                  },
                  body: JSON.stringify({
                    client_id: CLIENT_ID,
                    scope: "read:user",
                  }),
                });
    
                if (!deviceResponse.ok) {
                  throw new Error("Failed to initiate device authorization");
                }
    
                const deviceData = await deviceResponse.json();
    
                return {
                  url: deviceData.verification_uri,
                  instructions: `Enter code: ${deviceData.user_code}`,
                  method: "auto",
                  callback: async () => {
                    while (true) {
                      const response = await fetch(urls.ACCESS_TOKEN_URL, {
                        method: "POST",
                        headers: {
                          Accept: "application/json",
                          "Content-Type": "application/json",
                          "User-Agent": "GitHubCopilotChat/0.35.0",
                        },
                        body: JSON.stringify({
                          client_id: CLIENT_ID,
                          device_code: deviceData.device_code,
                          grant_type:
                            "urn:ietf:params:oauth:grant-type:device_code",
                        }),
                      });
    
                      if (!response.ok) return { type: "failed" };
    
                      const data = await response.json();
    
                      if (data.access_token) {
                        const result = {
                          type: "success",
                          refresh: data.access_token,
                          access: "",
                          expires: 0,
                        };
    
                        if (actualProvider === "github-copilot-enterprise") {
                          result.provider = "github-copilot-enterprise";
                          result.enterpriseUrl = domain;
                        }
    
                        return result;
                      }
    
                      if (data.error === "authorization_pending") {
                        await new Promise((resolve) =>
                          setTimeout(resolve, deviceData.interval * 1000),
                        );
                        continue;
                      }
    
                      if (data.error) return { type: "failed" };
    
                      await new Promise((resolve) =>
                        setTimeout(resolve, deviceData.interval * 1000),
                      );
                      continue;
                    }
                  },
                };
              },
            },
          ],
        },
      };
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

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