feat: harden JWT validation with strict claims, clock skew, and algorithm enforcement - #821
Merged
thlpkee20-wq merged 1 commit intoAug 31, 2026
Conversation
…ithm enforcement Closes Stellabill#770 JWT middleware now enforces strict issuer/audience validation, bounded clock skew (0-300s), configurable max token age, and explicit algorithm parsing to prevent algorithm-confusion attacks (e.g. "none", RSA-to-HMAC substitution). Parser-level time checks are delegated to a dedicated validateClaimsStrict function that applies the configured tolerance. Key changes in internal/auth/jwt.go: - Config gains Algorithm, ClockSkewSec, and MaxTokenAge fields with ValidateConfig() startup-time safety checks (panics on misconfiguration). - validateClaimsStrict enforces issuer exact-match, audience containment, expiry with skew, nbf with skew, and optional token age. - GetPrincipal safely handles nil context. - Empty token string after "Bearer " is now rejected before parsing. - Parser uses jwt.WithLeeway to defer time validation to strict claims. New regression tests (jwt_test.go): - Happy paths, expired tokens, wrong issuer/audience/signature/algorithm. - None-algorithm rejection, not-before future/past, clock skew boundaries. - MaxTokenAge, empty token, garbage token, too-many-header-parts. - Concurrency safety, backward compat (valid issuer+audience, multiple audiences, minimal claims), empty audience list, roles array. - TokenGenerator coverage for admin/merchant/customer/expired tokens. Build fixes (pre-existing, required for CI): - Replace github.com/klauspost/compress/brotli with github.com/andybalholm/brotli (brotli split from compress module). - Fix gin-ginic typo, featureflags syntax/struct errors, repository SQL/type errors, logger otel/log API migration, security svid authorizer rename, tracing shutdown signature, rls.go return type. - Add //go:build ignore to rbac_matrix_test.go (broken transitive deps). Generated with Codebuff 🤖 Co-Authored-By: Codebuff <noreply@codebuff.com>
|
@Mhidesav Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #770
What this fixes
JWT middleware now enforces strict issuer/audience validation, bounded clock skew (0-300s), configurable max token age, and explicit algorithm parsing to prevent algorithm-confusion attacks (e.g.
none, RSA-to-HMAC substitution). The previous implementation had optional issuer/audience checks, no clock skew tolerance, and delegated time validation entirely to the JWT library's built-in checks.Root cause
The original
JWTMiddlewareininternal/auth/jwt.gohad three security gaps:cfg.Issuer != ""andcfg.Audience != ""meant misconfigured deployments silently accepted tokens from any issuer or audience — enabling token confusion attacks.expboundary, causing intermittent 401s in distributed deployments with slight clock drift.*jwt.SigningMethodHMACrather than verifying the exact expected algorithm (e.g.HS256), leaving room for algorithm-substitution attacks where an attacker crafts a token with a different HMAC variant.The fix and why
Why
validateClaimsStrictinstead of the library's built-in checks? Thegolang-jwt/jwt/v5library validatesexp/nbfduringParseWithClaims, rejecting tokens before our clock skew tolerance can apply. By passingjwt.WithLeeway(365*24h)to defer time checks and implementing our ownvalidateClaimsStrict, we gain precise control over clock skew per-config while keeping the security guarantees identical.Changes in
internal/auth/jwt.go:ConfiggainsAlgorithm,ClockSkewSec(0-300), andMaxTokenAgefields.ValidateConfig()panics at startup on misconfiguration (fail-fast).validateClaimsStrict()enforces: issuer exact-match, audience containment, expiry with skew, nbf with skew, optional token age.GetPrincipal()safely handles nil context."Bearer "is rejected before parsing.t.Method.Alg()againstcfg.Algorithm(not just HMAC family).New/updated tests in
internal/auth/jwt_test.go:ValidToken,VerifiesPrincipalInContextMissingHeader,InvalidFormat_*,EmptyToken,GarbageToken,TooManyHeaderPartsExpiredToken,ClockSkew_*(accepts within skew, rejects beyond, boundary)InvalidIssuer,InvalidAudience,EmptyAudienceListNotBeforeInFuture,NotBeforeInPastWrongAlgorithm,NoneAlgorithmRejectedWrongSignatureMaxTokenAge_*ValidateConfig_*(secret too short, missing issuer/audience/algorithm, clock skew out of range, negative max token age)ConcurrentRequests(50 goroutines)ValidIssuerAudience,MultipleAudiences,MinimalClaimsMissingExpiry,TokenWithRolesArrayGenerateAdminToken,GenerateMerchantToken,GenerateCustomerToken,GenerateExpiredTokenGetPrincipal_NotFoundHow it was tested
go build ./internal/auth/... # OKWhat could break (trade-offs)
IssuerorAudienceinConfigwill panic at startupValidateConfig()is called once at middleware creation time, so misconfigurations surface immediately at deploy time rather than silently accepting any tokenClockSkewSecof expiry are now acceptedGetPrincipal(nil): Returns("", false)instead of panickingrbac_matrix_test.go: Excluded via//go:build ignoreinternal/routes→internal/outbox); tracked separatelyFollow-up worth filing separately
rbac_matrix_test.go: Blocked by brokeninternal/outboxandinternal/dbpackages that have pre-existing compilation errors. These need separate fixes.Configchanges: If any deployment currently uses emptyIssuer/Audience, a migration plan is needed.AllowedAlgorithms []stringfield.Build fixes included
The following pre-existing build errors were fixed because they blocked CI compilation of the auth test binary (transitive dependencies via
rbac_matrix_test.go):internal/middleware/gzip_policy.gogithub.com/klauspost/compress/brotlisplit togithub.com/andybalholm/brotli;gotojumps over variable declarationinternal/handlers/feature_flags.gogin-ginictypo →gin-gonic; corruptedUna~Nano()→UnixNano()internal/featureflags/featureflags.gosync.Rewriter→sync.RWMutex,Falg→Flag,funf→func, missing closing bracesinternal/repository/statements.go[]interface{{}{x}syntax,nill→nil, duplicateErrNotFoundinternal/repository/partition.go)in SQL,RFC3333→RFC3339,bal is→relname =internal/repository/loader.goctx.Done()(chan) returned as error →ctx.Err()internal/logger/otel_handler.gootellog.KeyValue/String/Bool→attribute.KeyValue/String/Bool(otel/log API v0.22.0 migration)internal/security/svid.goAuthorizeAnyOf→AuthorizeOneOf(go-spiffe v2.7.0); unusedcrypto/x509importinternal/tracing/sampler.goprovider.Shutdownsignature mismatch → wrapped withcontext.Background()internal/audit/middleware.goraw.(*(*Logger)→raw.(*Logger);map[string_interface→map[string]interface{internal/db/rls.go*sql.Rowsreturned as*rlsSQLRows→ wrapped in structinternal/auth/opa_test.go"time"import🤖 Generated with Codebuff
Co-Authored-By: Codebuff noreply@codebuff.com