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

  1. Regenerate the client secret and update the consumer
  2. Check the stored hash round-trips: it must look like $argon2id$v=19$m=...$salt$hash with valid base64
  3. 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

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


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/919888da6b51d26f. Report an issue: GitHub.