Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 6 additions & 6 deletions internal/apirouter/auth_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,19 +107,19 @@ func APIKeyOrTenantJWTAuthMiddleware(apiKey string, jwtKey string) gin.HandlerFu
}

// Try JWT auth
tokenTenantID, err := JWT.ExtractTenantID(jwtKey, token)
claims, err := JWT.Extract(jwtKey, token)
if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}

// If tenantID param exists, verify it matches token
if paramTenantID := c.Param("tenantID"); paramTenantID != "" && paramTenantID != tokenTenantID {
if paramTenantID := c.Param("tenantID"); paramTenantID != "" && paramTenantID != claims.TenantID {
c.AbortWithStatus(http.StatusUnauthorized)
return
}

c.Set("tenantID", tokenTenantID)
c.Set("tenantID", claims.TenantID)
c.Set(authRoleKey, RoleTenant)
c.Next()
}
Expand All @@ -143,19 +143,19 @@ func TenantJWTAuthMiddleware(apiKey string, jwtKey string) gin.HandlerFunc {
return
}

tokenTenantID, err := JWT.ExtractTenantID(jwtKey, token)
claims, err := JWT.Extract(jwtKey, token)
if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}

// If tenantID param exists, verify it matches token
if paramTenantID := c.Param("tenantID"); paramTenantID != "" && paramTenantID != tokenTenantID {
if paramTenantID := c.Param("tenantID"); paramTenantID != "" && paramTenantID != claims.TenantID {
c.AbortWithStatus(http.StatusUnauthorized)
return
}

c.Set("tenantID", tokenTenantID)
c.Set("tenantID", claims.TenantID)
c.Set(authRoleKey, RoleTenant)
c.Next()
}
Expand Down
6 changes: 3 additions & 3 deletions internal/apirouter/auth_middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ func TestAPIKeyOrTenantJWTAuthMiddleware(t *testing.T) {
c.Params = []gin.Param{{Key: "tenantID", Value: "different_tenant"}}

// Create JWT token for tenantID
token, err := apirouter.JWT.New(jwtSecret, tenantID)
token, err := apirouter.JWT.New(jwtSecret, apirouter.JWTClaims{TenantID: tenantID})
if err != nil {
t.Fatal(err)
}
Expand All @@ -201,7 +201,7 @@ func TestAPIKeyOrTenantJWTAuthMiddleware(t *testing.T) {
c.Params = []gin.Param{{Key: "tenantID", Value: tenantID}}

// Create JWT token for tenantID
token, err := apirouter.JWT.New(jwtSecret, tenantID)
token, err := apirouter.JWT.New(jwtSecret, apirouter.JWTClaims{TenantID: tenantID})
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -251,7 +251,7 @@ func TestAPIKeyOrTenantJWTAuthMiddleware(t *testing.T) {
}

func newJWTToken(t *testing.T, secret string, tenantID string) string {
token, err := apirouter.JWT.New(secret, tenantID)
token, err := apirouter.JWT.New(secret, apirouter.JWTClaims{TenantID: tenantID})
if err != nil {
t.Fatal(err)
}
Expand Down
40 changes: 32 additions & 8 deletions internal/apirouter/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,28 @@ var (
ErrInvalidToken = errors.New("invalid token")
)

func (_ jsonwebtoken) New(jwtSecret string, tenantID string) (string, error) {
// JWTClaims contains the custom claims for JWT tokens
type JWTClaims struct {
TenantID string
DeploymentID string
}

func (_ jsonwebtoken) New(jwtSecret string, claims JWTClaims) (string, error) {
now := time.Now()
token := jwt.NewWithClaims(signingMethod, jwt.MapClaims{
mapClaims := jwt.MapClaims{
"iss": issuer,
"sub": tenantID,
"sub": claims.TenantID,
"iat": now.Unix(),
"exp": now.Add(24 * time.Hour).Unix(),
})
}
if claims.DeploymentID != "" {
mapClaims["deployment_id"] = claims.DeploymentID
}
token := jwt.NewWithClaims(signingMethod, mapClaims)
return token.SignedString([]byte(jwtSecret))
}

func (_ jsonwebtoken) ExtractTenantID(jwtSecret string, tokenString string) (string, error) {
func (_ jsonwebtoken) Extract(jwtSecret string, tokenString string) (JWTClaims, error) {
token, err := jwt.Parse(
tokenString,
func(token *jwt.Token) (interface{}, error) {
Expand All @@ -39,14 +49,28 @@ func (_ jsonwebtoken) ExtractTenantID(jwtSecret string, tokenString string) (str
jwt.WithIssuer(issuer),
)
if err != nil || !token.Valid {
return "", ErrInvalidToken
return JWTClaims{}, ErrInvalidToken
}

claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return JWTClaims{}, ErrInvalidToken
}

tenantID, err := token.Claims.GetSubject()
if err != nil {
return "", ErrInvalidToken
return JWTClaims{}, ErrInvalidToken
}

var deploymentID string
if did, ok := claims["deployment_id"].(string); ok {
deploymentID = did
}
return tenantID, nil

return JWTClaims{
TenantID: tenantID,
DeploymentID: deploymentID,
}, nil
}

func (_ jsonwebtoken) Verify(jwtSecret string, tokenString string, tenantID string) (bool, error) {
Expand Down
63 changes: 53 additions & 10 deletions internal/apirouter/jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,34 @@ func TestJWT(t *testing.T) {
const issuer = "outpost"
const jwtKey = "supersecret"
const tenantID = "tenantID"
const deploymentID = "deployment123"
var signingMethod = jwt.SigningMethodHS256

t.Run("should generate a new jwt token", func(t *testing.T) {
t.Parallel()
token, err := apirouter.JWT.New(jwtKey, tenantID)
token, err := apirouter.JWT.New(jwtKey, apirouter.JWTClaims{TenantID: tenantID})
assert.Nil(t, err)
assert.NotEqual(t, "", token)
})

t.Run("should generate a new jwt token with deployment_id", func(t *testing.T) {
t.Parallel()
token, err := apirouter.JWT.New(jwtKey, apirouter.JWTClaims{
TenantID: tenantID,
DeploymentID: deploymentID,
})
assert.Nil(t, err)
assert.NotEqual(t, "", token)

// Verify deployment_id is in the token
claims, err := apirouter.JWT.Extract(jwtKey, token)
assert.Nil(t, err)
assert.Equal(t, deploymentID, claims.DeploymentID)
})

t.Run("should verify a valid jwt token", func(t *testing.T) {
t.Parallel()
token, err := apirouter.JWT.New(jwtKey, tenantID)
token, err := apirouter.JWT.New(jwtKey, apirouter.JWTClaims{TenantID: tenantID})
if err != nil {
t.Fatal(err)
}
Expand All @@ -36,24 +52,51 @@ func TestJWT(t *testing.T) {
assert.True(t, valid)
})

t.Run("should extract tenantID from valid token", func(t *testing.T) {
t.Run("should extract claims from valid token", func(t *testing.T) {
t.Parallel()
token, err := apirouter.JWT.New(jwtKey, tenantID)
token, err := apirouter.JWT.New(jwtKey, apirouter.JWTClaims{TenantID: tenantID})
if err != nil {
t.Fatal(err)
}
extractedTenantID, err := apirouter.JWT.ExtractTenantID(jwtKey, token)
claims, err := apirouter.JWT.Extract(jwtKey, token)
assert.Nil(t, err)
assert.Equal(t, tenantID, extractedTenantID)
assert.Equal(t, tenantID, claims.TenantID)
assert.Equal(t, "", claims.DeploymentID)
})

t.Run("should fail to extract tenantID from invalid token", func(t *testing.T) {
t.Run("should fail to extract claims from invalid token", func(t *testing.T) {
t.Parallel()
_, err := apirouter.JWT.ExtractTenantID(jwtKey, "invalid_token")
_, err := apirouter.JWT.Extract(jwtKey, "invalid_token")
assert.ErrorIs(t, err, apirouter.ErrInvalidToken)
})

t.Run("should fail to extract tenantID from token with invalid issuer", func(t *testing.T) {
t.Run("should extract all claims from valid token", func(t *testing.T) {
t.Parallel()
token, err := apirouter.JWT.New(jwtKey, apirouter.JWTClaims{
TenantID: tenantID,
DeploymentID: deploymentID,
})
if err != nil {
t.Fatal(err)
}
claims, err := apirouter.JWT.Extract(jwtKey, token)
assert.Nil(t, err)
assert.Equal(t, tenantID, claims.TenantID)
assert.Equal(t, deploymentID, claims.DeploymentID)
})

t.Run("should return empty deployment_id when not in token", func(t *testing.T) {
t.Parallel()
token, err := apirouter.JWT.New(jwtKey, apirouter.JWTClaims{TenantID: tenantID})
if err != nil {
t.Fatal(err)
}
claims, err := apirouter.JWT.Extract(jwtKey, token)
assert.Nil(t, err)
assert.Equal(t, "", claims.DeploymentID)
})

t.Run("should fail to extract claims from token with invalid issuer", func(t *testing.T) {
t.Parallel()
now := time.Now()
jwtToken := jwt.NewWithClaims(signingMethod, jwt.MapClaims{
Expand All @@ -66,7 +109,7 @@ func TestJWT(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = apirouter.JWT.ExtractTenantID(jwtKey, token)
_, err = apirouter.JWT.Extract(jwtKey, token)
assert.ErrorIs(t, err, apirouter.ErrInvalidToken)
})

Expand Down
3 changes: 2 additions & 1 deletion internal/apirouter/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type RouterConfig struct {
ServiceName string
APIKey string
JWTSecret string
DeploymentID string
Topics []string
Registry destregistry.Registry
PortalConfig portal.PortalConfig
Expand Down Expand Up @@ -138,7 +139,7 @@ func NewRouter(
apiRouter := r.Group("/api/v1")
apiRouter.Use(SetTenantIDMiddleware())

tenantHandlers := NewTenantHandlers(logger, telemetry, cfg.JWTSecret, entityStore)
tenantHandlers := NewTenantHandlers(logger, telemetry, cfg.JWTSecret, cfg.DeploymentID, entityStore)
destinationHandlers := NewDestinationHandlers(logger, telemetry, entityStore, cfg.Topics, cfg.Registry)
publishHandlers := NewPublishHandlers(logger, publishmqEventHandler)
logHandlers := NewLogHandlers(logger, logStore)
Expand Down
4 changes: 2 additions & 2 deletions internal/apirouter/router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func TestRouterWithAPIKey(t *testing.T) {
router, _, _ := setupTestRouter(t, apiKey, jwtSecret)

tenantID := "tenantID"
validToken, err := apirouter.JWT.New(jwtSecret, tenantID)
validToken, err := apirouter.JWT.New(jwtSecret, apirouter.JWTClaims{TenantID: tenantID})
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -201,7 +201,7 @@ func TestRouterWithoutAPIKey(t *testing.T) {
router, _, _ := setupTestRouter(t, apiKey, jwtSecret)

tenantID := "tenantID"
validToken, err := apirouter.JWT.New(jwtSecret, tenantID)
validToken, err := apirouter.JWT.New(jwtSecret, apirouter.JWTClaims{TenantID: tenantID})
if err != nil {
t.Fatal(err)
}
Expand Down
29 changes: 19 additions & 10 deletions internal/apirouter/tenant_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,26 @@ import (
)

type TenantHandlers struct {
logger *logging.Logger
telemetry telemetry.Telemetry
jwtSecret string
entityStore models.EntityStore
logger *logging.Logger
telemetry telemetry.Telemetry
jwtSecret string
deploymentID string
entityStore models.EntityStore
}

func NewTenantHandlers(
logger *logging.Logger,
telemetry telemetry.Telemetry,
jwtSecret string,
deploymentID string,
entityStore models.EntityStore,
) *TenantHandlers {
return &TenantHandlers{
logger: logger,
telemetry: telemetry,
jwtSecret: jwtSecret,
entityStore: entityStore,
logger: logger,
telemetry: telemetry,
jwtSecret: jwtSecret,
deploymentID: deploymentID,
entityStore: entityStore,
}
}

Expand Down Expand Up @@ -180,7 +183,10 @@ func (h *TenantHandlers) RetrieveToken(c *gin.Context) {
if tenant == nil {
return
}
jwtToken, err := JWT.New(h.jwtSecret, tenant.ID)
jwtToken, err := JWT.New(h.jwtSecret, JWTClaims{
TenantID: tenant.ID,
DeploymentID: h.deploymentID,
})
if err != nil {
AbortWithError(c, http.StatusInternalServerError, NewErrInternalServer(err))
return
Expand All @@ -193,7 +199,10 @@ func (h *TenantHandlers) RetrievePortal(c *gin.Context) {
if tenant == nil {
return
}
jwtToken, err := JWT.New(h.jwtSecret, tenant.ID)
jwtToken, err := JWT.New(h.jwtSecret, JWTClaims{
TenantID: tenant.ID,
DeploymentID: h.deploymentID,
})
if err != nil {
AbortWithError(c, http.StatusInternalServerError, NewErrInternalServer(err))
return
Expand Down
1 change: 1 addition & 0 deletions internal/services/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ func (b *ServiceBuilder) BuildAPIWorkers(baseRouter *gin.Engine) error {
ServiceName: b.cfg.OpenTelemetry.GetServiceName(),
APIKey: b.cfg.APIKey,
JWTSecret: b.cfg.APIJWTSecret,
DeploymentID: b.cfg.DeploymentID,
Topics: b.cfg.Topics,
Registry: svc.destRegistry,
PortalConfig: b.cfg.GetPortalConfig(),
Expand Down