fix(auth): return ErrAccountLocked for locked accounts on login

The login status check mapped a locked account to ErrAccountDisabled,
surfacing the disabled-account error code and message even though a
dedicated ErrAccountLocked exists (and the OIDC flow already uses it). Map
the locked status to ErrAccountLocked so credential login is consistent with
OIDC across both /api/v1 and /api/v2. Disabled accounts still return
ErrAccountDisabled.

This changes the v1 login error code for locked accounts on the wire (1020 ->
1026); the change is intentional and approved.
This commit is contained in:
kolaente 2026-06-12 11:08:36 +02:00 committed by kolaente
parent a32d8d6492
commit 5b7924b1f6
3 changed files with 33 additions and 1 deletions

View File

@ -101,10 +101,14 @@ func AuthenticateUserCredentials(ctx context.Context, login *user.Login) (*user.
return nil, err
}
if u.Status == user.StatusDisabled || u.Status == user.StatusAccountLocked {
if u.Status == user.StatusDisabled {
_ = s.Rollback()
return nil, &user.ErrAccountDisabled{UserID: u.ID}
}
if u.Status == user.StatusAccountLocked {
_ = s.Rollback()
return nil, &user.ErrAccountLocked{UserID: u.ID}
}
if err := enforceLoginTOTP(s, u, login.TOTPPasscode); err != nil {
return nil, err

View File

@ -87,6 +87,18 @@ func TestHumaLogin(t *testing.T) {
assert.Equal(t, user.ErrCodeEmailNotConfirmed, problemCode(t, rec))
})
t.Run("disabled account", func(t *testing.T) {
rec := login(`{"username":"user17","password":"12345678"}`)
assert.Equal(t, http.StatusPreconditionFailed, rec.Code)
assert.Equal(t, user.ErrCodeAccountDisabled, problemCode(t, rec))
})
t.Run("locked account", func(t *testing.T) {
rec := login(`{"username":"user18","password":"12345678"}`)
assert.Equal(t, http.StatusPreconditionFailed, rec.Code)
assert.Equal(t, user.ErrCodeAccountLocked, problemCode(t, rec))
})
t.Run("TOTP required but missing", func(t *testing.T) {
rec := login(`{"username":"user10","password":"12345678"}`)
assert.Equal(t, http.StatusPreconditionFailed, rec.Code)

View File

@ -68,6 +68,22 @@ func TestLogin(t *testing.T) {
require.Error(t, err)
assertHandlerErrorCode(t, err, user.ErrCodeEmailNotConfirmed)
})
t.Run("disabled account", func(t *testing.T) {
_, err := newTestRequest(t, http.MethodPost, apiv1.Login, `{
"username": "user17",
"password": "12345678"
}`, nil, nil)
require.Error(t, err)
assertHandlerErrorCode(t, err, user.ErrCodeAccountDisabled)
})
t.Run("locked account", func(t *testing.T) {
_, err := newTestRequest(t, http.MethodPost, apiv1.Login, `{
"username": "user18",
"password": "12345678"
}`, nil, nil)
require.Error(t, err)
assertHandlerErrorCode(t, err, user.ErrCodeAccountLocked)
})
}
func TestLoginTOTPLockout(t *testing.T) {