# How OAuth works: GitHub and Google login explained

> How OAuth 2.0 authorization code flow works for GitHub and Google login: redirects, codes, access tokens, scopes, and what happens behind the button.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-08-06 | Topics: [Networking](https://flaviocopes.com/tags/network/) | Canonical: https://flaviocopes.com/how-oauth-works/

You click "Login with GitHub" on a site you've never used before. GitHub asks you to approve. You click yes, and you're logged in.

You never typed your GitHub password into that app. So what actually happened?

## The players

Three parties are involved in every OAuth login:

- **Your app** — the client that wants to know who you are
- **You** — the user
- **The provider** — GitHub, Google, or whoever holds your account

The goal is simple. Your app gets a limited-permission **access token**. It never sees your password.

The provider handles login. Your app gets proof that you approved access, plus permission to call a few API endpoints on your behalf.

## The authorization code flow

OAuth 2.0 has a few flows. The one behind "Login with GitHub" and "Login with Google" is the **authorization code flow**. Here's the whole thing:

```mermaid
sequenceDiagram
  accTitle: OAuth authorization code flow
  accDescr: The browser moves between the application and provider while the application exchanges the code and calls the provider API.
  actor User
  participant App as Your app
  participant Provider as GitHub or Google

  User->>App: Choose provider login
  App-->>User: Redirect to provider
  User->>Provider: Log in and approve
  Provider-->>User: Redirect to app with code
  User->>App: Callback with code
  App->>Provider: Exchange code for token
  Provider-->>App: Return access token
  App->>Provider: Request user profile
  Provider-->>App: Return profile
```

Let's walk through it step by step.

1. You register your app with the provider, get a **client ID** and **client secret**, and set a **redirect URL**.
2. Your app sends the user to the provider's authorize URL.
3. The user approves. The provider redirects back to your redirect URL with a short-lived **authorization code**.
4. Your server exchanges the code plus client secret for an **access token**.
5. Your server calls the provider's API with that token to get the user's profile.

That's it. Five steps. Let's look at each one with GitHub as the example.

### Step 1: Register your app

Go to GitHub Settings → Developer settings → OAuth Apps. Create an app, set your callback URL (e.g. `https://myapp.com/auth/github/callback`), and GitHub gives you a client ID and client secret.

Keep the secret on your server. Never put it in frontend code.

### Step 2: Send the user to GitHub

Your app builds an authorize URL and redirects the browser there:

```
https://github.com/login/oauth/authorize?client_id=Ov23liABC123&redirect_uri=https://myapp.com/auth/github/callback&scope=read:user&state=random-csrf-token
```

The important query params:

- `client_id` — identifies your app
- `redirect_uri` — must match what you registered
- `scope` — what permissions you're asking for
- `state` — a random string we'll talk about later

The user sees GitHub's login page, then a screen asking "Authorize MyApp?"

### Step 3: GitHub redirects back with a code

After approval, GitHub sends the browser to your redirect URL:

```
https://myapp.com/auth/github/callback?code=abc123def456&state=random-csrf-token
```

That `code` is short-lived. Usually it expires in about 10 minutes. It's useless without your client secret, so an attacker who intercepts it can't do much on their own.

Your server should check that `state` matches what you sent in step 2. More on that below.

### Step 4: Exchange the code for a token

This happens server-side. Your backend POSTs the code to GitHub's token endpoint:

```bash
curl -X POST https://github.com/login/oauth/access_token \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
    "client_id": "Ov23liABC123",
    "client_secret": "your-client-secret",
    "code": "abc123def456",
    "redirect_uri": "https://myapp.com/auth/github/callback"
  }'
```

GitHub responds with JSON:

```json
{
  "access_token": "gho_xxxxxxxxxxxx",
  "token_type": "bearer",
  "scope": "read:user"
}
```

The same exchange in JavaScript on your server:

```js
const response = await fetch('https://github.com/login/oauth/access_token', {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    client_id: process.env.GITHUB_CLIENT_ID,
    client_secret: process.env.GITHUB_CLIENT_SECRET,
    code,
    redirect_uri: 'https://myapp.com/auth/github/callback',
  }),
})

const { access_token } = await response.json()
```

If something goes wrong here — bad code, wrong secret, expired code — you'll get an error response. Check the [HTTP status codes](https://flaviocopes.com/http-status-codes/) and the error body GitHub sends back.

### Step 5: Fetch the user profile

Now your server has a token. Use it to call GitHub's API:

```js
const userResponse = await fetch('https://api.github.com/user', {
  headers: {
    Authorization: `Bearer ${access_token}`,
  },
})

const user = await userResponse.json()
// { login: 'flavio', id: 12345, avatar_url: '...', ... }
```

You now know who logged in. Create a session, store the user in your database, set a cookie — whatever your app needs.

## Google: same flow, plus OpenID Connect

Google uses the same authorization code flow. The authorize URL looks like this:

```
https://accounts.google.com/o/oauth2/v2/auth?client_id=123.apps.googleusercontent.com&redirect_uri=https://myapp.com/auth/google/callback&response_type=code&scope=openid%20email%20profile&state=random-csrf-token
```

The scopes are different. Google login typically asks for `openid email profile` instead of GitHub's `read:user`.

Google also implements **OpenID Connect** on top of OAuth. When you exchange the code, the response includes an **id_token** — a [JWT](https://flaviocopes.com/jwt/) with the user's identity (email, name, Google user ID). You can verify that token instead of making a separate API call, though many apps still hit Google's userinfo endpoint to be safe.

The token exchange endpoint is `https://oauth2.googleapis.com/token`. Same idea: POST the code, client ID, client secret, and redirect URI.

## State and PKCE

Two security pieces worth knowing.

**State** protects against CSRF. Before redirecting to the provider, your app generates a random string and stores it (in a cookie or session). The provider sends it back in the callback. If it doesn't match, reject the request — someone else started that login flow.

**PKCE** (Proof Key for Code Exchange) protects against authorization code interception. Your app generates a random `code_verifier`, sends a hash of it as `code_challenge` in the authorize URL, then sends the original verifier during token exchange. Mobile apps and SPAs can't keep a client secret, so PKCE is required for them. GitHub and Google both support it.

## Scopes: ask for the minimum

Scopes define what the token can do. On GitHub:

- `read:user` — read your public profile (enough for login)
- `repo` — full access to your repositories

For login, `read:user` is all you need. Don't ask for `repo` unless your app actually needs repository access. Users notice, and they'll deny the request.

Google's `openid email profile` scopes are similarly minimal for a basic login button.

## You rarely hand-roll this

In practice you don't write these five steps from scratch every time. Libraries like [Better Auth](https://flaviocopes.com/better-auth/) handle the redirect, callback, token exchange, and session creation for you. You add the provider credentials and call `signIn.social({ provider: 'github' })`.

But knowing the flow helps when things break. Redirect URI mismatch? Check step 1. Callback returns an error instead of a code? The user denied access or your `state` check failed. Token exchange returns 401? Your client secret is wrong or the code expired.

OAuth looks complicated on paper. It's really just a redirect, a code swap, and an API call. Once you've traced it once, debugging auth issues gets a lot easier.
