OAuth is a secure authorization protocol that lets users grant applications access to their data (like GitHub profile info) without sharing their password.
Why use OAuth instead of passwords?
- Users never share their password with your app.
- Users can revoke access at any time.
- Apps only get the permissions they need.
OAuth Roles:
- Client: Your Flask app (wants access)
- Resource Owner: The user (owns the GitHub account)
- Authorization Server: GitHub (verifies user, issues tokens)
- Resource Server: GitHub API (serves user data)
Authorization Code Flow:
- User clicks “Login with GitHub”
- Browser is redirected to GitHub
- User logs in and approves
- GitHub redirects back with a code
- App exchanges code for an access token
- App uses token to fetch user info
- App creates a session and protects routes
- User visits
/and clicks “Login with GitHub” - App redirects to GitHub’s OAuth authorize endpoint
- User logs in and approves
- GitHub redirects back to
/callbackwith a code - App exchanges code for an access token
- App fetches user info from GitHub
- App stores user info in session
- User can access
/dashboard(protected) - User can log out via
/logout
Scripting-APIS/solution/
app_oauth.py
requirements.txt
templates/
login.html
dashboard.html
layout.html
Flask
requests
- Go to GitHub > Settings > Developer settings > OAuth Apps > New OAuth App
- Use these values:
- Application name: Flask OAuth Lab
- Homepage URL: http://localhost:8000
- Authorization callback URL: http://localhost:8000/callback
- After creating, copy your Client ID and Client Secret.
Set these in your shell before running the app:
Linux/macOS/Git Bash: Save to a .env file
export GITHUB_CLIENT_ID=your_client_id
export GITHUB_CLIENT_SECRET=your_client_secret
export GITHUB_REDIRECT_URI=http://localhost:8000/callbackthen source the file
source .envtemplates/layout.html
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}App{% endblock %}</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>templates/login.html
{% extends "layout.html" %}
{% block title %}Login{% endblock %}
{% block content %}
<h2>Login</h2>
<p>Click to log in with GitHub.</p>
<a href="/login">Login with GitHub</a>
{% endblock %}templates/dashboard.html
{% extends "layout.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<h2>Dashboard</h2>
<p>You are logged in as: <b>{{ username }}</b></p>
<p><a href="/logout">Logout</a></p>
{% endblock %}import os
import secrets
import time
import requests
from functools import wraps
from flask import Flask, request, redirect, session, url_for, render_template, abort
app = Flask(__name__)
# Session cookie encryption key (for lab only). Use env var in real apps.
app.secret_key = os.getenv("FLASK_SECRET_KEY", secrets.token_hex(32))
GITHUB_CLIENT_ID = os.getenv("GITHUB_CLIENT_ID", "")
GITHUB_CLIENT_SECRET = os.getenv("GITHUB_CLIENT_SECRET", "")
GITHUB_REDIRECT_URI = os.getenv("GITHUB_REDIRECT_URI", "http://localhost:8000/callback")
def login_required(func):
@wraps(func)
def wrapper(*args, **kwargs):
if "github_user" not in session:
return redirect(url_for("login_page"))
return func(*args, **kwargs)
return wrapper
@app.before_request
def before():
request.start = time.time()
ua = request.headers.get("User-Agent", "unknown")
ip = request.remote_addr
print(f"[REQ] {request.method} {request.path} from {ip} UA={ua}")
@app.after_request
def after(resp):
dur = (time.time() - request.start) * 1000
print(f"[DONE] {request.method} {request.path} {resp.status_code} in {dur:.2f}ms")
return resp
@app.route("/")
def login_page():
"""
Shows login page.
Example:
Open in browser:
http://localhost:8000/
"""
return render_template("login.html")
@app.route("/login")
def login():
"""
Redirects user to GitHub authorization screen.
Example:
Open in browser:
http://localhost:8000/login
"""
if not GITHUB_CLIENT_ID or not GITHUB_CLIENT_SECRET:
abort(500, description="Missing GITHUB_CLIENT_ID or GITHUB_CLIENT_SECRET env vars")
state = secrets.token_urlsafe(24)
session["oauth_state"] = state
params = {
"client_id": GITHUB_CLIENT_ID,
"redirect_uri": GITHUB_REDIRECT_URI,
"scope": "read:user",
"state": state,
"allow_signup": "true",
}
url = "https://github.com/login/oauth/authorize"
req = requests.Request("GET", url, params=params).prepare()
return redirect(req.url)
@app.route("/callback")
def callback():
"""
OAuth callback endpoint.
GitHub redirects here with ?code=...&state=...
Example:
This is hit by GitHub automatically after login.
"""
code = request.args.get("code", "")
state = request.args.get("state", "")
expected_state = session.get("oauth_state", "")
if not code:
abort(400, description="Missing code")
if not state or state != expected_state:
abort(400, description="Invalid state")
token_url = "https://github.com/login/oauth/access_token"
token_resp = requests.post(
token_url,
headers={"Accept": "application/json"},
data={
"client_id": GITHUB_CLIENT_ID,
"client_secret": GITHUB_CLIENT_SECRET,
"code": code,
"redirect_uri": GITHUB_REDIRECT_URI,
},
timeout=10,
)
token_resp.raise_for_status()
token_json = token_resp.json()
access_token = token_json.get("access_token")
if not access_token:
abort(401, description="Failed to obtain access token")
user_resp = requests.get(
"https://api.github.com/user",
headers={
"Authorization": f"Bearer {access_token}",
"Accept": "application/vnd.github+json",
},
timeout=10,
)
user_resp.raise_for_status()
user = user_resp.json()
session["github_user"] = {
"login": user.get("login"),
"id": user.get("id"),
}
return redirect(url_for("dashboard"))
@app.route("/dashboard")
@login_required
def dashboard():
"""
Protected page. Requires OAuth login.
Example:
Open in browser:
http://localhost:8000/dashboard
"""
username = session["github_user"]["login"]
return render_template("dashboard.html", username=username)
@app.route("/logout")
def logout():
"""
Logs user out by clearing session.
Example:
Open in browser:
http://localhost:8000/logout
"""
session.clear()
return redirect(url_for("login_page"))
@app.errorhandler(400)
@app.errorhandler(401)
@app.errorhandler(500)
def handle_error(e):
# For a web lab, keep it simple and readable.
return f"{e.code} {e.name}: {e.description}", e.code
if __name__ == "__main__":
app.run(host="localhost", port=8000, debug=True)python app_oauth.pyOpen: http://localhost:8000/
- Clicking “Login with GitHub” redirects to GitHub
- After authorization, you land on /dashboard
- /dashboard redirects to / if not logged in
- /logout clears session and you can no longer access /dashboard
- Callback URL mismatch: Make sure the callback URL in GitHub matches exactly (including http/https and port).
- Missing env vars: Ensure GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, and GITHUB_REDIRECT_URI are set.
- “bad verification code” errors: Usually means the code was already used or expired; try logging in again.
- localhost vs public IP: For local labs, always use http://localhost:8000 in both your app and GitHub settings.
- Add a
/meendpoint that returns the logged-in user as JSON - Add the
user:emailscope and display the user’s primary email - Add a simple role mapping (e.g.,
{"your_github_username": "admin"}) and protect an/adminpage - Add a logout confirmation page