Nobody has started this yet — be first.
Business impact
Every token this service issues is supposed to expire after TokenTTL (24 hours) -- that's the entire safety net if a token is ever leaked (a logged request, a compromised device, a browser history entry). Right now, issued tokens carry no expiry at all, so a token that leaks today remains a fully valid credential forever -- there is no way to force it to stop working short of rotating the JWT secret for every user at once. This silently removes a core security control without any visible symptom (everything looks like it's working -- tokens are issued, tokens are accepted -- right up until someone needs the expiry to have actually protected them).
Problem
GenerateToken builds the token's Claims without setting ExpiresAt. The JWT library used here (golang-jwt/jwt/v5) only validates an expiry claim if one is present -- it does not require every token to carry one -- so a token with no exp claim at all is treated by ParseToken as permanently valid, no matter how old it is.
Current behavior
A freshly issued JWT has no exp claim at all, so it remains a valid credential indefinitely -- there is no way to force it to expire short of rotating the signing secret.
Expected behavior
Every token GenerateToken issues carries an exp claim set to TokenTTL (24h) from issuance, exactly as the constant and the surrounding code's naming already implies.
Steps to reproduce
Call auth.GenerateToken(secret, someUserID). Parse the resulting token back (e.g. with jwt.ParseWithClaims) and inspect claims.ExpiresAt.
Why this matters
This is a well-known, real JWT footgun: an absent exp claim isn't a validation failure by default in most JWT libraries (including this one) -- it's simply treated as "no expiry constraint to check," which silently converts a supposedly short-lived credential into a permanent one. It requires no malformed input, no attacker action, and no visible error to trigger -- it's just a missing field in a struct literal.
Suggested approach
Compare the Claims struct literal built inside GenerateToken against the TokenTTL constant and the RegisteredClaims fields already being set alongside it (IssuedAt is there -- what's the equivalent field for when the token stops being valid?).
Acceptance criteria
Verification
go test ./practicetickets/... -run TestTicket10 -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
$git checkout -b fix/token-never-expiresFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/token-never-expiresSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign indocker-compose.ymlmigrations/0001_init.sqldocker-entrypoint-initdb.dexpense_trackerdbappcurl http://localhost:8082/healthz
To run locally against your own Postgres instead: cp .env.example .env (edit if your Postgres isn't on localhost:5434), then make migrate (applies migrations/*.sql via psql) and make run (go run ./cmd/server).
Work the tickets in PRACTICE_TICKETS.md (TICKET-01 through TICKET-10); each names one Go test in practicetickets/:
go test ./practicetickets/... -run TestTicket01 -v # a single ticket
./practice_tickets_run.sh # all 10, clean pass/fail summary
This project's own integration suite (cmd/server/*_test.go) needs a real, reachable Postgres -- point TEST_DATABASE_URL at any empty/disposable database (migrations are applied automatically, and every test truncates+reseeds before it runs) and run make test. The practicetickets/ suite defaults to postgres://postgres:postgres@localhost:5434/expense_tracker_test if TEST_DATABASE_URL is unset.
Level 1
Fix a bug
Read existing behaviour, correct it.