feat(svc): reject disabled users in Login, LoginWithPasskey, and Refresh

This commit is contained in:
2026-07-04 17:13:02 +08:00
parent bff131ba42
commit 118b7e4d2a
2 changed files with 179 additions and 1 deletions
+158 -1
View File
@@ -16,6 +16,13 @@ import (
)
func setupAuthService(t *testing.T) *AuthService {
svc, _ := setupAuthServiceWithRepos(t)
return svc
}
// setupAuthServiceWithRepos creates an AuthService and returns the UserRepository
// for tests that need direct repo access (e.g., soft-deleting users).
func setupAuthServiceWithRepos(t *testing.T) (*AuthService, repository.UserRepository) {
t.Helper()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
@@ -30,12 +37,13 @@ func setupAuthService(t *testing.T) *AuthService {
sessionRepo := repository.NewSessionRepository(db)
credentialRepo := repository.NewCredentialRepository(db)
return NewAuthService(
svc := NewAuthService(
userRepo, sessionRepo, credentialRepo,
[]byte("test-secret"),
15*time.Minute,
7*24*time.Hour,
)
return svc, userRepo
}
func TestAuthService_Register(t *testing.T) {
@@ -404,3 +412,152 @@ func TestAuthService_RefreshWithAccessToken(t *testing.T) {
t.Fatal("expected error when using access token for refresh, got nil")
}
}
func TestAuthService_LoginDisabledUser(t *testing.T) {
svc, userRepo := setupAuthServiceWithRepos(t)
ctx := context.Background()
user, err := svc.Register(ctx, "alice", "alice@example.com", "password123")
if err != nil {
t.Fatalf("Register = %v", err)
}
if err := userRepo.Delete(ctx, user.ID); err != nil {
t.Fatalf("Delete = %v", err)
}
_, err = svc.Login(ctx, "alice@example.com", "password123")
if err == nil {
t.Fatal("expected error for disabled user login, got nil")
}
var ae *model.AppError
if !errors.As(err, &ae) {
t.Fatalf("expected AppError, got %T: %v", err, err)
}
if ae.Status != http.StatusUnauthorized {
t.Errorf("status = %d, want %d", ae.Status, http.StatusUnauthorized)
}
if ae.Message != "invalid email or password" {
t.Errorf("message = %q, want %q", ae.Message, "invalid email or password")
}
}
func TestAuthService_LoginDisabledUserSameMessage(t *testing.T) {
svc, userRepo := setupAuthServiceWithRepos(t)
ctx := context.Background()
user, err := svc.Register(ctx, "alice", "alice@example.com", "password123")
if err != nil {
t.Fatalf("Register = %v", err)
}
// Get error for wrong password (active user)
_, wrongPwErr := svc.Login(ctx, "alice@example.com", "wrongpassword")
// Soft-delete user, then try to login
if err := userRepo.Delete(ctx, user.ID); err != nil {
t.Fatalf("Delete = %v", err)
}
_, disabledErr := svc.Login(ctx, "alice@example.com", "password123")
// Both errors should have the same message (no user enumeration)
if disabledErr == nil {
t.Fatal("expected error for disabled user login, got nil")
}
var aeDisabled *model.AppError
if !errors.As(disabledErr, &aeDisabled) {
t.Fatalf("expected AppError, got %T: %v", disabledErr, disabledErr)
}
var aeWrongPw *model.AppError
if !errors.As(wrongPwErr, &aeWrongPw) {
t.Fatalf("expected AppError for wrong password, got %T: %v", wrongPwErr, wrongPwErr)
}
if aeDisabled.Message != aeWrongPw.Message {
t.Errorf("disabled message = %q, want %q (must match wrong-password message to prevent user enumeration)", aeDisabled.Message, aeWrongPw.Message)
}
if aeDisabled.Message != "invalid email or password" {
t.Errorf("disabled message = %q, want %q", aeDisabled.Message, "invalid email or password")
}
}
func TestAuthService_LoginWithPasskeyDisabledUser(t *testing.T) {
svc, userRepo := setupAuthServiceWithRepos(t)
ctx := context.Background()
user, err := svc.Register(ctx, "alice", "alice@example.com", "password123")
if err != nil {
t.Fatalf("Register = %v", err)
}
pair, err := svc.Login(ctx, "alice@example.com", "password123")
if err != nil {
t.Fatalf("Login = %v", err)
}
claims, err := auth.ParseToken(pair.AccessToken, []byte("test-secret"))
if err != nil {
t.Fatalf("ParseToken = %v", err)
}
pk, err := svc.CreatePasskey(ctx, claims.UserID, "My Phone")
if err != nil {
t.Fatalf("CreatePasskey = %v", err)
}
// Soft-delete the user
if err := userRepo.Delete(ctx, user.ID); err != nil {
t.Fatalf("Delete = %v", err)
}
_, err = svc.LoginWithPasskey(ctx, pk.Raw)
if err == nil {
t.Fatal("expected error for disabled user passkey login, got nil")
}
var ae *model.AppError
if !errors.As(err, &ae) {
t.Fatalf("expected AppError, got %T: %v", err, err)
}
if ae.Status != http.StatusUnauthorized {
t.Errorf("status = %d, want %d", ae.Status, http.StatusUnauthorized)
}
if ae.Message != "invalid passkey" {
t.Errorf("message = %q, want %q", ae.Message, "invalid passkey")
}
}
func TestAuthService_RefreshDisabledUser(t *testing.T) {
svc, userRepo := setupAuthServiceWithRepos(t)
ctx := context.Background()
user, err := svc.Register(ctx, "alice", "alice@example.com", "password123")
if err != nil {
t.Fatalf("Register = %v", err)
}
pair, err := svc.Login(ctx, "alice@example.com", "password123")
if err != nil {
t.Fatalf("Login = %v", err)
}
// Soft-delete the user
if err := userRepo.Delete(ctx, user.ID); err != nil {
t.Fatalf("Delete = %v", err)
}
_, err = svc.Refresh(ctx, pair.RefreshToken)
if err == nil {
t.Fatal("expected error for disabled user refresh, got nil")
}
var ae *model.AppError
if !errors.As(err, &ae) {
t.Fatalf("expected AppError, got %T: %v", err, err)
}
if ae.Status != http.StatusUnauthorized {
t.Errorf("status = %d, want %d", ae.Status, http.StatusUnauthorized)
}
if ae.Message != "invalid token" {
t.Errorf("message = %q, want %q", ae.Message, "invalid token")
}
}