juanfont/headscale · error
failed to parse oauth client secret
Error message
failed to parse oauth client secret
What it means
Sentinel in hscontrol/db/oauth.go rejecting a malformed OAuth client secret during lookup/verification. Client credentials have a fixed shape: hskey-oauthcli- style prefix with a 12-char prefix and 64-char secret (and a legacy shorter format accepted). Parsing fails before any Argon2id hash comparison happens.
Source
Thrown at hscontrol/db/oauth.go:38
const (
// OAuth client secret: hskey-client-<clientID(12)>-<secret(64)>. The clientID
// is the public, indexed lookup key (the analogue of an API key's prefix) and
// 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 = 1View on GitHub (pinned to 565fd254d0)
Solutions
- Re-copy the client secret exactly as shown at creation; trim whitespace when loading it
- Confirm the credential kind matches the endpoint (client secret vs hskey-oauthtok access token)
- If the secret was regenerated, update all consumers — old secrets are revoked
Defensive patterns
Strategy: type-guard
Validate before calling
const oauthSecretPrefix = "hskey-oauthcli-" // per repo constants
if !strings.HasPrefix(secret, oauthSecretPrefix) {
return errors.New("not an OAuth client secret")
} Type guard
func isOAuthClientSecret(s string) bool {
rest, ok := strings.CutPrefix(s, "hskey-oauthcli-")
if !ok {
return false
}
prefix, secretPart, found := strings.Cut(rest, "-")
return found && len(prefix) == 12 && len(secretPart) == 64
} Try / catch
client, err := db.GetOAuthClientBySecret(secret)
if err != nil {
if errors.Is(err, db.ErrOAuthClientFailedToParse) {
return unauthorized("malformed OAuth client secret") // 401, no retry
}
if errors.Is(err, db.ErrOAuthClientNotFound) { /* also 401 */ }
return err
} Prevention
- Keep credential prefixes distinct per kind and dispatch on prefix at ingress
- Trim secrets when loading from env/secret stores
- Rotate secrets through the CLI, then update consumers atomically
When it happens
Trigger: Passing an empty string, a secret without the expected prefix, one whose prefix/secret segments have the wrong length, or a credential of another type (API key, auth-key, access token) to the OAuth client secret verifier.
Common situations: Automation storing the client secret with a trailing newline or truncated by shell word-splitting; pasting an oauth access token where the client secret belongs.
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 ApiKey
- failed to parse oauth access token
- oauth access token expired
- failed to parse auth-key
- ErrOAuthClientNotFound
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/6e100f741362864f.
Report an issue: GitHub.