Skip to content
Open
Show file tree
Hide file tree
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 Aug 5, 2025
8a1aa70
refactor(spnego): Restructure SPNEGO authentication middleware codebase
jarod2011 Aug 25, 2025
b795161
docs(spnego): Update README documentation
jarod2011 Aug 25, 2025
4f314bd
Merge branch 'gofiber:main' into feature-1367
jarod2011 Aug 25, 2025
5197bf6
fix(utils): Fix adding TRUNC flag when creating mock keytab files
jarod2011 Aug 26, 2025
952aaf6
fix: fix when write mock file fail, open file is not close before remove
jarod2011 Aug 26, 2025
ef3cd2d
Merge branch 'main' into feature-1367
jarod2011 Aug 29, 2025
ba84250
Merge branch 'main' into feature-1367
jarod2011 Sep 9, 2025
45ce4a3
Merge branch 'gofiber:main' into feature-1367
jarod2011 Nov 11, 2025
e04e024
major(spnego): migrate to v3 directory and drop v2 support
jarod2011 Nov 12, 2025
d428340
Merge branch 'gofiber:main' into feature-1367
jarod2011 Nov 12, 2025
000d40e
chore(spnego): fix ai suggestion
jarod2011 Nov 13, 2025
c798b96
chore(spnego): Remove Chinese documentation and add additional notes …
jarod2011 Nov 13, 2025
c32cc18
Merge branch 'gofiber:main' into feature-1367
jarod2011 Nov 13, 2025
80fa4dc
Merge remote-tracking branch 'origin/feature-1367' into feature-1367
jarod2011 Nov 13, 2025
eb40e2c
Merge branch 'main' into feature-1367
jarod2011 Nov 14, 2025
12bd868
refactor(spnego): Remove dependency on utils adapter package and simp…
jarod2011 Nov 15, 2025
e0a550c
Merge branch 'gofiber:main' into feature-1367
jarod2011 Nov 15, 2025
40c9876
Merge branch 'gofiber:main' into feature-1367
jarod2011 Nov 18, 2025
b773b0b
refactor: Replace custom wrapContext with adaptor.HTTPHandler for mid…
jarod2011 Nov 18, 2025
b3b33d1
Update v3/spnego/example/example.go
gaby Nov 20, 2025
4d1c990
Merge branch 'main' into feature-1367
gaby Nov 21, 2025
22a9b32
Merge branch 'main' into feature-1367
gaby Nov 22, 2025
b5c7f18
Merge branch 'main' into feature-1367
jarod2011 Nov 27, 2025
53be94e
Apply suggestions from code review
gaby Dec 10, 2025
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
34 changes: 34 additions & 0 deletions .github/workflows/test-spnego.yml
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 ./...
1 change: 1 addition & 0 deletions go.work
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ use (
./v3/websocket
./v3/zap
./v3/zerolog
./v3/spnego
)
148 changes: 148 additions & 0 deletions v3/spnego/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
---
id: spnego
---

# SPNEGO Kerberos Authentication Middleware for Fiber

![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=spnego*)
[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)
![Test](https://github.com/gofiber/contrib/workflows/Test%20spnego/badge.svg)

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`
47 changes: 47 additions & 0 deletions v3/spnego/config.go
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)
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

An error format string can contain at most one %w verb. You are using it twice, which is invalid Go code and will be flagged by go vet. To wrap ErrLoadKeytabFileFailed while including context about the file and the underlying error, you should use %w once for the error you are wrapping and format the rest of the information as strings.

Suggested change
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)

}
mergeKeytab.Entries = append(mergeKeytab.Entries, kt.Entries...)
}
return &mergeKeytab, nil
}, nil
}
132 changes: 132 additions & 0 deletions v3/spnego/config_test.go
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
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Harden test keytab creation and remove flakiness.

  • File handling: utils.NewMockKeytab persists with 0o666 in current impl, which is too permissive for keytab material. Use a temp directory and file (t.TempDir) and update the helper to use 0o600.
  • Time checks: You already compare at second granularity; good. Keep using the same tm for all entries in this case.
- 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)

Committable suggestion skipped: line range outside the PR's diff.

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())
})
}
4 changes: 4 additions & 0 deletions v3/spnego/doc.go
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
18 changes: 18 additions & 0 deletions v3/spnego/error.go
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")
Loading
Loading