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:
+14
-50
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user