Understanding Authentication with GitHub (OAuth)

NicolasBrondinBernard

Author
@NicolasBrondinBernard

Discover how login with GitHub works using OAuth and learn how to easily implement a "Sign in with GitHub" button in your application.

Article published on 15/07/2026, last updated on 10/08/2026

Offering a "Sign in with GitHub" button in an application is a great way to optimize your conversion rate: it's convenient for the user, who doesn't need to create a new password.

But it's also convenient for the developer, who delegates authentication to GitHub. Yet many developers use OAuth without really understanding what happens behind the scenes.

Your application never knows the user's GitHub password. Instead, it lets GitHub verify their identity, then retrieves only the information it needs.

That's the whole principle of OAuth: delegating authentication to a third-party service.

Implementing GitHub Connect

Creating an OAuth App

The first step is to create an application on GitHub.

To do this, go to your account settings, then to Developer settings > OAuth Apps.

You'll need to fill in in particular:

  • your application's name;
  • its URL;
  • a callback URL.

For example:

http://localhost:3000/auth/github/callback

Once the application is created, GitHub provides you with two important pieces of information:

Client ID
Client Secret

The Client ID identifies your application.

The Client Secret, on the other hand, is confidential.

It must never be sent to the browser or embedded in your frontend code.

Tip

You can create as many applications as you like, so it's at least recommended to create one for your production environment and one for local use.

You could call it "MyApp (local)" for example.

Redirecting the user to GitHub

When the user clicks "Sign in with GitHub," your application doesn't try to authenticate them itself.

It simply redirects them to GitHub.

For example:

const params = new URLSearchParams({
  client_id: process.env.GITHUB_CLIENT_ID,
  redirect_uri: "http://localhost:3000/auth/github/callback",
  scope: "read:user user:email",
  state: crypto.randomUUID(),
});

response.redirect(
  `https://github.com/login/oauth/authorize?${params}`
);

The scope parameter indicates the information your application wants to retrieve.

The state parameter is a random identifier used to protect the flow against certain attacks. It must be saved before the redirect and then verified when GitHub sends the user back.

GitHub authenticates the user

The user then lands on GitHub, and if they aren't already logged in, GitHub asks them to authenticate.

GitHub then shows them the permissions requested by your application and asks them to accept them. Your application never sees this step.

It simply waits for GitHub to redirect the user back to the callback URL.

Retrieving a temporary code

Once the permissions are accepted, GitHub redirects the browser to your callback.

The URL looks like this:

/auth/github/callback?code=...&state=...

The code is a temporary identifier. On its own, it doesn't allow you to call the GitHub API.

Your server must first exchange it for an Access Token.

const tokenResponse = 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,
    }),
  }
);

const { access_token } = await tokenResponse.json();

This request must always be made server-side, since it uses your Client Secret.

Retrieving the user's email

Once you have the token, you can call the GitHub API, for example to retrieve the account's email addresses:

const emailsResponse = await fetch(
  "https://api.github.com/user/emails",
  {
    headers: {
      Authorization: `Bearer ${access_token}`,
      Accept: "application/vnd.github+json",
    },
  }
);

const emails = await emailsResponse.json();

const primaryEmail = emails.find(
  (email) => email.primary && email.verified
)?.email;

The primary email address is generally the best identifier to use.

Indeed, if a user already has an account created with Google or with an email and password, it's likely that this address is the same.

This makes it possible to offer several login methods while keeping a single user account.

Generating your application's JWT

At this point, GitHub has finished its job, your application now knows the user's identity.

All that remains is to look up a user with this email address.

  • If they don't exist yet, you can create an account for them automatically.
  • Otherwise, you simply retrieve their information from the database.

Once this step is complete, you generate your own JWT token:

const token = jwt.sign(
  {
    userId: user.id,
    email: user.email,
  },
  process.env.JWT_SECRET,
  {
    expiresIn: "7d",
  }
);

This JWT is then sent back to the browser, usually in a secure cookie or in your API response.

The GitHub token is only used to communicate with GitHub. Once the user is authenticated, it's your own JWT that will identify them within your application.

How it works, summarized

Even though OAuth may seem impressive at first glance, the mechanism is actually quite simple:

  1. Your application redirects the user to GitHub.
  2. GitHub verifies their identity and returns a temporary code.
  3. Your server exchanges this code for a token, retrieves the user's email address, then generates its own JWT.
  4. Ultimately, GitHub only does one thing: confirm the user's identity.

All account management, permissions, and authentication within your application remain your responsibility.


Finished reading this article?
Our newsletter

No spam. Only free content, news, and ever more resources to level up your skills!

Join +1500 developers

Comments (0)

to leave a comment

No comments yet