diff --git a/example.env b/example.env index 30881e71..59a9cf0f 100644 --- a/example.env +++ b/example.env @@ -62,7 +62,7 @@ MYSQL_MEDICAL_SSLMODE=disable KEYCLOAK_ISSUER=https://auth.rssa.top/realms/sandbox KEYCLOAK_AUDIENCE=nuxtsim-pendaftaran KEYCLOAK_JWKS_URL=https://auth.rssa.top/realms/sandbox/protocol/openid-connect/certs -KEYCLOAK_ENABLED=false +KEYCLOAK_ENABLED=true # BPJS Configuration BPJS_BASEURL=https://apijkn.bpjs-kesehatan.go.id/vclaim-rest diff --git a/internal/middleware/auth_middleware.go b/internal/middleware/auth_middleware.go new file mode 100644 index 00000000..1d3969c3 --- /dev/null +++ b/internal/middleware/auth_middleware.go @@ -0,0 +1,59 @@ +package middleware + +import ( + "fmt" + "net/http" + + "api-service/internal/config" + + "github.com/gin-gonic/gin" +) + +// ConfigurableAuthMiddleware provides flexible authentication based on configuration +func ConfigurableAuthMiddleware(cfg *config.Config) gin.HandlerFunc { + return func(c *gin.Context) { + // Skip authentication for development/testing if explicitly disabled + if !cfg.Keycloak.Enabled { + fmt.Println("Authentication is disabled - allowing all requests") + c.Next() + return + } + + // Use Keycloak authentication when enabled + AuthMiddleware()(c) + } +} + +// StrictAuthMiddleware enforces authentication regardless of Keycloak.Enabled setting +func StrictAuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + if appConfig == nil { + fmt.Println("AuthMiddleware: Config not initialized") + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "authentication service not configured"}) + return + } + + // Always enforce authentication + AuthMiddleware()(c) + } +} + +// OptionalKeycloakAuthMiddleware allows requests but adds authentication info if available +func OptionalKeycloakAuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + if appConfig == nil || !appConfig.Keycloak.Enabled { + c.Next() + return + } + + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + // No token provided, but continue + c.Next() + return + } + + // Try to validate token, but don't fail if invalid + AuthMiddleware()(c) + } +} diff --git a/internal/middleware/auth.go b/internal/middleware/keycloak_middleware.go similarity index 79% rename from internal/middleware/auth.go rename to internal/middleware/keycloak_middleware.go index 5da76ca6..a336154c 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/keycloak_middleware.go @@ -13,6 +13,8 @@ import ( "sync" "time" + "api-service/internal/config" + "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" "golang.org/x/sync/singleflight" @@ -22,24 +24,19 @@ var ( ErrInvalidToken = errors.New("invalid token") ) -// Configurable Keycloak parameters - replace with your actual values or load from config/env -const ( - KeycloakIssuer = "https://keycloak.example.com/auth/realms/yourrealm" - KeycloakAudience = "your-client-id" - JwksURL = KeycloakIssuer + "/protocol/openid-connect/certs" -) - // JwksCache caches JWKS keys with expiration type JwksCache struct { mu sync.RWMutex keys map[string]*rsa.PublicKey expiresAt time.Time sfGroup singleflight.Group + config *config.Config } -func NewJwksCache() *JwksCache { +func NewJwksCache(cfg *config.Config) *JwksCache { return &JwksCache{ - keys: make(map[string]*rsa.PublicKey), + keys: make(map[string]*rsa.PublicKey), + config: cfg, } } @@ -74,7 +71,17 @@ func (c *JwksCache) GetKey(kid string) (*rsa.PublicKey, error) { } func (c *JwksCache) fetchKeys() (map[string]*rsa.PublicKey, error) { - resp, err := http.Get(JwksURL) + if !c.config.Keycloak.Enabled { + return nil, fmt.Errorf("keycloak authentication is disabled") + } + + jwksURL := c.config.Keycloak.JwksURL + if jwksURL == "" { + // Construct JWKS URL from issuer if not explicitly provided + jwksURL = c.config.Keycloak.Issuer + "/protocol/openid-connect/certs" + } + + resp, err := http.Get(jwksURL) if err != nil { return nil, err } @@ -138,11 +145,32 @@ func base64UrlDecode(s string) ([]byte, error) { return base64.URLEncoding.DecodeString(s) } -var jwksCache = NewJwksCache() +// Global config instance +var appConfig *config.Config +var jwksCacheInstance *JwksCache + +// InitializeAuth initializes the auth middleware with config +func InitializeAuth(cfg *config.Config) { + appConfig = cfg + jwksCacheInstance = NewJwksCache(cfg) +} // AuthMiddleware validates Bearer token as Keycloak JWT token func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { + if appConfig == nil { + fmt.Println("AuthMiddleware: Config not initialized") + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "authentication service not configured"}) + return + } + + if !appConfig.Keycloak.Enabled { + // Skip authentication if Keycloak is disabled but log for debugging + fmt.Println("AuthMiddleware: Keycloak authentication is disabled - allowing all requests") + c.Next() + return + } + fmt.Println("AuthMiddleware: Checking Authorization header") // Debug log authHeader := c.GetHeader("Authorization") @@ -174,8 +202,8 @@ func AuthMiddleware() gin.HandlerFunc { return nil, errors.New("kid header not found") } - return jwksCache.GetKey(kid) - }, jwt.WithIssuer(KeycloakIssuer), jwt.WithAudience(KeycloakAudience)) + return jwksCacheInstance.GetKey(kid) + }, jwt.WithIssuer(appConfig.Keycloak.Issuer), jwt.WithAudience(appConfig.Keycloak.Audience)) if err != nil || !token.Valid { fmt.Printf("AuthMiddleware: Invalid or expired token: %v\n", err) // Debug log diff --git a/internal/routes/v1/routes.go b/internal/routes/v1/routes.go index f7bc52a7..8b062436 100644 --- a/internal/routes/v1/routes.go +++ b/internal/routes/v1/routes.go @@ -17,6 +17,9 @@ import ( func RegisterRoutes(cfg *config.Config) *gin.Engine { router := gin.New() + // Initialize auth middleware configuration + middleware.InitializeAuth(cfg) + // Add global middleware router.Use(middleware.CORSConfig()) router.Use(middleware.ErrorHandler()) @@ -55,39 +58,42 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine { // BPJS endpoints bpjsPesertaHandler := bpjsPesertaHandlers.NewPesertaHandler(cfg.Bpjs) v1.GET("/bpjs/peserta/nik/:nik/tglSEP/:tglSEP", bpjsPesertaHandler.GetPesertaByNIK) - // Retribusi endpoints - retribusiHandler := retribusiHandlers.NewRetribusiHandler() - retribusiGroup := v1.Group("/retribusi") - { - retribusiGroup.GET("", retribusiHandler.GetRetribusi) - retribusiGroup.GET("/dynamic", retribusiHandler.GetRetribusiDynamic) // Route baru - retribusiGroup.GET("/search", retribusiHandler.SearchRetribusiAdvanced) // Route pencarian - retribusiGroup.GET("/:id", retribusiHandler.GetRetribusiByID) - retribusiGroup.POST("", retribusiHandler.CreateRetribusi) - retribusiGroup.PUT("/:id", retribusiHandler.UpdateRetribusi) - retribusiGroup.DELETE("/:id", retribusiHandler.DeleteRetribusi) - } + + // ============= PUBLISHED ROUTES =============================================== + + // // Retribusi endpoints + // retribusiHandler := retribusiHandlers.NewRetribusiHandler() + // retribusiGroup := v1.Group("/retribusi") + // { + // retribusiGroup.GET("", retribusiHandler.GetRetribusi) + // retribusiGroup.GET("/dynamic", retribusiHandler.GetRetribusiDynamic) // Route baru + // retribusiGroup.GET("/search", retribusiHandler.SearchRetribusiAdvanced) // Route pencarian + // retribusiGroup.GET("/:id", retribusiHandler.GetRetribusiByID) + // retribusiGroup.POST("", retribusiHandler.CreateRetribusi) + // retribusiGroup.PUT("/:id", retribusiHandler.UpdateRetribusi) + // retribusiGroup.DELETE("/:id", retribusiHandler.DeleteRetribusi) + // } // ============================================================================= // PROTECTED ROUTES (Authentication Required) // ============================================================================= - // Create protected group with AuthMiddleware + // Create protected group with configurable authentication protected := v1.Group("/") - protected.Use(middleware.AuthMiddleware()) // Use Keycloak AuthMiddleware + protected.Use(middleware.ConfigurableAuthMiddleware(cfg)) // Use configurable authentication // User profile (protected) protected.GET("/auth/me", authHandler.Me) // Retribusi endpoints (CRUD operations - should be protected) - // retribusiHandler := retribusiHandlers.NewRetribusiHandler() - // protectedRetribusi := protected.Group("/retribusi") - // { - // protectedRetribusi.GET("", retribusiHandler.GetRetribusi) // GET /api/v1/retribusi - // protectedRetribusi.GET("/:id", retribusiHandler.GetRetribusiByID) // GET /api/v1/retribusi/:id - // protectedRetribusi.POST("/", retribusiHandler.CreateRetribusi) // POST /api/v1/retribusi/ - // protectedRetribusi.PUT("/:id", retribusiHandler.UpdateRetribusi) // PUT /api/v1/retribusi/:id - // protectedRetribusi.DELETE("/:id", retribusiHandler.DeleteRetribusi) // DELETE /api/v1/retribusi/:id - // } + retribusiHandler := retribusiHandlers.NewRetribusiHandler() + protectedRetribusi := protected.Group("/retribusi") + { + protectedRetribusi.GET("", retribusiHandler.GetRetribusi) // GET /api/v1/retribusi + protectedRetribusi.GET("/:id", retribusiHandler.GetRetribusiByID) // GET /api/v1/retribusi/:id + protectedRetribusi.POST("/", retribusiHandler.CreateRetribusi) // POST /api/v1/retribusi/ + protectedRetribusi.PUT("/:id", retribusiHandler.UpdateRetribusi) // PUT /api/v1/retribusi/:id + protectedRetribusi.DELETE("/:id", retribusiHandler.DeleteRetribusi) // DELETE /api/v1/retribusi/:id + } // BPJS endpoints (sensitive data - should be protected) // bpjsPesertaHandler := bpjsPesertaHandlers.NewPesertaHandler(cfg.Bpjs) diff --git a/tools/generate-handler.go b/tools/generate-handler.go index 726b79c9..730fa332 100644 --- a/tools/generate-handler.go +++ b/tools/generate-handler.go @@ -117,7 +117,7 @@ func main() { // Create directories with improved logic var handlerDir, modelDir string if category != "" { - handlerDir = filepath.Join("internal", "handlers") + handlerDir = filepath.Join("internal", "handlers", category) modelDir = filepath.Join("internal", "models", category) } else { handlerDir = filepath.Join("internal", "handlers") @@ -248,6 +248,7 @@ func New` + data.Name + `Handler() *` + data.Name + `Handler { handlerContent += generateHelperMethods(data) writeFile(filepath.Join(handlerDir, data.NameLower+".go"), handlerContent) + } func generateGetMethods(data HandlerData) string { @@ -1490,7 +1491,7 @@ func updateRoutesFile(data HandlerData) { newRoutes := generateProtectedRouteBlock(data) // Insert above protected routes marker - insertMarker := "// =============================================================================" + insertMarker := "// ============= PUBLISHED ROUTES ===============================================" if strings.Contains(routesContent, insertMarker) { if !strings.Contains(routesContent, fmt.Sprintf("New%sHandler", data.Name)) { // Insert before the marker