When you build a Python app that needs to interact with a user’s GitHub account, the most secure and user‑friendly way to do it is through GitHub’s OAuth 2.0 authentication flow. In this guide we’ll walk through every step—from registering your OAuth app on GitHub to exchanging the authorization code for an access token and finally calling the GitHub API—all using clean, production‑ready Python code. Whether you’re using Flask, Django, or a simple script, mastering this flow will make your application feel native, protect user data, and boost your SEO by targeting the keyword‑rich phrase “Python GitHub OAuth authentication flow.”
Understanding OAuth 2.0 Basics
What is OAuth 2.0?
OAuth 2.0 is an open standard for delegated authorization. Instead of asking users for their password, an app redirects them to the service provider (GitHub) where they log in and grant specific permissions. The provider then returns a short‑lived access token that the app can use to act on the user’s behalf.
Why GitHub Uses OAuth
- Security: Passwords never leave GitHub’s domain.
- Granular scopes: You can request only the permissions you truly need (e.g.,
repo,read:user). - Revocable tokens: Users can revoke access at any time without changing their password.
Prerequisites for Python GitHub OAuth
- Python 3.8+ installed on your development machine.
- A GitHub account (obviously) and the ability to create an OAuth App in your profile settings.
- A web framework – the examples use Flask, but the same concepts apply to Django, FastAPI, or plain
http.server. - Familiarity with
requestslibrary for making HTTP calls. - Environment variable handling (e.g.,
python‑dotenv) to keep client secrets out of source control.
Step‑by‑Step Implementation in Python
1. Register a GitHub OAuth App
Go to GitHub Settings → Developer settings → OAuth Apps and click “New OAuth App”. Fill in:
- Application name – e.g., “My Python Dashboard”.
- Homepage URL – your local development URL, such as
http://localhost:5000. - Authorization callback URL – the endpoint that will receive the code, e.g.,
http://localhost:5000/callback.
After saving, GitHub will give you a Client ID and a Client Secret**. Store both securely; never commit the secret to Git.
2. Set Up a Flask Project
# app.py
from flask import Flask, redirect, request, session, url_for, jsonify
import os
import requests
from urllib.parse import urlencode
app = Flask(__name__)
app.secret_key = os.getenv('FLASK_SECRET_KEY', 'dev-secret') # replace in production
# Load GitHub credentials from environment
GITHUB_CLIENT_ID = os.getenv('GITHUB_CLIENT_ID')
GITHUB_CLIENT_SECRET = os.getenv('GITHUB_CLIENT_SECRET')
3. Build the Authorization URL
The user is sent to GitHub’s authorize endpoint with a few query parameters. The most common are client_id, redirect_uri, scope, and a random state token to prevent CSRF attacks.
@app.route('/')
def index():
state = os.urandom(16).hex()
session['oauth_state'] = state
params = {
'client_id': GITHUB_CLIENT_ID,
'redirect_uri': url_for('callback', _external=True),
'scope': 'read:user repo',
'state': state,
'allow_signup': 'true'
}
auth_url = f"https://github.com/login/oauth/authorize?{urlencode(params)}"
return f'<a href="{auth_url}">Login with GitHub</a>'
4. Handle the Callback and Exchange Code for a Token
GitHub redirects the user back to /callback with code and state. Verify the state, then POST to GitHub’s token endpoint.
@app.route('/callback')
def callback():
# Verify state parameter
received_state = request.args.get('state')
if received_state != session.get('oauth_state'):
return 'State mismatch. Potential CSRF attack.', 400
code = request.args.get('code')
token_url = 'https://github.com/login/oauth/access_token'
headers = {'Accept': 'application/json'}
data = {
'client_id': GITHUB_CLIENT_ID,
'client_secret': GITHUB_CLIENT_SECRET,
'code': code,
'redirect_uri': url_for('callback', _external=True),
'state': received_state
}
token_response = requests.post(token_url, headers=headers, data=data)
token_json = token_response.json()
access_token = token_json.get('access_token')
if not access_token:
return f"Error retrieving token: {token_json}", 400
session['github_token'] = access_token
return redirect(url_for('profile'))
5. Access the GitHub API with the Token
Now you can make authenticated requests on behalf of the user. Below we fetch the user’s public profile and list their repositories.
@app.route('/profile')
def profile():
token = session.get('github_token')
if not token:
return redirect(url_for('index'))
api_headers = {'Authorization': f'token {token}'}
user_resp = requests.get('https://api.github.com/user', headers=api_headers)
repos_resp = requests.get('https://api.github.com/user/repos', headers=api_headers, params={'per_page': 5})
user_data = user_resp.json()
repos = repos_resp.json()
return jsonify({
'login': user_data.get('login'),
'name': user_data.get('name'),
'public_repos': user_data.get('public_repos'),
'sample_repos': [repo['full_name'] for repo in repos]
})
Common Pitfalls and Debugging Tips
- Missing or mismatched
statevalue: Always store the generated state in the session and compare it on callback. - Wrong redirect URI: The URL registered on GitHub must exactly match the one you send in the request (including trailing slashes).
- Using the wrong token endpoint: GitHub expects a POST to
https://github.com/login/oauth/access_tokenwith anAccept: application/jsonheader; otherwise you’ll receive a URL‑encoded response. - Scope errors: Requesting a scope you haven’t enabled in your OAuth app (or that the user denied) will result in a 403 when calling the API.
- Token expiration: GitHub tokens are long‑lived but can be revoked. Implement graceful fallback (e.g., redirect to login) if an API call returns 401.
Enhancing Security and Best Practices
- Store
GITHUB_CLIENT_SECRETin environment variables or a secret manager; never hard‑code. - Use
httpsin production. OAuth redirects over plain HTTP expose thecodeand can be intercepted. - Set a short
session.permanentlifetime and rotate thestatetoken on each auth attempt. - Limit scopes to the minimum required. For read‑only operations, use
read:userinstead ofrepo. - Validate the
access_tokenby callingGET https://api.github.com/userbefore storing it in the session.
Testing the Flow Locally
Running the Flask app on
Leave a Reply