Authentication Testing Guide

How to Test GitHub OAuth Login with Playwright: Complete 2026 Guide

A scenario-by-scenario walkthrough of testing GitHub OAuth login with Playwright. Consent screens, scope escalation, OAuth Apps versus GitHub Apps, device flow authentication, organization access approval, and the pitfalls that silently break real GitHub OAuth test suites.

100M+

GitHub has over 100 million developers as of 2023, and GitHub OAuth is one of the most widely integrated social login providers across developer tools and SaaS platforms.

GitHub Blog, January 2023

0Redirect hops per login
0Auth scenarios covered
0OAuth app types compared
0%Fewer lines with Assrt

GitHub OAuth Login Flow

BrowserYour AppGitHub /authorizeConsent ScreenYour CallbackClick Sign in with GitHub302 /login/oauth/authorize?client_id=...&scope=...Render consent screenUser clicks Authorize302 /callback?code=abc123Exchange code for access tokenSet session, render dashboard

1. Why Testing GitHub OAuth Is Harder Than It Looks

GitHub OAuth appears straightforward on the surface: redirect the user to GitHub, they click “Authorize,” and GitHub redirects them back with an authorization code. But five structural complexities make it significantly harder to test reliably in an automated Playwright suite.

First, the consent screen. Unlike some OAuth providers that skip the consent screen for previously authorized apps, GitHub re-displays the consent screen whenever your app requests new scopes. Your test must handle both the first-time authorization and the returning-user flow where the consent screen may or may not appear, depending on whether scopes have changed since the last authorization.

Second, GitHub supports two distinct application types: OAuth Apps and GitHub Apps. They share the same OAuth flow on the surface, but their authorization URLs differ, their token formats differ, their scope models differ (OAuth Apps use broad scopes like repo while GitHub Apps use fine-grained permissions), and their consent screens present different information to the user. Tests written for one type will silently break when your team migrates to the other.

Third, organization access. When a GitHub user belongs to an organization with OAuth App access restrictions enabled, the consent screen includes a “Request” or “Grant” button for each organization. Your test user might authorize the app but still lack access to the org resources your app depends on. This produces cryptic 404 responses on API calls that have nothing to do with authentication itself.

Fourth, the device flow. GitHub supports a device authorization flow for CLI tools and headless environments. Instead of browser redirects, the user receives a code, navigates to github.com/login/device, enters the code, and authorizes from there. Testing this requires coordinating a browser session with an API polling loop.

Fifth, GitHub can enforce two-factor authentication on test accounts. If your test user has 2FA enabled (or the organization requires it), the OAuth flow will prompt for a 2FA code before the consent screen even appears. This adds an extra step your test needs to handle or explicitly avoid by using a test account with 2FA disabled.

GitHub OAuth Redirect Chain

🌐

Your App

User clicks Sign in

↪️

302 Redirect

/login/oauth/authorize

🔒

GitHub Login

If not already signed in

Consent Screen

Authorize application

↪️

302 Redirect

/callback?code=...

⚙️

Token Exchange

POST /access_token

Your App

User is authenticated

Organization Access Approval Flow

🌐

Consent Screen

App requests org access

🔒

Org Restriction

OAuth restricted by admin

📧

Request Access

User clicks Request

Admin Approval

Org admin approves app

↪️

Re-authorize

User re-authorizes

⚙️

Org Resources

API access granted

A complete GitHub OAuth test suite addresses all of these surfaces. The sections below walk through each scenario you need, with runnable Playwright TypeScript you can copy directly into your project.

2. Setting Up a Reliable Test Environment

Before writing any scenarios, you need a dedicated GitHub OAuth App (or GitHub App) and a test GitHub account. Never test against your production GitHub OAuth app, because GitHub rate limits OAuth token creation and a runaway test suite can exhaust your quota, blocking real users from signing in.

GitHub OAuth Test Environment Checklist

  • Create a dedicated GitHub account for testing (e.g., yourapp-e2e-bot)
  • Disable two-factor authentication on the test account
  • Register a new OAuth App at github.com/settings/developers
  • Set Authorization callback URL to http://localhost:3000/auth/callback
  • Note the Client ID and generate a Client Secret
  • Create a test organization for org-access scenarios (optional)
  • Enable OAuth App access restrictions on the test org (optional)
  • Store credentials in .env.test, never in source control

Environment Variables

.env.test

Revoking Previous Authorizations Before Each Run

GitHub remembers when a user has authorized an OAuth App. On subsequent logins, the consent screen is skipped entirely and the user is redirected straight to your callback. This is convenient for users but problematic for tests that need to verify the consent screen itself. Revoke the authorization before each test run using the GitHub API.

test/helpers/github-cleanup.ts

Playwright Configuration for GitHub OAuth

GitHub OAuth redirects the browser to github.com, which is a different origin from your app. Playwright handles cross-origin navigations natively, but you need generous timeouts because GitHub's login and consent pages can be slow, especially in CI environments with limited bandwidth.

playwright.config.ts
Verify Test Environment

3. Scenario: Basic OAuth Login Happy Path

The foundational scenario: your app redirects to GitHub, the test user signs in (if not already signed in on GitHub), the consent screen appears, the user clicks “Authorize,” GitHub redirects back to your callback URL with an authorization code, and your backend exchanges that code for an access token. This is your smoke test. If it breaks, no one can sign in via GitHub.

1

Basic GitHub OAuth Login Happy Path

Straightforward

Goal

Complete a full GitHub OAuth login from your app's sign-in button through the GitHub consent screen, land on the authenticated dashboard, and confirm the session contains the correct GitHub user identity.

Preconditions

  • App running at APP_BASE_URL
  • GitHub OAuth App registered with correct callback URL
  • Test GitHub account exists with no 2FA enabled
  • Previous authorization revoked (so consent screen appears)

Playwright Implementation

github-oauth-login.spec.ts

What to Assert Beyond the UI

  • The session cookie is set with httpOnly and secure flags
  • The GitHub username is displayed correctly in the app
  • A valid access token was stored server-side (check via API call)
  • The state parameter in the OAuth redirect matches the one your app sent (CSRF protection)

GitHub OAuth Happy Path: Playwright vs Assrt

import { test, expect } from '@playwright/test';

test('github oauth: happy path login', async ({ page }) => {
  await page.goto('/');
  await page.getByRole('button', { name: /sign in with github/i }).click();
  await page.waitForURL(/github\.com/);

  const loginField = page.getByLabel('Username or email address');
  if (await loginField.isVisible({ timeout: 3_000 }).catch(() => false)) {
    await loginField.fill(process.env.GITHUB_TEST_USERNAME!);
    await page.getByLabel('Password').fill(process.env.GITHUB_TEST_PASSWORD!);
    await page.getByRole('button', { name: /sign in/i }).click();
  }

  await page.waitForURL(/\/login\/oauth\/authorize/);
  await page.getByRole('button', { name: /authorize/i }).click();

  await page.waitForURL(/\/dashboard/, { timeout: 30_000 });
  await expect(page.getByRole('heading', { name: /dashboard/i }))
    .toBeVisible();
  await expect(page.getByText(process.env.GITHUB_TEST_USERNAME!))
    .toBeVisible();

  const cookies = await page.context().cookies();
  const sessionCookie = cookies.find(c => c.name === 'session');
  expect(sessionCookie).toBeDefined();
});
53% fewer lines

4. Scenario: Scope Escalation and Re-Authorization

GitHub OAuth scopes control what your app can access. A user who authorized your app with read:user scope will see the consent screen again if your app later requests repo or admin:org. This is called scope escalation. The consent screen explicitly shows the new permissions being requested, and the user must re-authorize. Your test must handle the case where the consent screen reappears for a user who has already authorized the app once.

The tricky part is that GitHub only re-displays the consent screen when the requested scopes are a superset of the previously granted scopes. If you request a subset or the same scopes, the user is redirected silently. Your tests need to verify both paths: the silent redirect when scopes match, and the re-authorization prompt when new scopes are added.

2

Scope Escalation and Re-Authorization

Complex

Playwright Implementation

github-scope-escalation.spec.ts

What to Assert Beyond the UI

  • After scope escalation, the stored access token includes the new scopes
  • API calls requiring the new scope succeed (e.g., listing private repos)
  • The consent screen only appears when new scopes are requested, not on every login

Scope Escalation: Playwright vs Assrt

import { test, expect } from '@playwright/test';

test('scope escalation triggers re-auth', async ({ page }) => {
  // First auth with read:user
  await page.goto('/auth/github?scope=read:user');
  await page.waitForURL(/github\.com/);
  await page.waitForURL(/\/login\/oauth\/authorize/);
  await page.getByRole('button', { name: /authorize/i }).click();
  await page.waitForURL(/\/dashboard/, { timeout: 30_000 });

  // Second auth with elevated scopes
  await page.goto('/auth/github?scope=read:user,repo');
  await page.waitForURL(/github\.com/);

  // Consent screen reappears for new scope
  await page.waitForURL(/\/login\/oauth\/authorize/);
  await expect(page.getByText('repo')).toBeVisible();
  await page.getByRole('button', { name: /authorize/i }).click();
  await page.waitForURL(/\/dashboard/, { timeout: 30_000 });

  const res = await page.request.get('/api/github/repos');
  expect(res.ok()).toBeTruthy();
});
49% fewer lines

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started

5. Scenario: OAuth App vs GitHub App Differences

GitHub supports two application types that use OAuth: OAuth Apps (the classic model) and GitHub Apps (the newer, recommended approach). While both use the /login/oauth/authorize endpoint, they differ in several ways that affect your tests. OAuth Apps use coarse-grained scopes like repo, user, and admin:org. GitHub Apps use fine-grained permissions scoped to specific repositories, and the consent screen shows which repositories the user is granting access to.

The critical difference for testing: GitHub Apps generate installation access tokens that expire after one hour, while OAuth App tokens persist until explicitly revoked. This means your CI session reuse strategy (Section 8) behaves differently depending on which app type you use. GitHub App tokens will expire mid-suite if your tests run longer than an hour.

3

OAuth App vs GitHub App Authorization

Moderate

Playwright Implementation: OAuth App

github-oauth-app.spec.ts

Playwright Implementation: GitHub App

github-app.spec.ts

6. Scenario: Organization Access Approval

When a GitHub user belongs to an organization that has enabled OAuth App access restrictions, the consent screen shows each organization with a “Grant” or “Request” button beside it. “Grant” appears if the user is an org admin; “Request” appears if the user is a regular member and needs admin approval. If the user authorizes the app without granting org access, your app will receive a valid access token but API calls to org resources (repositories, teams, org settings) will return 404, not 403. This is one of the most confusing GitHub OAuth behaviors and a common source of production bugs.

Your test must verify two paths: the happy path where the org admin grants access during authorization, and the error path where org access is not granted and your app handles the resulting 404 responses gracefully.

4

Organization Access Approval

Complex

Playwright Implementation

github-org-access.spec.ts

What to Assert Beyond the UI

  • With org access granted: API calls to /orgs/:org/repos return 200
  • Without org access: API calls return 404 (not 403), and the app displays a helpful message
  • The access token itself is valid in both cases; only the org resource access differs

7. Scenario: Device Flow Authentication

The GitHub device flow is used by CLI tools, smart TVs, and other environments where a browser redirect is not possible. Your app requests a device code from GitHub, displays a user code and a URL (github.com/login/device), and polls GitHub for the authorization result while the user opens a browser, navigates to that URL, enters the code, and authorizes the app. Testing this flow requires coordinating two parallel processes: the API polling loop and the browser-based authorization.

5

Device Flow Authentication

Complex

Playwright Implementation

github-device-flow.spec.ts

What to Assert Beyond the UI

  • The device code endpoint returns a valid user code and verification URI
  • The polling endpoint transitions from authorization_pending to a valid access token
  • The access token returned via device flow has the correct scopes
  • The token can authenticate GitHub API requests successfully

8. Scenario: Bypassing GitHub OAuth in CI with Token Injection

Running the full GitHub OAuth flow before every test is slow and fragile. GitHub's login page can introduce CAPTCHAs, change layout, or rate-limit your test account. The solution is to authenticate once in a setup project, save the session state, and reuse it across all subsequent tests. For even faster CI, you can skip the browser entirely and inject a pre-generated personal access token or OAuth token into the browser context.

6

Session Reuse with storageState

Moderate

Playwright Implementation: Browser-Based Setup

test/global-setup.ts

API-Based Token Injection (Faster)

test/helpers/github-token-inject.ts
GitHub OAuth Test Suite Run

9. Common Pitfalls That Break GitHub OAuth Test Suites

GitHub Login CAPTCHAs

GitHub may present a CAPTCHA on the login page when it detects automated access patterns, unfamiliar IP addresses, or repeated login attempts. CI runners with dynamic IPs are particularly susceptible. The CAPTCHA is invisible until it triggers, at which point your test silently hangs waiting for a locator that never appears. Mitigate this by using the session reuse pattern from Section 8, or by pre-authenticating on GitHub and saving the GitHub session cookies (not just your app's session) so the GitHub login page is bypassed entirely.

Consent Screen Caching

Once a user authorizes an OAuth App, GitHub remembers the grant. Subsequent OAuth flows skip the consent screen and redirect silently. If your test expects to interact with the consent screen but the grant already exists from a previous run, the test will fail because the authorize button never appears. Always revoke the OAuth grant before tests that need to verify the consent screen. Use the DELETE /applications/grants/:grant_id API endpoint in your test setup.

State Parameter Mismatches

The stateparameter in OAuth is a CSRF protection mechanism. Your app generates a random string, sends it in the authorize URL, and verifies it matches when GitHub redirects back. If your test manipulates cookies or storage between the redirect and the callback, the state parameter check fails silently. The symptom is usually a redirect loop or a generic “authorization failed” error with no indication that the state mismatch caused it.

Token Expiration in GitHub Apps

GitHub App installation access tokens expire after one hour. If your test suite takes longer than 60 minutes (common in large projects with many spec files), tests that run late in the suite will fail with 401 errors. The fix is to implement token refresh logic in your app and test it explicitly, or to generate a fresh installation token in a Playwright fixture that runs before each test file.

Two-Factor Authentication on Test Accounts

If your test GitHub account has 2FA enabled, the OAuth flow will prompt for a 2FA code after the password step. This adds complexity to your test and requires either TOTP code generation (similar to the Auth0 MFA pattern) or, more practically, a dedicated test account with 2FA explicitly disabled. Be aware that some GitHub organizations require 2FA for all members. If your test account is a member of such an org, you cannot disable 2FA without leaving the org first.

GitHub OAuth Pitfall Avoidance Checklist

  • Revoke OAuth grants before tests that verify the consent screen
  • Use session reuse to avoid GitHub login CAPTCHAs in CI
  • Handle both consent-screen and silent-redirect paths
  • Test with 2FA disabled on the test account
  • Implement token refresh for GitHub App installations
  • Hardcode expected consent screen selectors without checking
  • Assume the consent screen always appears
  • Share test accounts across parallel workers
  • Ignore the state parameter in your test assertions
Common GitHub OAuth Error Messages

10. Writing These Scenarios in Plain English with Assrt

Each scenario above is 25 to 70 lines of Playwright TypeScript. Multiply that by the eight scenarios you actually need and you have a substantial test file that silently breaks whenever GitHub updates the consent screen layout, renames a button, or changes the device flow input pattern. Assrt lets you describe the scenario in plain English, generates the equivalent Playwright code, and regenerates the selectors automatically when the underlying page changes.

The organization access scenario from Section 6 demonstrates the value of this approach. In raw Playwright, you need to know the exact data attribute for the org row, the button text for granting access, and the URL pattern for the consent page. In Assrt, you describe the intent and let the framework resolve the implementation details at runtime.

scenarios/github-oauth-full-suite.assrt

Assrt compiles each scenario block into the same Playwright TypeScript you saw in the preceding sections, committed to your repo as real tests you can read, run, and modify. When GitHub renames a button from “Authorize” to “Allow access” or restructures the device code input, Assrt detects the failure, analyzes the new DOM, and opens a pull request with the updated locators. Your scenario files stay untouched.

Start with the happy path login. Once it is green in your CI, add the scope escalation scenario, then organization access, then the device flow, then session reuse. In a single afternoon you can have complete GitHub OAuth coverage that most production applications never manage to achieve by hand.

Full Suite Comparison: Playwright vs Assrt

// 8 test files across 280+ lines of Playwright TypeScript
// github-oauth-login.spec.ts (30 lines)
// github-scope-escalation.spec.ts (45 lines)
// github-oauth-app.spec.ts (30 lines)
// github-app.spec.ts (35 lines)
// github-org-access.spec.ts (60 lines)
// github-device-flow.spec.ts (65 lines)
// global-setup.ts (30 lines)
// github-token-inject.ts (25 lines)
//
// Each file requires:
// - Exact GitHub selectors that break on layout changes
// - Manual URL pattern matching for redirect detection
// - Explicit timeout tuning per environment
// - Separate cleanup helpers for grant revocation
// - Token refresh logic for GitHub App installations
80% fewer lines

Related Guides

Ready to automate your testing?

Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.

$npm install @assrt/sdk