402 — Authenticating with REST APIs

Intermediate

Replace the placeholder token from lesson 401 with a real GitHub Personal Access Token stored as GITHUB_TOKEN and learn every authentication pattern used in production REST API work: Bearer token headers, API key headers, and query-string tokens. Credentials are loaded from .env via load_dotenv() and os.environ.get() — never hardcoded. Operational config (base URL, timeout) stays in config.yaml via load_config(). The lesson closes with a reusable requests.Session for authenticated sessions and explicit 401/403 error handling.

Learning Objectives

1
Create a GitHub Personal Access Token (classic) and store it in .env — never in source code
2
Send an authenticated GET request to the GitHub API using a Bearer token in the Authorization header
3
Compare unauthenticated (60 requests/hour) vs authenticated (5,000 requests/hour) GitHub rate limits
4
Demonstrate three auth patterns: Authorization header, X-API-Key header, and query-string parameter
5
Handle 401 Unauthorized and 403 Forbidden errors with specific, actionable error messages
6
Use requests.Session to set authentication headers once and reuse them across all calls
7
Build a reusable authenticated GitHub client that loads GITHUB_TOKEN from .env and operational settings from load_config()
8
Apply the .env + .gitignore + .env.example pattern to prevent credentials from reaching source control
Step 1

Create a GitHub PAT and store it in .env

Create a GitHub Personal Access Token and store it as GITHUB_TOKEN in a new .env file. Fine-grained PATs are preferred for new use, but classic PATs also work for this lesson. GITHUB_TOKEN is the standard environment variable name for GitHub credentials — the same name used by GitHub Actions and most CI/CD platforms. Scripts load it directly via os.environ.get('GITHUB_TOKEN') after calling load_dotenv(), keeping secrets separate from operational config (base URL, timeout) which stays in config.yaml.

Commands to Run

mkdir -p ~/devops-python/lesson-402
cd ~/devops-python/lesson-402
source ~/devops-python/lesson-101/devops-env/bin/activate
# --- Create your GitHub PAT (do this in your browser) ---
# 1. Go to: github.com → Settings → Developer settings → Personal access tokens
# 2. Create either a fine-grained PAT (preferred) or a classic PAT
# 3. Note: devopspath-lesson-402   Expiration: 7 days
# 4. Grant only the minimum access needed for public-repo examples
# 5. Click 'Generate token' — copy the token now (fine-grained tokens start with github_pat_, classic tokens with ghp_; shown only once)
# After copying your token, replace ghp_REPLACE_WITH_YOUR_TOKEN in the command below:

Files for This Step

.envPaste this into the file
# Lesson 402 — replace the placeholder with your real GitHub PAT.
# Never commit this file. It is listed in .gitignore.
GITHUB_TOKEN=ghp_REPLACE_WITH_YOUR_TOKEN
.gitignorePaste this into the file
.env
.env.local
__pycache__/
*.pyc
config.yamlPaste this into the file
environment: development
api_base_url: https://api.github.com
api_timeout: 10
debug: true
max_retries: 3
deploy_host: localhost
deploy_port: 8080

After Saving

python3 -c "
import os
import sys
from pathlib import Path
from dotenv import load_dotenv
sys.path.insert(0, str(Path.home() / 'devops-python' / 'lesson-304'))
from layered_config import load_config
load_dotenv()
config, sources = load_config(
    config_file=Path('config.yaml'),
    env_file=Path('.env'),
)
token = os.environ.get('GITHUB_TOKEN', '')
has_token = bool(token) and not token.startswith('ghp_REPLACE')
print(f'api_base_url: {config.api_base_url}  [source: {sources[\"api_base_url\"]}]')
print(f'api_timeout:  {config.api_timeout}s  [source: {sources[\"api_timeout\"]}]')
print(f'GITHUB_TOKEN set: {has_token} ({len(token)} chars)')
if not has_token:
    print()
    print('ACTION NEEDED: edit .env and replace ghp_REPLACE_WITH_YOUR_TOKEN with your real GITHUB_TOKEN.')
"

What This Does

A GitHub Personal Access Token authenticates your API requests as your GitHub account. A valid token upgrades your rate limit from 60 to 5,000 requests per hour and grants access according to the permissions you configured. Fine-grained PATs (which start with github_pat_) are preferred for new use because they let you scope access more tightly; classic PATs (which start with ghp_) also work for these examples. Store the token immediately in .env as GITHUB_TOKEN because GitHub shows it only once when created. Using GITHUB_TOKEN as the env var name is intentional: it is the standard name used by GitHub Actions, many CI/CD platforms, and real DevOps tooling. Scripts load it with load_dotenv() + os.environ.get('GITHUB_TOKEN', '') — credentials load from the environment, operational settings (base URL, timeout) load from config.yaml. The .gitignore entry for .env is the critical safety layer: it prevents git add . from staging the file that contains your credentials.

Expected Outcome

The python3 verification command prints api_base_url: https://api.github.com, api_timeout: 10s, and GITHUB_TOKEN set: True (<n> chars).

A real GitHub PAT typically starts with either ghp_ (classic) or github_pat_ (fine-grained).

If the placeholder is still in place, the script prints the ACTION NEEDED message.

Pro Tips

  • 1
    A quick sanity check is that a real GitHub PAT usually starts with `ghp_` or `github_pat_` and is much longer than the tutorial placeholder. If `len(token)` is very short, the placeholder is still in place.
  • 2
    Prefer short expiration dates (7–30 days) for tutorial tokens. You can always regenerate. A token that leaks but expires quickly limits the damage window.

Common Mistakes to Avoid

  • āš ļøLeaving the placeholder value `ghp_REPLACE_WITH_YOUR_TOKEN` in .env — the verification script detects this explicitly. Replace `GITHUB_TOKEN=ghp_REPLACE_WITH_YOUR_TOKEN` with your actual token before continuing.
  • āš ļøInstalling the token in the system Python environment instead of the activated venv — confirm the shell prompt shows the venv name `(devops-env)` before running any pip or python3 commands in this lesson.
Was this step helpful?

All Steps (0 / 8 completed)