-
Notifications
You must be signed in to change notification settings - Fork 151
feat: Add SPNEGO Authentication Middleware #1368
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
Open
jarod2011
wants to merge
25
commits into
gofiber:main
Choose a base branch
from
jarod2011:feature-1367
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
db1c1b7
feat: add a SPNEGO Kerberos Authentication Middleware for Fiber v2 an…
jarod2011 8a1aa70
refactor(spnego): Restructure SPNEGO authentication middleware codebase
jarod2011 b795161
docs(spnego): Update README documentation
jarod2011 4f314bd
Merge branch 'gofiber:main' into feature-1367
jarod2011 5197bf6
fix(utils): Fix adding TRUNC flag when creating mock keytab files
jarod2011 952aaf6
fix: fix when write mock file fail, open file is not close before remove
jarod2011 ef3cd2d
Merge branch 'main' into feature-1367
jarod2011 ba84250
Merge branch 'main' into feature-1367
jarod2011 45ce4a3
Merge branch 'gofiber:main' into feature-1367
jarod2011 e04e024
major(spnego): migrate to v3 directory and drop v2 support
jarod2011 d428340
Merge branch 'gofiber:main' into feature-1367
jarod2011 000d40e
chore(spnego): fix ai suggestion
jarod2011 c798b96
chore(spnego): Remove Chinese documentation and add additional notes …
jarod2011 c32cc18
Merge branch 'gofiber:main' into feature-1367
jarod2011 80fa4dc
Merge remote-tracking branch 'origin/feature-1367' into feature-1367
jarod2011 eb40e2c
Merge branch 'main' into feature-1367
jarod2011 12bd868
refactor(spnego): Remove dependency on utils adapter package and simp…
jarod2011 e0a550c
Merge branch 'gofiber:main' into feature-1367
jarod2011 40c9876
Merge branch 'gofiber:main' into feature-1367
jarod2011 b773b0b
refactor: Replace custom wrapContext with adaptor.HTTPHandler for mid…
jarod2011 b3b33d1
Update v3/spnego/example/example.go
gaby 4d1c990
Merge branch 'main' into feature-1367
gaby 22a9b32
Merge branch 'main' into feature-1367
gaby b5c7f18
Merge branch 'main' into feature-1367
jarod2011 53be94e
Apply suggestions from code review
gaby File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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@v5 | ||
| - 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 ./... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,4 +21,5 @@ use ( | |
| ./v3/websocket | ||
| ./v3/zap | ||
| ./v3/zerolog | ||
| ./v3/spnego | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| } | ||
| mergeKeytab.Entries = append(mergeKeytab.Entries, kt.Entries...) | ||
| } | ||
| return &mergeKeytab, nil | ||
| }, nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) { | ||
| err := os.WriteFile("./invalid.keytab", []byte("12345"), 0600) | ||
| require.NoError(t, err) | ||
| t.Cleanup(func() { | ||
| os.Remove("./invalid.keytab") | ||
| }) | ||
| fn, err := NewKeytabFileLookupFunc("./invalid.keytab") | ||
| require.NoError(t, err) | ||
| _, err = fn() | ||
| require.ErrorIs(t, err, ErrLoadKeytabFileFailed) | ||
| }) | ||
| t.Run("test one keytab file", func(t *testing.T) { | ||
| tm := time.Now() | ||
| _, clean, err := utils.NewMockKeytab( | ||
| utils.WithPrincipal("HTTP/sso.example.com"), | ||
| utils.WithRealm("TEST.LOCAL"), | ||
| utils.WithPairs(utils.EncryptTypePair{ | ||
| Version: 2, | ||
| EncryptType: 18, | ||
| CreateTime: tm, | ||
| }), | ||
| utils.WithFilename("./temp.keytab"), | ||
| ) | ||
| require.NoError(t, err) | ||
| t.Cleanup(clean) | ||
| fn, err := NewKeytabFileLookupFunc("./temp.keytab") | ||
| require.NoError(t, err) | ||
| kt1, err := fn() | ||
| require.NoError(t, err) | ||
| info := utils.GetKeytabInfo(kt1) | ||
| require.Len(t, info, 1) | ||
| require.Equal(t, info[0].PrincipalName, "HTTP/[email protected]") | ||
| require.Equal(t, info[0].Realm, "TEST.LOCAL") | ||
| require.Len(t, info[0].Pairs, 1) | ||
| require.Equal(t, info[0].Pairs[0].Version, uint8(2)) | ||
| require.Equal(t, info[0].Pairs[0].EncryptType, int32(18)) | ||
| // Note: The creation time of keytab is only accurate to the second. | ||
| require.Equal(t, info[0].Pairs[0].CreateTime.Unix(), tm.Unix()) | ||
| }) | ||
|
Comment on lines
+28
to
+55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Harden test keytab creation and remove flakiness.
- utils.WithFilename("./temp.keytab"),
+ dir := t.TempDir()
+ utils.WithFilename(filepath.Join(dir, "temp.keytab")),Follow-up change in spnego/utils/mock_keytab.go (outside this file): - file, err := defaultFileOperator.OpenFile(opt.Filename, os.O_RDWR|os.O_CREATE, 0o666)
+ file, err := defaultFileOperator.OpenFile(opt.Filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
|
||
| t.Run("test multiple keytab file but has invalid keytab", func(t *testing.T) { | ||
| tm := time.Now() | ||
| _, clean, err := utils.NewMockKeytab( | ||
| utils.WithPrincipal("HTTP/sso.example.com"), | ||
| utils.WithRealm("TEST.LOCAL"), | ||
| utils.WithPairs(utils.EncryptTypePair{ | ||
| Version: 2, | ||
| EncryptType: 18, | ||
| CreateTime: tm, | ||
| }), | ||
| utils.WithFilename("./temp.keytab"), | ||
| ) | ||
| require.NoError(t, err) | ||
| t.Cleanup(clean) | ||
| err = os.WriteFile("./invalid1.keytab", []byte("12345"), 0600) | ||
| require.NoError(t, err) | ||
| t.Cleanup(func() { | ||
| os.Remove("./invalid1.keytab") | ||
| }) | ||
| fn, err := NewKeytabFileLookupFunc("./temp.keytab", "./invalid1.keytab") | ||
| require.NoError(t, err) | ||
| _, err = fn() | ||
| require.ErrorIs(t, err, ErrLoadKeytabFileFailed) | ||
| }) | ||
| t.Run("test multiple keytab file", func(t *testing.T) { | ||
| tm := time.Now() | ||
| _, clean1, err1 := utils.NewMockKeytab( | ||
| utils.WithPrincipal("HTTP/sso.example1.com"), | ||
| utils.WithRealm("TEST.LOCAL"), | ||
| utils.WithPairs(utils.EncryptTypePair{ | ||
| Version: 2, | ||
| EncryptType: 18, | ||
| CreateTime: tm, | ||
| }), | ||
| utils.WithFilename("./temp1.keytab"), | ||
| ) | ||
| require.NoError(t, err1) | ||
| t.Cleanup(clean1) | ||
| _, clean2, err2 := utils.NewMockKeytab( | ||
| utils.WithPrincipal("HTTP/sso.example2.com"), | ||
| utils.WithRealm("TEST.LOCAL"), | ||
| utils.WithPairs(utils.EncryptTypePair{ | ||
| Version: 2, | ||
| EncryptType: 17, | ||
| CreateTime: tm, | ||
| }, utils.EncryptTypePair{ | ||
| Version: 2, | ||
| EncryptType: 18, | ||
| CreateTime: tm, | ||
| }), | ||
| utils.WithFilename("./temp2.keytab"), | ||
| ) | ||
| require.NoError(t, err2) | ||
| t.Cleanup(clean2) | ||
| fn, err := NewKeytabFileLookupFunc("./temp1.keytab", "./temp2.keytab") | ||
| require.NoError(t, err) | ||
| kt2, err := fn() | ||
| require.NoError(t, err) | ||
| info := utils.GetKeytabInfo(kt2) | ||
| require.Len(t, info, 2) | ||
| require.Equal(t, info[0].PrincipalName, "HTTP/[email protected]") | ||
| require.Equal(t, info[0].Realm, "TEST.LOCAL") | ||
| require.Len(t, info[0].Pairs, 1) | ||
| require.Equal(t, info[0].Pairs[0].Version, uint8(2)) | ||
| require.Equal(t, info[0].Pairs[0].EncryptType, int32(18)) | ||
| require.Equal(t, info[0].Pairs[0].CreateTime.Unix(), tm.Unix()) | ||
| require.Equal(t, info[1].PrincipalName, "HTTP/[email protected]") | ||
| require.Equal(t, info[1].Realm, "TEST.LOCAL") | ||
| require.Len(t, info[1].Pairs, 2) | ||
| require.Equal(t, info[1].Pairs[0].Version, uint8(2)) | ||
| require.Equal(t, info[1].Pairs[0].EncryptType, int32(17)) | ||
| require.Equal(t, info[1].Pairs[0].CreateTime.Unix(), tm.Unix()) | ||
| require.Equal(t, info[1].Pairs[1].Version, uint8(2)) | ||
| require.Equal(t, info[1].Pairs[1].EncryptType, int32(18)) | ||
| require.Equal(t, info[1].Pairs[1].CreateTime.Unix(), tm.Unix()) | ||
| }) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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") | ||
gaby marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
An error format string can contain at most one
%wverb. You are using it twice, which is invalid Go code and will be flagged bygo vet. To wrapErrLoadKeytabFileFailedwhile including context about the file and the underlying error, you should use%wonce for the error you are wrapping and format the rest of the information as strings.