Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
PORT=4000
API_PORT=4000
API_KEY=apikey
JWT_SECRET=jwtsecret

# REDIS_HOST=<auto_set_up_via_docker_compose>
REDIS_PORT=6379
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ go 1.23.0
require (
github.com/alicebob/miniredis/v2 v2.33.0
github.com/gin-gonic/gin v1.10.0
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/go-cmp v0.6.0
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/redis/go-redis/extra/redisotel/v9 v9.5.3
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
Expand Down
18 changes: 10 additions & 8 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ const (
)

type Config struct {
Service ServiceType
Port int
Hostname string
APIKey string
Service ServiceType
Port int
Hostname string
APIKey string
JWTSecret string
Copy link
Collaborator Author

@alexluong alexluong Aug 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume we would need to add a JWT_SECRET env here, correct?


Redis *redis.RedisConfig
OpenTelemetry *otel.OpenTelemetryConfig
Expand Down Expand Up @@ -79,10 +80,11 @@ func Parse(flags Flags) (*Config, error) {

// Initialize config values
config := &Config{
Hostname: hostname,
Service: service,
Port: getPort(viper),
APIKey: viper.GetString("API_KEY"),
Hostname: hostname,
Service: service,
Port: getPort(viper),
APIKey: viper.GetString("API_KEY"),
JWTSecret: viper.GetString("JWT_SECRET"),
Redis: &redis.RedisConfig{
Host: viper.GetString("REDIS_HOST"),
Port: mustInt(viper, "REDIS_PORT"),
Expand Down
12 changes: 11 additions & 1 deletion internal/services/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"time"

"github.com/hookdeck/EventKit/internal/config"
"github.com/hookdeck/EventKit/internal/destination"
"github.com/hookdeck/EventKit/internal/redis"
"github.com/hookdeck/EventKit/internal/tenant"
"github.com/uptrace/opentelemetry-go-extra/otelzap"
"go.uber.org/zap"
)
Expand All @@ -27,7 +29,15 @@ func NewService(ctx context.Context, wg *sync.WaitGroup, cfg *config.Config, log
return nil, err
}

router := NewRouter(cfg, logger, redisClient)
router := NewRouter(
RouterConfig{
Hostname: cfg.Hostname,
APIKey: cfg.APIKey,
JWTSecret: cfg.JWTSecret,
},
tenant.NewHandlers(logger, redisClient, cfg.JWTSecret),
destination.NewHandlers(redisClient),
)

service := &APIService{}
service.logger = logger
Expand Down
35 changes: 35 additions & 0 deletions internal/services/api/auth_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"strings"

"github.com/gin-gonic/gin"
"github.com/hookdeck/EventKit/internal/tenant"
)

func apiKeyAuthMiddleware(apiKey string) gin.HandlerFunc {
Expand Down Expand Up @@ -33,7 +34,41 @@ func apiKeyAuthMiddleware(apiKey string) gin.HandlerFunc {
}
}

func apiKeyOrTenantJWTAuthMiddleware(apiKey string, jwtKey string) gin.HandlerFunc {
return func(c *gin.Context) {
authorizationToken, err := extractBearerToken(c.GetHeader("Authorization"))
if err != nil {
// TODO: Consider sending a more detailed error message.
// Currently we don't have clear specs on how to send back error message.
c.AbortWithStatus(http.StatusUnauthorized)
return
}
if authorizationToken == apiKey {
c.Next()
return
}
tenantID := c.Param("tenantID")
valid, err := tenant.JWT.Verify(jwtKey, authorizationToken, tenantID)
if err != nil {
// TODO: Consider sending a more detailed error message.
// Currently we don't have clear specs on how to send back error message.
c.AbortWithStatus(http.StatusUnauthorized)
return
}
if !valid {
// TODO: Consider sending a more detailed error message.
// Currently we don't have clear specs on how to send back error message.
c.AbortWithStatus(http.StatusUnauthorized)
return
}
c.Next()
}
}

func extractBearerToken(header string) (string, error) {
if header == "" {
return "", nil
}
if !strings.HasPrefix(header, "Bearer ") {
return "", errors.New("invalid bearer token")
}
Expand Down
36 changes: 29 additions & 7 deletions internal/services/api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,46 @@ import (
"net/http"

"github.com/gin-gonic/gin"
"github.com/hookdeck/EventKit/internal/config"
"github.com/hookdeck/EventKit/internal/destination"
"github.com/hookdeck/EventKit/internal/redis"
"github.com/uptrace/opentelemetry-go-extra/otelzap"
"github.com/hookdeck/EventKit/internal/tenant"
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
)

func NewRouter(cfg *config.Config, logger *otelzap.Logger, redisClient *redis.Client) http.Handler {
type RouterConfig struct {
Hostname string
APIKey string
JWTSecret string
}

func NewRouter(
cfg RouterConfig,
tenantHandlers *tenant.TenantHandlers,
destinationHandlers *destination.DestinationHandlers,
) http.Handler {
r := gin.Default()
r.Use(otelgin.Middleware(cfg.Hostname))
r.Use(apiKeyAuthMiddleware(cfg.APIKey))

r.GET("/healthz", func(c *gin.Context) {
logger.Ctx(c.Request.Context()).Info("health check")
c.Status(http.StatusOK)
})

destinationHandlers := destination.NewHandlers(redisClient)
// Admin router is a router group with the API key auth mechanism.
adminRouter := r.Group("/", apiKeyAuthMiddleware(cfg.APIKey))

adminRouter.PUT("/:tenantID", tenantHandlers.Upsert)
adminRouter.GET("/:tenantID/portal", tenantHandlers.RetrievePortal)

// Tenant router is a router group that accepts either
// - a tenant's JWT token OR
// - the preconfigured API key
//
// If the EventKit service deployment isn't configured with an API key, then
// it's assumed that the service runs in a secure environment
// and the JWT check is NOT necessary either.
tenantRouter := r.Group("/", apiKeyOrTenantJWTAuthMiddleware(cfg.APIKey, cfg.JWTSecret))

tenantRouter.GET("/:tenantID", tenantHandlers.Retrieve)
tenantRouter.DELETE("/:tenantID", tenantHandlers.Delete)

r.GET("/destinations", destinationHandlers.List)
r.POST("/destinations", destinationHandlers.Create)
Expand Down
Loading