From 5b7924b1f6eef04679175265781133695658fd56 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 12 Jun 2026 11:08:36 +0200 Subject: [PATCH] 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. --- pkg/routes/api/shared/auth.go | 6 +++++- pkg/webtests/huma_auth_login_test.go | 12 ++++++++++++ pkg/webtests/login_test.go | 16 ++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/pkg/routes/api/shared/auth.go b/pkg/routes/api/shared/auth.go index d43deb358..153d851e8 100644 --- a/pkg/routes/api/shared/auth.go +++ b/pkg/routes/api/shared/auth.go @@ -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 diff --git a/pkg/webtests/huma_auth_login_test.go b/pkg/webtests/huma_auth_login_test.go index 423c66071..ad83fc811 100644 --- a/pkg/webtests/huma_auth_login_test.go +++ b/pkg/webtests/huma_auth_login_test.go @@ -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) diff --git a/pkg/webtests/login_test.go b/pkg/webtests/login_test.go index f271e1727..17b0f9b07 100644 --- a/pkg/webtests/login_test.go +++ b/pkg/webtests/login_test.go @@ -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) {