feat(auth): replace hand-rolled auth with Ory Kratos — phase 2 backend
Delegates all credential management (login, register, password reset, email verification, social sign-in) to Ory Kratos. The Go API now acts as a resource server: the new KratosAuth middleware validates sessions against the Kratos whoami endpoint, writes the local User mirror into Echo context, and all existing domain handlers continue working unchanged. Hand-rolled token auth, AuthToken model, apple_auth/ google_auth services, and the auth refresh flow are removed. Tests are updated to use the fake-token middleware pattern so existing integration assertions require no rewrite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -506,232 +506,6 @@ func TestTaskHandler_CreateCompletion_NoTaskID(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Auth Handler - Additional Coverage
|
||||
// =============================================================================
|
||||
|
||||
func TestAuthHandler_AppleSignIn_NotConfigured(t *testing.T) {
|
||||
handler, e, _ := setupAuthHandler(t)
|
||||
|
||||
e.POST("/api/auth/apple-sign-in/", handler.AppleSignIn)
|
||||
|
||||
t.Run("returns 500 when apple auth not configured", func(t *testing.T) {
|
||||
req := map[string]interface{}{
|
||||
"id_token": "fake-token",
|
||||
"user_id": "fake-user-id",
|
||||
}
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/apple-sign-in/", req, "")
|
||||
testutil.AssertStatusCode(t, w, http.StatusInternalServerError)
|
||||
})
|
||||
|
||||
t.Run("missing identity_token returns 400", func(t *testing.T) {
|
||||
req := map[string]interface{}{}
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/apple-sign-in/", req, "")
|
||||
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthHandler_GoogleSignIn_NotConfigured(t *testing.T) {
|
||||
handler, e, _ := setupAuthHandler(t)
|
||||
|
||||
e.POST("/api/auth/google-sign-in/", handler.GoogleSignIn)
|
||||
|
||||
t.Run("returns 500 when google auth not configured", func(t *testing.T) {
|
||||
req := map[string]interface{}{
|
||||
"id_token": "fake-token",
|
||||
}
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/google-sign-in/", req, "")
|
||||
testutil.AssertStatusCode(t, w, http.StatusInternalServerError)
|
||||
})
|
||||
|
||||
t.Run("missing id_token returns 400", func(t *testing.T) {
|
||||
req := map[string]interface{}{}
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/google-sign-in/", req, "")
|
||||
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
||||
})
|
||||
}
|
||||
|
||||
// setupAuthHandlerWithDB is like setupAuthHandler but also returns the underlying *gorm.DB
|
||||
// for tests that need to create records like ConfirmationCode directly.
|
||||
func setupAuthHandlerWithDB(t *testing.T) (*AuthHandler, *echo.Echo, *gorm.DB) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
userRepo := repositories.NewUserRepository(db)
|
||||
cfg := &config.Config{
|
||||
Security: config.SecurityConfig{
|
||||
SecretKey: "test-secret-key",
|
||||
PasswordResetExpiry: 15 * time.Minute,
|
||||
ConfirmationExpiry: 24 * time.Hour,
|
||||
MaxPasswordResetRate: 3,
|
||||
},
|
||||
}
|
||||
authService := services.NewAuthService(userRepo, cfg)
|
||||
handler := NewAuthHandler(authService, nil, nil)
|
||||
e := testutil.SetupTestRouter()
|
||||
return handler, e, db
|
||||
}
|
||||
|
||||
func TestAuthHandler_VerifyEmail(t *testing.T) {
|
||||
handler, e, db := setupAuthHandlerWithDB(t)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "verifytest", "verify@test.com", "Password123")
|
||||
|
||||
// Create confirmation code
|
||||
confirmCode := &models.ConfirmationCode{
|
||||
UserID: user.ID,
|
||||
Code: "123456",
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
IsUsed: false,
|
||||
}
|
||||
require.NoError(t, db.Create(confirmCode).Error)
|
||||
|
||||
authGroup := e.Group("/api/auth")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.POST("/verify-email/", handler.VerifyEmail)
|
||||
|
||||
t.Run("successful verification", func(t *testing.T) {
|
||||
req := requests.VerifyEmailRequest{
|
||||
Code: "123456",
|
||||
}
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/verify-email/", req, "test-token")
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, true, response["verified"])
|
||||
})
|
||||
|
||||
t.Run("wrong code returns error", func(t *testing.T) {
|
||||
req := requests.VerifyEmailRequest{
|
||||
Code: "999999",
|
||||
}
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/verify-email/", req, "test-token")
|
||||
// Code already used or wrong code
|
||||
assert.True(t, w.Code == http.StatusBadRequest || w.Code == http.StatusNotFound,
|
||||
"expected 400 or 404, got %d", w.Code)
|
||||
})
|
||||
|
||||
t.Run("missing code returns 400", func(t *testing.T) {
|
||||
req := map[string]interface{}{}
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/verify-email/", req, "test-token")
|
||||
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthHandler_ResendVerification(t *testing.T) {
|
||||
handler, e, db := setupAuthHandlerWithDB(t)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "resendtest", "resend@test.com", "Password123")
|
||||
|
||||
authGroup := e.Group("/api/auth")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.POST("/resend-verification/", handler.ResendVerification)
|
||||
|
||||
t.Run("successful resend", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/resend-verification/", nil, "test-token")
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, response, "message")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthHandler_RefreshToken(t *testing.T) {
|
||||
handler, e, db := setupAuthHandlerWithDB(t)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "refreshtest", "refresh@test.com", "Password123")
|
||||
|
||||
// Create auth token and use its actual key in the middleware
|
||||
authToken := testutil.CreateTestToken(t, db, user.ID)
|
||||
|
||||
authGroup := e.Group("/api/auth")
|
||||
authGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
c.Set("auth_user", user)
|
||||
c.Set("auth_token", authToken.Plaintext) // raw token — repo hashes for lookup (audit C1)
|
||||
return next(c)
|
||||
}
|
||||
})
|
||||
authGroup.POST("/refresh/", handler.RefreshToken)
|
||||
|
||||
t.Run("successful refresh", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/refresh/", nil, authToken.Plaintext)
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, response, "token")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthHandler_VerifyResetCode(t *testing.T) {
|
||||
handler, e, _ := setupAuthHandler(t)
|
||||
|
||||
e.POST("/api/auth/register/", handler.Register)
|
||||
e.POST("/api/auth/verify-reset-code/", handler.VerifyResetCode)
|
||||
|
||||
t.Run("invalid code returns error", func(t *testing.T) {
|
||||
req := requests.VerifyResetCodeRequest{
|
||||
Email: "nonexistent@test.com",
|
||||
Code: "999999",
|
||||
}
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/verify-reset-code/", req, "")
|
||||
// Should not be 200 since no valid code exists
|
||||
assert.NotEqual(t, http.StatusOK, w.Code)
|
||||
})
|
||||
|
||||
t.Run("missing fields returns 400", func(t *testing.T) {
|
||||
req := map[string]interface{}{}
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/verify-reset-code/", req, "")
|
||||
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthHandler_ResetPassword(t *testing.T) {
|
||||
handler, e, _ := setupAuthHandler(t)
|
||||
|
||||
e.POST("/api/auth/reset-password/", handler.ResetPassword)
|
||||
|
||||
t.Run("invalid reset token returns error", func(t *testing.T) {
|
||||
req := requests.ResetPasswordRequest{
|
||||
ResetToken: "invalid-token",
|
||||
NewPassword: "NewPassword123",
|
||||
}
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/reset-password/", req, "")
|
||||
assert.NotEqual(t, http.StatusOK, w.Code)
|
||||
})
|
||||
|
||||
t.Run("missing fields returns 400", func(t *testing.T) {
|
||||
req := map[string]interface{}{}
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/reset-password/", req, "")
|
||||
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
||||
})
|
||||
|
||||
t.Run("short password returns 400", func(t *testing.T) {
|
||||
req := requests.ResetPasswordRequest{
|
||||
ResetToken: "some-token",
|
||||
NewPassword: "short",
|
||||
}
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/reset-password/", req, "")
|
||||
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthHandler_ForgotPassword_MissingEmail(t *testing.T) {
|
||||
handler, e, _ := setupAuthHandler(t)
|
||||
|
||||
e.POST("/api/auth/forgot-password/", handler.ForgotPassword)
|
||||
|
||||
t.Run("missing email returns 400", func(t *testing.T) {
|
||||
req := map[string]interface{}{}
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/forgot-password/", req, "")
|
||||
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Residence Handler - Additional Error Paths
|
||||
// =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user