Skip to content

feat: add API Key authentication for GTFS-RT feed consumers#68

Open
ShinLiX wants to merge 2 commits intoOneBusAway:mainfrom
ShinLiX:feat/add_api_key_for_feed_consumers
Open

feat: add API Key authentication for GTFS-RT feed consumers#68
ShinLiX wants to merge 2 commits intoOneBusAway:mainfrom
ShinLiX:feat/add_api_key_for_feed_consumers

Conversation

@ShinLiX
Copy link
Copy Markdown
Contributor

@ShinLiX ShinLiX commented Mar 17, 2026

Summary

This PR adds API key authentication for feed consumers on GET /gtfs-rt/vehicle-positions.

Previously, the GTFS-Realtime vehicle positions feed could be accessed without any feed-specific authentication. As requested in Milestone 2, we need an authentication for accessing the feed.

The implementation hashes raw API keys with SHA-256 before lookup, rejects missing/invalid/inactive keys, and updates last_used_at on successful requests.

Changes

  1. Added a new APIKey model to represent rows in the api_keys table.
  2. Added a new requireAPIKey middleware for feed access.
  3. Added hashAPIKey(raw string) string, which computes the SHA-256 hash of the provided raw key and returns the hex-encoded string.
  4. Added database migration to create api_keys table and a trigger/function pair to automatically maintain updated_at on updates.
  5. Updated seed_dev.sql to include a development API key entry for local manual testing,

Testing

Test file added:

  • api_key_auth_go:
    • TestHashAPIKey
    • TestRequireAPIKey_MissingHeader
    • TestRequireAPIKey_InvalidKey
    • TestRequireAPIKey_InactiveKey
    • TestRequireAPIKey_StoreFailure
    • TestRequireAPIKey_UpdateLastUsedFailure
    • TestRequireAPIKey_ValidKey
  • store_api_keys_test.go:
    • TestStore_CreateAndGetAPIKeyByHash
    • TestStore_UpdateAPIKeyLastUsed
    • TestStore_GetAPIKeyByHash_Inactive

Running go test ./...: All tests passed

Manual Testing with curl:

  • Active api key loaded by seed_dev.sql:
    curl -i -H "X-API-Key: dev-feed-key" http://localhost:8080/gtfs-rt/vehicle-positions": return status 200 OK
  • Incorrect api key:
    curl -i -H "X-API-Key: dev-feed-key-1234567890abcdef" http://localhost:8080/gtfs-rt/vehicle-positions: return 401 unauthorized

Copy link
Copy Markdown
Member

@aaronbrethorst aaronbrethorst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amelia, this is a well-scoped feature — the SHA-256 key hashing, the APIKeyStore interface for testability, and the thorough test coverage across both the middleware and the store layer are all well done. The seed data with a pre-hashed dev key is a nice touch for local testing. A few things need attention before this can merge.

Critical

  1. Needs rebase on latest main — the branch is missing 000004_add_vehicle_received_index which has since landed. Your migration number (000005) is correct and won't need renumbering, but the branch needs to include 000004 for golang-migrate to run them in sequence.

Important

  1. UpdateAPIKeyLastUsed failure blocks feed access (api_key_auth.go:51-54). If the last_used_at update fails (transient DB error, connection hiccup), the middleware returns 500 and the feed consumer gets nothing — even though their API key was already validated. The last_used_at update is an audit convenience, not a security requirement. On a hot path like the GTFS-RT feed, this should not block the response. Log the error and proceed:

    if err := store.UpdateAPIKeyLastUsed(r.Context(), apiKey.ID); err != nil {
        slog.Error("failed to update api key last_used_at", "api_key_id", apiKey.ID, "error", err)
    }
  2. Missing slog logging on 500 responses (api_key_auth.go:42, api_key_auth.go:52). When GetAPIKeyByHash returns a non-pgx error, the handler returns 500 but doesn't log the error. The established pattern in this codebase (see handlers.go, auth.go) is to call slog.Error(...) before returning a 500 so operators can diagnose failures. Please add structured logging to both 500 paths.

  3. docker-compose.yml uses ${JWT_SECRET} from host environment (docker-compose.yml:26). The previous docker-compose had no JWT_SECRET set at all, and the app would fail on startup. Your fix is reasonable, but using ${JWT_SECRET} requires developers to set the variable in their host environment or a .env file, which is a friction change for anyone running docker compose up without extra setup. Consider using a hardcoded dev value (like other PRs have done) to keep local dev zero-config:

    JWT_SECRET: "this-is-a-local-dev-secret-1234567890"

Suggestions

  1. Missing trailing newlines in db/query.sql, migrations/000005_add_api_keys.down.sql, migrations/000005_add_api_keys.up.sql, and seed_dev.sql. Most editors and POSIX tools expect a trailing newline. Minor, but easy to fix while you're in there.

  2. The updated_at trigger (migrations/000005_add_api_keys.up.sql:12-23) is a valid approach, but the rest of the codebase handles updated_at at the application/SQL layer (SET updated_at = NOW() in queries). The trigger works fine, but the inconsistency could confuse future contributors who expect one pattern and find another. Not a blocker — just something to be aware of.

Strengths

  • SHA-256 key hashing: Raw keys are never stored, which is the right approach for API key security.
  • Complete test coverage: Seven middleware tests covering every branch (missing header, invalid key, inactive key, store failure, update failure, valid key) plus three integration tests on the store layer.
  • Clean interface design: APIKeyStore is minimal and follows the project's established pattern of handler-scoped interfaces.
  • Dev seed data: The pre-hashed dev-feed-key in seed_dev.sql with the raw key documented in a comment makes local testing frictionless.

Recommended Action

Request changes. Rebase on latest main, make the UpdateAPIKeyLastUsed failure non-blocking, and add slog logging to the 500 paths.

@ShinLiX ShinLiX force-pushed the feat/add_api_key_for_feed_consumers branch from 6c411eb to 567bcdf Compare March 28, 2026 04:16
@ShinLiX
Copy link
Copy Markdown
Contributor Author

ShinLiX commented Mar 28, 2026

Hi Aaron, thanks for your review! All issues mentioned are addressed:

  • rebased on latest main so migrations run in order
  • made UpdateAPIKeyLastUsed failure non-blocking
  • added slog.Error(...) logging for 500 paths
  • updated local Docker config to include a dev JWT_SECRET
  • fixed trailing newlines in touched SQL files
  • migration number changed to 7 as agreed

Thank you, please let me know if these work!

@ShinLiX ShinLiX requested a review from aaronbrethorst March 28, 2026 04:19
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.

2 participants