juanfont/headscale · error
failed to parse oauth access token
Error message
failed to parse oauth access token
What it means
Sentinel in hscontrol/db/oauth.go rejecting a malformed OAuth access token (format hskey-oauthtok-<prefix(12)>-<secret(64)>). Thrown during token parsing before a database lookup, distinguishing 'malformed token' from 'token not found', 'expired', or 'issuing client revoked'.
Source
Thrown at hscontrol/db/oauth.go:42
// is embedded in the secret so the token endpoint can derive it. The prefix
// itself lives in the types package ([types.OAuthClientPrefix]).
oauthClientIDLength = 12
oauthClientSecretLength = 64
// OAuth access token: hskey-oauthtok-<prefix(12)>-<secret(64)>. The distinct
// prefix (vs hskey-api- admin keys, [types.AccessTokenPrefix]) lets the auth
// middleware dispatch a scoped token from an all-access admin key alone.
accessTokenPrefixLength = 12
accessTokenSecretLength = 64
)
var (
ErrOAuthClientNotFound = fmt.Errorf("oauth client not found: %w", gorm.ErrRecordNotFound)
ErrOAuthClientFailedToParse = errors.New("failed to parse oauth client secret")
ErrOAuthClientRevoked = errors.New("oauth client revoked")
ErrAccessTokenNotFound = fmt.Errorf("oauth access token not found: %w", gorm.ErrRecordNotFound)
ErrAccessTokenFailedToParse = errors.New("failed to parse oauth access token")
ErrAccessTokenExpired = errors.New("oauth access token expired")
ErrAccessTokenClientRevoked = errors.New("oauth access token issuing client revoked or deleted")
errSecretHashMalformed = errors.New("malformed secret hash")
errSecretMismatch = errors.New("secret does not match hash")
)
// Argon2id parameters, OWASP's minimum recommendation (19 MiB, 2 iterations, 1
// lane). They are encoded into every stored hash, so raising them later still
// verifies credentials stored under the old cost.
const (
argon2Time = 2
argon2Memory = 19 * 1024
argon2Threads = 1
argon2KeyLen = 32
argon2SaltLen = 16
)
View on GitHub (pinned to 565fd254d0)
Solutions
- Use a token issued by the OAuth flow (starts with hskey-oauthtok-), not the admin API key
- Verify the token survives transport intact (no truncation/whitespace) by decoding it client-side first
- If format changed across versions, re-run the OAuth flow to mint fresh tokens
Defensive patterns
Strategy: type-guard
Validate before calling
func isAccessToken(s string) bool {
rest, ok := strings.CutPrefix(s, "hskey-oauthtok-")
if !ok {
return false
}
prefix, secret, found := strings.Cut(rest, "-")
return found && len(prefix) == 12 && len(secret) == 64
} Type guard
func isAccessToken(s string) bool {
rest, ok := strings.CutPrefix(s, "hskey-oauthtok-")
if !ok {
return false
}
prefix, secret, found := strings.Cut(rest, "-")
return found && len(prefix) == 12 && len(secret) == 64
} Try / catch
tok, err := db.ValidateAccessToken(bearer)
if err != nil {
switch {
case errors.Is(err, db.ErrAccessTokenFailedToParse):
return unauthorized("malformed access token")
case errors.Is(err, db.ErrAccessTokenNotFound):
return unauthorized("unknown token")
case errors.Is(err, db.ErrAccessTokenExpired):
return unauthorized("token expired")
}
return err
} Prevention
- Route hskey-oauthtok- and hskey-api- credentials by prefix before verification
- Pass tokens via Authorization headers, never query strings that proxies may truncate
- Client-side: validate token shape before sending to fail fast locally
When it happens
Trigger: Presenting an Authorization: Bearer token that is not an hskey-oauthtok- token — wrong prefix, wrong segment lengths, empty token, or an admin API key (hskey-api-) used against the scoped OAuth API.
Common situations: Client code sending the admin API key to the OAuth-scoped endpoints; token mangled by HTTP headers/proxies (truncation at whitespace); stale token format from before a headscale upgrade.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- failed to parse oauth client secret
- oauth access token expired
- failed to parse ApiKey
- ErrAccessTokenNotFound
- invalid oauth access token: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/5d5c913aab587ae6.
Report an issue: GitHub.