gotify/server · error

invalid credentials

Error message

invalid credentials

What it means

Returned by Login after the Basic credentials were parsed but the lookup failed: either no user with that name exists (a.DB.GetUserByName returned nil) or password.ComparePassword does not match the stored hash. The handler deliberately returns a single 401 'invalid credentials' for both cases to avoid leaking which usernames exist.

Source

Thrown at api/session.go:79

func (a *SessionAPI) Login(ctx *gin.Context) {
	if !a.LocalAuthEnabled {
		ctx.AbortWithError(403, errors.New("local authentication is disabled"))
		return
	}

	name, pass, ok := ctx.Request.BasicAuth()
	if !ok {
		ctx.AbortWithError(401, errors.New("basic auth required"))
		return
	}

	user, err := a.DB.GetUserByName(name)
	if err != nil {
		ctx.AbortWithError(500, err)
		return
	}
	if user == nil || !password.ComparePassword(user.Pass, []byte(pass)) {
		ctx.AbortWithError(401, errors.New("invalid credentials"))
		return
	}

	clientParams := ClientParams{}
	if err := ctx.Bind(&clientParams); err != nil {
		return
	}

	elevatedUntil := time.Now().Add(model.DefaultElevationDuration)
	tokenPublic, tokenPrivate := generateClientToken()
	client := model.Client{
		Name:                          clientParams.Name,
		Token:                         tokenPublic,
		UserID:                        user.ID,
		ElevatedUntil:                 &elevatedUntil,
		ExpiresAfterInactivitySeconds: auth.CookieMaxAge,
	}
	if success := successOrAbort(ctx, 500, a.DB.CreateClient(&client)); !success {

View on GitHub (pinned to 14bfc25627)

Solutions

  1. Re-enter the username/password carefully; test the same credentials with curl -u
  2. Verify the user exists in the DB (users table / GetUserByName) and reset the password if needed (admin UpdatePassword endpoint or CLI)
  3. Confirm you are connected to the environment/database you expect
  4. If hashes were migrated, re-hash/re-set passwords to match the current password.ComparePassword scheme

Example fix

// before
curl -X POST https://host/api/session -u 'jane:passwrod'
// after
curl -X POST https://host/api/session -u 'jane:correct-password'
Defensive patterns

Strategy: validation

Validate before calling

if (!username || !password) {
  throw new Error('username and password required before calling login');
}
// optionally pre-check user existence via admin API if available

Type guard

function hasCredentials(c) {
  return typeof c === 'object' && typeof c.username === 'string' && c.username.length > 0 && typeof c.password === 'string' && c.password.length > 0;
}

Try / catch

try {
  await api.login(username, password);
} catch (e) {
  if (e.status === 401) { showLoginFormError('Invalid username or password'); }
  else { throw e; }
}

Prevention

When it happens

Trigger: POST to the session/login endpoint with valid Basic header syntax but a wrong password; a username that does not exist in the database; a user created before a password-hashing scheme change so ComparePassword can no longer validate the stored hash; empty password bytes.

Common situations: Typo in username or password; password changed or reset elsewhere; database pointing at the wrong environment (staging user vs prod); migrating users without rehashing passwords; LDAP/external auth expected but local DB auth is what runs.

Understand the failure class

Related errors


AI-assisted analysis of gotify/server@14bfc25627 (2026-09-05). Data as JSON: /api/errors/f11d2d5bada5b75b. Report an issue: GitHub.