Skip to main content

Overview

The authentication script implements GitHub’s OAuth device code flow to obtain an access token for the Copilot API. It guides users through browser-based authentication and securely stores credentials.

Command

node scripts/auth.mjs

Authentication flow

The script follows a 5-step OAuth device code flow:
  1. Initiate device code - Request a user code from GitHub
  2. Display code - Show the code and verification URL to the user
  3. Poll for token - Wait for user authorization
  4. Verify token - Confirm the token works with GitHub API
  5. Save credentials - Store token to ~/.claude-copilot-auth.json

Functions

initiateDeviceCode()

Initiates the OAuth device code flow with GitHub.
async function initiateDeviceCode() {
  const response = await fetch(DEVICE_CODE_URL, {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      "User-Agent": USER_AGENT,
    },
    body: JSON.stringify({
      client_id: CLIENT_ID,
      scope: "read:user",
    }),
  })

  if (!response.ok) {
    throw new Error(
      `Failed to initiate device code flow: ${response.status} ${response.statusText}`
    )
  }

  return response.json()
}
Returns: Object containing device_code, user_code, verification_uri, and interval Error handling: Throws descriptive error if the initial request fails

pollForToken(deviceCode, interval)

Polls GitHub’s token endpoint until the user completes authorization.
deviceCode
string
required
Device code received from initiateDeviceCode()
interval
number
required
Polling interval in seconds (adds 1 second safety margin)
async function pollForToken(deviceCode, interval) {
  const pollInterval = (interval + 1) * 1000 // Add safety margin

  while (true) {
    await new Promise((resolve) => setTimeout(resolve, pollInterval))

    const response = await fetch(ACCESS_TOKEN_URL, {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        "User-Agent": USER_AGENT,
      },
      body: JSON.stringify({
        client_id: CLIENT_ID,
        device_code: deviceCode,
        grant_type: "urn:ietf:params:oauth:grant-type:device_code",
      }),
    })

    const data = await response.json()

    if (data.access_token) {
      return data.access_token
    }

    if (data.error === "authorization_pending") {
      process.stdout.write(".")
      continue
    }

    if (data.error === "slow_down") {
      await new Promise((resolve) => setTimeout(resolve, 5000))
      continue
    }

    if (data.error === "expired_token") {
      throw new Error("Device code expired. Please try again.")
    }

    if (data.error === "access_denied") {
      throw new Error("Authorization was denied by user.")
    }

    throw new Error(`Unexpected error: ${data.error} - ${data.error_description}`)
  }
}
Returns: GitHub access token string Error states:
  • authorization_pending - User hasn’t authorized yet (continues polling)
  • slow_down - Rate limit hit (waits 5 seconds)
  • expired_token - Code expired (throws error)
  • access_denied - User rejected authorization (throws error)

verifyToken(token)

Verifies the access token works by calling GitHub’s user API.
token
string
required
GitHub access token to verify
async function verifyToken(token) {
  const response = await fetch("https://api.github.com/user", {
    headers: {
      Authorization: `Bearer ${token}`,
      "User-Agent": USER_AGENT,
    },
  })

  if (!response.ok) {
    throw new Error("Token verification failed")
  }

  return response.json()
}
Returns: GitHub user object with login, name, etc. Error handling: Throws error if token is invalid

checkCopilotAccess(token)

Verifies the token has Copilot API access.
token
string
required
GitHub access token to check
async function checkCopilotAccess(token) {
  const response = await fetch("https://api.githubcopilot.com/models", {
    headers: {
      Authorization: `Bearer ${token}`,
      "User-Agent": USER_AGENT,
      "Openai-Intent": "conversation-edits",
    },
  })

  return response.ok || response.status === 401
}
Returns: true if access is confirmed or likely valid (401 may indicate different auth format)

Token storage

Credentials are saved to ~/.claude-copilot-auth.json with the following structure:
{
  "access_token": "gho_xxxxxxxxxxxxx",
  "provider": "github-copilot",
  "github_user": "username",
  "created_at": "2026-03-03T12:00:00.000Z"
}
The auth file location can be customized via the COPILOT_AUTH_FILE environment variable:
COPILOT_AUTH_FILE=/custom/path/auth.json node scripts/auth.mjs

Configuration

CLIENT_ID
string
OAuth client ID: Ov23li8tweQw6odWQebz
DEVICE_CODE_URL
string
GitHub device code endpoint: https://github.com/login/device/code
ACCESS_TOKEN_URL
string
GitHub token endpoint: https://github.com/login/oauth/access_token
AUTH_FILE
string
Token storage location (default: ~/.claude-copilot-auth.json)
USER_AGENT
string
HTTP user agent: claude-code-copilot-provider/1.0.0

User experience

The script provides a guided authentication experience:
  1. Automatic browser launch - Opens verification URL automatically on macOS, Windows, and Linux
  2. Visual feedback - Box-formatted user code display
  3. Progress indicator - Dots printed during polling
  4. Re-authentication check - Detects existing valid tokens and prevents unnecessary re-auth

Example output

╔══════════════════════════════════════════════════════════╗
║   GitHub Copilot Authentication for Claude Code        ║
╚══════════════════════════════════════════════════════════╝

Initiating GitHub OAuth device code flow...

┌──────────────────────────────────────────────────────────┐
│                                                          │
│   Your code:  ABCD-1234                                  │
│                                                          │
│   Open this URL in your browser:                         │
│   https://github.com/login/device                        │
│                                                          │
│   Enter the code above and authorize the application.    │
│                                                          │
└──────────────────────────────────────────────────────────┘

(Browser opened automatically)

Waiting for authorization......

✓ Authenticated as: username (Display Name)
✓ Token saved to: /home/user/.claude-copilot-auth.json

You can now start the proxy server:
  node scripts/proxy.mjs

Then run Claude Code with:
  ANTHROPIC_BASE_URL=http://localhost:18080 ANTHROPIC_API_KEY=copilot-proxy claude

Error handling

The script handles various error conditions:

Invalid existing token

if (existsSync(AUTH_FILE)) {
  try {
    const existing = JSON.parse(readFileSync(AUTH_FILE, "utf-8"))
    if (existing.access_token) {
      const user = await verifyToken(existing.access_token)
      console.log(`Already authenticated as: ${user.login}`)
      // ... instructions to re-authenticate
      return
    }
  } catch {
    // Token invalid, proceed with new auth
    console.log("Existing token is invalid, starting fresh authentication...\n")
  }
}

Authentication failure

main().catch((err) => {
  console.error("\n✗ Authentication failed:", err.message)
  process.exit(1)
})

Next steps

After successful authentication:
  1. Start the proxy server: node scripts/proxy.mjs
  2. Launch Claude Code with the proxy:
    ANTHROPIC_BASE_URL=http://localhost:18080 ANTHROPIC_API_KEY=copilot-proxy claude
    

Re-authentication

To re-authenticate with a different GitHub account:
rm ~/.claude-copilot-auth.json
node scripts/auth.mjs