If your audience is developers, GitHub OAuth is the right second sign-in option (or the only one). It's the same flow as Google OAuth — but registering the app is one form on GitHub instead of a tour through Google Cloud Console, and the user identity comes with their existing developer reputation attached.
The right set of OAuth providers depends on who's signing in. A consumer-facing app picks Google first because everyone has a Google account. A B2B app picks Microsoft because that's where the work accounts live. A developer-facing app picks GitHub — it's their professional identity, they're already logged in there for code work, and "the GitHub username on file" carries social proof that an arbitrary email doesn't. If your product touches code, devops, or developer workflows, GitHub OAuth is table stakes.
Mechanically, it's the same OAuth 2.0 flow as the Google version: redirect, consent screen, callback with auth code, server-side exchange for access token, user lookup. The differences are surface-level. GitHub's app registration is a single form on github.com/settings. There's no "verification process" — your app works for any GitHub user the moment you create it. GitHub's user object includes the username (which is meaningful in a way emails aren't), the avatar URL, and optionally the public email (private by default; you have to request the user:email scope to get something usable).
This tutorial covers the registration form, the scopes worth requesting, and the one-place-it-trips-up-first-timers: getting a stable email address out of the response.
user:email and why you almost always want itGo to your GitHub settings, scroll to Developer settings (left sidebar at the bottom), click OAuth Apps → New OAuth App. Fill in:
https://yourapp.com/api/auth/callback/github. For local dev, register a second app or use http://localhost:3000/api/auth/callback/github — exact match required.Click Register application. GitHub shows your Client ID immediately. Click Generate a new client secret to get the secret; copy it now, you only see it once.
Unlike Google, GitHub OAuth Apps accept exactly one callback URL. To support dev + production, the easiest approach is to register two apps:
http://localhost:3000/...https://yourapp.com/...Each gets its own Client ID + Secret. Your dev env points at app #1; production at app #2. The user-visible names can differ — your dev users see "MyApp (dev)" on the consent screen, which is actually nice for debugging.
https://yourapp.com/auth/callback/github, GitHub accepts callbacks to any subpath under that — so you can include state in the path. Most libraries don't take advantage of this; just remember the rule when debugging "why does this redirect URL not work."
GITHUB_CLIENT_ID=Iv1.xxxxxxxxx
GITHUB_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxx
In Auth.js / NextAuth, the config is the standard:
import GitHubProvider from "next-auth/providers/github";
export const authOptions = {
providers: [
GitHubProvider({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
}),
],
};
If you've already configured a Google provider, add GitHub alongside — the library lets you stack providers, and your sign-in UI shows buttons for each.
user:emailBy default, GitHub OAuth only grants access to the user's public profile, which may not include an email address (most users keep their primary email private). To get a usable email, request the user:email scope.
Auth.js does this by default. Manually constructed auth URLs need &scope=user:email in the query string. The user sees this on the consent screen ("Access your email addresses").
Other scopes exist for advanced use cases:
read:user — fetch profile info beyond the public defaults.repo — read/write the user's repositories (only if your app does that).read:org — see which orgs they belong to.The rule: request only what you'll use. Each additional scope is a friction point on the consent screen and a privacy implication for the user.
After a successful auth, GitHub's /user endpoint returns the user's profile. The email field on this response is the user's public email — often null, because many users keep their email private.
To get a usable email, hit /user/emails instead, which (with user:email scope) returns all the user's email addresses including the verified primary one:
const emailsRes = await fetch('https://api.github.com/user/emails', {
headers: { Authorization: `Bearer ${accessToken}` },
});
const emails = await emailsRes.json();
const primary = emails.find(e => e.primary && e.verified)?.email;
Always pick the verified primary; that's the one GitHub itself trusts for communication. Auth.js handles this automatically; manual implementations have to do it themselves and frequently don't.
The /user response includes both login (the username, e.g., octocat) and id (a stable numeric ID, e.g., 583231). Store both, but use the ID as the foreign key.
GitHub allows users to change their username; if you key on login, a user changing their username breaks your records. Key on id and store the current login as a separate field that you update on each sign-in.
GitHub has two app types:
For sign-in only, OAuth Apps is right and simpler. For anything that interacts with code (creating PRs, posting checks, accessing repo content), GitHub Apps is the modern path. They can coexist — many products ship an OAuth App for sign-in and a separate GitHub App for the repo-integration features.