How to Upgrade Your GitHub API Authentication

in Blog | Updated

JavaScript Code

GitHub deprecated authentication via URL query parameters back in 2020, and developers who still rely on the access_token query string are now seeing recurring email warnings — or worse, broken integrations. This guide explains why the change was made, what the new approach looks like, and how to migrate your code across cURL, PHP, JavaScript (XMLHttpRequest and Fetch API), and WordPress plugin updaters.

Why GitHub Deprecated Query Parameter Authentication

Passing access tokens as URL query parameters (?access_token=...) exposes credentials in server logs, browser history, referrer headers, and third-party analytics tools. Any intermediate proxy or CDN that logs request URLs captures the token in plain text, creating a significant security risk. GitHub’s decision to deprecate this method aligns with broader industry best practices around OAuth 2.0 and bearer token handling, which explicitly require tokens to be sent in the Authorization HTTP header.

If your integration is still using the old pattern, you may have received a deprecation email like this:

Deprecation notice for authentication via URL query parameters

Please use the Authorization HTTP header instead, as using the
access_token query parameter is deprecated. If this token is being
used by an app you don't have control over, be aware that it may
stop working as a result of this deprecation.

GitHub sends this reminder monthly for each unique token and User-Agent combination it detects — but only one example URL per combination, not every request. Once you migrate to header-based authentication, the emails stop and your integration becomes both more secure and forward-compatible.

The Old Way vs. The New Way

The old pattern embeds the token directly in the URL, which is what triggers the deprecation warning:

curl "https://api.github.com/user/repos?access_token=my_access_token"

The correct approach moves the token into the Authorization header using the token scheme (for personal access tokens) or the Bearer scheme (for OAuth apps and GitHub Apps). Both are accepted by the GitHub API:

# Using the token scheme (personal access tokens)
curl -H "Authorization: token my_access_token" https://api.github.com/user/repos

# Using the Bearer scheme (OAuth apps, GitHub Apps)
curl -H "Authorization: Bearer my_access_token" https://api.github.com/user/repos

Always include a descriptive User-Agent header as well. GitHub requires it for all API requests and uses it to identify your application in usage reports and deprecation notices.

Migrating to Header Authentication in PHP

PHP applications using cURL are straightforward to migrate. Move the token out of the URL and into the CURLOPT_HTTPHEADER array:

<?php
// Updated: Use Authorization header instead of access_token query parameter
$requestUri    = 'https://api.github.com/user/repos';
$authorizeToken = 'my_access_token'; // Replace with your PAT or OAuth token

$curl = curl_init();

curl_setopt_array($curl, [
    CURLOPT_URL            => $requestUri,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_ENCODING       => '',
    CURLOPT_MAXREDIRS      => 10,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_HTTP_VERSION   => CURL_HTTP_VERSION_1_1,
    CURLOPT_CUSTOMREQUEST  => 'GET',
    CURLOPT_HTTPHEADER     => [
        'Authorization: token ' . $authorizeToken,
        'User-Agent: YourAppName/1.0',
        'Accept: application/vnd.github.v3+json',
    ],
]);

$response = curl_exec($curl);
$httpCode  = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);

if ($httpCode === 200) {
    $data = json_decode($response, true);
    // Process $data...
} else {
    error_log('GitHub API error: HTTP ' . $httpCode);
}

A few things worth noting in this updated snippet: the timeout has been reduced from 0 (unlimited) to 30 seconds to avoid hanging requests; the Accept header explicitly requests the v3 JSON response format; and the response HTTP code is checked before processing the payload. These are small improvements worth making alongside the authentication migration.

Using WordPress HTTP API (wp_remote_get)

If your code runs inside a WordPress plugin or theme, prefer the built-in wp_remote_get() function over raw cURL. It respects WordPress proxy settings, handles SSL correctly, and is much easier to test:

<?php
$token    = 'my_access_token';
$endpoint = 'https://api.github.com/user/repos';

$response = wp_remote_get($endpoint, [
    'headers' => [
        'Authorization' => 'token ' . $token,
        'User-Agent'    => 'YourPluginName/1.0',
        'Accept'        => 'application/vnd.github.v3+json',
    ],
    'timeout' => 30,
]);

if (is_wp_error($response)) {
    error_log($response->get_error_message());
    return;
}

$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);

Migrating to Header Authentication in JavaScript

Modern JavaScript projects should use the Fetch API, which provides a cleaner, promise-based interface. The XMLHttpRequest approach is shown for completeness, but Fetch is preferred for all new code.

Using the Fetch API (Recommended)

const token    = 'my_access_token';
const endpoint = 'https://api.github.com/repos/user/repo/releases';

const response = await fetch(endpoint, {
    method: 'GET',
    headers: {
        'Authorization': `token ${token}`,
        'User-Agent':    'YourAppName/1.0',
        'Accept':        'application/vnd.github.v3+json',
    },
});

if (!response.ok) {
    throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
}

const data = await response.json();
console.log(data);

Note that the Fetch API does not allow setting the User-Agent header in browser environments due to the Fetch specification’s forbidden header list. This restriction does not apply in Node.js or server-side environments. For browser-based API calls, GitHub will use the browser’s default User-Agent, which is sufficient for most use cases.

Using XMLHttpRequest (Legacy)

const token    = 'my_access_token';
const endpoint = 'https://api.github.com/repos/user/repo/releases';

const xhr = new XMLHttpRequest();

xhr.addEventListener('readystatechange', function () {
    if (this.readyState === 4 && this.status === 200) {
        const data = JSON.parse(this.responseText);
        console.log(data);
    }
});

xhr.open('GET', endpoint);
xhr.setRequestHeader('Authorization', `token ${token}`);
xhr.setRequestHeader('Accept', 'application/vnd.github.v3+json');
xhr.send();

Personal Access Tokens vs. Fine-Grained Tokens

When creating or replacing tokens, GitHub now offers two types. Classic personal access tokens (PATs) grant broad, scope-based access and are what most older integrations use. Fine-grained personal access tokens, introduced in 2022, allow you to restrict a token to specific repositories and limit it to only the permissions your integration actually needs — following the principle of least privilege.

For any new token you create as part of this migration, consider switching to a fine-grained token. The Authorization header format is identical for both types, so no code change is required beyond swapping the token value itself:

# Both classic and fine-grained PATs use the same header format
curl -H "Authorization: token github_pat_..." https://api.github.com/user/repos

GitHub Authentication for WordPress Updater Solutions

If you are using GitHub to distribute updates for your WordPress plugins or themes — whether public or private — you need a proper updater library rather than rolling your own GitHub API integration. Maintaining custom GitHub API authentication code for WordPress updates is fragile, and every GitHub API change becomes a maintenance burden.

The recommended solution is Andy Fragen’s Git Updater (formerly GitHub Updater). It handles authentication correctly, supports multiple Git hosts (GitHub, GitLab, Bitbucket), and is actively maintained. After migrating 10 plugins and 2 themes to Git Updater, the recurring maintenance overhead of custom API authentication disappeared entirely.

If you prefer a lightweight, dependency-free approach for a single plugin, the Plugin Update Checker library by Yahnis Elsts is another solid option. It supports GitHub releases as an update source and handles the Authorization header internally.

Troubleshooting Common Errors

After migrating, you may encounter a few common issues:

  • 401 Unauthorized — The token is invalid, expired, or the header is malformed. Double-check that there is no extra whitespace in the token string and that the scheme (token or Bearer) matches the token type.
  • 403 Forbidden — The token is valid but lacks the required scope. For private repositories, the token needs the repo scope (classic PAT) or repository read/write permissions (fine-grained PAT).
  • 422 Unprocessable Entity — Usually indicates a malformed request body for POST/PATCH operations, not an authentication issue. Check your JSON payload.
  • Rate limit exceeded (403 or 429) — Authenticated requests get 5,000 requests per hour. Check the X-RateLimit-Remaining and X-RateLimit-Reset response headers to track usage. Unauthenticated requests are limited to 60 per hour, which is another reason to always authenticate.

Summary

Migrating from query parameter authentication to header-based authentication is a small but important change. The core fix — moving ?access_token=... from the URL to an Authorization: token ... header — takes minutes to implement in any language. Beyond stopping the deprecation emails, you gain better security, higher rate limits (authenticated vs. unauthenticated), and a codebase that is aligned with the current GitHub API v3 specification and ready for GitHub Apps or fine-grained token adoption in the future.

Related Posts