Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
64 changes: 53 additions & 11 deletions docs/guide/extractors.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,23 +274,65 @@ Choose extractors based on your specific use case and security needs, not blanke

## Standards Compliance

### Authorization header compliance (RFC 9110; previously RFC 7235)
### Authorization Header (RFC 9110 & RFC 7235)

The `FromAuthHeader` extractor aligns with HTTP authentication semantics:
The `FromAuthHeader` extractor provides comprehensive RFC compliance with strict security validation:

- **Case-insensitive scheme matching**: `Bearer`, `bearer`, `BEARER` all work
- **OWS handling**: Supports SP/HTAB around the scheme/value per RFC 9110
- **Proper error handling**: Validates header format and content
- **Security-conscious**: Prevents common parsing vulnerabilities
#### RFC 9110 Compliance (Authorization Header Format)

- **Section 11.6.2 Format**: Enforces `credentials = auth-scheme 1*SP token68` structure
- **1*SP Requirement**: Validates exactly one or more spaces between auth-scheme and token
- **Case-insensitive scheme matching**: `Bearer`, `bearer`, `BEARER` all work correctly
- **Proper whitespace handling**: Rejects tabs between scheme and token (only spaces allowed)

#### RFC 7235 Token68 Validation

The extractor implements strict token68 character validation per RFC 7235:

- **Allowed characters**: `A-Z`, `a-z`, `0-9`, `-`, `.`, `_`, `~`, `+`, `/`, `=`
- **Padding rules**: `=` characters only allowed at the end of tokens
- **Security validation**: Prevents tokens starting with `=` or having non-padding characters after `=`
- **Whitespace rejection**: Rejects tokens containing spaces, tabs, or any other whitespace

#### Security Features

- **Header injection prevention**: Strict parsing prevents malformed authorization headers from bypassing authentication
- **Token validation**: Ensures extracted tokens conform to standards, preventing authentication bypass
- **Consistent error handling**: Returns `ErrNotFound` for all invalid cases

#### Examples

```go
// All of these work correctly:
extractors.FromAuthHeader("Bearer") // Standard case
extractors.FromAuthHeader("bearer") // Lowercase
extractors.FromAuthHeader("BEARER") // Uppercase
extractors.FromAuthHeader("") // No scheme, returns header (or ErrNotFound if empty)
// Standard usage - strict validation
extractor := extractors.FromAuthHeader("Bearer")

// ✅ Valid cases:
// "Bearer abc123" -> "abc123"
// "bearer ABC123" -> "ABC123" (case-insensitive scheme)
// "Bearer token123=" -> "token123=" (valid padding)
// "Bearer token==" -> "token==" (valid multiple padding)

// ❌ Invalid cases (all return ErrNotFound):
// "Bearer abc def" -> rejected (space in token)
// "Bearer abc\tdef" -> rejected (tab in token)
// "Bearer =abc" -> rejected (padding at start)
// "Bearer ab=cd" -> rejected (padding in middle)
// "Bearer token" -> rejected (multiple spaces after scheme)
// "Bearer\ttoken" -> rejected (tab after scheme)
// "Bearertoken" -> rejected (no space after scheme)

// Raw header extraction (no validation)
rawExtractor := extractors.FromAuthHeader("")
// "CustomAuth anything goes here" -> "CustomAuth anything goes here"
```

#### Benefits

- **Standards Compliance**: Full adherence to HTTP authentication RFCs
- **Security Hardening**: Prevents common authentication bypass vulnerabilities
- **Consistent Behavior**: Reliable parsing across different client implementations
- **Developer Confidence**: Clear validation rules reduce authentication bugs

## Troubleshooting

### Extraction Fails
Expand Down
54 changes: 35 additions & 19 deletions docs/middleware/keyauth.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,36 +223,52 @@ curl --header "Authorization: Bearer my-super-secret-key" http://localhost:3000

## Key Extractors

The middleware extracts the API key from the request using an `Extractor`. You can specify one or more extractors in the configuration.
KeyAuth uses an `Extractor` from the shared [extractors](../guide/extractors) package to retrieve the API key from the request. You can specify one or more extractors in the configuration. For a full list of extractors, chaining, and advanced usage, see the [Extractors Guide](../guide/extractors).

### Built-in Extractors
### Typical Usage

The following extractors are available:
Specify the extractor in the config. For example, to extract from a cookie:

- `keyauth.FromHeader(header string)`: Extracts the key from the specified header.
- `keyauth.FromAuthHeader(header, authScheme string)`: Extracts the key from an authorization header (e.g., `Authorization: Bearer <key>`).
- `keyauth.FromQuery(param string)`: Extracts the key from a URL query parameter.
- `keyauth.FromParam(param string)`: Extracts the key from a URL path parameter.
- `keyauth.FromCookie(name string)`: Extracts the key from a cookie.
- `keyauth.FromForm(name string)`: Extracts the key from a form field.
```go
app.Use(keyauth.New(keyauth.Config{
Extractor: extractors.FromCookie("access_token"),
Validator: validateAPIKey,
}))
```

### Chaining Extractors
To use the default (Authorization header with Bearer scheme):

```go
app.Use(keyauth.New(keyauth.Config{
Validator: validateAPIKey, // Extractor defaults to FromAuthHeader("Bearer")
}))
```

You can use `keyauth.Chain` to try multiple extractors until one succeeds. The first successful extraction is used.
To try multiple sources (header, then query):

```go
// This will try to extract the key from:
// 1. The "X-API-Key" header
// 2. The "api_key" query parameter
app.Use(keyauth.New(keyauth.Config{
Extractor: keyauth.Chain(
keyauth.FromHeader("X-API-Key"),
keyauth.FromQuery("api_key"),
Extractor: extractors.Chain(
extractors.FromHeader("X-API-Key"),
extractors.FromQuery("api_key"),
),
Validator: validateAPIKey,
}))
```

For custom logic, use `extractors.FromCustom`:

```go
app.Use(keyauth.New(keyauth.Config{
Extractor: extractors.FromCustom(func(c fiber.Ctx) (string, error) {
return c.Get("X-My-API-Key"), nil
}),
Validator: validateAPIKey,
}))
```

Refer to the [Extractors Guide](../guide/extractors) for details, security notes, and advanced configuration.

## Config

| Property | Type | Description | Default |
Expand All @@ -261,7 +277,7 @@ app.Use(keyauth.New(keyauth.Config{
| SuccessHandler | `fiber.Handler` | SuccessHandler defines a function which is executed for a valid key. | `c.Next()` |
| ErrorHandler | `fiber.ErrorHandler` | ErrorHandler defines a function which is executed for an invalid key. By default a 401 response with a `WWW-Authenticate` challenge is sent. | Default error handler |
| Validator | `func(fiber.Ctx, string) (bool, error)` | **Required.** Validator is a function to validate the key. | `nil` (panic) |
| Extractor | `keyauth.Extractor` | Extractor defines how to retrieve the key from the request. Use helper functions like `keyauth.FromAuthHeader` or `keyauth.FromCookie`. | `keyauth.FromAuthHeader("Authorization", "Bearer")` |
| Extractor | `extractors.Extractor` | Extractor defines how to retrieve the key from the request. Use helper functions from the shared extractors package, e.g. `extractors.FromAuthHeader("Bearer")` or `extractors.FromCookie("access_token")`. | `extractors.FromAuthHeader("Bearer")` |
| Realm | `string` | Realm specifies the protected area name used in the `WWW-Authenticate` header. | `"Restricted"` |
| Challenge | `string` | Value of the `WWW-Authenticate` header when no `Authorization` scheme is present. | `ApiKey realm="Restricted"` |
| Error | `string` | Error code appended as the `error` parameter in Bearer challenges. Must be `invalid_request`, `invalid_token`, or `insufficient_scope`. | `""` |
Expand All @@ -280,6 +296,6 @@ var ConfigDefault = Config{
return c.Status(fiber.StatusUnauthorized).SendString(ErrMissingOrMalformedAPIKey.Error())
},
Realm: "Restricted",
Extractor: FromAuthHeader(fiber.HeaderAuthorization, "Bearer"),
Extractor: extractors.FromAuthHeader("Bearer"),
}
```
108 changes: 67 additions & 41 deletions extractors/extractors.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import (
utils "github.com/gofiber/utils/v2"
)

// Source represents the type of source from which a value is extracted.
// Source represents the type of source from which an API key is extracted.
// This is informational metadata that helps developers understand the extractor behavior.
type Source int

Expand Down Expand Up @@ -73,15 +73,28 @@ type Extractor struct {
}

// FromAuthHeader extracts a value from the Authorization header with an optional prefix.
// This function implements RFC 9110 compliant Authorization header parsing.
// This function implements RFC 9110 compliant Authorization header parsing with strict token68 validation.
//
// The function supports:
// - Case-insensitive auth scheme matching
// - Empty auth scheme for raw header extraction
// RFC Compliance:
// - Follows RFC 9110 Section 11.6.2 for Authorization header format
// - Enforces 1*SP (one or more spaces) between auth-scheme and credentials
// - Implements RFC 7235 token68 character validation for extracted tokens
// - Case-insensitive auth scheme matching per HTTP standards
//
// Token68 Validation:
// - Only allows characters: A-Z, a-z, 0-9, -, ., _, ~, +, /, =
// - Rejects tokens containing spaces, tabs, or other whitespace
// - Validates proper padding: = only at end, no characters after padding starts
// - Prevents tokens starting with = (invalid padding)
//
// Security Features:
// - Strict validation prevents header injection attacks
// - Rejects malformed tokens that could bypass authentication
// - Consistent error handling for missing or invalid credentials
//
// Parameters:
// - authScheme: The auth scheme to strip from the header value (e.g., "Bearer", "Basic").
// If empty, the entire header value is returned without modification.
// If empty, the entire header value is returned without validation.
//
// Returns:
//
Expand All @@ -90,12 +103,13 @@ type Extractor struct {
//
// Examples:
//
// // Extract Bearer token
// // Extract Bearer token with validation
// extractor := FromAuthHeader("Bearer")
// // Input: "Bearer abc123" -> Output: "abc123"
// // Input: "Basic dXNlcjpwYXNz" -> Output: ErrNotFound
// // Input: "Bearer abc def" -> Output: ErrNotFound (space in token)
// // Input: "Basic dXNlcjpwYXNz" -> Output: ErrNotFound (wrong scheme)
//
// // Extract raw header value
// // Extract raw header value (no validation)
// extractor := FromAuthHeader("")
// // Input: "CustomAuth token123" -> Output: "CustomAuth token123"
func FromAuthHeader(authScheme string) Extractor {
Expand All @@ -107,45 +121,30 @@ func FromAuthHeader(authScheme string) Extractor {
}

// Check if the header starts with the specified auth scheme
if authScheme == "" {
if authHeader == "" {
if authScheme != "" {
schemeLen := len(authScheme)
if len(authHeader) <= schemeLen || !utils.EqualFold(authHeader[:schemeLen], authScheme) {
return "", ErrNotFound
}
rest := authHeader[schemeLen:]
if len(rest) == 0 || rest[0] != ' ' {
return "", ErrNotFound
}
return authHeader, nil
}

// Early return if header is too short for scheme + space + token
if len(authHeader) < len(authScheme)+2 {
return "", ErrNotFound
}

// Check if header starts with auth scheme (case-insensitive)
if !utils.EqualFold(authHeader[:len(authScheme)], authScheme) {
return "", ErrNotFound
}

// While RFC 9110 technically specifies 1*SP, HTTP implementations are generally lenient with whitespace (SP/HTAB)
if authHeader[len(authScheme)] != ' ' && authHeader[len(authScheme)] != '\t' {
return "", ErrNotFound
}

// Get the part after the scheme and required space
rest := authHeader[len(authScheme)+1:]
// Extract token after the required space
token := rest[1:]
if token == "" {
return "", ErrNotFound
}

// Skip any additional whitespace (SP/HTAB allowed per RFC 9110)
i := 0
for i < len(rest) && (rest[i] == ' ' || rest[i] == '\t') {
i++
}
if !isValidToken68(token) {
return "", ErrNotFound
}

// Must have some content after whitespace
if i == len(rest) {
return "", ErrNotFound
return token, nil
}

// Extract the token
token := rest[i:]
return token, nil
return authHeader, nil
},
Key: fiber.HeaderAuthorization,
Source: SourceAuthHeader,
Expand Down Expand Up @@ -502,3 +501,30 @@ func Chain(extractors ...Extractor) Extractor {
Chain: append([]Extractor(nil), extractors...), // Defensive copy for introspection
}
}

// isValidToken68 checks if a string is a valid token68 per RFC 7235/9110.
func isValidToken68(token string) bool {
if token == "" {
return false
}
paddingStarted := false
for i, c := range []byte(token) {
switch {
case (c >= 'A' && c <= 'Z') ||
(c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') ||
c == '-' || c == '.' || c == '_' || c == '~' || c == '+' || c == '/':
if paddingStarted {
return false // No characters allowed after padding starts
}
case c == '=':
if i == 0 {
return false // Cannot start with padding
}
paddingStarted = true
default:
return false // Invalid character
}
}
return true
}
Loading
Loading