juanfont/headscale · error
invalid oauth access token: %w
Error message
invalid oauth access token: %w
What it means
AuthenticateAccessToken found a token row by prefix but verifySecret rejected the presented secret half: either errSecretMismatch (wrong token — hash comparison failed) or errSecretHashMalformed (stored Hash column is not a valid PHC string). Expiry and client-binding checks come after, so this error isolates the credential check itself.
Source
Thrown at hscontrol/db/oauth.go:353
}
prefix, secret, err := parsePrefixedKey(
rest,
accessTokenPrefixLength,
accessTokenSecretLength,
ErrAccessTokenFailedToParse,
)
if err != nil {
return nil, err
}
var token types.OAuthAccessToken
if err := hsdb.DB.First(&token, "prefix = ?", prefix).Error; err != nil { //nolint:noinlineerr
return nil, ErrAccessTokenNotFound
}
if err := verifySecret(token.Hash, secret); err != nil { //nolint:noinlineerr
return nil, fmt.Errorf("invalid oauth access token: %w", err)
}
if token.Expiration != nil && token.Expiration.Before(time.Now()) {
return nil, ErrAccessTokenExpired
}
// Bind validity to the issuing client: a token whose client has been
// revoked or deleted is rejected. This closes a mint/revoke race (where a
// token could be inserted after the client's tokens were purged) and any
// orphan left by manual deletion or a future soft-revoke path.
var client types.OAuthClient
if err := hsdb.DB.First(&client, "client_id = ?", token.ClientID).Error; err != nil { //nolint:noinlineerr
return nil, ErrAccessTokenClientRevoked
}
if client.Revoked != nil {
return nil, ErrAccessTokenClientRevoked
}View on GitHub (pinned to 565fd254d0)
Solutions
- Mint a fresh access token and replace the stored credential
- Verify the stored Hash looks like a valid $argon2id$ PHC string with intact base64
- Ensure the token is passed whole in the Authorization header
Example fix
// before
if err != nil {
return err
}
// after
switch {
case errors.Is(err, db.ErrAccessTokenNotFound):
return errUnauthorized("unknown token")
case err != nil: // includes invalid-token and expiry
return errUnauthorized("invalid or expired token")
} Defensive patterns
Strategy: try-catch
Type guard
func isTokenRejected(err error) bool {
return errors.Is(err, db.ErrAccessTokenNotFound) ||
errors.Is(err, db.ErrAccessTokenExpired) ||
errors.Is(err, db.ErrAccessTokenClientRevoked) ||
err != nil && strings.Contains(err.Error(), "invalid oauth access token")
} Try / catch
if _, err := hsdb.AuthenticateAccessToken(bearer); err != nil {
if isTokenRejected(err) {
return errUnauthorized
}
return err // genuine 500 (DB issue)
} Prevention
- Unwrap errSecretMismatch vs errSecretHashMalformed to tell wrong token from corrupted row
- Validate token shape (prefix + length) before hitting the DB
- Keep bearer tokens out of logs and shell history
When it happens
Trigger: Bearer token from a different installation or a rotated-away token; token string truncated or with stray characters; Hash column corrupted.
Common situations: Environments copied between prod/staging sharing prefix configuration; secrets mangled by YAML/env serialization (line breaks in the base64).
Related errors
- ErrAccessTokenNotFound
- invalid oauth client secret: %w
- failed to parse oauth client secret
- failed to parse oauth access token
- oauth access token expired
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/d168df33ee4b9861.
Report an issue: GitHub.