[TT-17065] Fix MinTokenLength: reject silent-accept on create + correct off-by-one operator#8154
Conversation
Tyk's auth middleware short-circuits in CheckSessionAndIdentityForValidKey (middleware.go:603-613) with `if len(key) <= minLength`, so any custom-ID key POSTed at or below `min_token_length` is stored in Redis but silently 403s (response_flag=AKI) on every subsequent auth attempt with the raw ID. Only the wrapped `eyJ...` envelope form authenticates — which consumers migrating from external systems (Apigee, AWS API Gateway, Kong, etc.) don't have. This cost an Evri Apigee migration two weeks of debugging before the length gate surfaced, because the import path gave no signal that every created key was unusable. Fix: at the POST branch of handleAddOrUpdate, reject with 400 when a user-supplied keyName has `len(keyName) <= MinTokenLength`. The error names both the submitted length and the configured minimum so the operator can act on it immediately. Autogenerated creates (empty keyName) are unaffected because generateToken produces an envelope that is always well above any reasonable MinTokenLength. PUT/update is untouched. Test added: TestKeyHandler_RejectCustomKeyBelowMinTokenLength exercises below/equal/above the limit, plus autogenerated create, under MinTokenLength=32. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
This PR addresses two related defects in Files Changed Analysis
Architecture & Impact Assessment
API Flow ComparisonBefore Change (Silent Failure for key length == MinTokenLength): sequenceDiagram
participant Admin
participant Tyk Gateway
participant Redis
participant Consumer
Admin->>+Tyk Gateway: POST /tyk/keys/key-with-len-32 (MinTokenLength=32)
Tyk Gateway->>+Redis: Store session for key
Redis-->>-Tyk Gateway: OK
Tyk Gateway-->>-Admin: 200 OK (Key created)
Note over Consumer, Redis: Later...
Consumer->>+Tyk Gateway: GET /api (Authorization: key-with-len-32)
Note right of Tyk Gateway: Runtime check `len(key) <= 32` fails
Tyk Gateway-->>-Consumer: 403 Forbidden
After Change (Correct Validation and Acceptance): sequenceDiagram
participant Admin
participant Tyk Gateway
participant Consumer
Admin->>+Tyk Gateway: POST /tyk/keys/too-short-key (MinTokenLength=32)
Note right of Tyk Gateway: Create-time check `len(key) < 32` fails
Tyk Gateway-->>-Admin: 400 Bad Request
Admin->>+Tyk Gateway: POST /tyk/keys/key-with-len-32 (MinTokenLength=32)
Tyk Gateway-->>-Admin: 200 OK (Key created)
Note over Consumer, Tyk Gateway: Later...
Consumer->>+Tyk Gateway: GET /api (Authorization: key-with-len-32)
Note right of Tyk Gateway: Runtime check `len(key) < 32` passes
Tyk Gateway-->>-Consumer: 200 OK
Scope Discovery & Context Expansion
Metadata
Powered by Visor from Probelabs Last updated: 2026-06-08T12:17:05.540Z | Triggered by: pr_updated | Commit: 45ea2f3 💡 TIP: You can chat with Visor using |
Security Issues (1)
Architecture Issues (1)
✅ Performance Check PassedNo performance issues found – changes LGTM. Security Issues (1)
Quality Issues (1)
Powered by Visor from Probelabs Last updated: 2026-06-08T12:17:03.916Z | Triggered by: pr_updated | Commit: 45ea2f3 💡 TIP: You can chat with Visor using |
The auth-time gate at gateway/middleware.go:618 used `len(key) <= minLength`, which rejects a key of length exactly equal to MinTokenLength. The config field name reads as a "minimum accepted length" but was enforced as a strict lower bound -- which is what bit Evri (32-char Apigee keys vs MinTokenLength=32). Field name and docs both describe a minimum; the code now matches. Mirroring change in handleAddOrUpdate's create-time guard so the create path keeps refusing exactly what the runtime gate refuses. Error message updated: "less than or equal to" -> "less than", "longer than" -> "at least". The previous commit on this branch added the create-time guard but used `<=` to match the runtime gate as it stood. Shipping only that half would have left a worse bug behind: the operator now gets 400 for the very case the field name promises (MinTokenLength=32 still rejecting 32-char keys) but still can't make 32-char keys work without lowering the value. The two defects are coupled; mirroring forces them to ship together. Test TestKeyHandler_RejectCustomKeyBelowMinTokenLength updated: - "below" case bumped to len=31 so it sits at the boundary - "equal_to_min_token_length_rejected" -> "equal_to_min_token_length_accepted", wantCode flips from 400 to 200 - Docstring rewritten to describe the new operator Pure-relaxation backwards-compat: no traffic that previously authenticated stops authenticating; no key creation that previously produced a usable key now fails. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
bojank93
left a comment
There was a problem hiding this comment.
PR #8154 — [TT-17065] Fix MinTokenLength: reject silent-accept on create + correct off-by-one operator
Review by Quinn · Tyk QA · 2026-06-04
PR Summary
Fixes two coupled defects in MinTokenLength enforcement. In middleware.go, the boundary operator <= is corrected to < so keys of exactly MinTokenLength characters are no longer rejected at auth time. In api.go, a new guard rejects custom-key creates where len(customID) < MinTokenLength with a descriptive 400 response, preventing the silent-accept/deferred-403 trap. Implementation matches the ticket description. Correctly scoped — only the POST create path is affected; updates, autogenerated keys, and hashed endpoints are not.
Jira Alignment
| Field | Value |
|---|---|
| Ticket | TT-17065 — MinTokenLength: silent-accept of unusable custom keys + off-by-one operator at auth time |
| Type | Bug |
| Status | Ready for Testing |
| Acceptance Criteria Met? |
Criteria gaps:
| # | Acceptance Criterion | Covered in PR? | Note |
|---|---|---|---|
| 1 | Key of exactly min_token_length chars reaches Redis lookup (auth-time fix) |
middleware.go fix is present; no test issues a proxy request with an exactly-minLength key and asserts 200 |
|
| 2 | Key strictly shorter than min_token_length still short-circuits |
✅ Yes | < preserves this invariant |
| 3 | Default config (min_token_length=0 → fallback 3) preserved |
✅ Yes | minLen > 0 guard on create; fallback minLength = 3 in middleware unchanged |
| 4 | POST with short custom ID → 400 Bad Request with descriptive message | ✅ Yes | Covered by new test |
| 5 | POST with custom ID ≥ min_token_length → 200 OK, key stored | ✅ Yes | Covered by new test |
| 6 | min_token_length=0 → create guard is a no-op |
✅ Yes | minLen > 0 short-circuit present |
| 7 | Autogenerated-key creates unaffected | ✅ Yes | keyName != "" guard |
| 8 | PUT (update) path unaffected | ✅ Yes | Guard placed in else (create-only) branch |
| 9 | Error message stable and operator-matchable | ✅ Yes | Hardcoded format string; test verifies content |
Unit & Integration Test Coverage
| Package | Coverage | Tests Added? | Gap Description | Severity |
|---|---|---|---|---|
gateway — api.go changes |
CI passed | ✅ Yes | TestKeyHandler_RejectCustomKeyBelowMinTokenLength covers all create-path scenarios |
✅ None |
gateway — middleware.go fix |
CI passed | ❌ No dedicated test | The <= → < off-by-one fix has no test that exercises a proxy request with a key of exactly MinTokenLength chars and verifies it authenticates (200, not 403) |
🟡 Medium |
The middleware.go change is the more operationally impactful of the two fixes — it is what caused Evri's deployment to 403 every request. Yet there is no regression guard for it. A subtest should:
- Configure
MinTokenLength = 32 - Create a key with a 32-character custom ID
- Issue a proxy request using that exact key
- Assert
200 OK
Without this, the off-by-one can be re-introduced without breaking the test suite.
UI Test Coverage (Playwright)
No frontend files changed — not applicable.
Security Findings
| # | Severity | Location | Finding | Recommendation |
|---|---|---|---|---|
| 1 | 🟢 Low | gateway/api.go:602–608 |
Error response discloses min_token_length config value and the submitted key length |
Admin API — low practical risk. Consider omitting the submitted length if this endpoint is ever proxied through a less-trusted layer |
No security issues require changes before merge.
Code Quality Issues
| # | Severity | Location | Issue | Recommendation |
|---|---|---|---|---|
| 1 | 🟢 Low | gateway/api.go:597–604 |
Comment references the internal implementation of CheckSessionAndIdentityForValidKey (see middleware.go) — will become stale if middleware logic changes |
Keep only the business rationale; remove the cross-file implementation reference |
| 2 | 🟢 Low | gateway/api.go:603–607 |
Format string split across + concatenation — valid Go but a raw string literal or intermediate variable would be cleaner |
Non-blocking |
Tyk Domain Issues
| # | Severity | Location | Issue | Recommendation |
|---|---|---|---|---|
| 1 | 🟢 Low | gateway/api.go:597 |
The create-time guard is absent from the hashed-key path (isHashed == true). A short custom key on a hashed create endpoint bypasses validation |
Confirm intentional. If hashed custom IDs are user-supplied, apply the same guard; if the hash is always gateway-generated, add a comment explaining why validation is not needed |
CI Failures — Must Be Resolved Before Merge
| Check | Status | Action Required |
|---|---|---|
s1-shift-left-cli (SentinelOne CNS Scan) |
🔴 FAIL | Inspect the scan report — confirm whether the new fmt.Sprintf format string or any added code triggered a rule. If false-positive, document suppression rationale |
validate (Validate PR against Jira) |
🔴 FAIL | Confirm TT-17065 is in the correct Jira state for this workflow step; perform any required transition |
Multiple upgrade-rpm jobs are CANCELLED — these appear to be infra-level race artefacts (the amazonlinux:2 variant re-ran and passed); no action needed unless the RPM maintainer says otherwise.
What Was Done Well
- Surgical fix: the operator change in
middleware.gois a single character — no collateral modification. - Correct enforcement point: rejecting short custom IDs at create time (rather than silently storing and failing later) is the right place for this invariant; consistent with fail-fast design.
- Clear error message: the 400 body names both the submitted length and the configured minimum — operators can diagnose without reading source.
- Good create-path test coverage: all relevant scenarios (short key → 400, exact-length → 200, autogenerated → 200, unset min_token_length → no-op) appear to be covered by the new test.
Gap Summary
| Area | Gaps Found | Highest Severity |
|---|---|---|
| Unit / Integration Tests | 1 | 🟡 Medium |
| UI Tests (Playwright) | 0 | — N/A |
| Security | 1 | 🟢 Low |
| Code Quality | 2 | 🟢 Low |
| Tyk Domain | 1 | 🟢 Low |
| Jira Alignment | 1 criterion partial |
Verdict
REQUEST CHANGES
Two blocking items before this can merge:
-
Add an auth-flow regression test for the
middleware.gofix. The<=→<change is the fix that was breaking Evri's production traffic, but it has no test that verifies a key of exactlyMinTokenLengthcharacters can successfully authenticate through the gateway. Add a subtest toTestKeyHandler_RejectCustomKeyBelowMinTokenLength(or a dedicated test) that creates a 32-char key and issues a proxy request, asserting200 OK. This is AC1 coverage and prevents silent regression. -
Resolve the two failing CI checks. Both
s1-shift-left-cliandvalidateare failing. Explain and resolve each — or formally dismiss as infrastructure noise with documented rationale — before merge.
All other findings (comment fragility, hashed-key path gap, minor style) are non-blocking and can be addressed as a follow-up.
🚨 Jira Linter FailedCommit: The Jira linter failed to validate your PR. Please check the error details below: 🔍 Click to view error detailsNext Steps
This comment will be automatically deleted once the linter passes. |
🎯 Recommended Merge TargetsBased on JIRA ticket TT-17065: [Innersource] MinTokenLength: silent-accept of unusable custom keys + off-by-one operator at auth time Fix Version: Tyk 5.8.15Required:
Fix Version: Tyk 5.13.1Required:
Fix Version: Tyk 5.14.0
Required:
📋 Workflow
|
|
|
/release to release-5.8 |
|
✅ Cherry-pick successful. A PR was created: #8287 |
|
/release to release-5.13 |
|
✅ Cherry-pick successful. A PR was created: #8288 |



Summary
Two related defects in the
MinTokenLengthenforcement chain. They were originally surfaced together by a 2-week investigation on the Evri Apigee→Tyk migration; this PR fixes both in one go because they are coupled — the create-time guard has to mirror whatever the runtime gate does, and changing only one of them just moves the trap.POST /tyk/keys/<customID>historically stored any custom-ID key regardless of length. If the key was unreachable at auth time, the operator got no signal at create — they got200 OK, the session went into Redis, and then every consumer request 403'd withresponse_flag=AKI. This PR rejects the create with400 Bad Requestwheneverlen(customID) < MinTokenLength, with an error message that names both the submitted length and the configured minimum.gateway/middleware.go::CheckSessionAndIdentityForValidKeyusedif len(key) <= minLength, which rejects a key whose length is exactlyMinTokenLength. The config field reads as a minimum accepted length, but was enforced as a strict lower bound — which is what bit Evri (32-char Apigee keys vsMinTokenLength=32). This PR changes the gate toif len(key) < minLength, so a key of length equal toMinTokenLengthis accepted, matching the field's name and documentation. The create-time guard above mirrors the same<semantic.Background — how this was found
The Evri Apigee-to-Tyk migration PoC was blocked for ~2 weeks on 403s across every imported key. Apigee default consumer keys are 32 chars; Evri had set
TYK_GW_MINTOKENLENGTH=32thinking it was the minimum accepted length. Tyk's auth middleware usedlen(key) <= minLength, so 32-char keys were rejected before any Redis lookup — but the create path happily accepted and stored them. The only form that worked was theeyJ…envelope (120+ chars), which external-system consumers don't have. Two weeks of investigation chased Portal/Dashboard/REGO/hash-function code paths before the length gate surfaced.There are two distinct mistakes made by the operator+code interaction here, and fixing only one of them leaves a real trap behind:
MinTokenLength=32still can't create a 32-char key — the field name lied to them, and they have to hunt for the off-by-one in code.MinTokenLength=64can still POST a 30-char custom-ID key, get200 OK, and silently 403 every consumer request.So both fixes ship together.
Behavior matrix (before vs after)
Reproduced on tyk-demo with
min_token_length: 32:POST /tyk/keys/shortkey(custom id, len=8)200 OK, session stored, traffic later 403s400 Bad Requestat create; no session writtenPOST /tyk/keys/apigeecustomkey0123456789abcdefg(custom id, len=32 — equal to limit)200 OK, session stored, traffic later 403s200 OKat create; traffic with raw key now200 OK← the case that bit EvriPOST /tyk/keys/apigeecustomkey0123456789abcdefg_lon(custom id, len=35)200 OK200 OK(unchanged)POST /tyk/keys/create(autogenerate)200 OK200 OK(unchanged —generateTokenenvelope is well above any reasonableMinTokenLength)PUT /tyk/keys/<existingShortID>(update an old pre-fix import)200 OK200 OK(unchanged — admin API still allows mutate/read/delete on already-stored keys)eyJ…envelope of any of the above200 OK200 OK(unchanged)Implementation
1. Runtime gate (
gateway/middleware.go)Default fallback (
minLength = 3whenMinTokenLengthis unset) is unchanged in placement; only the comparison flips. Effect: in the default-config case, a key of length exactly 3 now authenticates instead of being rejected. There are no tests asserting the old boundary behavior.2. Create-time guard (
gateway/api.go::handleAddOrUpdate, POST branch)Gated on
keyName != \"\"so autogenerated creates fall through. Gated onminLen > 0so the default-config path (whereMinTokenLengthis 0 / unset, and the middleware falls back to 3) is not affected by this guard — that case is handled by the runtime gate's default; this PR intentionally doesn't over-apply.PUT/update path unchanged. Existing keys created before this fix remain returnable/mutable/deletable via the admin API — important for cleanup of pre-fix imports.
Test plan
TestKeyHandler_RejectCustomKeyBelowMinTokenLength(ingateway/api_test.go) exercises, withMinTokenLength=32:MinTokenLength(len=31) → 400 (rejected)MinTokenLength(len=32) → 200 (accepted — the case that bit Evri, now the canonical "works" case)MinTokenLength(len=34) → 200 (unchanged)POST /tyk/keys/create) → 200 (unchanged)TestKeyHandler|TestCreateKey|TestHashKeyHandler|TestCheckSession|TestAuthKeysuite passes locally.Backwards compatibility
Both changes are pure relaxations or loud-failure additions — no traffic that previously authenticated will stop authenticating, and no key creation that previously produced a usable key will now fail.
<=→<): keys whose raw length equalsMinTokenLengthgo from rejected at auth time → accepted at auth time. Anyone relying on the old<=semantics intentionally (e.g., wanting "strictly greater than 32") would have configuredMinTokenLength=33to express that, so this is not a breaking change in practice. The field name and docs both read as "minimum accepted," which is now what the code does.MinTokenLengthunset): runtime gate's default isminLength = 3. The operator change means a key of length exactly 3 now authenticates instead of being rejected. Tyk-generated tokens are always far above this; the only practical effect is that hand-typed 3-char custom IDs (uncommon — and arguably should work given the field is called "min_token_length") now work.References
Follow-up (separate ticket): add the same create-time guard to the Dashboard layer (`tyk-analytics/dashboard/api_keys.go::createKey`). Not required for correctness — the Gateway 400 here bubbles up — but it's a nicer UX for Dashboard UI/API clients to fail fast without a Gateway round-trip.
🤖 Generated with Claude Code