Skip to content

fix(auth): validate issuer, audience and expiry on the token response; tell users to retry a rejected code - #18

Merged
jzhao234 merged 1 commit into
mainfrom
fix/central-auth-review-followups
Sep 11, 2026
Merged

jzhao234 merged 1 commit into
mainfrom
fix/central-auth-review-followups

Conversation

@jzhao234

Copy link
Copy Markdown
Contributor

TLDR

Review follow-ups on the central-auth client (#17), cherry-picked onto main because #17 merged while the review was in flight. Explorer now checks that the identity it receives was minted by the issuer it asked, for this client, and is still within its window; a consumed or superseded sign-in code tells the user to try again instead of blaming an outage; two constant-time compares can no longer crash on non-ASCII input; and the bootstrap writes .env owner-only.

Problem

  • exchange() trusted sub, email and nonce and ignored iss, aud and exp. Over an authenticated TLS backchannel this is a misconfiguration risk rather than an attack path, but a response minted for another client or replayed after its window would still have created a session.
  • Every token-endpoint failure became a 502 "authentication service is unavailable". The auth service answers 400 invalid_grant for a consumed, expired, or superseded code, which two tabs signing in at once produce routinely. That is a retry, not an outage.
  • hmac.compare_digest raises TypeError on str arguments containing non-ASCII characters. state comes from the callback query string, so any visitor could turn the callback into a 500. The nonce compare had the same shape.
  • .env was written 0640 although it now holds the client secret, the session secret, and AWS keys.

Fix

  • exchange() requires iss to equal the configured issuer origin, aud to equal the client id, and exp to be an integer not more than 60 seconds in the past (assertions live five minutes). Booleans are rejected as exp.
  • New CodeExchangeRejectedError for HTTP 400 from the token endpoint; the callback maps it to 400 "Sign-in expired. Try again." 401 and 5xx stay 502.
  • State and nonce compares operate on UTF-8 bytes.
  • bootstrap.sh writes .env with mode 0600.

Tests

  • New: wrong aud, wrong iss, expired, string and boolean exp are rejected; 10 seconds of skew is tolerated; a non-ASCII nonce is rejected rather than raising; HTTP 400 is CodeExchangeRejectedError while 401 is a plain CentralAuthError; the callback returns 400 with "Try again" for a rejected code and never exchanges a code when the state is non-ASCII.
  • Updated: the two unit-test fakes return iss, aud and exp like the real service.
  • Ran: pytest -q (98 passed), ruff check, ruff format --check, bash -n and shellcheck on bootstrap.sh, update.sh and explorer-cli.

Review notes and the items left for the owner are on #17.

…; tell users to retry a rejected code

TL;DR: Review follow-ups on the central-auth client. Explorer now checks that the identity it receives was minted by the issuer it asked, for this client, and is still within its window; a consumed or superseded sign-in code tells the user to try again instead of blaming an outage; and two constant-time compares can no longer crash on non-ASCII input.

Problem:
- exchange() trusted sub, email and nonce from the token response and ignored iss, aud and exp. The response arrives over an authenticated TLS backchannel, so this was a misconfiguration risk rather than an attack path, but a response minted for another client or replayed after its window would still have created a session.
- Every token-endpoint failure became a 502 "authentication service is unavailable". The auth service answers 400 invalid_grant for a consumed, expired, or superseded code, which happens whenever a user has two tabs sign in at once (the newer /authorize invalidates the older code). That is a retry, not an outage, and the message sent people looking for a problem that did not exist.
- hmac.compare_digest raises TypeError on str arguments containing non-ASCII characters. The state value comes from the callback query string, so any visitor could turn the callback into a 500. The nonce compare had the same shape.
- The bootstrap wrote .env with mode 0640 although it now holds the central-auth client secret alongside the session secret and AWS keys.

Fix:
- exchange() requires iss to equal the configured issuer origin, aud to equal the client id, and exp to be an integer not more than 60 seconds in the past (assertions live five minutes; the skew tolerance covers clock drift between hosts). Booleans are rejected as exp even though they are ints in Python.
- New CodeExchangeRejectedError for HTTP 400 from the token endpoint; the callback maps it to 400 "Sign-in expired. Try again." 401 (invalid_client) and 5xx stay 502 because they are deployment or availability problems.
- State and nonce compares operate on UTF-8 bytes.
- bootstrap.sh writes .env with mode 0600.

Tests:
- New: wrong aud, wrong iss, expired exp, string exp, and boolean exp are all rejected; 10 seconds of skew is tolerated; a non-ASCII nonce is rejected rather than raising; HTTP 400 from the token endpoint is CodeExchangeRejectedError while 401 stays a plain CentralAuthError; the callback returns 400 with "Try again" for a rejected code and never exchanges a code when the state is non-ASCII.
- Updated: the two unit-test fakes now return iss, aud and exp like the real service.
- Ran: .venv/bin/python -m pytest -q (98 passed), ruff check, ruff format --check, bash -n and shellcheck on bootstrap.sh, update.sh and explorer-cli.

(cherry picked from commit 5cb7fc3)
@jzhao234
jzhao234 merged commit 619202c into main Sep 11, 2026
4 checks passed
@jzhao234
jzhao234 deleted the fix/central-auth-review-followups branch September 11, 2026 17:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant