juanfont/headscale · error
invalid oauth client secret: %w
Error message
invalid oauth client secret: %w
What it means
AuthenticateOAuthClient found the client row but verifySecret rejected the presented secret. The wrapped error is either errSecretMismatch (argon2 hash comparison failed — wrong secret) or errSecretHashMalformed (stored hash is not a valid PHC string — corrupted data or legacy format). Revocation is checked after, so this error specifically means the credential itself failed.
Source
Thrown at hscontrol/db/oauth.go:216
}
clientID, secret, err := parsePrefixedKey(
rest,
oauthClientIDLength,
oauthClientSecretLength,
ErrOAuthClientFailedToParse,
)
if err != nil {
return nil, err
}
var client types.OAuthClient
if err := hsdb.DB.First(&client, "client_id = ?", clientID).Error; err != nil { //nolint:noinlineerr
return nil, ErrOAuthClientNotFound
}
if err := verifySecret(client.SecretHash, secret); err != nil { //nolint:noinlineerr
return nil, fmt.Errorf("invalid oauth client secret: %w", err)
}
if client.Revoked != nil {
return nil, ErrOAuthClientRevoked
}
return &client, nil
}
// GetOAuthClientByClientID returns a [types.OAuthClient] by its public client id.
func (hsdb *HSDatabase) GetOAuthClientByClientID(clientID string) (*types.OAuthClient, error) {
var client types.OAuthClient
if result := hsdb.DB.First(&client, "client_id = ?", clientID); result.Error != nil {
return nil, result.Error
}
return &client, nil
}View on GitHub (pinned to 565fd254d0)
Solutions
- Regenerate the client secret and update the consumer
- Check the stored hash round-trips: it must look like $argon2id$v=19$m=...$salt$hash with valid base64
- Audit for manual writes to the oauth_clients table
Example fix
// before
client, err := hsdb.AuthenticateOAuthClient(secret)
if err != nil {
log.Fatal(err)
}
// after
if err != nil {
var msg string
if errors.Is(err, db.ErrOAuthClientNotFound) {
msg = "unknown client"
} else {
msg = "invalid client credentials"
}
http.Error(w, msg, http.StatusUnauthorized)
return
} Defensive patterns
Strategy: try-catch
Type guard
func isOAuthCredentialRejected(err error) bool {
return err != nil && !errors.Is(err, db.ErrOAuthClientNotFound)
} Try / catch
if _, err := hsdb.AuthenticateOAuthClient(secret); err != nil {
switch {
case errors.Is(err, db.ErrOAuthClientNotFound):
return errUnknownClient
case errors.Is(err, db.ErrOAuthClientRevoked):
return errRevokedClient
default: // includes invalid secret and malformed hash
return errBadCredentials
}
} Prevention
- Distinguish wrong-secret from corrupted-hash by unwrapping errSecretMismatch vs errSecretHashMalformed
- Store secrets via secret managers that preserve exact bytes
- Never hand-edit the secret_hash column
When it happens
Trigger: Wrong or rotated client secret; secret copied with whitespace/newline; SecretHash column corrupted by manual edits or a truncated column type.
Common situations: Stale secret in CI/CD variables after rotation; database dumped/restored with encoding loss breaking the base64 in the hash.
Related errors
- invalid oauth access token: %w
- failed to parse oauth client secret
- ErrOAuthClientNotFound
- ErrAccessTokenNotFound
- generating salt: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/919888da6b51d26f.
Report an issue: GitHub.