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
13 changes: 13 additions & 0 deletions _examples/auth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,19 @@ func main() {
EnableOpenAPIDocs: true,
EnableAuthorization: true,
AuthService: authService,
// Custom error handler for authentication/authorization errors
AuthErrorHandler: func(c *fiber.Ctx, err *fiberoapi.AuthError) error {
errType := "authentication_error"
if err.StatusCode == 403 {
errType = "authorization_error"
}
return c.Status(err.StatusCode).JSON(fiber.Map{
"error": errType,
"message": err.Message,
"status": err.StatusCode,
"service": "fiber-oapi auth example",
})
},
SecuritySchemes: map[string]fiberoapi.SecurityScheme{
"bearerAuth": {
Type: "http",
Expand Down
67 changes: 67 additions & 0 deletions auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,73 @@ func TestRequiredRoles(t *testing.T) {
})
}

// TestCustomAuthErrorHandler tests that a custom AuthErrorHandler overrides the default error response
func TestCustomAuthErrorHandler(t *testing.T) {
mockAuth := NewMockAuthService()

app := fiber.New()
oapi := New(app, Config{
EnableOpenAPIDocs: true,
EnableValidation: true,
EnableAuthorization: true,
AuthService: mockAuth,
SecuritySchemes: map[string]SecurityScheme{
"bearerAuth": {Type: "http", Scheme: "bearer", BearerFormat: "JWT"},
},
DefaultSecurity: []map[string][]string{
{"bearerAuth": {}},
},
AuthErrorHandler: func(c *fiber.Ctx, err *AuthError) error {
return c.Status(err.StatusCode).JSON(fiber.Map{
"custom": true,
"message": err.Message,
"status": err.StatusCode,
})
},
})

Get(oapi, "/protected", func(c *fiber.Ctx, input struct{}) (fiber.Map, *ErrorResponse) {
return fiber.Map{"ok": true}, nil
}, OpenAPIOptions{Summary: "Protected"})

t.Run("custom handler for 401", func(t *testing.T) {
req := httptest.NewRequest("GET", "/protected", nil)
// No auth header -> 401
resp, _ := app.Test(req)
if resp.StatusCode != 401 {
t.Fatalf("Expected 401, got %d", resp.StatusCode)
}
var body map[string]any
json.NewDecoder(resp.Body).Decode(&body)
resp.Body.Close()
if body["custom"] != true {
t.Error("Expected custom error handler to be invoked")
}
})

t.Run("custom handler for 403", func(t *testing.T) {
Get(oapi, "/admin-only", func(c *fiber.Ctx, input struct{}) (fiber.Map, *ErrorResponse) {
return fiber.Map{"ok": true}, nil
}, WithRoles(OpenAPIOptions{Summary: "Admin only"}, "admin"))

req := httptest.NewRequest("GET", "/admin-only", nil)
req.Header.Set("Authorization", "Bearer valid-token") // has "user" role, not "admin"
resp, _ := app.Test(req)
if resp.StatusCode != 403 {
t.Fatalf("Expected 403, got %d", resp.StatusCode)
}
var body map[string]any
json.NewDecoder(resp.Body).Decode(&body)
resp.Body.Close()
if body["custom"] != true {
t.Error("Expected custom error handler to be invoked for 403")
}
if body["status"] != float64(403) {
t.Errorf("Expected status 403 in body, got %v", body["status"])
}
})
}

// TestWithRolesHelper tests the WithRoles helper function
func TestWithRolesHelper(t *testing.T) {
opts := WithRoles(OpenAPIOptions{Summary: "test"}, "admin", "editor")
Expand Down
9 changes: 8 additions & 1 deletion fiberoapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ func New(app *fiber.App, config ...Config) *OApiApp {
provided.OpenAPIDocsPath != "" ||
provided.OpenAPIJSONPath != "" ||
provided.OpenAPIYamlPath != "" ||
provided.ValidationErrorHandler != nil
provided.ValidationErrorHandler != nil ||
provided.AuthErrorHandler != nil

// Only override boolean defaults if the config appears to be explicitly set
if hasExplicitConfig {
Expand Down Expand Up @@ -96,6 +97,9 @@ func New(app *fiber.App, config ...Config) *OApiApp {
if provided.ValidationErrorHandler != nil {
cfg.ValidationErrorHandler = provided.ValidationErrorHandler
}
if provided.AuthErrorHandler != nil {
cfg.AuthErrorHandler = provided.AuthErrorHandler
}
}

oapi := &OApiApp{
Expand Down Expand Up @@ -860,6 +864,9 @@ func Method[TInput any, TOutput any, TError any](
// Check for authentication/authorization errors first
var authErr *AuthError
if errors.As(err, &authErr) {
if app.config.AuthErrorHandler != nil {
return app.config.AuthErrorHandler(c, authErr)
}
errType := "authentication_error"
if authErr.StatusCode == 403 {
errType = "authorization_error"
Expand Down
5 changes: 5 additions & 0 deletions types.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ type PathInfo struct {
// It receives the fiber context and the validation error, and returns a fiber error response
type ValidationErrorHandler func(c *fiber.Ctx, err error) error

// AuthErrorHandler is a function type for handling authentication/authorization errors
// It receives the fiber context and the AuthError, and returns a fiber error response
type AuthErrorHandler func(c *fiber.Ctx, err *AuthError) error

// Config represents configuration for the OApi wrapper
type Config struct {
EnableValidation bool // Enable request validation (default: true)
Expand All @@ -69,6 +73,7 @@ type Config struct {
SecuritySchemes map[string]SecurityScheme // OpenAPI security schemes
DefaultSecurity []map[string][]string // Default security requirements
ValidationErrorHandler ValidationErrorHandler // Custom handler for validation errors
AuthErrorHandler AuthErrorHandler // Custom handler for auth errors (401/403)
Comment on lines 75 to +76
}

// OpenAPIOptions represents options for OpenAPI operations
Expand Down
Loading