{"record":{"id":"c1b164b5231e44d3","repo":"juanfont/headscale","slug":"erraccesstokennotfound","errorCode":"ErrAccessTokenNotFound","errorMessage":"oauth access token not found: %w","messagePattern":"oauth access token not found: %w","errorType":"error_code","errorClass":"gorm.ErrRecordNotFound","httpStatus":401,"severity":"error","filePath":"hscontrol/db/oauth.go","lineNumber":41,"sourceCode":"\t// is the public, indexed lookup key (the analogue of an API key's prefix) and\n\t// is embedded in the secret so the token endpoint can derive it. The prefix\n\t// itself lives in the types package ([types.OAuthClientPrefix]).\n\toauthClientIDLength     = 12\n\toauthClientSecretLength = 64\n\n\t// OAuth access token: hskey-oauthtok-<prefix(12)>-<secret(64)>. The distinct\n\t// prefix (vs hskey-api- admin keys, [types.AccessTokenPrefix]) lets the auth\n\t// middleware dispatch a scoped token from an all-access admin key alone.\n\taccessTokenPrefixLength = 12\n\taccessTokenSecretLength = 64\n)\n\nvar (\n\tErrOAuthClientNotFound      = fmt.Errorf(\"oauth client not found: %w\", gorm.ErrRecordNotFound)\n\tErrOAuthClientFailedToParse = errors.New(\"failed to parse oauth client secret\")\n\tErrOAuthClientRevoked       = errors.New(\"oauth client revoked\")\n\n\tErrAccessTokenNotFound      = fmt.Errorf(\"oauth access token not found: %w\", gorm.ErrRecordNotFound)\n\tErrAccessTokenFailedToParse = errors.New(\"failed to parse oauth access token\")\n\tErrAccessTokenExpired       = errors.New(\"oauth access token expired\")\n\tErrAccessTokenClientRevoked = errors.New(\"oauth access token issuing client revoked or deleted\")\n\n\terrSecretHashMalformed = errors.New(\"malformed secret hash\")\n\terrSecretMismatch      = errors.New(\"secret does not match hash\")\n)\n\n// Argon2id parameters, OWASP's minimum recommendation (19 MiB, 2 iterations, 1\n// lane). They are encoded into every stored hash, so raising them later still\n// verifies credentials stored under the old cost.\nconst (\n\targon2Time    = 2\n\targon2Memory  = 19 * 1024\n\targon2Threads = 1\n\targon2KeyLen  = 32\n\targon2SaltLen = 16\n)","sourceCodeStart":23,"sourceCodeEnd":59,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/oauth.go#L23-L59","documentation":"ErrAccessTokenNotFound wraps gorm.ErrRecordNotFound for OAuth access tokens: a First() by the token's 12-char prefix found no row. As a sentinel wrapping gorm's not-found, it should be checked with errors.Is. Distinct from a token that exists but fails verification or is expired, which return different errors.","triggerScenarios":"Presenting a bearer token whose prefix is not in the database; token deleted by RevokeOAuthClient purging all of a client's tokens; malformed token string that parses but derives an unknown prefix.","commonSituations":"Long-lived API client holding a token after the oauth client was revoked; rotated tokens not propagated; truncated bearer header.","solutions":["Handle with errors.Is(err, db.ErrAccessTokenNotFound) as 401","Mint a new access token via the client credential flow","Confirm the Authorization header carries the full hskey-oauthtok-... string"],"exampleFix":"// before\nclient, err := hsdb.AuthenticateAccessToken(tok)\nif err != nil {\n\treturn err // 500 on unknown token\n}\n\n// after\nif errors.Is(err, db.ErrAccessTokenNotFound) {\n\treturn ErrUnauthorized // 401 with WWW-Authenticate\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"func isAccessTokenNotFound(err error) bool {\n\treturn errors.Is(err, db.ErrAccessTokenNotFound)\n}","tryCatchPattern":"if _, err := hsdb.AuthenticateAccessToken(bearer); err != nil {\n\tif errors.Is(err, db.ErrAccessTokenNotFound) {\n\t\tw.Header().Set(\"WWW-Authenticate\", \"Bearer\")\n\t\thttp.Error(w, \"invalid token\", http.StatusUnauthorized)\n\t\treturn\n\t}\n\treturn err\n}","preventionTips":["Re-mint tokens when an OAuth client is revoked — all its tokens die with it","Pass the full hskey-oauthtok-... string in the header","Log token prefixes, never full secrets, when debugging"],"tags":["go","oauth","access-token","authentication","sentinel-error"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}