Skip to content

Latest commit

 

History

History
339 lines (269 loc) · 8.34 KB

File metadata and controls

339 lines (269 loc) · 8.34 KB

Lab: OAuth Login with GitHub in Flask

What is OAuth?

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

Lab Flow

  1. User visits / and clicks “Login with GitHub”
  2. App redirects to GitHub’s OAuth authorize endpoint
  3. User logs in and approves
  4. GitHub redirects back to /callback with a code
  5. App exchanges code for an access token
  6. App fetches user info from GitHub
  7. App stores user info in session
  8. User can access /dashboard (protected)
  9. User can log out via /logout

Project Structure

Scripting-APIS/solution/
  app_oauth.py
  requirements.txt
  templates/
    login.html
    dashboard.html
    layout.html

requirements.txt

Flask
requests

Step 1: Register a GitHub OAuth App

  1. Go to GitHub > Settings > Developer settings > OAuth Apps > New OAuth App
  2. Use these values:
  3. After creating, copy your Client ID and Client Secret.

Step 2: Set Environment Variables

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/callback

then source the file

source .env

Step 3: Example HTML Templates

templates/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 %}

Step 4: Example app_oauth.py

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)

Step 5: Run the App

python app_oauth.py

Open: http://localhost:8000/


Validation Checklist

  • 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

Troubleshooting

  • 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.

Challenge Ideas

  • Add a /me endpoint that returns the logged-in user as JSON
  • Add the user:email scope and display the user’s primary email
  • Add a simple role mapping (e.g., {"your_github_username": "admin"}) and protect an /admin page
  • Add a logout confirmation page