Computing FoundationsAPIs and the Web › Day 25

Hands-on lab — Day 25: API Authentication: Keys, Tokens, and OAuth

Commands

Setup

cd labs/sections/computing-foundations/day-025-api-authentication-keys-tokens-and-oauth

Run

bash examples/auth_demo.sh
bash starter/auth_demo.sh

Test

bash tests/run_tests.sh

File tree

examples/auth_demo.sh
expected-output/FIELDS.md
expected-output/sample-run.txt
metadata.yml
README.md
requirements/README.md
security.md
starter/auth_demo.sh
starter/auth-worksheet.md
tests/run_tests.sh
troubleshooting.md

Lab README

Day 025 lab — Authenticate to an API

Lesson

Purpose

Day 25's lesson explains how APIs verify who is calling. This lab makes it concrete: you use curl to send each common authentication scheme to a public test server and watch it succeed and fail. You will see Basic auth return 200 for the right password and 401 for the wrong one, send a bearer token, echo a key header back off the server, and — the habit that matters most — read a token from an environment variable instead of typing it inline. Every credential here is an obviously fake placeholder, and the test server accepts any of them, so no API key and no account are required.

Learning objectives

  • Send Basic auth, a bearer token, and an API key header with curl.
  • Read authentication outcomes as HTTP status codes (200 success, 401 a missing or wrong credential).
  • Confirm a bearer token is accepted and echoed by the server.
  • Read a credential from an environment variable so the secret never appears inline — the safe pattern you will use with every real key.
  • Run an automated test that degrades gracefully offline and exits 0.

Prerequisites

  • The Day 25 lesson (read it first — it explains every scheme this lab sends).
  • Day 18 (HTTP status codes) and Day 11 (environment variables) help.
  • A terminal with curl: Terminal.app (macOS), any terminal (Linux), or WSL/Git Bash (Windows).
  • No programming experience required; every command is given and explained.

Supported operating systems

  • macOS — fully supported (tested on macOS with curl 8.7.1).
  • Linux — fully supported (any distribution with curl).
  • Windows — run the scripts unchanged in WSL or Git Bash.

Hardware requirements

Any computer that can run a terminal and reach the internet. The lab only sends small HTTP requests; it needs no minimum RAM, disk, or GPU.

Required software

  • bash (3.2 or newer — preinstalled on macOS and Linux).
  • curl (preinstalled on macOS and most Linux; sudo apt install curl on Debian/Ubuntu). Verify with curl --version.

Free and open-source options

Everything in this lab is free: bash and curl are open-source or ship with your OS, and httpbin.org is a free public test service. No account, API key, or purchase is needed — the endpoints accept any (fake) credentials on purpose.

Installation

None. Clone the repository (or copy this directory) and you are ready:

cd labs/sections/computing-foundations/day-025-api-authentication-keys-tokens-and-oauth

File structure

day-025-api-authentication-keys-tokens-and-oauth/
├── README.md                       ← you are here
├── metadata.yml                    ← machine-readable lab metadata
├── starter/
│   ├── auth_demo.sh                ← YOUR working file (4 exercises)
│   └── auth-worksheet.md           ← worksheet for the practice assignment
├── examples/
│   └── auth_demo.sh                ← completed reference implementation
├── tests/
│   └── run_tests.sh                ← automated checks (skip gracefully offline)
├── expected-output/
│   ├── sample-run.txt              ← real captured run (macOS, online)
│   └── FIELDS.md                   ← required output lines on every platform
├── requirements/
│   └── README.md                   ← dependency statement (curl only)
├── troubleshooting.md
└── security.md

How to run

From this directory:

## 1. See the finished result first
bash examples/auth_demo.sh

## 2. Your task: complete the four exercises in the starter, then run it
bash starter/auth_demo.sh

## 3. Check your work
bash tests/run_tests.sh

What the commands do

  • bash examples/auth_demo.sh — runs the reference script: it checks connectivity, then sends Basic auth with correct credentials (expecting 200) and wrong credentials (expecting 401), a bearer token in the Authorization header, an X-API-Key header the server echoes back, and a bearer token read from the DEMO_TOKEN environment variable. Offline, it announces the skip and exits 0.
  • bash starter/auth_demo.sh — the same skeleton with four exercises left as "NOT DONE YET" placeholder echoes; each comment names the exact curl command to run. Edit the file in any text editor and replace each placeholder line with the command given.
  • bash tests/run_tests.sh — runs structure checks (files present, the example exercises every scheme, the starter names its exercises, both scripts parse, no realistic long key-shaped string is present) and, when online, network checks: correct Basic auth returns 200, wrong credentials return 401, and the bearer endpoint returns 200. Offline or during a transient httpbin 503, the network checks are skipped, not failed. Exits 0 unless a structure check fails.

Expected output

See expected-output/sample-run.txt — a real captured run. The key lines:

--- 1. Basic auth with CORRECT credentials (expect 200) ---
HTTP status: 200

--- 2. Basic auth with WRONG credentials (expect 401) ---
HTTP status: 401

--- 3. Bearer token in the Authorization header ---
{
  "authenticated": true,
  "token": "token-example-123"
}

Your X-Amzn-Trace-Id value differs on every request — httpbin assigns it. expected-output/FIELDS.md lists exactly which lines must appear on every platform.

Validation steps

  1. Run bash examples/auth_demo.sh — it must reach === End of demo === without errors.
  2. Confirm Basic auth printed 200 for correct credentials and 401 for wrong ones.
  3. Confirm the bearer endpoint returned "authenticated": true and echoed your token.
  4. Complete starter/auth_demo.sh (no "NOT DONE YET" placeholder lines left) and run it — it must produce the same statuses.
  5. Run the tests (next section) — all structure checks must pass and the script must exit 0.

Tests

bash tests/run_tests.sh

The command exits 0 on success (no structure check failed), whether online or offline, so it can run in CI. Online with httpbin healthy, the final line reads 16 checks, 0 failure(s), 0 skip(s).; when httpbin is transiently overloaded, the three network checks are skipped (13 checks, 0 failure(s), 3 skip(s).) and the script still exits 0 — a server outage is not your bug.

Cleanup

Nothing to clean up: the scripts only send HTTP requests and write no files. To reset your work, restore the starter from git: git checkout -- starter/auth_demo.sh.

Troubleshooting

See troubleshooting.md for the full list (000 and 503 responses, Basic auth 401, empty $DEMO_TOKEN, missing curl, Windows).

Security notes

See security.md. Short version: this lab uses only fake credentials against a public test server. Never put a real API key in a script or commit one — store real keys in an environment variable or a gitignored .env, use least privilege, and rotate any leaked key immediately.

Extension exercises

  1. Prove base64 is not encryption: printf 'user:pass' | base64 then pipe the result to base64 --decode and watch user:pass come straight back with no key.
  2. Send the X-API-Key in the URL query string instead of a header (https://httpbin.org/get?api_key=demo-key), see it echoed under args, and write one sentence on why that placement leaks the key into logs.
  3. Verify a missing credential fails: curl -s -o /dev/null -w '%{http_code}\n' https://httpbin.org/bearer and confirm you get 401 with no token sent.
  • Previous day: Day 24 — JSON and Data Serialization (labs/sections/computing-foundations/day-024-json-and-data-serialization/).
  • Next day: Day 26 — Webhooks and Event-Driven APIs (labs/sections/computing-foundations/day-026-webhooks-and-event-driven-apis/, to be written).

Expected output

FIELDS.md

# Required output lines (all platforms)

A correct online run of `examples/auth_demo.sh` prints, in order:

1. `=== API Authentication Demo ===`
2. Basic auth, correct credentials: `HTTP status: 200`
3. Basic auth, wrong credentials: `HTTP status: 401`
4. Bearer endpoint: JSON with `"authenticated": true` and `"token": "token-example-123"`
5. Headers endpoint: JSON echoing your `X-Api-Key: demo-key` (httpbin normalizes the
   header name's capitalization to `X-Api-Key`)
6. Env-var bearer: JSON with `"authenticated": true` and `"token": "token-from-env-example"`
7. `=== End of demo ===`

`sample-run.txt` in this directory is a real captured run (macOS, curl 8.7.1,
online, 2026-07-12).

## Platform notes

- **Linux** produces byte-for-byte the same output; only the `User-Agent`
  string (the local curl version) and the `X-Amzn-Trace-Id` value differ.
- **Windows** users run the same commands in WSL or Git Bash; native
  PowerShell uses `Invoke-RestMethod` with `-Headers`, which is out of scope
  for this lab.
- The `X-Amzn-Trace-Id` value is assigned fresh by httpbin on every request,
  so it will never match the sample exactly — that is expected.
- If any request prints `503`, httpbin is transiently overloaded (a shared
  free service). That is the server, not your credential; retry shortly. The
  test script treats a transient 5xx as a SKIP, not a failure.

sample-run.txt

Real captured run of `bash examples/auth_demo.sh`
(macOS, curl 8.7.1, online, 2026-07-12). Your X-Amzn-Trace-Id value will
differ on every request — it is assigned by httpbin, not by you.

=== API Authentication Demo ===
Target: https://httpbin.org (public test server; accepts any credentials)

--- 1. Basic auth with CORRECT credentials (expect 200) ---
$ curl -u user:pass https://httpbin.org/basic-auth/user/pass
HTTP status: 200

--- 2. Basic auth with WRONG credentials (expect 401) ---
$ curl -u user:wrongpass https://httpbin.org/basic-auth/user/pass
HTTP status: 401

--- 3. Bearer token in the Authorization header ---
$ curl -H 'Authorization: Bearer token-example-123' https://httpbin.org/bearer
{
  "authenticated": true,
  "token": "token-example-123"
}

--- 4. API key in a custom header, echoed back by the server ---
$ curl -H 'X-API-Key: demo-key' https://httpbin.org/headers
{
  "headers": {
    "Accept": "*/*",
    "Host": "httpbin.org",
    "User-Agent": "curl/8.7.1",
    "X-Amzn-Trace-Id": "Root=1-6a538ecc-412e97e705dd106650616580",
    "X-Api-Key": "demo-key"
  }
}

--- 5. Bearer token READ FROM AN ENVIRONMENT VARIABLE (the safe pattern) ---
$ export DEMO_TOKEN=...   (kept out of code and git)
$ curl -H "Authorization: Bearer $DEMO_TOKEN" https://httpbin.org/bearer
{
  "authenticated": true,
  "token": "token-from-env-example"
}

=== End of demo ===

--- Offline behavior ---
With no network, the script prints:

=== API Authentication Demo ===
Target: https://httpbin.org (public test server; accepts any credentials)

No network access — skipping the live requests.
Online, this script sends Basic auth, a bearer token, and a key header to https://httpbin.org.
=== End of demo ===

Source files

examples/auth_demo.sh (2755 bytes)
#!/usr/bin/env bash
# Day 025 lab — completed reference implementation.
# Authenticate to a public test API (httpbin.org) with each common scheme:
#   1. Basic auth, correct credentials      -> 200
#   2. Basic auth, wrong credentials        -> 401
#   3. Bearer token in the Authorization header
#   4. An API key in a custom header, echoed back
#   5. A bearer token read from an environment variable (the safe pattern)
#
# httpbin's auth endpoints accept ANY credentials, so no real key is needed.
# Every credential in this script is an obviously fake placeholder.
# If the network is unavailable, the script says so and exits 0 (nothing to prove offline).
set -uo pipefail

API="https://httpbin.org"
# curl options: fail quietly on server errors, cap the wait, ride past transient 5xx.
CURL=(curl -s --max-time 25 --retry 5 --retry-delay 2)

echo "=== API Authentication Demo ==="
echo "Target: ${API} (public test server; accepts any credentials)"
echo

# Bail out gracefully when offline so the demo never hangs or errors.
if ! curl -s -o /dev/null --max-time 12 "${API}/status/200" 2>/dev/null; then
  echo "No network access — skipping the live requests."
  echo "Online, this script sends Basic auth, a bearer token, and a key header to ${API}."
  echo "=== End of demo ==="
  exit 0
fi

echo "--- 1. Basic auth with CORRECT credentials (expect 200) ---"
echo "\$ curl -u user:pass ${API}/basic-auth/user/pass"
code="$("${CURL[@]}" -o /dev/null -w '%{http_code}' -u user:pass "${API}/basic-auth/user/pass")"
echo "HTTP status: ${code}"
echo

echo "--- 2. Basic auth with WRONG credentials (expect 401) ---"
echo "\$ curl -u user:wrongpass ${API}/basic-auth/user/pass"
code="$("${CURL[@]}" -o /dev/null -w '%{http_code}' -u user:wrongpass "${API}/basic-auth/user/pass")"
echo "HTTP status: ${code}"
echo

echo "--- 3. Bearer token in the Authorization header ---"
echo "\$ curl -H 'Authorization: Bearer token-example-123' ${API}/bearer"
"${CURL[@]}" -H "Authorization: Bearer token-example-123" "${API}/bearer"
echo

echo "--- 4. API key in a custom header, echoed back by the server ---"
echo "\$ curl -H 'X-API-Key: demo-key' ${API}/headers"
"${CURL[@]}" -H "X-API-Key: demo-key" "${API}/headers"
echo

echo "--- 5. Bearer token READ FROM AN ENVIRONMENT VARIABLE (the safe pattern) ---"
# The secret lives in a variable, not inline in the command. Here it is a fake value;
# for a real API you would 'export' the key from a gitignored .env, never hard-code it.
export DEMO_TOKEN="token-from-env-example"
echo "\$ export DEMO_TOKEN=...   (kept out of code and git)"
echo "\$ curl -H \"Authorization: Bearer \$DEMO_TOKEN\" ${API}/bearer"
"${CURL[@]}" -H "Authorization: Bearer ${DEMO_TOKEN}" "${API}/bearer"
echo

echo "=== End of demo ==="
metadata.yml (739 bytes)
lesson_id: D025
day: 25
kind: api-example
languages: [bash]
setup_commands:
  - cd labs/sections/computing-foundations/day-025-api-authentication-keys-tokens-and-oauth
run_commands:
  - bash examples/auth_demo.sh
  - bash starter/auth_demo.sh
test_commands:
  - bash tests/run_tests.sh
cleanup_commands:
  - 'git checkout -- starter/auth_demo.sh  # optional: reset your work'
requires_network: true
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-12'
executed_on: 'macOS (curl 8.7.1), online, bash tests/run_tests.sh → 16 checks, 0 failure(s), 0 skip(s); example run gave Basic 200 / Basic 401 / bearer authenticated:true. When httpbin returns a transient 503 the network checks SKIP and the script still exits 0.'
requirements/README.md (794 bytes)
# Dependencies — Day 025 lab

**Only `curl`.** This lab has no installable dependencies beyond a POSIX
shell and the `curl` command:

- `bash` ≥ 3.2 (preinstalled on macOS and every mainstream Linux distribution)
- `curl` (preinstalled on macOS and most Linux; on Debian/Ubuntu install with
  `sudo apt install curl`, on Fedora with `sudo dnf install curl`)

Check that curl is present:

```bash
curl --version
```

There is deliberately no `requirements.txt` or `package.json` here. The lab
talks to a public test server (`httpbin.org`) whose authentication endpoints
accept **any** credentials, so **no API key and no account are required**.
The lab needs network access to reach that server; with no network, the
scripts and tests degrade gracefully (they announce the skip and exit 0).
starter/auth_demo.sh (2897 bytes)
#!/usr/bin/env bash
# Day 025 lab — YOUR working file.
# Complete the 4 numbered exercises below. Each names the exact curl command
# to run. httpbin.org accepts ANY credentials, so no real key is needed —
# every value here is an obviously fake placeholder.
#
# Run it with:   bash starter/auth_demo.sh
# When you are done, none of the "NOT DONE YET" placeholder lines should remain.
set -uo pipefail

API="https://httpbin.org"
CURL=(curl -s --max-time 25 --retry 5 --retry-delay 2)

echo "=== API Authentication Demo (starter) ==="

# Skip the live requests when offline so the script never hangs.
if ! curl -s -o /dev/null --max-time 12 "${API}/status/200" 2>/dev/null; then
  echo "No network access — connect to the internet and re-run to complete the exercises."
  exit 0
fi

# ---------------------------------------------------------------------------
# Exercise 1: Basic auth with CORRECT credentials — expect HTTP 200.
#   Replace the placeholder echo below with a command that prints just the code:
#     "${CURL[@]}" -o /dev/null -w '%{http_code}\n' -u user:pass \
#       "${API}/basic-auth/user/pass"
echo "--- Exercise 1: Basic auth, correct credentials (expect 200) ---"
echo "NOT DONE YET — run the curl -u user:pass command described above"

# ---------------------------------------------------------------------------
# Exercise 2: Basic auth with WRONG credentials — expect HTTP 401.
#   Replace the placeholder echo below with the same command but a wrong password:
#     "${CURL[@]}" -o /dev/null -w '%{http_code}\n' -u user:wrongpass \
#       "${API}/basic-auth/user/pass"
echo "--- Exercise 2: Basic auth, wrong credentials (expect 401) ---"
echo "NOT DONE YET — run the curl -u user:wrongpass command described above"

# ---------------------------------------------------------------------------
# Exercise 3: Send a BEARER TOKEN in the Authorization header.
#   Replace the placeholder echo below with:
#     "${CURL[@]}" -H "Authorization: Bearer token-example-123" "${API}/bearer"
#   The endpoint should return JSON with "authenticated": true and your token.
echo "--- Exercise 3: Bearer token in the Authorization header ---"
echo "NOT DONE YET — run the curl -H 'Authorization: Bearer ...' command described above"

# ---------------------------------------------------------------------------
# Exercise 4: Read a bearer token from an ENVIRONMENT VARIABLE (the safe pattern).
#   Set a fake token in a variable, then reference the variable so the secret
#   never appears inline. Replace the placeholder echo below with these two lines:
#     export DEMO_TOKEN="token-example-123"
#     "${CURL[@]}" -H "Authorization: Bearer ${DEMO_TOKEN}" "${API}/bearer"
echo "--- Exercise 4: Bearer token from an environment variable ---"
echo "NOT DONE YET — export DEMO_TOKEN and run curl with \$DEMO_TOKEN as described above"

echo "=== End of demo ==="
starter/auth-worksheet.md (2444 bytes)
# Authentication worksheet — Day 025

Fill this in from a real run of the commands (via `bash examples/auth_demo.sh`
or by running each `curl` yourself). Every credential here is fake and
httpbin.org accepts any of them, so you need no account and no real key.

## 1. Basic auth status codes

Run both and record the exact HTTP status code:

```bash
curl -s -o /dev/null -w '%{http_code}\n' -u user:pass       https://httpbin.org/basic-auth/user/pass
curl -s -o /dev/null -w '%{http_code}\n' -u user:wrongpass  https://httpbin.org/basic-auth/user/pass
```

- Status code for CORRECT credentials (`user:pass`): __________
- Status code for WRONG credentials (`user:wrongpass`): __________
- What does each code mean, in one sentence?
  - 200: ______________________________________________________
  - 401: ______________________________________________________

## 2. Bearer token

```bash
curl -s -H "Authorization: Bearer token-example-123" https://httpbin.org/bearer
```

- Did the endpoint accept your token (did the JSON say `"authenticated": true`)?  __________
- What token did it echo back? __________
- Which HTTP header carried the token, and in what exact format? __________

## 3. Key in a header

```bash
curl -s -H "X-API-Key: demo-key" https://httpbin.org/headers
```

- Did your `X-API-Key` header appear in the server's echo of the headers it saw?  __________
- Why is sending a key in a header safer than sending it in the URL query string?
  Name two places a query-string key can leak:
  1. ______________________________________________________
  2. ______________________________________________________

## 4. Storing a real key safely (the important part)

Imagine you will call a paid API tomorrow. Write 4–6 sentences answering:

- What environment variable name would you read the key from?
- Where would the `.env` file live, and why must it be in `.gitignore`?
- Why should you never hard-code the key in your source?
- What would you do the *instant* you discovered the key had been committed to a
  public repository — and why is deleting the commit not enough?

Your answer:

_________________________________________________________________
_________________________________________________________________
_________________________________________________________________
_________________________________________________________________
_________________________________________________________________
tests/run_tests.sh (4613 bytes)
#!/usr/bin/env bash
# Tests for the Day 025 lab. Run from the lab directory:
#   bash tests/run_tests.sh
#
# Two kinds of checks:
#   * Structure checks always run and must pass (files present, the example
#     script exercises each scheme, the starter names its exercises, both
#     scripts parse, and no realistic long key-shaped string is present).
#   * Network checks run only when the internet is reachable. Offline, they
#     are SKIPPED with a message. When online but the public test server
#     (httpbin.org) is transiently returning 5xx, the affected check is also
#     SKIPPED rather than failed — an external outage is not the learner's bug.
#
# The script exits 0 when no structure check failed, whether online or offline.
set -u

lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
API="https://httpbin.org"
failures=0
checks=0
skips=0

pass() { checks=$((checks + 1)); echo "  ok:   $1"; }
fail() { checks=$((checks + 1)); failures=$((failures + 1)); echo "  FAIL: $1"; }
skip() { skips=$((skips + 1)); echo "  skip: $1"; }

echo "== Structure checks =="
example="${lab_dir}/examples/auth_demo.sh"
starter="${lab_dir}/starter/auth_demo.sh"

[ -f "${example}" ] && pass "example script exists" || fail "example script missing"
[ -f "${starter}" ] && pass "starter script exists" || fail "starter script missing"
[ -f "${lab_dir}/starter/auth-worksheet.md" ] && pass "worksheet exists" || fail "worksheet missing"

# The example must exercise each scheme.
for needle in "user:pass" "user:wrongpass" "Authorization: Bearer" "X-API-Key" "DEMO_TOKEN"; do
  if grep -q -- "${needle}" "${example}"; then pass "example uses '${needle}'"; else fail "example missing '${needle}'"; fi
done

# The starter must name its four numbered exercises.
ex_count="$(grep -cE 'Exercise [1-4]:' "${starter}" 2>/dev/null || true)"
if [ "${ex_count:-0}" -ge 4 ]; then pass "starter has 4 numbered exercises"; else fail "starter has 4 numbered exercises (found ${ex_count:-0})"; fi

# Both scripts must be valid bash.
if bash -n "${example}" 2>/dev/null; then pass "example has valid bash syntax"; else fail "example has a syntax error"; fi
if bash -n "${starter}" 2>/dev/null; then pass "starter has valid bash syntax"; else fail "starter has a syntax error"; fi

# Safety: no realistic long key-shaped string should appear anywhere in the lab.
# (Fake placeholders like token-example-123 are short and hyphenated; a real
# key is a long unbroken run of letters/digits. Flag any 32+ char run.)
if grep -rInE '[A-Za-z0-9]{32,}' "${lab_dir}" >/dev/null 2>&1; then
  fail "a long key-shaped string is present — use only short fake placeholders"
else
  pass "no realistic long key-shaped strings present"
fi

echo
echo "== Network checks =="
if ! curl -s -o /dev/null --max-time 12 "${API}/status/200" 2>/dev/null; then
  skip "no network access — skipping all live-request checks (expected offline)"
else
  pass "network reachable (${API} responded)"

  # Check 1: correct Basic auth returns 200.
  ok="$(curl -o /dev/null -s --max-time 25 --retry 5 --retry-delay 2 \
    -w '%{http_code}' -u user:pass "${API}/basic-auth/user/pass" 2>/dev/null)" || ok="000"
  case "${ok}" in
    200) pass "correct Basic auth returned HTTP 200" ;;
    5* | 000) skip "Basic-auth-correct check — httpbin transiently unavailable (got '${ok}'); retry later" ;;
    *) fail "correct Basic auth should return 200 (got '${ok}')" ;;
  esac

  # Check 2: wrong Basic auth returns 401. A 401 is not a transient error, so
  # --retry returns it without retrying; a 5xx means the server is overloaded.
  bad="$(curl -o /dev/null -s --max-time 25 --retry 5 --retry-delay 2 \
    -w '%{http_code}' -u user:wrongpass "${API}/basic-auth/user/pass" 2>/dev/null)" || bad="000"
  case "${bad}" in
    401) pass "wrong Basic auth returned HTTP 401" ;;
    5* | 000) skip "Basic-auth-wrong check — httpbin transiently unavailable (got '${bad}'); retry later" ;;
    *) fail "wrong Basic auth should return 401 (got '${bad}')" ;;
  esac

  # Check 3: the bearer endpoint accepts a token and returns 200.
  br="$(curl -o /dev/null -s --max-time 25 --retry 5 --retry-delay 2 \
    -w '%{http_code}' -H "Authorization: Bearer token-example-123" "${API}/bearer" 2>/dev/null)" || br="000"
  case "${br}" in
    200) pass "bearer endpoint accepted the token (HTTP 200)" ;;
    5* | 000) skip "bearer check — httpbin transiently unavailable (got '${br}'); retry later" ;;
    *) fail "bearer endpoint should return 200 (got '${br}')" ;;
  esac
fi

echo
echo "${checks} checks, ${failures} failure(s), ${skips} skip(s)."
[ "${failures}" -eq 0 ]

Troubleshooting

Troubleshooting — Day 025 lab

Every request prints 000

000 from curl -w '%{http_code}' means no HTTP response arrived — you are offline, or a proxy/firewall is blocking the request. Check connectivity with curl -s -o /dev/null -w '%{http_code}\n' https://httpbin.org/status/200. The scripts and tests detect this and skip the live requests, exiting 0.

A request prints 503

httpbin.org is a shared free service and is occasionally overloaded, returning 503 Service Temporarily Unavailable. That is the server, not your credential. Wait a minute and retry; the scripts already pass --retry 5 --retry-delay 2 to ride past brief outages, and the test script treats a transient 5xx as a SKIP rather than a failure.

Basic auth prints 401 even with user:pass

The path and the credentials must agree: /basic-auth/user/pass expects username user and password pass, exactly what -u user:pass sends. Check for a typo, and make sure you did not change one without the other.

$DEMO_TOKEN comes through empty

You either opened a new shell after export, or mistyped the variable name. Run the export DEMO_TOKEN=... line and the curl line in the same shell, and confirm with echo "$DEMO_TOKEN" before sending.

curl: command not found

Install curl: macOS ships it; on Debian/Ubuntu run sudo apt install curl, on Fedora sudo dnf install curl. Verify with curl --version.

Permission denied when running a script

Run it through bash explicitly: bash starter/auth_demo.sh. You do not need to mark it executable; if you prefer ./starter/auth_demo.sh, first run chmod +x starter/auth_demo.sh.

The bearer endpoint returned 401 instead of 200

httpbin's /bearer endpoint requires the header format to be exactly Authorization: Bearer <token> with a non-empty token. Check the header spelling and that a token follows Bearer (with one space).

Windows: bash is not recognized

Use WSL (wsl --install, then open Ubuntu) or Git Bash, and run the Linux commands unchanged. The scripts are plain curl and behave identically there.

Security notes

Security notes — Day 025 lab

CRITICAL: this lab uses only fake credentials against a public test server. Nothing here is a real secret, and nothing here should ever be a real secret.

  • Only fake credentials. Every value the scripts send — user:pass, token-example-123, demo-key, token-from-env-example — is an obviously fake placeholder. httpbin.org's auth endpoints accept any credentials, so the lab proves how each scheme works without any real key existing.
  • Never put a real API key in a script. A key pasted into source code is one accidental git push away from the entire internet. Read real keys from an environment variable at runtime; the literal string must never appear in a file you might commit, share, or screenshot.
  • Never commit a .env file. Store real keys in a .env file and add that file to .gitignore so git never tracks it. Commit only a .env.example that lists the variable names with empty values.
  • Rotate leaked keys immediately. The instant a real key appears in any public place — a repository, a log, a chat, a screenshot — treat it as burned: revoke it at the provider and issue a new one. Deleting the commit is not enough, because automated scanners find committed keys within minutes and the key may already be copied elsewhere.
  • Least privilege. When a provider lets you scope a key (read-only, one project, a spend cap), create the narrowest key that does the job so a leak has the smallest possible blast radius.
  • Always use TLS. The lab only ever contacts https:// URLs. A credential sent over plain HTTP — including Basic auth, whose base64 encoding hides nothing — is exposed to anyone on the network path.
  • What the scripts do: send HTTP requests with curl to httpbin.org and print the responses. They write no files, need no elevated privileges, and contain no real secret. Read them before running — a habit this course reinforces for every script.