{"record":{"id":"49269cf60552e270","repo":"netbirdio/netbird","slug":"jwt-already-used","errorCode":null,"errorMessage":"JWT already used","messagePattern":"JWT already used","errorType":"http","errorClass":"ErrTokenAlreadyUsed","httpStatus":401,"severity":"error","filePath":"management/server/auth/session.go","lineNumber":21,"sourceCode":"import (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/eko/gocache/lib/v4/cache\"\n\t\"github.com/eko/gocache/lib/v4/store\"\n)\n\nconst (\n\tusedTokenKeyPrefix = \"jwt-used:\"\n\tusedTokenMarker    = \"1\"\n)\n\nvar (\n\tErrTokenAlreadyUsed = errors.New(\"JWT already used\")\n\tErrTokenExpired     = errors.New(\"JWT expired\")\n)\n\ntype SessionStore struct {\n\tcache *cache.Cache[string]\n}\n\nfunc NewSessionStore(cacheStore store.StoreInterface) *SessionStore {\n\treturn &SessionStore{cache: cache.New[string](cacheStore)}\n}\n\n// RegisterToken records a JWT until its exp time and rejects reuse.\nfunc (s *SessionStore) RegisterToken(ctx context.Context, token string, expiresAt time.Time) error {\n\tttl := time.Until(expiresAt)\n\tif ttl <= 0 {\n\t\treturn ErrTokenExpired\n\t}\n","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/netbirdio/netbird/blob/93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c/management/server/auth/session.go#L3-L39","documentation":"auth.ErrTokenAlreadyUsed is returned by SessionStore.RegisterToken, which implements single-use JWT claiming: it stores a sha256 marker of the token in a cache until its exp time and returns this error when the marker already exists. The management gRPC login server calls it via claimLoginToken (management/internals/shared/grpc/server.go:948) and maps it to codes.Unauthenticated, so replaying a login JWT is rejected at Login.","triggerScenarios":"An agent calls the Login gRPC twice with the same jwtToken, e.g. a retry after the first attempt already succeeded server-side; a second peer or daemon instance reuses the same login token; a duplicated request after a client timeout.","commonSituations":"Retry loops that resend the identical JWT instead of fetching a new one; two machines configured with the same token for convenience; token replay by a copied client config; the marker lives until exp, so even a much later replay within the token lifetime fails.","solutions":["Run the login/device-authorization flow again to obtain a fresh JWT and retry Login with the new token.","Make the client consume the JWT exactly once: mark it used locally after the first Login attempt and never resend it.","Give each peer or daemon its own token instead of sharing one JWT across installations."],"exampleFix":"// before\nresp, err := client.Login(ctx, &proto.LoginRequest{JwtToken: jwt})\nif err != nil { \n    resp, err = client.Login(ctx, &proto.LoginRequest{JwtToken: jwt}) // replay: rejected\n}\n// after\nresp, err := client.Login(ctx, &proto.LoginRequest{JwtToken: jwt})\nif status.Code(err) == codes.Unauthenticated {\n    jwt, err = fetchNewLoginToken(ctx) // fresh token, then retry once\n    if err != nil { return err }\n    resp, err = client.Login(ctx, &proto.LoginRequest{JwtToken: jwt})\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"err := s.sessionStore.RegisterToken(ctx, token, exp)\nif errors.Is(err, auth.ErrTokenAlreadyUsed) {\n    // single-use token was replayed: fetch a NEW token and retry the login once\n    token = fetchNewLoginToken(ctx)\n    err = s.sessionStore.RegisterToken(ctx, token, exp)\n}\nif err != nil {\n    return err\n}","preventionTips":["Treat login JWTs as single-use: consume once, then discard and re-run the auth flow for the next login.","Never share one login token across multiple agents or machines.","Distinguish Unauthenticated-because-reused from other failures before retrying: only the former is fixed by a new token.","Back the SessionStore cache with a shared store (e.g. Redis) when running multiple management replicas, so reuse detection is global."],"tags":["auth","jwt","grpc","session","replay-protection"],"backgroundTag":null,"analyzedSha":"93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c","analyzedAt":"2026-08-16T03:09:19.136Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}