-
-
Notifications
You must be signed in to change notification settings - Fork 161
🔥 feat: Add SPNEGO Authentication Middleware #1836
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
db1c1b7
8a1aa70
b795161
4f314bd
5197bf6
952aaf6
ef3cd2d
ba84250
45ce4a3
e04e024
d428340
000d40e
c798b96
c32cc18
80fa4dc
eb40e2c
12bd868
e0a550c
40c9876
b773b0b
b3b33d1
4d1c990
22a9b32
b5c7f18
53be94e
dcf9f8a
3179f56
3b123c9
58204e3
b227c43
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| name: "Test spnego" | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - master | ||
| - main | ||
| paths: | ||
| - 'v3/spnego/**/*.go' | ||
| - 'v3/spnego/go.mod' | ||
| - 'v3/spnego/go.sum' | ||
| pull_request: | ||
| paths: | ||
| - 'v3/spnego/**/*.go' | ||
| - 'v3/spnego/go.mod' | ||
| - 'v3/spnego/go.sum' | ||
|
|
||
| jobs: | ||
| Tests: | ||
| runs-on: ubuntu-latest | ||
| strategy: | ||
| matrix: | ||
| go-version: | ||
| - 1.25.x | ||
| steps: | ||
| - name: Fetch Repository | ||
| uses: actions/checkout@v6 | ||
| - name: Install Go | ||
| uses: actions/setup-go@v6 | ||
| with: | ||
| go-version: '${{ matrix.go-version }}' | ||
| - name: Run Test | ||
| working-directory: ./v3/spnego | ||
| run: go test -v -race ./... | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ use ( | |
| ./v3/otel | ||
| ./v3/paseto | ||
| ./v3/sentry | ||
| ./v3/spnego | ||
| ./v3/socketio | ||
| ./v3/swaggerui | ||
| ./v3/swaggo | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| --- | ||
| id: spnego | ||
| --- | ||
|
|
||
| # SPNEGO Kerberos Authentication Middleware for Fiber | ||
|
|
||
|  | ||
| [](https://gofiber.io/discord) | ||
|  | ||
|
|
||
| This middleware provides SPNEGO (Simple and Protected GSSAPI Negotiation Mechanism) authentication for [Fiber](https://github.com/gofiber/fiber) applications, enabling Kerberos authentication for HTTP requests and inspired by [gokrb5](https://github.com/jcmturner/gokrb5) | ||
|
|
||
| ## Features | ||
|
|
||
| - Kerberos authentication via SPNEGO mechanism | ||
| - Flexible keytab lookup system | ||
| - Support for dynamic keytab retrieval from various sources | ||
| - Integration with Fiber context for authenticated identity storage | ||
| - Configurable logging | ||
|
|
||
| ## Version Compatibility | ||
|
|
||
| This middleware is compatible with: | ||
|
|
||
| - **Fiber v3** | ||
|
|
||
| ## Installation | ||
|
|
||
| ```bash | ||
| # For Fiber v3 | ||
| go get github.com/gofiber/contrib/v3/spnego | ||
| ``` | ||
|
|
||
| ## Usage | ||
|
|
||
| ```go | ||
| package main | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "time" | ||
|
|
||
| "github.com/gofiber/contrib/v3/spnego" | ||
| "github.com/gofiber/contrib/v3/spnego/utils" | ||
| "github.com/gofiber/fiber/v3" | ||
| "github.com/gofiber/fiber/v3/log" | ||
| ) | ||
|
|
||
| func main() { | ||
| app := fiber.New() | ||
| // Create a configuration with a keytab lookup function | ||
| // For testing, you can create a mock keytab file using utils.NewMockKeytab | ||
| // In production, use a real keytab file | ||
| _, clean, err := utils.NewMockKeytab( | ||
| utils.WithPrincipal("HTTP/sso1.example.com"), | ||
| utils.WithRealm("EXAMPLE.LOCAL"), | ||
| utils.WithFilename("./temp-sso1.keytab"), | ||
| utils.WithPairs(utils.EncryptTypePair{ | ||
| Version: 2, | ||
| EncryptType: 18, | ||
| CreateTime: time.Now(), | ||
| }), | ||
| ) | ||
| if err != nil { | ||
| log.Fatalf("Failed to create mock keytab: %v", err) | ||
| } | ||
| defer clean() | ||
| keytabLookup, err := spnego.NewKeytabFileLookupFunc("./temp-sso1.keytab") | ||
| if err != nil { | ||
| log.Fatalf("Failed to create keytab lookup function: %v", err) | ||
| } | ||
|
|
||
| // Create the middleware | ||
| authMiddleware, err := spnego.New(spnego.Config{ | ||
| KeytabLookup: keytabLookup, | ||
| }) | ||
| if err != nil { | ||
| log.Fatalf("Failed to create middleware: %v", err) | ||
| } | ||
|
|
||
| // Apply the middleware to protected routes | ||
| app.Use("/protected", authMiddleware) | ||
|
|
||
| // Access authenticated identity | ||
| app.Get("/protected/resource", func(c fiber.Ctx) error { | ||
| identity, ok := spnego.GetAuthenticatedIdentityFromContext(c) | ||
| if !ok { | ||
| return c.Status(fiber.StatusUnauthorized).SendString("Unauthorized") | ||
| } | ||
| return c.SendString(fmt.Sprintf("Hello, %s!", identity.UserName())) | ||
| }) | ||
|
|
||
| log.Info("Server is running on :3000") | ||
| app.Listen(":3000") | ||
| } | ||
| ``` | ||
|
|
||
| ## Dynamic Keytab Lookup | ||
|
|
||
| The middleware is designed with extensibility in mind, allowing keytab retrieval from various sources beyond static files: | ||
|
|
||
| ```go | ||
| // Example: Retrieve keytab from a database | ||
| func dbKeytabLookup() (*keytab.Keytab, error) { | ||
| // Your database lookup logic here | ||
| // ... | ||
| return keytabFromDatabase, nil | ||
| } | ||
|
|
||
| // Example: Retrieve keytab from a remote service | ||
| func remoteKeytabLookup() (*keytab.Keytab, error) { | ||
| // Your remote service call logic here | ||
| // ... | ||
| return keytabFromRemote, nil | ||
| } | ||
| ``` | ||
|
|
||
| ## API Reference | ||
|
|
||
| ### `New(cfg Config) (fiber.Handler, error)` | ||
|
|
||
| Creates a new SPNEGO authentication middleware. | ||
|
|
||
| ### `GetAuthenticatedIdentityFromContext(ctx fiber.Ctx) (goidentity.Identity, bool)` | ||
|
|
||
| Retrieves the authenticated identity from the Fiber context. | ||
|
|
||
| ### `NewKeytabFileLookupFunc(keytabFiles ...string) (KeytabLookupFunc, error)` | ||
|
|
||
| Creates a new KeytabLookupFunc that loads keytab files. | ||
|
|
||
| ## Configuration | ||
|
|
||
| The `Config` struct supports the following fields: | ||
|
|
||
| - `KeytabLookup`: A function that retrieves the keytab (required) | ||
| - `Log`: The logger used for middleware logging (optional, defaults to Fiber's default logger) | ||
|
|
||
| ## Requirements | ||
|
|
||
| - Fiber v3 | ||
| - Kerberos infrastructure | ||
|
|
||
| ## Notes | ||
|
|
||
| - Ensure your Kerberos infrastructure is properly configured | ||
| - The middleware handles the SPNEGO negotiation process | ||
| - Authenticated identities are stored in the Fiber context using `contextKeyOfIdentity` |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,47 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| package spnego | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||||||||||||||
| "log" | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/jcmturner/gokrb5/v8/keytab" | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| type contextKey string | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // contextKeyOfIdentity is the key used to store the authenticated identity in the Fiber context | ||||||||||||||||||||||||||||||||||||||||||||||
| const contextKeyOfIdentity contextKey = "middleware.spnego.Identity" | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // KeytabLookupFunc is a function type that returns a keytab or an error | ||||||||||||||||||||||||||||||||||||||||||||||
| // It's used to look up the keytab dynamically when needed | ||||||||||||||||||||||||||||||||||||||||||||||
| // This design allows for extensibility, enabling keytab retrieval from various sources | ||||||||||||||||||||||||||||||||||||||||||||||
| // such as databases, remote services, or other custom implementations beyond static files | ||||||||||||||||||||||||||||||||||||||||||||||
| type KeytabLookupFunc func() (*keytab.Keytab, error) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Config holds the configuration for the SPNEGO middleware | ||||||||||||||||||||||||||||||||||||||||||||||
| // It includes the keytab lookup function and a logger | ||||||||||||||||||||||||||||||||||||||||||||||
| type Config struct { | ||||||||||||||||||||||||||||||||||||||||||||||
| // KeytabLookup is a function that retrieves the keytab | ||||||||||||||||||||||||||||||||||||||||||||||
| KeytabLookup KeytabLookupFunc | ||||||||||||||||||||||||||||||||||||||||||||||
| // Log is the logger used for middleware logging | ||||||||||||||||||||||||||||||||||||||||||||||
| Log *log.Logger | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // NewKeytabFileLookupFunc creates a new KeytabLookupFunc that loads keytab files | ||||||||||||||||||||||||||||||||||||||||||||||
| // It accepts one or more keytab file paths and returns a function that loads them | ||||||||||||||||||||||||||||||||||||||||||||||
| func NewKeytabFileLookupFunc(keytabFiles ...string) (KeytabLookupFunc, error) { | ||||||||||||||||||||||||||||||||||||||||||||||
| if len(keytabFiles) == 0 { | ||||||||||||||||||||||||||||||||||||||||||||||
| return nil, ErrConfigInvalidOfAtLeastOneKeytabFileRequired | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| return func() (*keytab.Keytab, error) { | ||||||||||||||||||||||||||||||||||||||||||||||
| var mergeKeytab keytab.Keytab | ||||||||||||||||||||||||||||||||||||||||||||||
| for _, keytabFile := range keytabFiles { | ||||||||||||||||||||||||||||||||||||||||||||||
| kt, err := keytab.Load(keytabFile) | ||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||
| return nil, fmt.Errorf("%w: file %s load failed: %w", ErrLoadKeytabFileFailed, keytabFile, err) | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
| return nil, fmt.Errorf("%w: file %s load failed: %w", ErrLoadKeytabFileFailed, keytabFile, err) | |
| return nil, fmt.Errorf("%w: file %s load failed: %v", ErrLoadKeytabFileFailed, keytabFile, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current implementation of NewKeytabFileLookupFunc returns a closure that performs disk I/O and parses keytab files on every HTTP request, which will significantly degrade performance.
It is recommended to load and merge the keytab files once during initialization and return the cached result. Additionally, using keytab.New() is preferred over a zero-value struct to ensure internal fields (like the keytab version) are correctly initialized.
| return func() (*keytab.Keytab, error) { | |
| var mergeKeytab keytab.Keytab | |
| for _, keytabFile := range keytabFiles { | |
| kt, err := keytab.Load(keytabFile) | |
| if err != nil { | |
| return nil, fmt.Errorf("%w: file %s load failed: %w", ErrLoadKeytabFileFailed, keytabFile, err) | |
| } | |
| mergeKeytab.Entries = append(mergeKeytab.Entries, kt.Entries...) | |
| } | |
| return &mergeKeytab, nil | |
| }, nil | |
| ktMerged := keytab.New() | |
| for _, keytabFile := range keytabFiles { | |
| kt, err := keytab.Load(keytabFile) | |
| if err != nil { | |
| return nil, fmt.Errorf("%w: file %s load failed: %w", ErrLoadKeytabFileFailed, keytabFile, err) | |
| } | |
| ktMerged.Entries = append(ktMerged.Entries, kt.Entries...) | |
| } | |
| return func() (*keytab.Keytab, error) { | |
| return ktMerged, nil | |
| }, nil |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,132 @@ | ||||||
| package spnego | ||||||
|
|
||||||
| import ( | ||||||
| "os" | ||||||
| "testing" | ||||||
| "time" | ||||||
|
|
||||||
| "github.com/gofiber/contrib/v3/spnego/utils" | ||||||
| "github.com/stretchr/testify/require" | ||||||
| ) | ||||||
|
|
||||||
| func TestNewKeytabFileLookupFunc(t *testing.T) { | ||||||
| t.Run("test didn't give any keytab files", func(t *testing.T) { | ||||||
| _, err := NewKeytabFileLookupFunc() | ||||||
| require.ErrorIs(t, err, ErrConfigInvalidOfAtLeastOneKeytabFileRequired) | ||||||
| }) | ||||||
| t.Run("test not found keytab file", func(t *testing.T) { | ||||||
|
||||||
| t.Run("test not found keytab file", func(t *testing.T) { | |
| t.Run("test invalid keytab file", func(t *testing.T) { |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| // Package spnego provides SPNEGO (Simple and Protected GSSAPI Negotiation Mechanism) | ||
| // authentication middleware for Fiber applications. | ||
| // It enables Kerberos authentication for HTTP requests. | ||
| package spnego |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package spnego | ||
|
|
||
| import "errors" | ||
|
|
||
| // ErrConfigInvalidOfKeytabLookupFunctionRequired is returned when the KeytabLookup function is not set in Config | ||
| var ErrConfigInvalidOfKeytabLookupFunctionRequired = errors.New("config invalid: keytab lookup function is required") | ||
|
|
||
| // ErrLookupKeytabFailed is returned when the keytab lookup fails | ||
| var ErrLookupKeytabFailed = errors.New("keytab lookup failed") | ||
|
|
||
| // ErrConvertRequestFailed is returned when the request conversion to HTTP request fails | ||
| var ErrConvertRequestFailed = errors.New("convert request failed") | ||
|
|
||
| // ErrConfigInvalidOfAtLeastOneKeytabFileRequired is returned when no keytab files are provided | ||
| var ErrConfigInvalidOfAtLeastOneKeytabFileRequired = errors.New("config invalid: at least one keytab file required") | ||
|
|
||
| // ErrLoadKeytabFileFailed is returned when load keytab files failed | ||
| var ErrLoadKeytabFileFailed = errors.New("load keytab failed") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All other test-* workflows in this repo include a workflow_dispatch trigger (e.g., .github/workflows/test-jwt.yml:16), but this one doesn’t. Adding workflow_dispatch would keep workflow behavior consistent and allow manually rerunning SPNEGO tests from the GitHub UI.