feat(auth): replace hand-rolled auth with Ory Kratos — phase 2 backend
Backend CI / Test (push) Has been cancelled
Backend CI / Contract Tests (push) Has been cancelled
Backend CI / Lint (push) Has been cancelled
Backend CI / Secret Scanning (push) Has been cancelled
Backend CI / Build (push) Has been cancelled

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:
Trey t
2026-05-18 17:55:56 -05:00
parent b66151ddd9
commit 81578f6e27
36 changed files with 927 additions and 7002 deletions
-226
View File
@@ -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
// =============================================================================