GitHub
GitHub uses the standard OAuth 2.0 authorization-code flow: your frontend redirects the user to GitHub, GitHub redirects back with a temporary code, and your backend exchanges that code for the user’s profile. Because a redirect is involved, this package also issues a signed, time-boxed state token that your frontend has to carry through the whole flow to protect against CSRF.
This page assumes you’ve already finished the shared setup in Getting Started.
How it works
Section titled “How it works”- Your frontend asks the backend for a state token:
GET /api/social-auth/github/state/. - Your frontend redirects the user to GitHub’s authorize URL, including your
client_idand that state token. - The user approves the app on GitHub and is redirected back to your
redirect_uriwith acodeand the samestatevalue. - Your frontend sends
codeandstatetoPOST /api/social-auth/github/login/. - The backend verifies the state token, exchanges the code for a GitHub access token, and returns JWT
accessandrefreshtokens.
Get your credentials
Section titled “Get your credentials”- Go to your GitHub account → Settings → Developer settings → OAuth Apps.
- Click New OAuth App.
- Fill in the application details, and set Authorization callback URL to wherever your frontend handles the redirect - for example
http://localhost:5173/auth/github/callback. - Copy the Client ID, and generate a Client Secret.
Add the settings
Section titled “Add the settings”# settings.pyGITHUB_CLIENT_ID = "your-github-client-id"GITHUB_CLIENT_SECRET = "your-github-client-secret"Both values are required - GitHub stays disabled until both are set.
Wire up the URLs
Section titled “Wire up the URLs”# urls.pyfrom django.urls import path, include
urlpatterns = [ path("api/social-auth/", include("django_social_auth_rest.urls.github")),]Frontend integration
Section titled “Frontend integration”This works the same regardless of framework: request a state token, redirect to GitHub, and forward the resulting code and state to your backend.
<!doctype html><html lang="en"><head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>GitHub OAuth Test</title> <script src="https://cdn.tailwindcss.com"></script></head><body class="bg-zinc-950 text-zinc-100"> <div class="min-h-screen flex items-center justify-center px-4"> <div class="w-full max-w-sm rounded-lg border border-zinc-800 bg-zinc-900 p-6 space-y-4"> <h1 class="text-xl font-semibold text-center">GitHub OAuth Test</h1>
<button id="github-button" class="w-full rounded-md bg-zinc-100 px-4 py-2 text-sm font-medium text-zinc-900 hover:bg-zinc-200" > Continue with GitHub </button>
<pre id="status" class="text-xs text-zinc-400 whitespace-pre-wrap break-all"></pre> </div> </div>
<script> const GITHUB_CLIENT_ID = "YOUR_GITHUB_CLIENT_ID"; // Must exactly match this page's own URL, and the callback URL // registered on the GitHub OAuth App. const GITHUB_REDIRECT_URI = window.location.origin + window.location.pathname; const BACKEND_BASE_URL = "http://localhost:8000";
const statusEl = document.getElementById("status");
function logStatus(label, data) { statusEl.textContent = `${label}\n${JSON.stringify(data, null, 2)}`; }
async function startGithubLogin() { try { const response = await fetch(`${BACKEND_BASE_URL}/api/social-auth/github/state/`);
if (!response.ok) { throw new Error("Could not fetch a GitHub state token."); }
const { state } = await response.json();
const authorizeUrl = "https://github.com/login/oauth/authorize" + `?client_id=${GITHUB_CLIENT_ID}` + `&redirect_uri=${encodeURIComponent(GITHUB_REDIRECT_URI)}` + "&scope=user:email" + `&state=${encodeURIComponent(state)}`;
window.location.href = authorizeUrl; } catch (error) { logStatus("Could not start GitHub sign-in", { message: error.message }); } }
async function finishGithubLogin() { const params = new URLSearchParams(window.location.search); const code = params.get("code"); const state = params.get("state");
if (!code || !state) { return; // not on the callback redirect - nothing to do }
// Strip code/state from the URL either way, so a page refresh // doesn't try to redeem the same code a second time. window.history.replaceState({}, document.title, window.location.pathname);
try { const response = await fetch(`${BACKEND_BASE_URL}/api/social-auth/github/login/`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code, state }), });
const data = await response.json();
if (!response.ok) { logStatus("GitHub sign-in failed", data); return; }
logStatus("Signed in", data); } catch (error) { logStatus("Network error", { message: error.message }); } }
document.getElementById("github-button").addEventListener("click", startGithubLogin); window.addEventListener("load", finishGithubLogin); </script></body></html>To link an already-signed-in user to a GitHub account instead of logging in, send code and state to /api/social-auth/github/link/ with the user’s existing access token in the Authorization header. The redirect and state handling are identical.
Endpoints
Section titled “Endpoints”Get a state token
Section titled “Get a state token”GET /api/social-auth/github/state/Response:
{ "state": "<SIGNED_STATE_TOKEN>" }No authentication required. State tokens expire after SOCIAL_AUTH_STATE_MAX_AGE seconds (5 minutes by default) - request a fresh one if the user takes a while to approve on GitHub’s side.
POST /api/social-auth/github/login/Content-Type: application/json
{ "code": "<AUTHORIZATION_CODE>", "state": "<SIGNED_STATE_TOKEN>"}Response:
{ "access": "<JWT_ACCESS_TOKEN>", "refresh": "<JWT_REFRESH_TOKEN>" }Logs the user in if a matching GitHub account is already linked, or creates a new user and link if this is the first time. No authentication required.
Link account
Section titled “Link account”Requires authentication.
POST /api/social-auth/github/link/Authorization: Bearer <JWT_ACCESS_TOKEN>Content-Type: application/json
{ "code": "<AUTHORIZATION_CODE>", "state": "<SIGNED_STATE_TOKEN>"}Returns 204 No Content on success. Fails if that GitHub account is already linked to a different user.
Unlink account
Section titled “Unlink account”Requires authentication.
POST /api/social-auth/github/unlink/Authorization: Bearer <JWT_ACCESS_TOKEN>Content-Type: application/json
{ "password": "<USER_PASSWORD>"}Returns 204 No Content on success.
Common errors
Section titled “Common errors”Every failure comes back as a 400:
| What happened | Response body |
|---|---|
| State token missing, invalid, or expired | {"message": ["Invalid or expired authentication state."]} |
| GitHub unreachable or the request times out | {"message": ["Unable to contact GitHub. Please try again later."]} (wording varies slightly by failure type, but the shape is always the same) |
| GitHub account has no verified primary email | {"message": ["Your GitHub account does not have a primary verified email address."]} |
| Account is soft-deleted | {"message": ["This account is no longer available."]} |
| GitHub account already linked to another user | {"message": ["This GitHub account is already linked to another user."]} |
| Unlink: wrong password | {"non_field_errors": ["Incorrect password."]} |
| Unlink: no GitHub account linked | {"non_field_errors": ["No linked github account found."]} |