Skip to main content
The proxy uses GitHub’s OAuth device code flow to authenticate with GitHub Copilot. This is the same authentication method used by VS Code and other GitHub Copilot clients.

Device code flow

The device code flow is designed for devices that don’t have a web browser or have limited input capabilities. It works by:
  1. Requesting a device code from GitHub
  2. Displaying a user code for the user to enter on GitHub’s website
  3. Polling GitHub’s token endpoint until the user authorizes the application
  4. Saving the access token for future use

Authentication endpoints

Device code initiation

The authentication script requests a device code from GitHub:
const CLIENT_ID = "Ov23li8tweQw6odWQebz"
const DEVICE_CODE_URL = "https://github.com/login/device/code"

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",
    }),
  })

  return response.json()
}
client_id
string
required
The OAuth application client ID. Uses GitHub Copilot’s public client ID.
scope
string
required
OAuth scopes to request. Only read:user is needed for Copilot access.

Response

device_code
string
Device verification code used for polling.
user_code
string
User-friendly code displayed to the user (e.g., "ABCD-1234").
verification_uri
string
URL where the user enters the code (typically https://github.com/login/device).
interval
integer
Minimum number of seconds to wait between polling requests.
expires_in
integer
Number of seconds until the device code expires.
{
  "device_code": "3584d83530557fdd1f46af8289938c8ef79f9dc5",
  "user_code": "ABCD-1234",
  "verification_uri": "https://github.com/login/device",
  "interval": 5,
  "expires_in": 900
}

Token polling

The script polls GitHub’s token endpoint until the user authorizes the application:
const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"

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
    }

    // Handle other error cases...
  }
}

Polling errors

The polling loop handles several error states:
authorization_pending
string
The user hasn’t authorized yet. Continue polling.
slow_down
string
Polling too frequently. Add a 5-second delay before the next request.
expired_token
string
The device code has expired. Start the flow over.
access_denied
string
The user denied authorization. Stop polling.

Success response

{
  "access_token": "gho_16C7e42F292c6912E7710c838347Ae178B4a",
  "token_type": "bearer",
  "scope": "read:user"
}

Token verification

After receiving an access token, the script verifies it by calling GitHub’s user API:
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()
}
This returns the authenticated user’s information:
{
  "login": "octocat",
  "id": 1,
  "name": "The Octocat",
  "email": "[email protected]",
  "avatar_url": "https://avatars.githubusercontent.com/u/1?v=4"
}

Token storage

The access token is saved to a JSON file for persistence:
const AUTH_FILE = process.env.COPILOT_AUTH_FILE || 
  join(homedir(), ".claude-copilot-auth.json")

const authData = {
  access_token: accessToken,
  provider: "github-copilot",
  github_user: user.login,
  created_at: new Date().toISOString(),
}

writeFileSync(AUTH_FILE, JSON.stringify(authData, null, 2))

Token file format

The authentication file contains:
access_token
string
The OAuth access token used for API requests.
provider
string
Always "github-copilot" to identify the token source.
github_user
string
The GitHub username of the authenticated user.
created_at
string
ISO 8601 timestamp of when the token was created.
{
  "access_token": "gho_16C7e42F292c6912E7710c838347Ae178B4a",
  "provider": "github-copilot",
  "github_user": "octocat",
  "created_at": "2026-03-03T12:00:00.000Z"
}

Token loading

The proxy server loads the token at startup:
function loadAuth() {
  if (!existsSync(AUTH_FILE)) {
    console.error(`✗ Auth file not found: ${AUTH_FILE}`)
    console.error("  Run 'node scripts/auth.mjs' first to authenticate.")
    process.exit(1)
  }

  try {
    const data = JSON.parse(readFileSync(AUTH_FILE, "utf-8"))
    if (!data.access_token) {
      throw new Error("No access_token in auth file")
    }
    return data.access_token
  } catch (err) {
    console.error(`✗ Failed to read auth file: ${err.message}`)
    process.exit(1)
  }
}
If the auth file is missing or invalid, the proxy will exit with an error. Run node scripts/auth.mjs to authenticate.

Re-authentication

To re-authenticate with a different GitHub account:
  1. Delete the existing auth file:
    rm ~/.claude-copilot-auth.json
    
  2. Run the authentication script again:
    node scripts/auth.mjs
    
The script checks for an existing valid token before starting the flow:
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}`)
      return
    }
  } catch {
    console.log("Existing token is invalid, starting fresh authentication...")
  }
}

Security considerations

The auth file contains a sensitive access token. Keep it secure and never commit it to version control.
  • The token grants access to your GitHub Copilot subscription
  • Store the token file with restricted permissions (chmod 600)
  • Use environment variable COPILOT_AUTH_FILE to customize the location
  • Tokens don’t expire automatically but can be revoked from GitHub settings

Environment variables

COPILOT_AUTH_FILE
string
default:"~/.claude-copilot-auth.json"
Path to the authentication file. Both the auth script and proxy server use this variable.
# Custom auth file location
export COPILOT_AUTH_FILE="/secure/location/copilot-auth.json"
node scripts/auth.mjs
node scripts/proxy.mjs