Skip to content

test(jwt): pin the claim checks and every stored format; fix the rotation and blind-index guides - #481

Merged
Jaro-c merged 3 commits into
developfrom
fix/tokens-review-round-2
Sep 26, 2026
Merged

Jaro-c merged 3 commits into
developfrom
fix/tokens-review-round-2

Conversation

@Jaro-c

@Jaro-c Jaro-c commented Sep 26, 2026

Copy link
Copy Markdown
Member

Closes #480 in three commits: two are tests only, one is documentation and a package doc comment. No production behaviour changes.

Guides

  • docs/secure-login.md section 5 replaces the refresh hash with the conditional UPDATE ... WHERE id = $2 AND refresh_hash = $3 and a row-count check, the recipe docs/jwt.md step 5 got in docs: fix the recipes that lose credentials #427, and says that reuse detection depends on it.
  • The auth/field package doc's read path recomputes the index of the decrypted value and compares it with the one looked up; the sentence claiming an index hit proves the plaintext is gone.
  • docs/configuration.md describes the credential binding as the length-prefixed construction the code uses; docs/errors.md gains the jwt.ErrTokenOversized and jwt.ErrNotInitialised rows; docs/key-management.md says which modules share one HMAC key and why that waits for a release that can change stored hashes.

jwt tests

  • The claim checks: a token without exp and one issued a day in the future are refused on the access path (the signer fixture now carries a UUIDv7 jti, since fix(jwt): make issuance and verification agree #443's shape check refused the old "jti" first and left these tests green with the option deleted) and on the refresh path; each is paired with the same token accepted. mapJWTError collapses the reason into ErrTokenInvalid, so the acceptance twin is what proves which check fired.
  • The leeway through RotateTokens (20 s past exp accepted with a 30 s leeway, 40 s refused); the refresh-token issuance cap with a 5806-byte issuer refused and a 5600-byte one issuing and rotating; validateConfig refusing an empty audience directly, which New never reaches; the zero-value VerifyRefreshTokenHash guard against the hash an empty key produces, which the old fixture ("any-hash") never reached.
  • FuzzIsUUIDv7 compares against RFC 9562 written as a regular expression apart from the code, with seeds for a moved dash, hex digits where the dashes go, a non-hex byte and a short input; FuzzVerifyAccessToken accepts only the issued access token and requires every refusal to be a documented sentinel.

apikey, credential, field tests

  • Known answers for every stored format, with the expected HMAC and HKDF values computed outside Go under a fixed secret: the apikey hash, the refresh-token hash, the credential hash (length-prefixed purpose, subject, token) and the blind index; and a ciphertext produced today that must keep decrypting, which pins the encryption label, the AAD and the nonce placement.
  • FuzzParseID seeds that reach the lowercase-hex rule and the prefix check; a zero-value apikey.Hash refused.

Sabotage, each edit confirmed to change the file and to compile:

Sabotage Result
drop WithExpirationRequired / WithIssuedAt from parserOptions the access and refresh tests for that claim fail
ignore the leeway both leeway tests fail
drop the zero-value guard in VerifyRefreshTokenHash / in apikey.Hash their tests fail
drop the dash, variant or hex check in isUUIDv7 FuzzIsUUIDv7 fails on a seed
widen isHex to A-F / drop the prefix check in ParseID FuzzParseID fails on a seed
accept a refresh token in VerifyAccessToken FuzzVerifyAccessToken fails on the refresh seed
drop the empty-audience check TestValidateConfig_refusesAnEmptyAudience fails
change the blind-index label / drop the AAD / swap the credential fields / change the apikey or refresh hash input the matching known-answer test fails

The refresh-token issuance cap has a test; its sabotage was not run separately because the shared verifier refuses the oversized token on rotation anyway, so the test fails under it through that path.

gofmt, go vet ./..., go test -race -count=1 ./..., gosec and the nine example builds are clean. Coverage: jwt 95.9%, apikey 96.2%, credential 98.4%, field 92.9%.

Signed-off-by: Jaro-c 75870284+Jaro-c@users.noreply.github.com

…ex read path, and the credential binding

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
…ro-value guard, and the refresh-hash format; give both fuzz targets oracles

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
… and the seeds that reach ParseID's rules

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
@Jaro-c Jaro-c added type:docs Documentation type:test Tests prio:P2 Medium priority status:review In review effort:M Medium area:jwt Subsystem: jwt area:apikey Area: apikey area:field Area: field area:credential Area: credential area:docs Area: docs labels Sep 26, 2026
@Jaro-c
Jaro-c merged commit ee3c562 into develop Sep 26, 2026
19 checks passed
@Jaro-c
Jaro-c deleted the fix/tokens-review-round-2 branch September 26, 2026 01:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:apikey Area: apikey area:credential Area: credential area:docs Area: docs area:field Area: field area:jwt Subsystem: jwt effort:M Medium prio:P2 Medium priority status:review In review type:docs Documentation type:test Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant