GitHub OAuth is a safe first SSO to ship for a dev-tool: your users already have GitHub accounts, onboarding is one click, and GitHub’s docs are excellent. Here’s the practical guide.
OAuth Apps vs GitHub Apps
The first decision: which kind of integration?
OAuth Apps: the classic flow. The user grants scopes, you get an access token, you act as the user. Good for sign-in and reading profile data. Bad for anything where you want fine-grained, revocable permissions.
GitHub Apps: installed on a specific user or organization, have admin-controlled permissions, can do background work without a user present (via installation tokens). Use this for anything touching repos, issues, PRs, or org admin actions.
For pure sign-in, OAuth Apps are fine. For a product that does real work on GitHub’s behalf, use a GitHub App.
The OAuth App flow
Register your app at github.com/settings/developers. Set the callback URL to your exact redirect endpoint.
Redirect users to:
https://github.com/login/oauth/authorize
?client_id=YOUR_CLIENT_ID
&scope=read:user user:email
&state=RANDOM_CSRF_TOKEN
GitHub supports PKCE now — use it. After the user authorizes, they’re redirected to your callback with a code. Exchange it:
POST https://github.com/login/oauth/access_token
Accept: application/json
Content-Type: application/x-www-form-urlencoded
code=AUTH_CODE
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
You get an access_token back. Unlike most OIDC providers, GitHub doesn’t issue an ID token — you call GET /user with the access token to get the user’s profile.
The email gotcha
/user returns the user’s public email, which may be null. If the user hasn’t made their email public, you need the user:email scope and a call to /user/emails to get verified addresses. Always use the verified, primary address for account matching — public emails can be spoofed as account aliases in some edge cases.
Storing the token
Unlike an ID token (ephemeral, used once), the GitHub access token is a real API token the user has granted to you. Encrypt it at rest, scope your scopes to the minimum, and offer a way for users to revoke from within your app (which calls DELETE /applications/:client_id/token/:token).