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
10 changes: 5 additions & 5 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,22 @@ import (
"github.com/ebrickdev/ebrick/logger"
"github.com/ebrickdev/ebrick/module"
"github.com/ebrickdev/ebrick/transport/grpc"
"github.com/ebrickdev/ebrick/transport/httpserver"
"github.com/ebrickdev/ebrick/transport/http"
)

// Application defines the interface for the application.
type Application interface {
RegisterModules(ctx context.Context, modules ...module.Module) error
GrpcServer() grpc.GRPCServer
HTTPServer() httpserver.HTTPServer
HTTPServer() http.HTTPServer
Start(ctx context.Context) error
Options() *Options
}

// application is the implementation of the Application interface.
type application struct {
mm *module.ModuleManager
httpServer httpserver.HTTPServer
httpServer http.HTTPServer
grpcServer grpc.GRPCServer
options *Options
}
Expand All @@ -34,7 +34,7 @@ func (app *application) GrpcServer() grpc.GRPCServer {
}

// HTTPServer returns the web server instance.
func (app *application) HTTPServer() httpserver.HTTPServer {
func (app *application) HTTPServer() http.HTTPServer {
return app.httpServer
}

Expand Down Expand Up @@ -89,7 +89,7 @@ func (app *application) Start(ctx context.Context) error {
return combinedErr
}

// registerRoutesAndServices registers all routes for modules implementing httpserver.Routable or grpc.ServiceRegistrar.
// registerRoutesAndServices registers all routes for modules implementing http.Routable or grpc.ServiceRegistrar.
func (app *application) registerRoutesAndServices(log logger.Logger) error {
for _, mod := range app.mm.GetModules() {
if svcReg, ok := mod.(grpc.ServiceRegistrar); ok {
Expand Down
8 changes: 4 additions & 4 deletions ebrick.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import (
"github.com/ebrickdev/ebrick/module"
"github.com/ebrickdev/ebrick/security/auth"
"github.com/ebrickdev/ebrick/transport/grpc"
"github.com/ebrickdev/ebrick/transport/httpserver"
"github.com/ebrickdev/ebrick/transport/http"
"gorm.io/gorm"
)

var (
EventBus messaging.EventBus
Logger logger.Logger
HTTPServer httpserver.HTTPServer
HTTPServer http.HTTPServer
GRPCServer grpc.GRPCServer
AuthManager auth.AuthManager
DB *gorm.DB
Expand All @@ -25,7 +25,7 @@ func NewApplication(opts ...Option) Application {
// Set the global variables here.
EventBus = options.EventBus
Logger = options.Logger
HTTPServer = options.HTTPServer
HTTPServer = options.http
GRPCServer = options.GRPCServer
AuthManager = options.AuthManager
DB = options.DB
Expand All @@ -39,7 +39,7 @@ func NewApplication(opts ...Option) Application {

app := &application{
mm: moduleManager,
httpServer: options.HTTPServer,
httpServer: options.http,
grpcServer: options.GRPCServer,
options: options,
}
Expand Down
34 changes: 17 additions & 17 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ import (
"github.com/ebrickdev/ebrick/messaging"
"github.com/ebrickdev/ebrick/security/auth"
"github.com/ebrickdev/ebrick/transport/grpc"
"github.com/ebrickdev/ebrick/transport/httpserver"
"github.com/ebrickdev/ebrick/transport/http"
"gorm.io/gorm"
)

// Options holds both configuration values and runtime dependencies
type Options struct {
Name string // Application name
Version string // Application version
Cache cache.Cache // Cache instance
Logger logger.Logger // Logger instance
EventBus messaging.EventBus // Event bus instance for inter-component communication
HTTPServer httpserver.HTTPServer // HTTP server instance
GRPCServer grpc.GRPCServer // gRPC server instance; optional
Name string // Application name
Version string // Application version
Cache cache.Cache // Cache instance
Logger logger.Logger // Logger instance
EventBus messaging.EventBus // Event bus instance for inter-component communication
http http.HTTPServer // HTTP server instance
GRPCServer grpc.GRPCServer // gRPC server instance; optional
DB *gorm.DB
AuthManager auth.AuthManager
}
Expand Down Expand Up @@ -61,19 +61,19 @@ func newOptions(opts ...Option) *Options {
opt.EventBus = eventBus
}

// Initialize HTTPServer if not provided.
// NewHTTPServer uses the application config to configure the server.
if opt.HTTPServer == nil {
// Initialize http if not provided.
// Newhttp uses the application config to configure the server.
if opt.http == nil {
var webMode string
if cfg.Env == "development" {
webMode = "debug"
} else {
webMode = "release"
}

opt.HTTPServer = httpserver.NewHTTPServer(
httpserver.WithAddress(fmt.Sprintf(":%s", cfg.Server.Port)),
httpserver.WithMode(webMode),
opt.http = http.NewHTTPServer(
http.WithAddress(fmt.Sprintf(":%s", cfg.Server.Port)),
http.WithMode(webMode),
)
}

Expand Down Expand Up @@ -116,9 +116,9 @@ func WithEventBus(eventBus messaging.EventBus) Option {
return func(o *Options) { o.EventBus = eventBus }
}

// WithHTTPServer sets the HTTPServer dependency.
func WithHTTPServer(httpServer httpserver.HTTPServer) Option {
return func(o *Options) { o.HTTPServer = httpServer }
// Withhttp sets the http dependency.
func Withhttp(http http.HTTPServer) Option {
return func(o *Options) { o.http = http }
}

// WithGRPCServer sets the GRPCServer dependency.
Expand Down
60 changes: 21 additions & 39 deletions security/auth/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,112 +2,95 @@ package auth

import (
"errors"
"net/http"
"strings"

"github.com/ebrickdev/ebrick/logger"
"github.com/ebrickdev/ebrick/transport/httpserver"
"github.com/ebrickdev/ebrick/transport/http"
)

const (
// bearerPrefix is the expected prefix in the Authorization header.
bearerPrefix = "Bearer "
)

// AuthMiddleware provides middleware for token authentication and role-based authorization.
type AuthMiddleware struct {
authManager AuthManager
logger logger.Logger
}

// NewAuthMiddleware creates a new AuthMiddleware instance.
func NewAuthMiddleware(authManager AuthManager, logger logger.Logger) *AuthMiddleware {
return &AuthMiddleware{
authManager: authManager,
logger: logger,
}
}

// TokenAuth is middleware that validates a JWT token from the Authorization header.
func (am *AuthMiddleware) TokenAuth() httpserver.HandlerFunc {
return func(c httpserver.Context) {
authHeader := c.Request().Header.Get("Authorization")
func (am *AuthMiddleware) TokenAuth() http.HandlerFunc {
return func(c *http.Context) {
authHeader := c.Request.Header.Get("Authorization")
if authHeader == "" {
am.logger.Warn("Authorization header missing")
abortWithError(c, http.StatusUnauthorized, "missing token")
c.AbortWithError(http.StatusUnauthorized, errors.New("missing token"))
return
}

if !strings.HasPrefix(authHeader, bearerPrefix) {
if len(authHeader) < len(bearerPrefix) || !strings.EqualFold(authHeader[:len(bearerPrefix)], bearerPrefix) {
am.logger.Warn("Authorization header missing Bearer prefix")
abortWithError(c, http.StatusUnauthorized, "invalid token format")
c.AbortWithError(http.StatusUnauthorized, errors.New("invalid token format"))
return
}

tokenString := strings.TrimPrefix(authHeader, bearerPrefix)
tokenString := strings.TrimSpace(authHeader[len(bearerPrefix):])
if tokenString == "" {
am.logger.Warn("Empty token after Bearer prefix removal")
abortWithError(c, http.StatusUnauthorized, "missing token")
c.AbortWithError(http.StatusUnauthorized, errors.New("missing token"))
return
}

principal, err := am.authManager.Authenticate(c.Request().Context(), tokenString)
principal, err := am.authManager.Authenticate(c.Request.Context(), tokenString)
if err != nil {
am.logger.Error("Token authentication failed", logger.Error(err))
if isTokenExpired(err) {
abortWithError(c, http.StatusUnauthorized, "token expired")
if errors.Is(err, ErrTokenExpired) {
c.AbortWithError(http.StatusUnauthorized, errors.New("token expired"))
} else {
abortWithError(c, http.StatusUnauthorized, "invalid or malformed token")
c.AbortWithError(http.StatusUnauthorized, errors.New("invalid or malformed token"))
}
return
}

// Store authentication claims and identifier in the context for later use.
c.Set("claims", principal)
c.Set("user_id", principal.GetID())
c.Next()
}
}

// RequireRoles is middleware that restricts access to endpoints based on user roles.
func (am *AuthMiddleware) RequireRoles(requiredRoles ...string) httpserver.HandlerFunc {
return func(c httpserver.Context) {
func (am *AuthMiddleware) RequireRoles(requiredRoles ...string) http.HandlerFunc {
return func(c *http.Context) {
rawClaims, exists := c.Get("claims")
if !exists {
abortWithError(c, http.StatusForbidden, "access forbidden")
am.logger.Warn("Claims not found in context")
c.AbortWithError(http.StatusForbidden, ErrAccessForbidden)
return
}

principal, ok := rawClaims.(Principal)
if !ok {
abortWithError(c, http.StatusForbidden, "access forbidden")
am.logger.Warn("Invalid claims type in context")
c.AbortWithError(http.StatusForbidden, ErrAccessForbidden)
return
}

if !hasAnyRole(principal.GetRoles(), requiredRoles) {
abortWithError(c, http.StatusForbidden, "access forbidden")
c.AbortWithError(http.StatusForbidden, ErrAccessForbidden)
return
}

c.Next()
}
}

// abortWithError sends a JSON error response and aborts further processing.
func abortWithError(c httpserver.Context, statusCode int, message string) {
c.JSON(statusCode, httpserver.H{"error": message})
c.Abort()
}

// isTokenExpired checks if the provided error indicates an expired token.
func isTokenExpired(err error) bool {
return errors.Is(err, ErrTokenExpired)
}

// ErrTokenExpired is returned when a token has expired.
var ErrTokenExpired = errors.New("token expired")
var ErrAccessForbidden = errors.New("access forbidden")

// hasAnyRole returns true if any of the requiredRoles is present in userRoles.
func hasAnyRole(userRoles, requiredRoles []string) bool {
for _, role := range requiredRoles {
if containsString(userRoles, role) {
Expand All @@ -117,7 +100,6 @@ func hasAnyRole(userRoles, requiredRoles []string) bool {
return false
}

// containsString checks if a slice contains the specified string.
func containsString(slice []string, item string) bool {
for _, s := range slice {
if s == item {
Expand Down
Loading