feat(svc): reject disabled users in Login, LoginWithPasskey, and Refresh
This commit is contained in:
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user