Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ff21c1a
feat: Add mock authentication services and corresponding tests
moutonjeremy Feb 7, 2026
6d5aaea
feat: Implement authentication validators for Basic Auth, API Key, an…
moutonjeremy Feb 7, 2026
98b1518
feat: Enhance validateAuthorization to support multiple security sche…
moutonjeremy Feb 7, 2026
794ab63
feat: Implement authentication methods for Basic Auth, API Key, and A…
moutonjeremy Feb 7, 2026
d567d64
feat: Implement SmartAuthMiddleware and MultiSchemeAuthMiddleware for…
moutonjeremy Feb 7, 2026
65db835
feat: Update parseInput to pass configuration to validateAuthorization
moutonjeremy Feb 7, 2026
413d589
feat: Enhance error handling with AuthError and ScopeError types; val…
moutonjeremy Feb 7, 2026
bf3fd26
fix: Update authentication tests to expect 401 status code instead of…
moutonjeremy Feb 7, 2026
07fa2ab
fix: Enhance error handling in validateAuthorization to return AuthEr…
moutonjeremy Feb 7, 2026
32ee533
fix: Improve authentication error handling in MultiSchemeAuthMiddlewa…
moutonjeremy Feb 7, 2026
62516ab
fix: Enhance error handling in Method to differentiate between authen…
moutonjeremy Feb 7, 2026
65e64aa
fix: Update validateResourceAccess to return AuthError with appropria…
moutonjeremy Feb 7, 2026
9e2ec67
test: Add unit test for parseAWSSigV4Header to handle missing SignedH…
moutonjeremy Feb 7, 2026
de157fd
fix: Enhance AWS SigV4 header validation to check for missing SignedH…
moutonjeremy Feb 7, 2026
58009f6
fix: Improve error handling in MultiSchemeAuthMiddleware to return se…
moutonjeremy Feb 7, 2026
0ef0d49
feat: Add MockBearerAndAPIKeyAuthService and tests for AND-semantics …
moutonjeremy Feb 7, 2026
9612f01
fix: Update error handling in authentication validation functions to …
moutonjeremy Feb 7, 2026
dde8e82
fix: Propagate typed errors in validateAuthorization without re-wrapping
moutonjeremy Feb 7, 2026
6e7385c
fix: Improve error handling in authentication middlewares to return a…
moutonjeremy Feb 7, 2026
34ead02
fix: Enhance validateAuthorization to short-circuit on server configu…
moutonjeremy Feb 7, 2026
8a07212
fix: Short-circuit on server configuration errors in MultiSchemeAuthM…
moutonjeremy Feb 7, 2026
3dcaa86
fix: Add error label for authorization failure in MultiSchemeAuthMidd…
moutonjeremy Feb 7, 2026
9799cd9
test: Add tests for unsupported API Key location and per-route securi…
moutonjeremy Feb 7, 2026
23b31a4
fix: Return AuthError for unsupported API Key location in validateAPIKey
moutonjeremy Feb 7, 2026
a61f2a0
fix: Implement per-route security requirements fallback in parseInput
moutonjeremy Feb 7, 2026
5853c84
fix: Add specific error label for authorization failure in classifyAu…
moutonjeremy Feb 7, 2026
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
141 changes: 114 additions & 27 deletions _examples/auth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import (
"github.com/gofiber/fiber/v2"
)

// Authentication service with role management
// Authentication service with role management.
// Implements AuthorizationService, BasicAuthValidator, APIKeyValidator, and AWSSignatureValidator.
type ExampleAuthService struct{}

func (s *ExampleAuthService) ValidateToken(token string) (*fiberoapi.AuthContext, error) {
Expand Down Expand Up @@ -116,6 +117,75 @@ func (s *ExampleAuthService) GetUserPermissions(ctx *fiberoapi.AuthContext, reso
}, nil
}

// ValidateBasicAuth implements BasicAuthValidator for HTTP Basic authentication (curl --user).
func (s *ExampleAuthService) ValidateBasicAuth(username, password string) (*fiberoapi.AuthContext, error) {
// Example credentials
users := map[string]string{
"admin": "admin-pass",
"user": "user-pass",
}

expectedPassword, exists := users[username]
if !exists || password != expectedPassword {
return nil, fmt.Errorf("invalid credentials for user: %s", username)
}

roles := []string{"user"}
scopes := []string{"read", "write"}
if username == "admin" {
roles = []string{"admin", "user"}
scopes = []string{"read", "write", "delete", "share"}
}

return &fiberoapi.AuthContext{
UserID: username,
Roles: roles,
Scopes: scopes,
}, nil
}

// ValidateAPIKey implements APIKeyValidator for API Key authentication.
func (s *ExampleAuthService) ValidateAPIKey(key string, location string, paramName string) (*fiberoapi.AuthContext, error) {
validKeys := map[string]string{
"my-secret-api-key": "apikey-user-1",
"another-api-key": "apikey-user-2",
}

userID, exists := validKeys[key]
if !exists {
return nil, fmt.Errorf("invalid API key")
}

return &fiberoapi.AuthContext{
UserID: userID,
Roles: []string{"user"},
Scopes: []string{"read"},
}, nil
}

// ValidateAWSSignature implements AWSSignatureValidator for AWS SigV4 authentication.
func (s *ExampleAuthService) ValidateAWSSignature(params *fiberoapi.AWSSignatureParams) (*fiberoapi.AuthContext, error) {
// In a real implementation, you would verify the HMAC-SHA256 signature
// using the secret key associated with the AccessKeyID.
validKeys := map[string]bool{
"AKIAIOSFODNN7EXAMPLE": true,
}

if !validKeys[params.AccessKeyID] {
return nil, fmt.Errorf("invalid access key: %s", params.AccessKeyID)
}

return &fiberoapi.AuthContext{
UserID: "aws-service-" + params.AccessKeyID,
Roles: []string{"service"},
Scopes: []string{"read", "write"},
Claims: map[string]interface{}{
"region": params.Region,
"service": params.Service,
},
}, nil
}

type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=2,max=50"`
}
Expand Down Expand Up @@ -175,11 +245,31 @@ func main() {
Type: "http",
Scheme: "bearer",
BearerFormat: "JWT",
Description: "JWT Bearer token",
Description: "JWT Bearer token authentication",
},
"basicAuth": {
Type: "http",
Scheme: "basic",
Description: "HTTP Basic authentication (curl --user user:pass)",
},
"apiKeyAuth": {
Type: "apiKey",
In: "header",
Name: "X-API-Key",
Description: "API Key authentication via header",
},
"awsSigV4": {
Type: "http",
Scheme: "AWS4-HMAC-SHA256",
Description: "AWS Signature V4 authentication",
},
},
// Any of these schemes can be used (OR semantics)
DefaultSecurity: []map[string][]string{
{"bearerAuth": {}},
{"basicAuth": {}},
{"apiKeyAuth": {}},
{"awsSigV4": {}},
},
}

Expand Down Expand Up @@ -411,44 +501,41 @@ func main() {
fmt.Println("📚 Documentation: http://localhost:3002/docs")
fmt.Println("📄 OpenAPI JSON: http://localhost:3002/openapi.json")
fmt.Println("")
fmt.Println("🔑 Méthodes d'authentification supportées:")
fmt.Println(" Bearer Token: Authorization: Bearer <token>")
fmt.Println(" Basic Auth: Authorization: Basic base64(user:pass) (curl --user user:pass)")
fmt.Println(" API Key: X-API-Key: <key>")
fmt.Println(" AWS SigV4: Authorization: AWS4-HMAC-SHA256 Credential=...")
fmt.Println("")
fmt.Println("🔑 Tokens de test disponibles:")
fmt.Println(" admin-token -> rôles: [admin, user], scopes: [read, write, delete, share]")
fmt.Println(" editor-token -> rôles: [editor, user], scopes: [read, write, share]")
fmt.Println(" user-token -> rôles: [user], scopes: [read, write]")
fmt.Println(" readonly-token -> rôles: [user], scopes: [read]")
fmt.Println("")
fmt.Println("🌍 Endpoints par niveau d'accès:")
fmt.Println(" GET /health (public)")
fmt.Println(" GET /me (auth simple)")
fmt.Println(" GET /documents/:id (user + read)")
fmt.Println(" PUT /documents/:id (user + write)")
fmt.Println(" POST /documents/:id/share (scope: share)")
fmt.Println(" DELETE /documents/:id (admin + delete)")
fmt.Println(" POST /users (admin + write)")
fmt.Println("🔑 Comptes Basic Auth:")
fmt.Println(" admin:admin-pass -> rôles: [admin, user]")
fmt.Println(" user:user-pass -> rôles: [user]")
fmt.Println("")
fmt.Println("🧪 Tests suggérés:")
fmt.Println(" # Test admin - création d'utilisateur")
fmt.Println(` curl -X POST -H 'Authorization: Bearer admin-token' -H 'Content-Type: application/json' -d '{"name":"John Doe"}' http://localhost:3002/users`)
fmt.Println("")
fmt.Println(" # Test utilisateur normal (devrait échouer)")
fmt.Println(` curl -X POST -H 'Authorization: Bearer readonly-token' -H 'Content-Type: application/json' -d '{"name":"Jane Doe"}' http://localhost:3002/users`)
fmt.Println("🔑 API Keys:")
fmt.Println(" my-secret-api-key -> read only")
fmt.Println(" another-api-key -> read only")
fmt.Println("")
fmt.Println(" # Test lecture document")
fmt.Println(" curl -H 'Authorization: Bearer user-token' http://localhost:3002/documents/33cd10d7-d80f-4fd2-9107-7423997393d2")
fmt.Println("🧪 Tests suggérés:")
fmt.Println(" # Bearer Token")
fmt.Println(" curl -H 'Authorization: Bearer admin-token' http://localhost:3002/me")
fmt.Println("")
fmt.Println(" # Test modification document")
fmt.Println(` curl -X PUT -H 'Authorization: Bearer user-token' -H 'Content-Type: application/json' -d '{"title":"Mon Document","content":"Contenu modifié"}' http://localhost:3002/documents/33cd10d7-d80f-4fd2-9107-7423997393d2`)
fmt.Println(" # Basic Auth (curl --user)")
fmt.Println(" curl --user admin:admin-pass http://localhost:3002/me")
fmt.Println("")
fmt.Println(" # Test partage (éditeur/admin seulement)")
fmt.Println(" curl -X POST -H 'Authorization: Bearer editor-token' http://localhost:3002/documents/33cd10d7-d80f-4fd2-9107-7423997393d2/share")
fmt.Println(" # API Key")
fmt.Println(" curl -H 'X-API-Key: my-secret-api-key' http://localhost:3002/documents/doc-1")
fmt.Println("")
fmt.Println(" # Test suppression (admin seulement)")
fmt.Println(" curl -X DELETE -H 'Authorization: Bearer admin-token' http://localhost:3002/documents/33cd10d7-d80f-4fd2-9107-7423997393d2")
fmt.Println(" # AWS SigV4")
fmt.Println(" curl -H 'Authorization: AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20250101/us-east-1/execute-api/aws4_request, SignedHeaders=host;x-amz-date, Signature=abc123' http://localhost:3002/me")
fmt.Println("")
fmt.Println(" # Test endpoints publics")
fmt.Println(" # Public endpoint")
fmt.Println(" curl http://localhost:3002/health")
fmt.Println(" curl -H 'Authorization: Bearer user-token' http://localhost:3002/me")
fmt.Println(" curl -H 'Authorization: Bearer user-token' http://localhost:3002/status")

app.Listen(":3002")
}
66 changes: 44 additions & 22 deletions auth.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package fiberoapi

import (
"errors"
"fmt"
"reflect"
"strings"
Expand Down Expand Up @@ -139,36 +140,57 @@ func RoleGuard(validator AuthorizationService, requiredRoles ...string) fiber.Ha
}
}

// validateAuthorization validates permissions based on tags
func validateAuthorization(c *fiber.Ctx, input interface{}, authService AuthorizationService) error {
// validateAuthorization validates permissions based on configured security schemes.
// When SecuritySchemes is empty, it falls back to Bearer-only validation for backward compatibility.
func validateAuthorization(c *fiber.Ctx, input interface{}, authService AuthorizationService, config *Config) error {
if authService == nil {
return nil
}

// Extract and validate the token directly
authHeader := c.Get("Authorization")
if authHeader == "" {
return fmt.Errorf("authentication required")
// Backward compatibility: if no SecuritySchemes are configured,
// fall back to Bearer-only validation (original behavior).
if config == nil || len(config.SecuritySchemes) == 0 {
authCtx, err := validateBearerToken(c, authService)
if err != nil {
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

In the backward-compatibility branch (len(config.SecuritySchemes)==0), any error from validateBearerToken is wrapped into AuthError{StatusCode:401,...}. This discards typed errors coming from authService.ValidateToken (e.g., an *AuthError with a 5xx status for an internal failure), which can cause internal errors to be reported as 401. Consider preserving *AuthError (and its status code) when ValidateToken returns one, and only default to 401 for untyped errors.

Suggested change
if err != nil {
if err != nil {
// Preserve typed AuthError (including 5xx) and map ScopeError to 403,
// falling back to 401 only for untyped errors.
var authErr *AuthError
if errors.As(err, &authErr) {
return err
}
var scopeErr *ScopeError
if errors.As(err, &scopeErr) {
return &AuthError{StatusCode: 403, Message: err.Error()}
}

Copilot uses AI. Check for mistakes.
return &AuthError{StatusCode: 401, Message: err.Error()}
}
c.Locals("auth", authCtx)
return validateResourceAccess(c, authCtx, input, authService)
}

// Check Bearer format
if !strings.HasPrefix(authHeader, "Bearer ") {
return fmt.Errorf("invalid authorization header format")
// Multi-scheme validation path
securityReqs := config.DefaultSecurity
if len(securityReqs) == 0 {
securityReqs = buildDefaultFromSchemes(config.SecuritySchemes)
}

token := strings.TrimPrefix(authHeader, "Bearer ")

// Validate the token
authCtx, err := authService.ValidateToken(token)
if err != nil {
return fmt.Errorf("invalid token: %v", err)
// Try each security requirement (OR semantics per OpenAPI spec).
// Server configuration errors (5xx) short-circuit immediately since
// no alternative requirement can fix a misconfigured scheme.
var lastErr error
for _, requirement := range securityReqs {
authCtx, err := validateSecurityRequirement(c, requirement, config.SecuritySchemes, authService)
if err == nil {
c.Locals("auth", authCtx)
return validateResourceAccess(c, authCtx, input, authService)
}
var authErr *AuthError
if errors.As(err, &authErr) && authErr.StatusCode >= 500 {
return err
}
lastErr = err
}

// Store auth context for later use
c.Locals("auth", authCtx)

// Analyze authorization tags in the struct
return validateResourceAccess(c, authCtx, input, authService)
// Propagate typed errors (AuthError, ScopeError) without re-wrapping
var existingAuthErr *AuthError
if errors.As(lastErr, &existingAuthErr) {
return lastErr
}
var scopeErr *ScopeError
if errors.As(lastErr, &scopeErr) {
return &AuthError{StatusCode: 403, Message: lastErr.Error()}
}
return &AuthError{StatusCode: 401, Message: lastErr.Error()}
}

// validateResourceAccess validates resource access based on tags
Expand Down Expand Up @@ -202,11 +224,11 @@ func validateResourceAccess(c *fiber.Ctx, authCtx *AuthContext, input interface{

canAccess, err := authService.CanAccessResource(authCtx, resourceTag, resourceID, actionTag)
if err != nil {
return fmt.Errorf("authorization check failed: %w", err)
return &AuthError{StatusCode: 500, Message: fmt.Sprintf("authorization check failed: %v", err)}
}

if !canAccess {
return fmt.Errorf("insufficient permissions for %s %s on %s", actionTag, resourceTag, resourceID)
return &AuthError{StatusCode: 403, Message: fmt.Sprintf("insufficient permissions for %s %s on %s", actionTag, resourceTag, resourceID)}
}
}
}
Expand Down
Loading
Loading