{"record":{"id":"bd807c32ebe5f204","repo":"hashicorp/nomad","slug":"nonce-reuse-detected","errorCode":null,"errorMessage":"nonce reuse detected","messagePattern":"nonce reuse detected","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"lib/auth/oidc/request.go","lineNumber":17,"sourceCode":"// Copyright IBM Corp. 2015, 2026\n// SPDX-License-Identifier: BUSL-1.1\n\npackage oidc\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/hashicorp/cap/oidc\"\n\t\"github.com/hashicorp/golang-lru/v2/expirable\"\n)\n\nvar (\n\tErrNonceReuse = errors.New(\"nonce reuse detected\")\n\t// ErrTooManyRequests is returned if the request cache is full.\n\t// Realistically, we expect this only to happen if the auth-url\n\t// API endpoint is being DOS'd.\n\tErrTooManyRequests = errors.New(\"too many auth requests\")\n)\n\n// MaxRequests is how many requests are allowed to be stored at a time.\n// It needs to be large enough for legitimate user traffic, but small enough\n// to prevent a DOS from eating up server memory.\nconst MaxRequests = 1000\n\n// NewRequestCache creates a cache for OIDC requests.\n// The JWT expiration time in the cap library is 5 minutes,\n// so timeout should be around that long.\nfunc NewRequestCache(timeout time.Duration) *RequestCache {\n\treturn &RequestCache{\n\t\tc: expirable.NewLRU[string, *oidc.Req](MaxRequests, nil, timeout),\n\t}","sourceCodeStart":1,"sourceCodeEnd":35,"githubUrl":"https://github.com/hashicorp/nomad/blob/482b49bf1aec006f089bcfc7e632d8f6ac303e5e/lib/auth/oidc/request.go#L1-L35","documentation":"ErrNonceReuse is returned when the OIDC request cache already holds an entry for the same client nonce. Nonces must be unique per auth-url request; a collision indicates a bug or forged/duplicated callback traffic, so the store refuses the request.","triggerScenarios":"RequestCache.store (storeLocked) is called twice with an oidc.Req whose Nonce() is already present in the LRU cache — checked at request.go:58 via rc.c.Get(req.Nonce()).","commonSituations":"A client replaying the same auth-url request; two browser tabs racing with an identical nonce; a broken client that regenerates the same nonce; tests deliberately re-storing the same req (request_test.go:26).","solutions":["Generate a fresh unique nonce for every auth-url request on the client side and retry.","Investigate the client generating the duplicate nonce — nonce generation should use a CSPRNG per request.","If triggered by a replayed callback, treat it as suspicious traffic and drop it rather than retrying."],"exampleFix":"// before\nreq, _ := oidc.NewReq(provider, clientID, clientSecret, oidc.WithNonce(fixedNonce))\nrc.store(req) // second store of same nonce fails\n// after\nnonce, _ := uuid.GenerateUUID()\nreq, _ := oidc.NewReq(provider, clientID, clientSecret, oidc.WithNonce(nonce))\nrc.store(req)","handlingStrategy":"try-catch","validationCode":"if rc.Has(req.Nonce()) {\n    req = regenerateWithNewNonce(req)\n}","typeGuard":null,"tryCatchPattern":"err := rc.store(req)\nif errors.Is(err, oidc.ErrNonceReuse) {\n    // regenerate nonce and retry once, or reject the callback as replay\n    return fmt.Errorf(\"rejecting duplicate OIDC nonce: %w\", err)\n}","preventionTips":["Generate nonce with a CSPRNG (crypto/rand) per request","Never reuse or cache nonces client-side across logins","Treat nonce reuse callbacks as potential replay attacks"],"tags":["oidc","nonce","replay","cache"],"backgroundTag":"nonce-reuse-detected","analyzedSha":"482b49bf1aec006f089bcfc7e632d8f6ac303e5e","analyzedAt":"2026-09-04T07:54:14.808Z","contentChangedAt":"2026-09-04T07:54:14.808Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}