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
+14 -50
View File
@@ -22,6 +22,7 @@ import (
"github.com/treytartt/honeydue-api/internal/dto/responses"
"github.com/treytartt/honeydue-api/internal/handlers"
"github.com/treytartt/honeydue-api/internal/i18n"
"github.com/treytartt/honeydue-api/internal/kratos"
custommiddleware "github.com/treytartt/honeydue-api/internal/middleware"
"github.com/treytartt/honeydue-api/internal/monitoring"
"github.com/treytartt/honeydue-api/internal/prom"
@@ -200,7 +201,7 @@ func SetupRouter(deps *Dependencies) *echo.Echo {
// Initialize services
authService := services.NewAuthService(userRepo, cfg)
authService.SetNotificationRepository(notificationRepo) // For creating notification preferences on registration
authService.SetNotificationRepository(notificationRepo)
userService := services.NewUserService(userRepo)
residenceService := services.NewResidenceService(residenceRepo, userRepo, cfg)
residenceService.SetTaskRepository(taskRepo) // Wire up task repo for statistics
@@ -220,7 +221,7 @@ func SetupRouter(deps *Dependencies) *echo.Echo {
// Wire Redis cache for residence-ID lookups across the four services that
// read it on the request hot path. Cache is best-effort; nil cache is OK.
if deps.Cache != nil {
authService.SetCacheService(deps.Cache) // per-account login lockout (audit M5)
authService.SetCacheService(deps.Cache)
residenceService.SetCacheService(deps.Cache)
taskService.SetCacheService(deps.Cache)
contractorService.SetCacheService(deps.Cache)
@@ -244,20 +245,15 @@ func SetupRouter(deps *Dependencies) *echo.Echo {
subscriptionWebhookHandler.SetStripeService(stripeService)
subscriptionWebhookHandler.SetCacheService(deps.Cache)
// Initialize middleware
authMiddleware := custommiddleware.NewAuthMiddlewareWithConfig(deps.DB, deps.Cache, cfg)
// Initialize Apple auth service
appleAuthService := services.NewAppleAuthService(deps.Cache, cfg)
googleAuthService := services.NewGoogleAuthService(deps.Cache, cfg)
// Initialize Kratos auth middleware (replaces hand-rolled token auth).
kratosClient := kratos.NewClient(cfg.Security.KratosPublicURL)
authMiddleware := custommiddleware.NewKratosAuth(kratosClient, deps.Cache, deps.DB)
// Initialize audit service for security event logging
auditService := services.NewAuditService(deps.DB)
// Initialize handlers
authHandler := handlers.NewAuthHandler(authService, deps.EmailService, deps.Cache)
authHandler.SetAppleAuthService(appleAuthService)
authHandler.SetGoogleAuthService(googleAuthService)
authHandler.SetStorageService(deps.StorageService)
authHandler.SetAuditService(auditService)
userHandler := handlers.NewUserHandler(userService)
@@ -318,8 +314,8 @@ func SetupRouter(deps *Dependencies) *echo.Echo {
// API group
api := e.Group("/api")
{
// Public auth routes (no auth required)
setupPublicAuthRoutes(api, authHandler, cfg.Server.Debug)
// Session lifecycle (login, register, logout, password reset) is
// handled by Ory Kratos — no public auth routes in this service.
// Public data routes (no auth required)
setupPublicDataRoutes(api, residenceHandler, taskHandler, contractorHandler, staticDataHandler, subscriptionHandler, taskTemplateHandler)
@@ -329,7 +325,7 @@ func SetupRouter(deps *Dependencies) *echo.Echo {
// Protected routes (auth required)
protected := api.Group("")
protected.Use(authMiddleware.TokenAuth())
protected.Use(authMiddleware.Authenticate())
protected.Use(custommiddleware.TimezoneMiddleware())
{
setupProtectedAuthRoutes(protected, authHandler)
@@ -516,50 +512,18 @@ func prometheusMetrics(monSvc *monitoring.Service) echo.HandlerFunc {
}
}
// setupPublicAuthRoutes configures public authentication routes with
// per-endpoint rate limiters to mitigate brute-force and credential-stuffing.
// Rate limiters are disabled in debug mode to allow UI test suites to run
// without hitting 429 errors.
func setupPublicAuthRoutes(api *echo.Group, authHandler *handlers.AuthHandler, debug bool) {
auth := api.Group("/auth")
// setupPublicAuthRoutes was removed — session lifecycle (login, register,
// logout, password reset, Apple/Google sign-in) is delegated to Ory Kratos.
if debug {
// No rate limiters in debug/local mode
auth.POST("/login/", authHandler.Login)
auth.POST("/register/", authHandler.Register)
auth.POST("/forgot-password/", authHandler.ForgotPassword)
auth.POST("/verify-reset-code/", authHandler.VerifyResetCode)
auth.POST("/reset-password/", authHandler.ResetPassword)
auth.POST("/apple-sign-in/", authHandler.AppleSignIn)
auth.POST("/google-sign-in/", authHandler.GoogleSignIn)
} else {
// Rate limiters — created once, shared across requests.
loginRL := custommiddleware.LoginRateLimiter() // 10 req/min
registerRL := custommiddleware.RegistrationRateLimiter() // 5 req/min
passwordRL := custommiddleware.PasswordResetRateLimiter() // 3 req/min
auth.POST("/login/", authHandler.Login, loginRL)
auth.POST("/register/", authHandler.Register, registerRL)
auth.POST("/forgot-password/", authHandler.ForgotPassword, passwordRL)
auth.POST("/verify-reset-code/", authHandler.VerifyResetCode, passwordRL)
auth.POST("/reset-password/", authHandler.ResetPassword, passwordRL)
auth.POST("/apple-sign-in/", authHandler.AppleSignIn, loginRL)
auth.POST("/google-sign-in/", authHandler.GoogleSignIn, loginRL)
}
}
// setupProtectedAuthRoutes configures protected authentication routes
// setupProtectedAuthRoutes configures protected auth routes.
// Session lifecycle (login, logout, password reset, email verification) is
// delegated to Ory Kratos — only profile and account-deletion routes remain.
func setupProtectedAuthRoutes(api *echo.Group, authHandler *handlers.AuthHandler) {
auth := api.Group("/auth")
{
auth.POST("/logout/", authHandler.Logout)
auth.POST("/refresh/", authHandler.RefreshToken)
auth.GET("/me/", authHandler.CurrentUser)
auth.PUT("/profile/", authHandler.UpdateProfile)
auth.PATCH("/profile/", authHandler.UpdateProfile)
auth.POST("/verify/", authHandler.VerifyEmail) // Alias for mobile app compatibility
auth.POST("/verify-email/", authHandler.VerifyEmail) // Original route
auth.POST("/resend-verification/", authHandler.ResendVerification)
auth.DELETE("/account/", authHandler.DeleteAccount)
}
}