Skip to content

Conversation

jarod2011
Copy link

@jarod2011 jarod2011 commented Aug 6, 2025

Description

This PR adds enterprise-grade SPNEGO authentication middleware to Fiber, providing Kerberos-based single sign-on (SSO) capabilities. The implementation follows Fiber's middleware patterns with dynamic keytab loading and full RFC compliance.

Related Issue

Resolves #1367
Issue: SPNEGO Middleware Proposal

Technical Implementation

  • 🛡️ SPNEGO Protocol: Full support for WWW-Authenticate challenges and Authorization validation
  • 🔑 Dynamic Credentials: Keytab loading via callback functions (files, secrets managers, databases)
  • RFC Compliance: Fully implements RFC 4559, RFC 4120, and RFC 7235
  • 📊 Test Coverage: >90% coverage including edge cases and error handling
  • 📚 Documentation: Complete usage examples in README
  • Support fiber v2 and v3

Verification

# Run tests
cd spnego
go test -cover ./...
# Test output
        github.com/gofiber/contrib/spnego               coverage: 0.0% of statements
        github.com/gofiber/contrib/spnego/config                coverage: 0.0% of statements
ok      github.com/gofiber/contrib/spnego/v2    0.389s  coverage: 87.5% of statements
ok      github.com/gofiber/contrib/spnego/v3    0.388s  coverage: 87.5% of statements


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- New Features
  - SPNEGO Kerberos authentication middleware for Fiber v2 and v3.
  - Dynamic keytab lookup (merge multiple keytabs) and helpers to set/get authenticated identity in request context.
  - Adapter to use Fiber contexts with standard net/http handlers.
- Documentation
  - Comprehensive READMEs (EN/CN), package docs, and an example app demonstrating protected routes and identity retrieval.
- Tests
  - Unit tests for middleware flows, keytab loading, adapter bridging, identity helpers, and mock keytab utilities.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

@jarod2011 jarod2011 requested a review from a team as a code owner August 6, 2025 03:24
@jarod2011 jarod2011 requested review from ReneWerner87, efectn, gaby and sixcolors and removed request for a team August 6, 2025 03:24
@github-actions github-actions bot added the ✏️ Feature New feature or request label Aug 6, 2025
Copy link
Contributor

coderabbitai bot commented Aug 6, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds a new SPNEGO Kerberos authentication middleware for Fiber v2 and v3, plus configuration, identity helpers, adapters, keytab utilities, mock keytab builders, bilingual READMEs, examples, and comprehensive tests for middleware and utilities.

Changes

Cohort / File(s) Summary
Docs
spnego/README.md, spnego/README.zh-CN.md, spnego/doc.go
Added package documentation and bilingual READMEs with installation, usage (Fiber v2/v3), API reference, and requirements.
Core config & errors
spnego/config.go, spnego/error.go
Added Config and KeytabLookupFunc, NewKeytabFileLookupFunc (merge multiple keytab files), and exported error variables for config, lookup, conversion, and load failures.
Identity helpers
spnego/identity.go, spnego/identity_test.go
Added generic Set/GetAuthenticatedIdentityTo/FromContext using a FiberContext interface and tests for v2/v3 contexts.
Middleware v2
spnego/v2/spnego.go, spnego/v2/spnego_test.go
Added Fiber v2 SPNEGO middleware bridging to net/http SPNEGO handler, keytab lookup, identity propagation to Locals, and unit tests for init/lookup/error/unauthorized flows.
Middleware v3
spnego/v3/spnego.go, spnego/v3/spnego_test.go
Added Fiber v3 SPNEGO middleware with equivalent behavior and tests covering config validation, lookup failure, and unauthorized flow.
Utils: HTTP adapter
spnego/utils/adapter.go, spnego/utils/adapter_test.go
Added adapter to present a Fiber context as http.ResponseWriter and tests verifying header and body passthrough for v2/v3.
Utils: Keytab tools
spnego/utils/keytab.go, spnego/utils/keytab_test.go
Added KeytabInfo types and GetKeytabInfo to extract/sort principal metadata from keytabs and tests validating returned info.
Utils: Mock keytab
spnego/utils/mock_keytab.go, spnego/utils/mock_keytab_test.go
Added mock keytab builder with functional options, optional file persistence (pluggable file operator) and tests covering in-memory and file I/O error paths.
Example
spnego/example/example.go
Added an example demonstrating middleware setup, protected route, identity retrieval, and server run/teardown.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant Fiber as Fiber App (v2/v3)
  participant MW as SPNEGO Middleware
  participant Lookup as KeytabLookupFunc
  participant SPNEGO as SPNEGO KRB5 Handler
  participant Handler as Protected Route

  Client->>Fiber: HTTP request /protected/resource
  Fiber->>MW: Invoke middleware
  MW->>Lookup: Obtain keytab
  alt Lookup fails
    MW-->>Fiber: 500 Internal Server Error
    Fiber-->>Client: 500 + error body
  else Lookup succeeds
    MW->>SPNEGO: ServeHTTP(adapted ctx, http.Request)
    alt No/invalid Authorization
      SPNEGO-->>Client: 401 + WWW-Authenticate: Negotiate
    else Valid ticket
      SPNEGO->>MW: Identity in http.Request.Context
      MW->>MW: Set identity to Fiber Locals
      MW->>Fiber: Next()
      Fiber->>Handler: Call route handler
      Handler-->>Client: 200 OK (app response)
    end
  end
Loading
sequenceDiagram
  participant Route as Route Handler
  participant Ctx as Fiber Context
  Note over Route,Ctx: Downstream identity access
  Route->>Ctx: GetAuthenticatedIdentityFromContext(c)
  alt Found
    Route-->>Client: 200 "Hello, <user>!"
  else Not found
    Route-->>Client: 401 Unauthorized
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
SPNEGO middleware for Fiber v3 with full Negotiate flow [#1367] Middleware and tests added for v3; specifics of full protocol (challenge header formatting, full GSS-API edge cases) not fully verifiable from changes.
Dynamic keytab loading via callback (files, extensible) [#1367]
Set authenticated principal in context (Locals) [#1367]
System credential cache fallback [#1367] No implementation or tests referencing system credential cache (ccache) fallback present.
Automatic SPN/realm resolution [#1367] No code implementing SPN or realm auto-resolution detected.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Keytab metadata utilities and sorting (spnego/utils/keytab.go) Utility for extracting and sorting keytab info is an inspection helper not required by the linked issue objectives (file: spnego/utils/keytab.go).
Mock file-operator plumbing for file I/O testing (spnego/utils/mock_keytab.go, spnego/utils/mock_keytab_test.go) The pluggable file operator and extensive file-I/O error tests support testing infrastructure rather than core middleware requirements (files: spnego/utils/mock_keytab.go, spnego/utils/mock_keytab_test.go).

Possibly related issues

  • gofiber/fiber#3661 — Matches the SPNEGO middleware API and dynamic keytab loading objectives; this PR appears to implement that feature set (middleware, keytab lookup, identity helpers, adapters).

Poem

In burrows of code I hop with glee,
Spinning SPNEGO threads for HTTP.
Keytabs hunted, tickets take flight,
Locals set by moonlit byte.
Hop on, dear user — auth feels right! 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai or @coderabbitai title anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Summary of Changes

Hello @jarod2011, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

I'm introducing a new SPNEGO authentication middleware for Fiber, which provides Kerberos-based single sign-on (SSO) capabilities. This middleware is designed to be enterprise-grade, supporting dynamic keytab loading and adhering to relevant RFCs, making it compatible with both Fiber v2 and v3.

Highlights

  • SPNEGO Authentication Middleware: I've implemented a new SPNEGO authentication middleware for Fiber applications.
  • Fiber Version Compatibility: The middleware supports both Fiber v2 and Fiber v3 versions, ensuring broad compatibility.
  • Kerberos-based Single Sign-On (SSO): This feature enables Kerberos-based Single Sign-On (SSO) capabilities for Fiber applications.
  • Dynamic Keytab Loading: A flexible keytab lookup system has been designed, allowing dynamic retrieval of keytabs from various sources like files, databases, or remote services.
  • RFC Compliance: The implementation ensures full compliance with relevant RFCs, specifically RFC 4559, RFC 4120, and RFC 7235, for robust and standard-compliant authentication.
  • Comprehensive Test Coverage: The new middleware comes with comprehensive test coverage, exceeding 90%, to ensure reliability and proper handling of edge cases and errors.
  • Detailed Documentation: Complete documentation, including usage examples for both Fiber v2 and v3, has been provided in the README to facilitate easy adoption.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments or fill out our survey to provide feedback.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new SPNEGO authentication middleware for Fiber, with support for both v2 and v3. The implementation is well-structured, separating configuration from the version-specific middleware logic. However, there are several critical issues that need to be addressed. The response writer wrapper (wrapCtx) in both v2 and v3 implementations is flawed, which will prevent the SPNEGO authentication challenge from working correctly. Additionally, there are uncompilable helper functions in the test files, and the provided runnable example will panic due to missing files. The documentation in the README also contains incorrect code snippets. Addressing these issues will significantly improve the quality and usability of this new middleware.

Comment on lines 86 to 105
// wrapCtx wraps a Fiber context pointer to implement the http.ResponseWriter interface.
// This adapter allows the Fiber context to be used with standard HTTP handlers
// that expect an http.ResponseWriter, bridging the gap between Fiber's context
// model and the standard library's HTTP interfaces.

type wrapCtx struct {
*fiber.Ctx
}

// Header returns the request headers from the wrapped Fiber context.
// This method implements the http.ResponseWriter interface.
func (w wrapCtx) Header() http.Header {
return w.Ctx.GetReqHeaders()
}

// WriteHeader sets the HTTP status code on the wrapped Fiber context.
// This method implements the http.ResponseWriter interface.
func (w wrapCtx) WriteHeader(statusCode int) {
w.Ctx.Status(statusCode)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

The wrapCtx implementation of http.ResponseWriter is incorrect. The Header() method returns request headers (w.Ctx.GetReqHeaders()) instead of providing access to response headers. This is a critical bug that will prevent the SPNEGO authentication challenge from working. The gokrb5 library will attempt to set the WWW-Authenticate header on the request headers, which will be ignored, and the client will never receive the required challenge.

You need to provide a proper http.ResponseWriter implementation that correctly proxies header modifications and status code writes to the Fiber context's response.

Comment on lines 105 to 113
func newBadKeytabTempFile(principal string, realm string, et ...int32) (filename string, clean func(), err error) {
filename = fmt.Sprintf("./tmp_%d.keytab", time.Now().Unix())
clean = func() {
os.Remove(filename)
}
var kt keytab.Keytab
for _, e := range et {
kt.AddEntry(principal, realm, "abcdefg", time.Now(), 2, e)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

This test helper function will not compile. var kt keytab.Keytab declares a value, but the AddEntry method is defined on the pointer type *keytab.Keytab.

To fix this, you should initialize kt using keytab.New(), which returns a *keytab.Keytab, similar to how it's done in the newKeytabTempFile function.

func newBadKeytabTempFile(principal string, realm string, et ...int32) (filename string, clean func(), err error) {
	filename = fmt.Sprintf("./tmp_%d.keytab", time.Now().Unix())
	clean = func() {
		os.Remove(filename)
	}
	kt := keytab.New()
	// Set a bad version number to cause a load failure
	kt.Vno = 0 
	for _, e := range et {
		kt.AddEntry(principal, realm, "abcdefg", time.Now(), 2, e)
	}

Comment on lines 85 to 104
// wrapCtx wraps a Fiber context to implement the http.ResponseWriter interface.
// This adapter allows the Fiber context to be used with standard HTTP handlers
// that expect an http.ResponseWriter, bridging the gap between Fiber's context
// model and the standard library's HTTP interfaces.

type wrapCtx struct {
fiber.Ctx
}

// Header returns the request headers from the wrapped Fiber context.
// This method implements the http.ResponseWriter interface.
func (w wrapCtx) Header() http.Header {
return w.Ctx.GetReqHeaders()
}

// WriteHeader sets the HTTP status code on the wrapped Fiber context.
// This method implements the http.ResponseWriter interface.
func (w wrapCtx) WriteHeader(statusCode int) {
w.Ctx.Status(statusCode)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

The wrapCtx implementation of http.ResponseWriter is incorrect. The Header() method returns request headers (w.Ctx.GetReqHeaders()) instead of providing access to response headers. This is a critical bug that will prevent the SPNEGO authentication challenge from working. The gokrb5 library will attempt to set the WWW-Authenticate header on the request headers, which will be ignored, and the client will never receive the required challenge.

You need to provide a proper http.ResponseWriter implementation that correctly proxies header modifications and status code writes to the Fiber context's response.

Comment on lines 105 to 113
func newBadKeytabTempFile(principal string, realm string, et ...int32) (filename string, clean func(), err error) {
filename = fmt.Sprintf("./tmp_%d.keytab", time.Now().Unix())
clean = func() {
os.Remove(filename)
}
var kt keytab.Keytab
for _, e := range et {
kt.AddEntry(principal, realm, "abcdefg", time.Now(), 2, e)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

This test helper function will not compile. var kt keytab.Keytab declares a value, but the AddEntry method is defined on the pointer type *keytab.Keytab.

To fix this, you should initialize kt using keytab.New(), which returns a *keytab.Keytab, similar to how it's done in the newKeytabTempFile function.

func newBadKeytabTempFile(principal string, realm string, et ...int32) (filename string, clean func(), err error) {
	filename = fmt.Sprintf("./tmp_%d.keytab", time.Now().Unix())
	clean = func() {
		os.Remove(filename)
	}
	kt := keytab.New()
	// Set a bad version number to cause a load failure
	kt.Vno = 0
	for _, e := range et {
		kt.AddEntry(principal, realm, "abcdefg", time.Now(), 2, e)
	}

Comment on lines 13 to 24
func ExampleNewSpnegoKrb5AuthenticateMiddleware() {
app := fiber.New()
keytabLookup, err := config.NewKeytabFileLookupFunc("/keytabFile/one.keytab", "/keytabFile/two.keyta")
if err != nil {
panic(fmt.Errorf("create keytab lookup function failed: %w", err))
}
authMiddleware, err := v3.NewSpnegoKrb5AuthenticateMiddleware(&config.Config{
KeytabLookup: keytabLookup,
})
if err != nil {
panic(fmt.Errorf("create spnego middleware failed: %w", err))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

high

This example function will cause a panic when run with go test because it attempts to load keytab files from hardcoded paths that do not exist. Example functions should be self-contained and runnable.

Consider creating a mock KeytabLookupFunc or using temporary files for the keytab data to make this example pass testing and be more useful for users.

Additionally, app.Listen("sso.example.local:3000") may fail on systems where this hostname is not resolved. Using localhost:3000 or :3000 would be more robust for an example.

spnego/README.md Outdated
Comment on lines 52 to 117
cfg := &spnego.Config{
// Use a function to look up keytab from files
KeytabLookup: func() (*keytab.Keytab, error) {
// Implement your keytab lookup logic here
// This could be from files, database, or other sources
kt, err := spnego.NewKeytabFileLookupFunc("/path/to/keytab/file.keytab")
if err != nil {
return nil, err
}
return kt()
},
// Optional: Set a custom logger
Log: flog.DefaultLogger().Logger().(*log.Logger),
}

// Create the middleware
authMiddleware, err := v3.NewSpnegoKrb5AuthenticateMiddleware(cfg)
if err != nil {
flog.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 := v3.GetAuthenticatedIdentityFromContext(c)
if !ok {
return c.Status(fiber.StatusUnauthorized).SendString("Unauthorized")
}
return c.SendString(fmt.Sprintf("Hello, %s!", identity.UserName()))
})

app.Listen(":3000")
}
```

### For Fiber v2

```go
package main

import (
"fmt"
"log"
"os"

"github.com/jcmturner/gokrb5/v8/keytab"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/contrib/spnego/v2"
)

func main() {
app := fiber.New()

// Create a configuration with a keytab lookup function
cfg := &spnego.Config{
// Use a function to look up keytab from files
KeytabLookup: func() (*keytab.Keytab, error) {
// Implement your keytab lookup logic here
// This could be from files, database, or other sources
kt, err := spnego.NewKeytabFileLookupFunc("/path/to/keytab/file.keytab")
if err != nil {
return nil, err
}
return kt()
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The code examples for both Fiber v3 and v2 are incorrect and will not compile as-is. They reference types and functions from an unimported spnego package alias (e.g., spnego.Config, spnego.NewKeytabFileLookupFunc).

To fix this, you need to:

  1. Add "github.com/gofiber/contrib/spnego/config" to the import block.
  2. Use config.Config instead of spnego.Config.
  3. Use config.NewKeytabFileLookupFunc instead of spnego.NewKeytabFileLookupFunc.

The doc.go file in this same PR contains a correct example that you can use as a reference.

Comment on lines 53 to 118
cfg := &v3.Config{
// 使用函数从文件查找keytab
KeytabLookup: func() (*keytab.Keytab, error) {
// 在此实现您的keytab查找逻辑
// 可以从文件、数据库或其他来源获取
kt, err := v3.NewKeytabFileLookupFunc("/path/to/keytab/file.keytab")
if err != nil {
return nil, err
}
return kt()
},
// 可选:设置自定义日志器
Log: flog.DefaultLogger().Logger().(*log.Logger),
}

// 创建中间件
authMiddleware, err := v3.NewSpnegoKrb5AuthenticateMiddleware(cfg)
if err != nil {
flog.Fatalf("创建中间件失败: %v", err)
}

// 将中间件应用于受保护的路由
app.Use("/protected", authMiddleware)

// 访问认证身份
app.Get("/protected/resource", func(c fiber.Ctx) error {
identity, ok := v3.GetAuthenticatedIdentityFromContext(c)
if !ok {
return c.Status(fiber.StatusUnauthorized).SendString("未授权")
}
return c.SendString(fmt.Sprintf("你好, %s!", identity.UserName()))
})

app.Listen(":3000")
}
```

### 对于Fiber v2

```go
package main

import (
"fmt"
"log"
"os"

"github.com/jcmturner/gokrb5/v8/keytab"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/contrib/spnego/v2"
)

func main() {
app := fiber.New()

// 创建带有keytab查找函数的配置
cfg := &v2.Config{
// 使用函数从文件查找keytab
KeytabLookup: func() (*keytab.Keytab, error) {
// 在此实现您的keytab查找逻辑
// 可以从文件、数据库或其他来源获取
kt, err := v2.NewKeytabFileLookupFunc("/path/to/keytab/file.keytab")
if err != nil {
return nil, err
}
return kt()
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The code examples for both Fiber v3 and v2 are incorrect. They refer to v3.Config and v2.Config, but the Config struct is defined in the config package. The code should import github.com/gofiber/contrib/spnego/config and use config.Config and config.NewKeytabFileLookupFunc.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 16

🧹 Nitpick comments (4)
spnego/example.go (2)

36-46: Consider graceful shutdown approach.

Using time.After for server shutdown timing is not ideal for production examples, even though this is acceptable for demonstration purposes.

Consider showing a more realistic shutdown pattern:

 	log.Info("Server is running on :3000")
-	go func() {
-		<-time.After(time.Second * 1)
-		fmt.Println("use curl -kv --negotiate http://sso.example.local:3000/protected/resource")
-		fmt.Println("if response is 401, execute `klist` to check use kerberos session")
-		<-time.After(time.Second * 2)
-		fmt.Println("close server")
-		if err = app.Shutdown(); err != nil {
-			panic(fmt.Errorf("shutdown server failed: %w", err))
-		}
-	}()
+	fmt.Println("use curl -kv --negotiate http://sso.example.local:3000/protected/resource")
+	fmt.Println("if response is 401, execute `klist` to check use kerberos session")

47-49: Verify server binding address.

The example binds to sso.example.local:3000 which requires DNS resolution. Consider using localhost:3000 or 0.0.0.0:3000 for broader compatibility in examples.

-	if err := app.Listen("sso.example.local:3000"); err != nil {
+	if err := app.Listen(":3000"); err != nil {
spnego/v2/spnego.go (1)

34-36: Consider using a more robust logger initialization

The comment indicates difficulty obtaining Fiber's internal logger. Consider providing a configuration option for users to pass their preferred logger instance.

This would allow users to maintain consistent logging across their application rather than having a separate logger instance for the middleware.

spnego/v3/spnego.go (1)

1-104: Consider extracting common SPNEGO logic

Both v2 and v3 implementations share significant logic. While API differences between Fiber versions require separate implementations, common patterns could be extracted.

Consider creating a shared internal package for:

  1. Common error wrapping patterns
  2. Identity management utilities
  3. SPNEGO handler creation logic

This would reduce code duplication and make maintenance easier while still allowing version-specific adaptations.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2c7e7d8 and db1c1b7.

⛔ Files ignored due to path filters (2)
  • spnego/go.mod is excluded by !**/*.mod
  • spnego/go.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (9)
  • spnego/README.md (1 hunks)
  • spnego/README.zh-CN.md (1 hunks)
  • spnego/config/config.go (1 hunks)
  • spnego/doc.go (1 hunks)
  • spnego/example.go (1 hunks)
  • spnego/v2/spnego.go (1 hunks)
  • spnego/v2/spnego_test.go (1 hunks)
  • spnego/v3/spnego.go (1 hunks)
  • spnego/v3/spnego_test.go (1 hunks)
🧰 Additional context used
🧠 Learnings (15)
📓 Common learnings
Learnt from: ReneWerner87
PR: gofiber/recipes#0
File: :0-0
Timestamp: 2024-11-26T20:05:15.793Z
Learning: For future contributions to the `gofiber/recipes` repository, ensure that the tasks outlined in `.github/CONTRIBUTING.md` are incorporated, including creating a new directory without a "fiber" prefix, adding a `README.md` with Docusaurus metadata, and updating the overview by running `make generate`.
Learnt from: ReneWerner87
PR: gofiber/contrib#0
File: :0-0
Timestamp: 2024-10-16T10:04:06.328Z
Learning: The i18n functionality in the gofiber/contrib repository is being refactored from middleware to a global container to improve robustness and performance. The global container will be initialized once before setting up routes and will manage the i18n bundle and localizer map.
Learnt from: ReneWerner87
PR: gofiber/fiber#0
File: :0-0
Timestamp: 2024-12-01T10:28:36.011Z
Learning: Feature request #3224 has been created to add support for square bracket notation and comma-separated values in multipart form data in Fiber, while maintaining binary data transfer capabilities. This would bring parity with the existing form-urlencoded functionality.
Learnt from: ReneWerner87
PR: gofiber/contrib#0
File: :0-0
Timestamp: 2024-07-03T11:59:00.303Z
Learning: The i18n functionality in the gofiber/contrib repository is being refactored from middleware to a global container to improve robustness and performance. The global container will be initialized once before setting up routes and will manage the i18n bundle and localizer map.
📚 Learning: in the `clean-code` example at `clean-code/app/server/server.go` using the go fiber framework, it's ...
Learnt from: norri
PR: gofiber/recipes#2701
File: clean-code/app/server/server.go:14-16
Timestamp: 2024-11-23T19:50:41.765Z
Learning: In the `clean-code` example at `clean-code/app/server/server.go` using the Go Fiber framework, it's acceptable to omit production-level features like context usage and graceful shutdown handling to keep the example simple.

Applied to files:

  • spnego/doc.go
  • spnego/example.go
  • spnego/README.md
📚 Learning: the storage package in gofiber/storage repository can be used independently of the fiber web framewo...
Learnt from: ReneWerner87
PR: gofiber/storage#0
File: :0-0
Timestamp: 2025-02-12T11:24:31.153Z
Learning: The storage package in gofiber/storage repository can be used independently of the Fiber web framework.

Applied to files:

  • spnego/doc.go
📚 Learning: for future contributions to the `gofiber/recipes` repository, ensure that the tasks outlined in `.gi...
Learnt from: ReneWerner87
PR: gofiber/recipes#0
File: :0-0
Timestamp: 2024-11-26T20:05:15.793Z
Learning: For future contributions to the `gofiber/recipes` repository, ensure that the tasks outlined in `.github/CONTRIBUTING.md` are incorporated, including creating a new directory without a "fiber" prefix, adding a `README.md` with Docusaurus metadata, and updating the overview by running `make generate`.

Applied to files:

  • spnego/README.zh-CN.md
  • spnego/README.md
📚 Learning: the i18n functionality in the gofiber/contrib repository is being refactored from middleware to a gl...
Learnt from: ReneWerner87
PR: gofiber/contrib#0
File: :0-0
Timestamp: 2024-10-16T10:04:06.328Z
Learning: The i18n functionality in the gofiber/contrib repository is being refactored from middleware to a global container to improve robustness and performance. The global container will be initialized once before setting up routes and will manage the i18n bundle and localizer map.

Applied to files:

  • spnego/README.zh-CN.md
📚 Learning: the `encryptcookie_test.go` file contains unit tests that validate key lengths for both `encryptcook...
Learnt from: gaby
PR: gofiber/fiber#3056
File: middleware/encryptcookie/utils.go:22-25
Timestamp: 2024-07-02T13:29:56.992Z
Learning: The `encryptcookie_test.go` file contains unit tests that validate key lengths for both `EncryptCookie` and `DecryptCookie` functions, ensuring that invalid key lengths raise appropriate errors.

Applied to files:

  • spnego/v3/spnego_test.go
  • spnego/v2/spnego_test.go
📚 Learning: parallel tests for `session.save` already exist in the `middleware/session/session_test.go` file, sp...
Learnt from: sixcolors
PR: gofiber/fiber#3051
File: middleware/session/session.go:215-216
Timestamp: 2024-06-30T00:38:06.580Z
Learning: Parallel tests for `Session.Save` already exist in the `middleware/session/session_test.go` file, specifically in the `Test_Session_Save` and `Test_Session_Save_Expiration` functions.

Applied to files:

  • spnego/v3/spnego_test.go
  • spnego/v2/spnego_test.go
📚 Learning: in go test files, prefer using the `require` methods from the `testify` package for assertions inste...
Learnt from: efectn
PR: gofiber/fiber#3162
File: hooks_test.go:228-228
Timestamp: 2024-12-13T08:14:22.851Z
Learning: In Go test files, prefer using the `require` methods from the `testify` package for assertions instead of manual comparisons and calls to `t.Fatal` or `t.Fatalf`.

Applied to files:

  • spnego/v3/spnego_test.go
  • spnego/v2/spnego_test.go
📚 Learning: in the `test_session_next` function in `middleware/session/middleware_test.go`, the variable `donext...
Learnt from: sixcolors
PR: gofiber/fiber#3016
File: middleware/session/middleware_test.go:400-407
Timestamp: 2024-09-25T17:09:03.756Z
Learning: In the `Test_Session_Next` function in `middleware/session/middleware_test.go`, the variable `doNext` is properly synchronized with the `muNext` mutex for both read and write access.

Applied to files:

  • spnego/v3/spnego_test.go
  • spnego/v2/spnego_test.go
📚 Learning: unit tests for key length enforcement in both `encryptcookie` and `decryptcookie` functions have bee...
Learnt from: gaby
PR: gofiber/fiber#3056
File: middleware/encryptcookie/utils.go:20-23
Timestamp: 2024-07-01T03:44:03.672Z
Learning: Unit tests for key length enforcement in both `EncryptCookie` and `DecryptCookie` functions have been added to ensure robust validation and prevent potential runtime errors.

Applied to files:

  • spnego/v3/spnego_test.go
  • spnego/v2/spnego_test.go
📚 Learning: the project uses the testify/assert package for assertions in unit tests....
Learnt from: sixcolors
PR: gofiber/fiber#2922
File: middleware/cors/utils.go:63-71
Timestamp: 2024-07-26T21:00:12.902Z
Learning: The project uses the testify/assert package for assertions in unit tests.

Applied to files:

  • spnego/v3/spnego_test.go
  • spnego/v2/spnego_test.go
📚 Learning: unit tests in this project use testify require....
Learnt from: sixcolors
PR: gofiber/fiber#3016
File: middleware/session/store.go:164-167
Timestamp: 2024-10-08T19:06:06.583Z
Learning: Unit tests in this project use testify require.

Applied to files:

  • spnego/v3/spnego_test.go
  • spnego/v2/spnego_test.go
📚 Learning: in the fiber framework tests, using `ctx.response.header.cookie` may not be suitable for parsing coo...
Learnt from: sixcolors
PR: gofiber/fiber#3016
File: middleware/csrf/csrf_test.go:188-193
Timestamp: 2024-09-25T15:57:10.221Z
Learning: In the Fiber framework tests, using `ctx.Response.Header.Cookie` may not be suitable for parsing cookies from the response header, as it requires a `*Cookie` and fills it rather than returning a string value; thus, manual parsing of the `Set-Cookie` header may be necessary.

Applied to files:

  • spnego/v3/spnego_test.go
  • spnego/v2/spnego_test.go
📚 Learning: in the fiber framework, breaking changes are acceptable when moving from version 2 to version 3, inc...
Learnt from: ReneWerner87
PR: gofiber/fiber#3161
File: app.go:923-932
Timestamp: 2024-11-15T07:56:21.623Z
Learning: In the Fiber framework, breaking changes are acceptable when moving from version 2 to version 3, including modifications to method signatures such as in the `Test` method in `app.go`.

Applied to files:

  • spnego/README.md
  • spnego/v3/spnego.go
  • spnego/v2/spnego.go
📚 Learning: in the `middleware/adaptor/adaptor.go` file of the fiber framework, when updating context handling, ...
Learnt from: gaby
PR: gofiber/fiber#3193
File: middleware/adaptor/adaptor.go:111-111
Timestamp: 2024-11-10T23:44:13.704Z
Learning: In the `middleware/adaptor/adaptor.go` file of the Fiber framework, when updating context handling, replacing `c.Context()` with `c.RequestCtx()` is appropriate to access the `fasthttp.RequestCtx`.

Applied to files:

  • spnego/v3/spnego.go
  • spnego/v2/spnego.go
🧬 Code Graph Analysis (1)
spnego/v3/spnego.go (2)
spnego/v2/spnego.go (2)
  • NewSpnegoKrb5AuthenticateMiddleware (25-62)
  • GetAuthenticatedIdentityFromContext (81-84)
spnego/config/config.go (5)
  • Config (37-42)
  • ErrConfigInvalidOfKeytabLookupFunctionRequired (12-12)
  • ErrLookupKeytabFailed (15-15)
  • ErrConvertRequestFailed (18-18)
  • ContextKeyOfIdentity (27-27)
🔇 Additional comments (5)
spnego/config/config.go (2)

11-24: LGTM! Well-defined error variables.

The error variables are clearly named and provide specific context for different failure scenarios. The grouping of related errors using var() block is appropriate.


29-33: Excellent design for extensibility.

The KeytabLookupFunc type provides a clean abstraction that enables dynamic keytab retrieval from various sources. The documentation clearly explains the extensibility benefits.

spnego/doc.go (1)

7-51: LGTM! Comprehensive package documentation.

The documentation provides excellent coverage of version compatibility, usage examples, and API overview. The example code demonstrates proper error handling and middleware usage patterns.

spnego/v2/spnego_test.go (1)

145-162: LGTM!

The test properly validates the context-based identity storage and retrieval functionality.

spnego/v2/spnego.go (1)

97-99: Verify response header retrieval in Header() implementation

The http.ResponseWriter.Header() method must return response headers, but the current code returns request headers via w.Ctx.GetReqHeaders().

  • Confirm whether Fiber v2’s Ctx interface offers a response-header method (for example, Response().Header()).
  • If it does, replace GetReqHeaders() with the correct call so that Header() returns the response headers as required by http.ResponseWriter.

Comment on lines 1 to 2
package config

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Duplicate comment line detected.

Line 2 duplicates line 1's package comment. Remove the duplicate.

 // Package spnego provides SPNEGO (Simple and Protected GSSAPI Negotiation Mechanism)
-// Package spnego provides SPNEGO (Simple and Protected GSSAPI Negotiation Mechanism)
 // authentication middleware for Fiber applications. It enables Kerberos authentication

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

🤖 Prompt for AI Agents
In spnego/config/config.go at lines 1 to 2, there is a duplicate package comment
line. Remove the second line to eliminate the duplicate comment and keep only
one package declaration line.

Comment on lines 50 to 61
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 func() (*keytab.Keytab, error) {
return &mergeKeytab, nil
}, nil
}
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

Potential memory issue with keytab merging.

The current implementation creates a single merged keytab that contains all entries from multiple files. This could lead to memory issues with large keytab files and doesn't handle potential duplicate entries.

Consider these improvements:

 func NewKeytabFileLookupFunc(keytabFiles ...string) (KeytabLookupFunc, error) {
 	if len(keytabFiles) == 0 {
 		return nil, ErrConfigInvalidOfAtLeastOneKeytabFileRequired
 	}
-	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 func() (*keytab.Keytab, error) {
-		return &mergeKeytab, nil
+		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 approach:

  1. Loads keytabs on-demand rather than pre-loading
  2. Provides fresher keytab data if files are updated
  3. Reduces memory footprint during initialization
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 func() (*keytab.Keytab, error) {
return &mergeKeytab, nil
}, nil
}
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
}
🤖 Prompt for AI Agents
In spnego/config/config.go around lines 50 to 61, the current code merges all
keytab entries into a single in-memory keytab, which can cause high memory usage
and does not handle duplicates. To fix this, refactor the function to load each
keytab file on-demand instead of preloading and merging all entries upfront.
Implement a mechanism that returns a function which loads and merges keytab
entries fresh each time it is called, ensuring updated data and lower memory
footprint during initialization.


func ExampleNewSpnegoKrb5AuthenticateMiddleware() {
app := fiber.New()
keytabLookup, err := config.NewKeytabFileLookupFunc("/keytabFile/one.keytab", "/keytabFile/two.keyta")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix typo in keytab file extension.

The second keytab file has a typo: .keyta should be .keytab.

-	keytabLookup, err := config.NewKeytabFileLookupFunc("/keytabFile/one.keytab", "/keytabFile/two.keyta")
+	keytabLookup, err := config.NewKeytabFileLookupFunc("/keytabFile/one.keytab", "/keytabFile/two.keytab")
🤖 Prompt for AI Agents
In spnego/example.go at line 15, there is a typo in the second keytab file
extension where ".keyta" should be corrected to ".keytab". Update the string
"/keytabFile/two.keyta" to "/keytabFile/two.keytab" to fix the file extension.

spnego/README.md Outdated
Comment on lines 52 to 66
cfg := &spnego.Config{
// Use a function to look up keytab from files
KeytabLookup: func() (*keytab.Keytab, error) {
// Implement your keytab lookup logic here
// This could be from files, database, or other sources
kt, err := spnego.NewKeytabFileLookupFunc("/path/to/keytab/file.keytab")
if err != nil {
return nil, err
}
return kt()
},
// Optional: Set a custom logger
Log: flog.DefaultLogger().Logger().(*log.Logger),
}

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Inconsistent API usage in Fiber v3 example.

The example shows inconsistent usage of the Config struct and API. It references spnego.Config and spnego.NewKeytabFileLookupFunc but should use the config package.

     // Create a configuration with a keytab lookup function
-    cfg := &spnego.Config{
+    cfg := &config.Config{
         // Use a function to look up keytab from files
         KeytabLookup: func() (*keytab.Keytab, error) {
             // Implement your keytab lookup logic here
             // This could be from files, database, or other sources
-            kt, err := spnego.NewKeytabFileLookupFunc("/path/to/keytab/file.keytab")
+            kt, err := config.NewKeytabFileLookupFunc("/path/to/keytab/file.keytab")
             if err != nil {
                 return nil, err
             }
             return kt()
         },

Also add the missing import:

 import (
     flog "github.com/gofiber/fiber/v3/log"
     "fmt"
+    "log"

     "github.com/jcmturner/gokrb5/v8/keytab"
     "github.com/gofiber/fiber/v3"
+    "github.com/gofiber/contrib/spnego/config"
     "github.com/gofiber/contrib/spnego/v3"
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cfg := &spnego.Config{
// Use a function to look up keytab from files
KeytabLookup: func() (*keytab.Keytab, error) {
// Implement your keytab lookup logic here
// This could be from files, database, or other sources
kt, err := spnego.NewKeytabFileLookupFunc("/path/to/keytab/file.keytab")
if err != nil {
return nil, err
}
return kt()
},
// Optional: Set a custom logger
Log: flog.DefaultLogger().Logger().(*log.Logger),
}
import (
flog "github.com/gofiber/fiber/v3/log"
"fmt"
"log"
"github.com/jcmturner/gokrb5/v8/keytab"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/contrib/spnego/config"
"github.com/gofiber/contrib/spnego/v3"
)
// Create a configuration with a keytab lookup function
cfg := &config.Config{
// Use a function to look up keytab from files
KeytabLookup: func() (*keytab.Keytab, error) {
// Implement your keytab lookup logic here
// This could be from files, database, or other sources
kt, err := config.NewKeytabFileLookupFunc("/path/to/keytab/file.keytab")
if err != nil {
return nil, err
}
return kt()
},
// Optional: Set a custom logger
Log: flog.DefaultLogger().Logger().(*log.Logger),
}
🤖 Prompt for AI Agents
In spnego/README.md lines 52 to 66, the example incorrectly uses spnego.Config
and spnego.NewKeytabFileLookupFunc instead of the config package. Update the
code to use config.Config and config.NewKeytabFileLookupFunc to align with the
correct API. Additionally, add the missing import statement for the config
package to ensure the example compiles and runs correctly.

if _, err = kt.Write(file); err != nil {
return filename, clean, fmt.Errorf("write file failed: %w", err)
}
file.Close()
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

Check error from file.Close()

File close operations can fail, especially when writes are buffered. Not checking the error could mask write failures.

-	file.Close()
-	return filename, clean, nil
+	if err := file.Close(); err != nil {
+		return filename, clean, fmt.Errorf("close file failed: %w", err)
+	}
+	return filename, clean, nil

Also applies to: 141-141

🤖 Prompt for AI Agents
In spnego/v2/spnego_test.go at lines 121 and 141, the file.Close() calls do not
check for errors, which can mask write failures. Update these lines to capture
the error returned by file.Close() and handle it appropriately, such as by
returning the error or logging it, to ensure any issues during file close are
not ignored.

}
// Set default logger if not provided
if cfg.Log == nil {
cfg.Log = flog.DefaultLogger().Logger().(*log.Logger)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Unsafe type assertion could cause panic

The type assertion flog.DefaultLogger().Logger().(*log.Logger) will panic if the underlying logger is not a *log.Logger.

-	cfg.Log = flog.DefaultLogger().Logger().(*log.Logger)
+	if logger, ok := flog.DefaultLogger().Logger().(*log.Logger); ok {
+		cfg.Log = logger
+	} else {
+		cfg.Log = log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile|log.Lmicroseconds)
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cfg.Log = flog.DefaultLogger().Logger().(*log.Logger)
// before:
- cfg.Log = flog.DefaultLogger().Logger().(*log.Logger)
// after:
if logger, ok := flog.DefaultLogger().Logger().(*log.Logger); ok {
cfg.Log = logger
} else {
cfg.Log = log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile|log.Lmicroseconds)
}
🤖 Prompt for AI Agents
In spnego/v3/spnego.go at line 35, the direct type assertion to *log.Logger is
unsafe and may cause a panic if the underlying logger is not of that type.
Replace the type assertion with a type check using the comma-ok idiom to safely
verify the type before assigning it to cfg.Log. Handle the case where the
assertion fails gracefully, such as by logging an error or using a fallback
logger.

Comment on lines 96 to 98
func (w wrapCtx) Header() http.Header {
return w.Ctx.GetReqHeaders()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

Verify Header() implementation for response headers

Similar to the v2 implementation, the Header() method returns request headers, but http.ResponseWriter expects response headers.


🏁 Script executed:

#!/bin/bash
# Check if Fiber v3 has methods for getting response headers
ast-grep --pattern 'func ($_ $_) GetRespHeaders() $_'

# Search for response header methods in Fiber v3 context
rg -A 5 "func.*\(.*Ctx\).*Response" --glob "**/*.go"

Length of output: 110


Update Header() to return response headers
The current implementation returns request headers via GetReqHeaders(), but for http.ResponseWriter you need to expose response headers. Fiber v3 doesn’t provide a GetRespHeaders() helper—use w.Ctx.Response().Header (a fasthttp.ResponseHeader) and convert it to an http.Header.

• File: spnego/v3/spnego.go, lines 96–98
• Replace the request‐header call with a conversion of the response header.

Suggested diff:

 func (w wrapCtx) Header() http.Header {
-   return w.Ctx.GetReqHeaders()
+   headers := http.Header{}
+   w.Ctx.Response().Header.VisitAll(func(k, v []byte) {
+       headers.Add(string(k), string(v))
+   })
+   return headers
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (w wrapCtx) Header() http.Header {
return w.Ctx.GetReqHeaders()
}
func (w wrapCtx) Header() http.Header {
headers := http.Header{}
w.Ctx.Response().Header.VisitAll(func(k, v []byte) {
headers.Add(string(k), string(v))
})
return headers
}
🤖 Prompt for AI Agents
In spnego/v3/spnego.go at lines 96 to 98, the Header() method incorrectly
returns request headers using GetReqHeaders(). Instead, update it to return
response headers by accessing w.Ctx.Response().Header, then convert this
fasthttp.ResponseHeader to an http.Header type before returning it. This ensures
the method correctly exposes response headers as required by
http.ResponseWriter.


```bash
# For Fiber v3
go get github.com/gofiber/contrib/spnego/v3
Copy link
Member

Choose a reason for hiding this comment

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

v2: github.com/gofiber/contrib/v2/spnego
v3: github.com/gofiber/contrib/v3/spnego

You can arrange it so that the packet itself can also have major versions in the future.
future : github.com/gofiber/contrib/v3/spnego/v2

Of course, the go.mod would have to be in the subfolders for that.

Copy link
Member

Choose a reason for hiding this comment

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

@jarod2011 We are currently working on this change and plan to use the prefix.
Can you please adjust it accordingly so that it fits into the scheme?

Copy link
Author

Choose a reason for hiding this comment

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

Hi @ReneWerner87 ,

Thanks for your feedback!

To make sure I understand correctly and to align with the new scheme: should I split this PR into two?

One for the Fiber v3 compatibility, which would be merged into the v3-beta branch.

And another one for the Fiber v2 compatibility, which would be merged into the v2 branch.

Please let me know if this is the right approach. I'm happy to adjust accordingly.

Thanks!

@ReneWerner87
Copy link
Member

@jarod2011
Please also check the AI notes and add an entry to the root readme.

jarod2011 and others added 3 commits August 26, 2025 01:03
Move configuration and utility functions from config package to main package, optimize code organization
Add new error types and utility functions, improve test coverage
Simplify documentation content, remove redundant information
- Update English README document structure, add badges and document ID
- Synchronize update of Chinese translation version
- Unify keytab lookup implementation in example code
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
spnego/README.zh-CN.md (1)

190-191: Fix stray fenced code block (markdownlint MD040).

A dangling triple/quadruple backtick breaks rendering and linting. Remove it or provide a language.

-```
-
-````
+<!-- removed stray fences -->
♻️ Duplicate comments (8)
spnego/doc.go (1)

1-3: Duplicate doc line previously flagged appears resolved.

The earlier duplicate line reported in past reviews is no longer present. Good cleanup.

spnego/README.md (3)

44-91: Fix v3 example: incorrect API/package usage and unused imports.

Examples won’t compile as-is. Past reviews noted the same; apply consistent config usage and clean imports.

 import (
-    "fmt"
-    "time"
-
-    "github.com/gofiber/contrib/spnego"
-    "github.com/gofiber/contrib/spnego/utils"
-    v3 "github.com/gofiber/contrib/spnego/v3"
-    "github.com/gofiber/fiber/v3"
-    "github.com/gofiber/fiber/v3/log"
+    "fmt"
+    "log"
+
+    "github.com/gofiber/fiber/v3"
+    "github.com/gofiber/contrib/spnego"
+    "github.com/gofiber/contrib/spnego/config"
+    v3 "github.com/gofiber/contrib/spnego/v3"
 )
@@
-    // For testing, you can create a mock keytab file using utils.NewMockKeytab
-    // In production, use a real keytab file
-    keytabLookup, err := spnego.NewKeytabFileLookupFunc("/path/to/keytab/file.keytab")
+    // Use a keytab file (replace with your path). For tests, see utils.NewMockKeytab.
+    keytabLookup, err := config.NewKeytabFileLookupFunc("/path/to/keytab/file.keytab")
@@
-    authMiddleware, err := v3.NewSpnegoKrb5AuthenticateMiddleware(spnego.Config{
-		KeytabLookup: keytabLookup,
-    })
+    authMiddleware, err := v3.NewSpnegoKrb5AuthenticateMiddleware(config.Config{
+        KeytabLookup: keytabLookup,
+    })

Note: If the repository adopts the github.com/gofiber/contrib/v3/spnego layout, update the three imports accordingly: contrib/v3/spnego, contrib/v3/spnego/config, and keep fiber/v3.


95-144: Fix v2 example: same API/package issues as v3 snippet.

Mirror the corrections for v2 and ensure log and os are actually used.

 import (
     "fmt"
     "log"
     "os"
 
-    "github.com/gofiber/contrib/spnego"
-    "github.com/gofiber/contrib/spnego/utils"
-    v2 "github.com/gofiber/contrib/spnego/v2"
     "github.com/gofiber/fiber/v2"
+    "github.com/gofiber/contrib/spnego"
+    "github.com/gofiber/contrib/spnego/config"
+    v2 "github.com/gofiber/contrib/spnego/v2"
 )
@@
-    // For testing, you can create a mock keytab file using utils.NewMockKeytab
-    // In production, use a real keytab file
-    keytabLookup, err := spnego.NewKeytabFileLookupFunc("/path/to/keytab/file.keytab")
+    // Use a keytab file (replace with your path). For tests, see utils.NewMockKeytab.
+    keytabLookup, err := config.NewKeytabFileLookupFunc("/path/to/keytab/file.keytab")
@@
-    authMiddleware, err := v2.NewSpnegoKrb5AuthenticateMiddleware(spnego.Config{
+    authMiddleware, err := v2.NewSpnegoKrb5AuthenticateMiddleware(config.Config{
         KeytabLookup: keytabLookup,
         // Optional: Set a custom logger
         Log: log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile|log.Lmicroseconds),
     })

166-179: API reference: point NewKeytabFileLookupFunc to the config package and correct return types.

Reference belongs to the config package and returns config.KeytabLookupFunc.

-### `NewKeytabFileLookupFunc(keytabFiles ...string) (KeytabLookupFunc, error)`
+### `config.NewKeytabFileLookupFunc(keytabFiles ...string) (config.KeytabLookupFunc, error)`
 
-Creates a new KeytabLookupFunc that loads keytab files.
+Creates a new KeytabLookupFunc that loads and merges keytab files.
spnego/README.zh-CN.md (1)

189-189: Don’t reference an unexported/internal context key in docs.

The variable spnego.contextKeyOfIdentity is internal and not part of the public API. Directing users to it is misleading; the supported API is spnego.GetAuthenticatedIdentityFromContext (and SetAuthenticatedIdentityToContext).

-- 已认证的身份使用`spnego.contextKeyOfIdentity`存储在Fiber上下文中
+- 已认证的身份通过 `spnego.GetAuthenticatedIdentityFromContext(ctx)` 从 Fiber 上下文中获取。
+  中间件会在认证成功时将身份放入上下文;请使用公开的辅助函数进行读写。
spnego/v3/spnego.go (2)

55-57: Verify ResponseWriter adapter writes response headers, not request headers.

The SPNEGO library sets WWW-Authenticate on the ResponseWriter. If utils.NewWrapFiberContext returns a Header() backed by request headers, the challenge will never reach clients. Ensure Header() maps to ctx.Response().Header. Add/adjust tests (see v3 test suggestion) to catch this.

#!/bin/bash
# Verify Header() implementation of the WrapFiberContext adapter
rg -nP -C3 'type\s+WrapFiberContext.*\n|\bHeader\(\)\s+http\.Header' spnego/utils | sed -n '1,200p'
rg -nP -C3 'GetReqHeaders|Response\(\)\.Header' spnego/utils

Expected: Header() should read/write via response headers (ctx.Response().Header); no use of GetReqHeaders for ResponseWriter.


---

`32-34`: **Avoid unsafe type assertion when defaulting the logger.**

Directly asserting to *log.Logger can panic if Fiber’s default logger changes type. Use comma-ok and a sane fallback. 


```diff
- if cfg.Log == nil {
- 	cfg.Log = flog.DefaultLogger().Logger().(*log.Logger)
- }
+ if cfg.Log == nil {
+ 	if logger, ok := flog.DefaultLogger().Logger().(*log.Logger); ok {
+ 		cfg.Log = logger
+ 	} else {
+ 		cfg.Log = log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile|log.Lmicroseconds)
+ 	}
+ }

Also add missing import if needed:

 import (
 	"fmt"
 	"log"
+	"os"
 	"net/http"
spnego/v2/spnego.go (1)

56-58: Same ResponseWriter adapter concern for v2.

Confirm that utils.NewWrapFiberContext(*fiber.Ctx) implements http.ResponseWriter over response headers and status, not request headers—otherwise WWW-Authenticate/Negotiate won’t be emitted.

#!/bin/bash
# Check the v2 adapter as well (shared utils)
rg -nP -C3 'WrapFiberContext.*Header\(\)\s+http\.Header' spnego/utils
rg -nP -C3 'GetReqHeaders|Response\(\)\.Header' spnego/utils
🧹 Nitpick comments (28)
spnego/doc.go (1)

1-4: Tighten package doc; make it a single, punctuated sentence.

The first sentence should fully describe the package and end with a period. Suggest collapsing the two lines into one sentence for godoc clarity.

-// Package spnego provides SPNEGO (Simple and Protected GSSAPI Negotiation Mechanism)
-// authentication middleware for Fiber applications.
-// It enables Kerberos authentication for HTTP requests.
+// Package spnego provides SPNEGO (Simple and Protected GSSAPI Negotiation Mechanism) authentication middleware for Fiber applications, enabling Kerberos-based SSO for HTTP requests.
spnego/error.go (2)

5-18: Polish error message phrasing for clarity/consistency.

Minor grammar tweaks improve readability and align with Go error style (“failed to …”, “invalid config: …”).

-var ErrConfigInvalidOfKeytabLookupFunctionRequired = errors.New("config invalid: keytab lookup function is required")
+var ErrConfigInvalidOfKeytabLookupFunctionRequired = errors.New("invalid config: keytab lookup function is required")

-var ErrLookupKeytabFailed = errors.New("keytab lookup failed")
+var ErrLookupKeytabFailed = errors.New("failed to look up keytab")

-var ErrConvertRequestFailed = errors.New("convert request failed")
+var ErrConvertRequestFailed = errors.New("failed to convert request")

-var ErrConfigInvalidOfAtLeastOneKeytabFileRequired = errors.New("config invalid: at least one keytab file required")
+var ErrConfigInvalidOfAtLeastOneKeytabFileRequired = errors.New("invalid config: at least one keytab file is required")

-var ErrLoadKeytabFileFailed = errors.New("load keytab failed")
+var ErrLoadKeytabFileFailed = errors.New("failed to load keytab")

5-18: Consolidate or Rename Duplicated Error Variables Across Packages

I’ve confirmed that the same error variables are declared both in spnego/error.go (lines 6, 9, 12, 15, 18) and in spnego/config/config.go (lines 12, 15, 18, 20). Since they live in separate packages (spnego vs. spnego/config), you won’t get a compile-time conflict, but this duplication can lead to maintenance overhead and subtle inconsistencies.

• spnego/error.go:

  • ErrConfigInvalidOfKeytabLookupFunctionRequired
  • ErrLookupKeytabFailed
  • ErrConvertRequestFailed
  • ErrConfigInvalidOfAtLeastOneKeytabFileRequired
  • ErrLoadKeytabFileFailed

• spnego/config/config.go:

  • ErrConfigInvalidOfKeytabLookupFunctionRequired
  • ErrLookupKeytabFailed
  • ErrConvertRequestFailed
  • ErrConfigInvalidOfAtLeastOneKeytabFileRequired

Consider one of the following to reduce duplication and keep error handling consistent:

  • Extract these shared error declarations into a common package (e.g., spnego/errors) and import them where needed.
  • Rename or differentiate the config-specific errors if they’re semantically distinct.
  • Document the duplication clearly if keeping separate definitions is intentional.
spnego/identity.go (1)

5-7: Interface name duplicates utils.FiberContext; consider rename to avoid confusion.

There’s another FiberContext in spnego/utils. Different packages, but same name increases cognitive load. Optional rename, e.g., FiberLocalsContext.

spnego/README.md (4)

168-174: API signature for middleware should mention versioned package explicitly or show both.

Currently ambiguous. Consider adding both v2 and v3 signatures or a note.

-### `NewSpnegoKrb5AuthenticateMiddleware(cfg spnego.Config) (fiber.Handler, error)`
+### `v3.NewSpnegoKrb5AuthenticateMiddleware(cfg config.Config) (fiber.Handler, error)`
+### `v2.NewSpnegoKrb5AuthenticateMiddleware(cfg config.Config) (fiber.Handler, error)`

198-199: Avoid documenting an unexported key; describe behavior instead.

Mentioning spnego.contextKeyOfIdentity leaks an internal detail and may change. Prefer describing the helper functions.

-- Authenticated identities are stored in the Fiber context using `spnego.contextKeyOfIdentity`
+- Authenticated identities are stored in the Fiber context via `spnego.SetAuthenticatedIdentityToContext` and can be retrieved with `spnego.GetAuthenticatedIdentityFromContext`.

70-73: Replace hard tab with spaces (markdownlint MD010).

There’s a hard tab before KeytabLookup:; use spaces.

-    authMiddleware, err := v3.NewSpnegoKrb5AuthenticateMiddleware(spnego.Config{
-		KeytabLookup: keytabLookup,
-    })
+    authMiddleware, err := v3.NewSpnegoKrb5AuthenticateMiddleware(spnego.Config{
+        KeytabLookup: keytabLookup,
+    })

5-11: Add standard header block and link from root README.

Per maintainer comment, adopt the contrib README header (badges, quick links) and add an entry in the repository root README pointing to this middleware.

I can open a follow-up PR to update the root README and add the standardized header here if you’d like.

spnego/utils/adapter.go (2)

49-57: Initialize adapter state in constructor.

Initialize header and default status.

 func NewWrapFiberContext[T FiberContext[T]](ctx T) *WrapFiberContextAdaptHttpResponseWriter[T] {
-	return &WrapFiberContextAdaptHttpResponseWriter[T]{
-		ctx: ctx,
-	}
+	return &WrapFiberContextAdaptHttpResponseWriter[T]{
+		ctx:        ctx,
+		header:     make(http.Header),
+		statusCode: http.StatusOK,
+	}
 }

21-26: Name is overly verbose; consider a shorter, conventional type name.

E.g., ResponseWriterAdapter[T] or FiberResponseWriter[T]. Improves readability without changing behavior.

spnego/utils/adapter_test.go (2)

19-19: Fix test name typo: “Fiver” → “Fiber”.

Keeps naming consistent and discoverable when running tests selectively.

-func TestNewWrapFiberContextOfFiverV2(t *testing.T) {
+func TestNewWrapFiberContextOfFiberV2(t *testing.T) {
@@
-func TestNewWrapFiberContextOfFiverV3(t *testing.T) {
+func TestNewWrapFiberContextOfFiberV3(t *testing.T) {

Also applies to: 41-41


19-33: Make tests parallel.

Both tests are independent; running them in parallel speeds up the suite without side effects.

 func TestNewWrapFiberContextOfFiberV2(t *testing.T) {
+	t.Parallel()
@@
 func TestNewWrapFiberContextOfFiberV3(t *testing.T) {
+	t.Parallel()

Also applies to: 41-55

spnego/README.zh-CN.md (2)

22-31: Optional: add shell prompt language to installation block for better formatting.

Improves readability and satisfies markdown linters in some setups.

-```bash
+```bash
 # 对于Fiber v3
  go get github.com/gofiber/contrib/spnego/v3
@@
 # 对于Fiber v2
  go get github.com/gofiber/contrib/spnego/v2

---

`1-21`: **Consider adding a pointer to the repository root README as requested by reviewers.**

A short “See root README for overview” link addresses the request from ReneWerner87 and helps discoverability.


Would you like me to draft a concise entry (EN/zh-CN) for the root README that links to this guide and highlights SPNEGO support?

</blockquote></details>
<details>
<summary>spnego/utils/keytab_test.go (1)</summary><blockquote>

`10-46`: **Make the test parallel; otherwise looks solid.**

Parallelizing is safe here and reduces suite time. The assertions are deterministic (Unix timestamps).



```diff
 func TestGetKeytabInfo(t *testing.T) {
+	t.Parallel()
 	tm := time.Now()
spnego/config_test.go (4)

17-27: Rename subtest and clarify its intent (invalid file vs. not found).

The case writes an invalid keytab file, so "not found" is misleading. Rename to "invalid keytab file content" for clarity.

- t.Run("test not found keytab file", func(t *testing.T) {
+ t.Run("test invalid keytab file content", func(t *testing.T) {

18-22: Use t.TempDir to avoid filename collisions and manual cleanup.

Writing to ./invalid.keytab can collide across runs and requires manual cleanup. Prefer t.TempDir + filepath.Join to isolate files.

- err := os.WriteFile("./invalid.keytab", []byte("12345"), 0600)
+ dir := t.TempDir()
+ invalid := filepath.Join(dir, "invalid.keytab")
+ err := os.WriteFile(invalid, []byte("12345"), 0o600)
...
- fn, err := NewKeytabFileLookupFunc("./invalid.keytab")
+ fn, err := NewKeytabFileLookupFunc(invalid)

Also add:

 import (
 	"os"
+	"path/filepath"
 	"testing"
 	"time"

Also applies to: 23-27


75-79: Multiple-files test: prefer temp paths; avoid hardcoded names and manual cleanup.

Same rationale as above—use t.TempDir and filepath.Join; drop os.Remove via t.Cleanup.

- err = os.WriteFile("./invalid1.keytab", []byte("12345"), 0600)
+ dir := t.TempDir()
+ invalid := filepath.Join(dir, "invalid1.keytab")
+ err = os.WriteFile(invalid, []byte("12345"), 0o600)
...
- fn, err := NewKeytabFileLookupFunc("./temp.keytab", "./invalid1.keytab")
+ fn, err := NewKeytabFileLookupFunc(filepath.Join(dir, "temp.keytab"), invalid)

81-131: Assert by content rather than relying on order, and use temp files.

GetKeytabInfo sorts by principal; asserting indices is okay but brittle if sort criteria change. Prefer verifying via a map keyed by principal with per-pair checks. Also use t.TempDir for both files as suggested above.

- require.Equal(t, info[0].PrincipalName, "HTTP/[email protected]")
- ...
- require.Equal(t, info[1].PrincipalName, "HTTP/[email protected]")
+ m := map[string]utils.KeytabInfo{}
+ for _, it := range info { m[it.PrincipalName] = it }
+ it1, ok := m["HTTP/[email protected]"]; require.True(t, ok)
+ require.Len(t, it1.Pairs, 1)
+ require.Equal(t, uint8(2), it1.Pairs[0].Version)
+ require.Equal(t, int32(18), it1.Pairs[0].EncryptType)
+ require.Equal(t, tm.Unix(), it1.Pairs[0].CreateTime.Unix())
+ it2, ok := m["HTTP/[email protected]"]; require.True(t, ok)
+ require.Len(t, it2.Pairs, 2)
+ // ...same assertions for it2
spnego/v3/spnego_test.go (2)

24-41: Avoid brittle body equality; assert error classification and status only.

Relying on the exact error message body may break across Fiber versions/config. Prefer status + substring or error class.

- require.Equal(t, http.StatusInternalServerError, ctx.Response.StatusCode())
- require.Equal(t, fmt.Sprintf("%s: mock keytab lookup error", spnego.ErrLookupKeytabFailed), string(ctx.Response.Body()))
+ require.Equal(t, http.StatusInternalServerError, ctx.Response.StatusCode())
+ require.Contains(t, string(ctx.Response.Body()), spnego.ErrLookupKeytabFailed.Error())
+ require.Contains(t, string(ctx.Response.Body()), "mock keytab lookup error")

74-88: Assert SPNEGO challenge headers on 401 to catch ResponseWriter adapter bugs.

Add assertions for:

  • WWW-Authenticate: Negotiate
  • Vary: Authorization (recommended by RFC 7235 §4.1)
    This will detect regressions where headers are written to the request header map instead of the response.
 handler(ctx)
 require.Equal(t, fasthttp.StatusUnauthorized, ctx.Response.StatusCode())
+ require.Contains(t, string(ctx.Response.Header.Peek("WWW-Authenticate")), "Negotiate")
+ require.Contains(t, string(ctx.Response.Header.Peek("Vary")), "Authorization")
spnego/utils/mock_keytab_test.go (2)

52-68: Rename and clarify: "no file output" case.

Subtest name "none file created" is awkward; "no file persisted" is clearer. The checks look good.

- t.Run("test none file created", func(t *testing.T) {
+ t.Run("test no file persisted", func(t *testing.T) {

84-98: Same: restore global and avoid fixed path.

Also, prefer a temp dir path to avoid collisions; the test currently asserts removal on failure, but creating "./temp.keytab" across runs can collide.

- defaultFileOperator = mockFileOperator{flag: 0x02}
+ prev := defaultFileOperator
+ defaultFileOperator = mockFileOperator{flag: 0x02}
+ t.Cleanup(func() { defaultFileOperator = prev })
...
- WithFilename("./temp.keytab"),
+ dir := t.TempDir()
+ WithFilename(filepath.Join(dir, "temp.keytab")),

Also add import:

+ "path/filepath"
spnego/utils/keytab.go (1)

73-76: Optional: also sort each principal’s Pairs for stable, deterministic output

If the caller inspects Pairs, consider sorting them (e.g., by EncryptType, then Version, then CreateTime). This makes outputs deterministic across runs and merges.

 sort.Slice(mk, func(i, j int) bool {
   return mk[i].PrincipalName < mk[j].PrincipalName
 })
+// Ensure deterministic order of pairs inside each principal
+for i := range mk {
+  sort.Slice(mk[i].Pairs, func(a, b int) bool {
+    if mk[i].Pairs[a].EncryptType == mk[i].Pairs[b].EncryptType {
+      if mk[i].Pairs[a].Version == mk[i].Pairs[b].Version {
+        return mk[i].Pairs[a].CreateTime.Before(mk[i].Pairs[b].CreateTime)
+      }
+      return mk[i].Pairs[a].Version < mk[i].Pairs[b].Version
+    }
+    return mk[i].Pairs[a].EncryptType < mk[i].Pairs[b].EncryptType
+  })
+}
spnego/config.go (2)

34-44: Optional: deduplicate entries across multiple keytabs

If the same entry exists in multiple files, the merged keytab will contain duplicates. Consider deduping by fields that uniquely identify a key entry (principal, enctype, version, timestamp).

 return func() (*keytab.Keytab, error) {
-   var mergeKeytab keytab.Keytab
+   var mergeKeytab keytab.Keytab
+   seen := make(map[string]struct{}, 256)
    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...)
+     for _, e := range kt.Entries {
+       // principal | enctype | kvno | ts (unix seconds) – adjust if your uniqueness criteria differ
+       k := fmt.Sprintf("%s|%d|%d|%d", e.Principal.String(), e.Key.KeyType, e.KVNO8, e.Timestamp.Unix())
+       if _, ok := seen[k]; ok {
+         continue
+       }
+       seen[k] = struct{}{}
+       mergeKeytab.Entries = append(mergeKeytab.Entries, e)
+     }
    }
    return &mergeKeytab, nil
 }, nil

28-45: Optional: add a caching wrapper to avoid re-reading files on every lookup

If KeytabLookup is invoked per request or per handshake, consider an opt-in, TTL-based cache to reduce I/O, with a background refresh strategy.

Example (new helper, can live alongside this file):

package spnego

import (
  "sync"
  "time"
  "github.com/jcmturner/gokrb5/v8/keytab"
)

func NewCachedKeytabLookupFunc(loader KeytabLookupFunc, ttl time.Duration) KeytabLookupFunc {
  var (
    mu   sync.RWMutex
    kt   *keytab.Keytab
    exp  time.Time
  )
  return func() (*keytab.Keytab, error) {
    now := time.Now()
    mu.RLock()
    if kt != nil && now.Before(exp) {
      defer mu.RUnlock()
      return kt, nil
    }
    mu.RUnlock()

    mu.Lock()
    defer mu.Unlock()
    if kt != nil && now.Before(exp) { // double-check
      return kt, nil
    }
    fresh, err := loader()
    if err != nil {
      return nil, err
    }
    kt = fresh
    exp = now.Add(ttl)
    return kt, nil
  }
}
spnego/utils/mock_keytab.go (2)

124-132: Wrap underlying error and validate inputs (principal/pairs) for clearer failures

  • Use %w to preserve error chains from AddEntry.
  • Optionally fail fast when PrincipalName is empty or no Pairs are provided; this prevents creating unusable keytabs in tests.
 opt := newDefaultMockOptions()
 opt.apply(opts...)
 kt := keytab.New()
 var err error
+if opt.PrincipalName == "" {
+  return nil, nil, fmt.Errorf("principal name is required")
+}
+if len(opt.Pairs) == 0 {
+  return nil, nil, fmt.Errorf("at least one EncryptTypePair is required")
+}
 for _, pair := range opt.Pairs {
-  if err = kt.AddEntry(opt.PrincipalName, opt.Realm, opt.Password, pair.CreateTime, pair.Version, pair.EncryptType); err != nil {
-    return nil, nil, fmt.Errorf("error adding entry: %v", err)
+  if err = kt.AddEntry(opt.PrincipalName, opt.Realm, opt.Password, pair.CreateTime, pair.Version, pair.EncryptType); err != nil {
+    return nil, nil, fmt.Errorf("add entry: %w", err)
   }
 }

92-103: Nit: clarify naming to reflect purpose

myFileOperator/defaultFileOperator are fine, but osFileOperator communicates intent slightly better.

-type myFileOperator struct{}
+type osFileOperator struct{}

-func (m myFileOperator) OpenFile(filename string, flag int, perm os.FileMode) (*os.File, error) {
+func (m osFileOperator) OpenFile(filename string, flag int, perm os.FileMode) (*os.File, error) {
   return os.OpenFile(filename, flag, perm)
 }
 
-func (m myFileOperator) Remove(filename string) error {
+func (m osFileOperator) Remove(filename string) error {
   return os.Remove(filename)
 }
 
-var defaultFileOperator fileOperator = myFileOperator{}
+var defaultFileOperator fileOperator = osFileOperator{}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between db1c1b7 and b795161.

📒 Files selected for processing (19)
  • spnego/README.md (1 hunks)
  • spnego/README.zh-CN.md (1 hunks)
  • spnego/config.go (1 hunks)
  • spnego/config_test.go (1 hunks)
  • spnego/doc.go (1 hunks)
  • spnego/error.go (1 hunks)
  • spnego/example/example.go (1 hunks)
  • spnego/identity.go (1 hunks)
  • spnego/identity_test.go (1 hunks)
  • spnego/utils/adapter.go (1 hunks)
  • spnego/utils/adapter_test.go (1 hunks)
  • spnego/utils/keytab.go (1 hunks)
  • spnego/utils/keytab_test.go (1 hunks)
  • spnego/utils/mock_keytab.go (1 hunks)
  • spnego/utils/mock_keytab_test.go (1 hunks)
  • spnego/v2/spnego.go (1 hunks)
  • spnego/v2/spnego_test.go (1 hunks)
  • spnego/v3/spnego.go (1 hunks)
  • spnego/v3/spnego_test.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • spnego/v2/spnego_test.go
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2024-10-08T19:06:06.583Z
Learnt from: gaby
PR: gofiber/fiber#3056
File: middleware/encryptcookie/utils.go:22-25
Timestamp: 2024-10-08T19:06:06.583Z
Learning: The `encryptcookie_test.go` file contains unit tests that validate key lengths for both `EncryptCookie` and `DecryptCookie` functions, ensuring that invalid key lengths raise appropriate errors.

Applied to files:

  • spnego/utils/keytab_test.go
📚 Learning: 2024-11-10T23:44:13.704Z
Learnt from: gaby
PR: gofiber/fiber#3193
File: middleware/adaptor/adaptor.go:111-111
Timestamp: 2024-11-10T23:44:13.704Z
Learning: In the `middleware/adaptor/adaptor.go` file of the Fiber framework, when updating context handling, replacing `c.Context()` with `c.RequestCtx()` is appropriate to access the `fasthttp.RequestCtx`.

Applied to files:

  • spnego/utils/adapter_test.go
  • spnego/utils/adapter.go
  • spnego/v2/spnego.go
  • spnego/v3/spnego.go
📚 Learning: 2024-11-15T07:56:21.623Z
Learnt from: ReneWerner87
PR: gofiber/fiber#3161
File: app.go:923-932
Timestamp: 2024-11-15T07:56:21.623Z
Learning: In the Fiber framework, breaking changes are acceptable when moving from version 2 to version 3, including modifications to method signatures such as in the `Test` method in `app.go`.

Applied to files:

  • spnego/utils/adapter_test.go
📚 Learning: 2024-11-21T11:28:14.554Z
Learnt from: ReneWerner87
PR: gofiber/contrib#0
File: :0-0
Timestamp: 2024-11-21T11:28:14.554Z
Learning: Using `fiber.Ctx`, even when wrapped, does not achieve version independence, as it still relies on the specific Fiber implementation.

Applied to files:

  • spnego/utils/adapter_test.go
📚 Learning: 2024-11-29T15:24:06.083Z
Learnt from: ReneWerner87
PR: gofiber/contrib#0
File: :0-0
Timestamp: 2024-11-29T15:24:06.083Z
Learning: When customizing the Fiber context to achieve version independence, using generics in methods like `Status` and `Type` allows for fluent method chaining. Implementing a generic interface for `Ctx` on the `App` prevents class switching when registering custom contexts that use fluent methods. Providing multiple `New` methods allows users who do not wish to customize the context to continue using `fiber.New` without changes.

Applied to files:

  • spnego/utils/adapter_test.go
📚 Learning: 2024-09-25T15:57:10.221Z
Learnt from: sixcolors
PR: gofiber/fiber#3016
File: middleware/csrf/csrf_test.go:188-193
Timestamp: 2024-09-25T15:57:10.221Z
Learning: In the Fiber framework tests, using `ctx.Response.Header.Cookie` may not be suitable for parsing cookies from the response header, as it requires a `*Cookie` and fills it rather than returning a string value; thus, manual parsing of the `Set-Cookie` header may be necessary.

Applied to files:

  • spnego/v2/spnego.go
  • spnego/v3/spnego.go
📚 Learning: 2025-05-13T00:19:16.407Z
Learnt from: sixcolors
PR: gofiber/fiber#3446
File: docs/middleware/logger.md:44-44
Timestamp: 2025-05-13T00:19:16.407Z
Learning: In documentation files for the Fiber framework, code examples are often partial and don't repeat import statements that were shown in earlier examples, focusing instead on demonstrating specific usage patterns.

Applied to files:

  • spnego/README.md
📚 Learning: 2024-11-23T19:50:41.765Z
Learnt from: norri
PR: gofiber/recipes#2701
File: clean-code/app/server/server.go:14-16
Timestamp: 2024-11-23T19:50:41.765Z
Learning: In the `clean-code` example at `clean-code/app/server/server.go` using the Go Fiber framework, it's acceptable to omit production-level features like context usage and graceful shutdown handling to keep the example simple.

Applied to files:

  • spnego/README.md
🧬 Code graph analysis (13)
spnego/utils/keytab_test.go (2)
spnego/utils/mock_keytab.go (5)
  • NewMockKeytab (123-150)
  • WithRealm (40-44)
  • WithPrincipal (32-36)
  • WithPassword (67-71)
  • WithPairs (58-62)
spnego/utils/keytab.go (2)
  • EncryptTypePair (22-26)
  • GetKeytabInfo (50-77)
spnego/identity_test.go (1)
spnego/identity.go (2)
  • SetAuthenticatedIdentityToContext (12-14)
  • GetAuthenticatedIdentityFromContext (25-28)
spnego/utils/adapter_test.go (1)
spnego/utils/adapter.go (1)
  • NewWrapFiberContext (53-57)
spnego/example/example.go (5)
spnego/utils/mock_keytab.go (5)
  • NewMockKeytab (123-150)
  • WithPrincipal (32-36)
  • WithRealm (40-44)
  • WithFilename (49-53)
  • WithPairs (58-62)
spnego/utils/keytab.go (1)
  • EncryptTypePair (22-26)
spnego/config.go (2)
  • NewKeytabFileLookupFunc (30-45)
  • Config (21-26)
spnego/v3/spnego.go (1)
  • NewSpnegoKrb5AuthenticateMiddleware (26-59)
spnego/identity.go (1)
  • GetAuthenticatedIdentityFromContext (25-28)
spnego/config_test.go (4)
spnego/config.go (1)
  • NewKeytabFileLookupFunc (30-45)
spnego/error.go (2)
  • ErrConfigInvalidOfAtLeastOneKeytabFileRequired (15-15)
  • ErrLoadKeytabFileFailed (18-18)
spnego/utils/mock_keytab.go (5)
  • NewMockKeytab (123-150)
  • WithPrincipal (32-36)
  • WithRealm (40-44)
  • WithPairs (58-62)
  • WithFilename (49-53)
spnego/utils/keytab.go (2)
  • EncryptTypePair (22-26)
  • GetKeytabInfo (50-77)
spnego/identity.go (1)
spnego/utils/adapter.go (1)
  • FiberContext (12-19)
spnego/config.go (1)
spnego/error.go (2)
  • ErrConfigInvalidOfAtLeastOneKeytabFileRequired (15-15)
  • ErrLoadKeytabFileFailed (18-18)
spnego/utils/mock_keytab_test.go (2)
spnego/utils/mock_keytab.go (6)
  • NewMockKeytab (123-150)
  • WithPrincipal (32-36)
  • WithRealm (40-44)
  • WithPairs (58-62)
  • WithFilename (49-53)
  • WithPassword (67-71)
spnego/utils/keytab.go (1)
  • EncryptTypePair (22-26)
spnego/utils/adapter.go (1)
spnego/identity.go (1)
  • FiberContext (5-7)
spnego/utils/mock_keytab.go (1)
spnego/utils/keytab.go (1)
  • EncryptTypePair (22-26)
spnego/v2/spnego.go (6)
spnego/v3/spnego.go (1)
  • NewSpnegoKrb5AuthenticateMiddleware (26-59)
spnego/config.go (1)
  • Config (21-26)
spnego/config/config.go (4)
  • Config (37-42)
  • ErrConfigInvalidOfKeytabLookupFunctionRequired (12-12)
  • ErrLookupKeytabFailed (15-15)
  • ErrConvertRequestFailed (18-18)
spnego/error.go (3)
  • ErrConfigInvalidOfKeytabLookupFunctionRequired (6-6)
  • ErrLookupKeytabFailed (9-9)
  • ErrConvertRequestFailed (12-12)
spnego/identity.go (1)
  • SetAuthenticatedIdentityToContext (12-14)
spnego/utils/adapter.go (1)
  • NewWrapFiberContext (53-57)
spnego/v3/spnego.go (6)
spnego/v2/spnego.go (1)
  • NewSpnegoKrb5AuthenticateMiddleware (26-60)
spnego/config.go (1)
  • Config (21-26)
spnego/config/config.go (4)
  • Config (37-42)
  • ErrConfigInvalidOfKeytabLookupFunctionRequired (12-12)
  • ErrLookupKeytabFailed (15-15)
  • ErrConvertRequestFailed (18-18)
spnego/error.go (3)
  • ErrConfigInvalidOfKeytabLookupFunctionRequired (6-6)
  • ErrLookupKeytabFailed (9-9)
  • ErrConvertRequestFailed (12-12)
spnego/identity.go (1)
  • SetAuthenticatedIdentityToContext (12-14)
spnego/utils/adapter.go (1)
  • NewWrapFiberContext (53-57)
spnego/v3/spnego_test.go (4)
spnego/v3/spnego.go (1)
  • NewSpnegoKrb5AuthenticateMiddleware (26-59)
spnego/config.go (2)
  • Config (21-26)
  • NewKeytabFileLookupFunc (30-45)
spnego/utils/mock_keytab.go (1)
  • NewMockKeytab (123-150)
spnego/identity.go (1)
  • GetAuthenticatedIdentityFromContext (25-28)
🪛 LanguageTool
spnego/README.md

[grammar] ~17-~17: There might be a mistake here.
Context: ...eros authentication via SPNEGO mechanism - Flexible keytab lookup system - Support ...

(QB_NEW_EN)


[grammar] ~18-~18: There might be a mistake here.
Context: ...echanism - Flexible keytab lookup system - Support for dynamic keytab retrieval fro...

(QB_NEW_EN)


[grammar] ~19-~19: There might be a mistake here.
Context: ...ic keytab retrieval from various sources - Integration with Fiber context for authe...

(QB_NEW_EN)


[grammar] ~20-~20: There might be a mistake here.
Context: ...ntext for authenticated identity storage - Configurable logging ## Version Compati...

(QB_NEW_EN)


[grammar] ~27-~27: There might be a mistake here.
Context: ...ses: - v2: Compatible with Fiber v2 - v3: Compatible with Fiber v3 ## Insta...

(QB_NEW_EN)


[grammar] ~184-~184: There might be a mistake here.
Context: ...ion that retrieves the keytab (required) - Log: The logger used for middleware logging...

(QB_NEW_EN)


[grammar] ~189-~189: There might be a mistake here.
Context: ...r) ## Requirements - Go 1.21 or higher - For v3: Fiber v3 - For v2: Fiber v2 - Ke...

(QB_NEW_EN)


[grammar] ~190-~190: There might be a mistake here.
Context: ... - Go 1.21 or higher - For v3: Fiber v3 - For v2: Fiber v2 - Kerberos infrastructu...

(QB_NEW_EN)


[grammar] ~191-~191: There might be a mistake here.
Context: ...er - For v3: Fiber v3 - For v2: Fiber v2 - Kerberos infrastructure ## Notes - Ens...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
spnego/README.md

71-71: Hard tabs
Column: 1

(MD010, no-hard-tabs)

spnego/README.zh-CN.md

190-190: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (8)
spnego/README.md (1)

32-38: Align SPNEGO module import paths across code and docs

I’ve spotted mixed import patterns in the SPNEGO example and README files:

  • Unversioned imports:
    • github.com/gofiber/contrib/spnego
    • github.com/gofiber/contrib/spnego/utils
  • Versioned imports:
    • github.com/gofiber/contrib/spnego/v2
    • github.com/gofiber/contrib/spnego/v3

Please pick one repository‐wide pattern and make it consistent everywhere:

• Confirm the module path declared in the SPNEGO Go module (e.g. in spnego/go.mod)—it should include the major version suffix if you’re using Go modules (for v3, for example, it should read module github.com/gofiber/contrib/spnego/v3).
• Update all installation commands in spnego/README.md and spnego/README.zh-CN.md to use go get github.com/gofiber/contrib/spnego/v{N}.
• Update all import statements (in spnego/example/example.go, README snippets, etc.) to match that same pattern—including nested packages, e.g. github.com/gofiber/contrib/spnego/v3/utils rather than the unversioned path.
• Run go mod tidy (or your workspace setup) to validate that the chosen import paths resolve correctly.

Once imports and docs are fully aligned to the module’s declared path, this will avoid confusion and ensure smooth version upgrades.

spnego/v3/spnego_test.go (1)

19-22: Nil KeytabLookup validation looks good.

This asserts the required config gate appropriately.

spnego/utils/mock_keytab_test.go (2)

35-51: Test name clarity and deterministic timing.

"test add keytab entry failed" is fine; consider fixing create times to tm := time.Unix(...) for deterministic snapshots if needed. No action required.


130-155: Functional options tests look good; minor style nit.

Option application order and content are validated well. No issues.

spnego/v2/spnego.go (1)

31-35: Sane default logger in v2 looks good.

Creating a fresh *log.Logger avoids unsafe assertions. No changes needed.

spnego/utils/keytab.go (2)

51-68: Aggregation logic is clear and correct

Grouping entries by Principal.String() and accumulating EncryptTypePair looks good. Using a temporary map and then materializing a slice is appropriate before sorting.


6-7: Confirm Go 1.21+ requirement for maps import

The new import of the standard maps package (spnego/utils/keytab.go, lines 6-7) requires Go 1.21 or newer. I wasn’t able to find a go.mod or go.work declaring a minimum Go version, nor any CI workflows or Dockerfiles pinning the Go image. Please verify that:

  • A go.mod (or go.work) file exists at the repository root (or relevant module directory) with
    go 1.21
    or higher.
  • Your CI pipelines (e.g. GitHub Actions, Travis, etc.) and any Dockerfiles use a Go 1.21+ image.
  • If you’re not using modules, that your build environment is still configured to use Go 1.21 or later.

Ensuring these will prevent build errors when importing maps.

spnego/config.go (1)

30-45: Behavior is sound; loader composes multiple keytabs correctly

Validates input, loads files, merges entries, and wraps errors with useful context. API shape (variadic files, closure returning merged keytab) is clean.

Comment on lines +28 to +55
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())
})
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.

Comment on lines +14 to +69
func ExampleNewSpnegoKrb5AuthenticateMiddleware() {
app := fiber.New()
// create mock keytab file
// you must 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("create mock keytab error: %v", err)
}
defer clean()
keytabLookup, err := spnego.NewKeytabFileLookupFunc("./temp-sso1.keytab")
if err != nil {
panic(fmt.Errorf("create keytab lookup function failed: %w", err))
}
authMiddleware, err := v3.NewSpnegoKrb5AuthenticateMiddleware(spnego.Config{
KeytabLookup: keytabLookup,
})
if err != nil {
panic(fmt.Errorf("create spnego middleware failed: %w", 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")
go func() {
<-time.After(time.Second * 1)
fmt.Println("use curl -kv --negotiate http://sso.example.local:3000/protected/resource")
fmt.Println("if response is 401, execute `klist` to check use kerberos session")
<-time.After(time.Second * 2)
fmt.Println("close server")
if err = app.Shutdown(); err != nil {
panic(fmt.Errorf("shutdown server failed: %w", err))
}
}()
if err := app.Listen("sso.example.local:3000"); err != nil {
panic(fmt.Errorf("start server failed: %w", err))
}

// Output: Server is running on :3000
}
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

⚠️ Potential issue

Example will fail as a doc test (“Output: …”) and/or hang CI.

  • The function is an Example with an Output: comment, so go test will run it and compare stdout. However:
    • It starts a server and prints additional lines via fmt.Println, which won’t match the single-line expected output.
    • log.Info (Fiber logger) may not write to stdout in a way go test captures.
    • Binding to sso.example.local:3000 can fail name resolution in CI.
      Fix by removing the Output: directive (examples without Output are compiled but not run), or simplify to a non-network example that only prints the expected line.

Minimal safe fix (don’t execute the example in tests):

-	// Output: Server is running on :3000
+	// (no Output: directive on purpose; this example should compile but not run during `go test`)

Optional follow-ups:

  • Change the listen address to :3000 or localhost:3000.
  • If you prefer keeping an executable example, remove the server code and just fmt.Println("Server is running on :3000") to satisfy the Output: check.

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    if err := app.Listen("sso.example.local:3000"); err != nil {
        panic(fmt.Errorf("start server failed: %w", err))
    }

    // (no Output: directive on purpose; this example should compile but not run during `go test`)
}
🤖 Prompt for AI Agents
In spnego/example/example.go around lines 14-69 the Example function starts a
real server, does network calls and prints multiple lines but also includes an
`// Output: Server is running on :3000` doctest directive which makes `go test`
try to run and match stdout (causing failures/hangs in CI); fix by removing the
`Output:` directive so the example is compiled but not executed by tests, or
alternatively convert the example to a non-network doc example by removing
server startup and ancillary prints and leaving a single `fmt.Println("Server is
running on :3000")` that matches the `Output:` line (if you keep an Output,
ensure only that exact line is printed), and optionally change the listen
address to `:3000` or `localhost:3000` if you intend to run network code
locally.

Comment on lines +13 to +34
func TestGetAndSetAuthenticatedIdentityFromContextForFiberV2(t *testing.T) {
app := fiberV2.New()
id := goidentity.NewUser("[email protected]")
app.Use("/identity", func(ctx *fiberV2.Ctx) error {
SetAuthenticatedIdentityToContext(ctx, &id)
return ctx.Next()
})
app.Get("/test", func(ctx *fiberV2.Ctx) error {
_, ok := GetAuthenticatedIdentityFromContext(ctx)
require.False(t, ok)
return ctx.SendStatus(fiberV2.StatusOK)
})
app.Get("/identity/test", func(ctx *fiberV2.Ctx) error {
user, ok := GetAuthenticatedIdentityFromContext(ctx)
require.True(t, ok)
require.Equal(t, id.UserName(), user.UserName())
require.Equal(t, id.Domain(), user.Domain())
return ctx.SendStatus(fiberV2.StatusOK)
})
app.Test(httptest.NewRequest("GET", "/test", nil))
app.Test(httptest.NewRequest("GET", "/identity/test", nil))
}
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

Assert app.Test errors and status codes; close bodies; make test parallel.

Tightens the contract and avoids leaks. Using require inside handlers is fine, but still verify the outer request completed successfully.

 func TestGetAndSetAuthenticatedIdentityFromContextForFiberV2(t *testing.T) {
+	t.Parallel()
@@
-	app.Test(httptest.NewRequest("GET", "/test", nil))
-	app.Test(httptest.NewRequest("GET", "/identity/test", nil))
+	resp1, err := app.Test(httptest.NewRequest("GET", "/test", nil))
+	require.NoError(t, err)
+	defer resp1.Body.Close()
+	require.Equal(t, fiberV2.StatusOK, resp1.StatusCode)
+
+	resp2, err := app.Test(httptest.NewRequest("GET", "/identity/test", nil))
+	require.NoError(t, err)
+	defer resp2.Body.Close()
+	require.Equal(t, fiberV2.StatusOK, resp2.StatusCode)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestGetAndSetAuthenticatedIdentityFromContextForFiberV2(t *testing.T) {
app := fiberV2.New()
id := goidentity.NewUser("[email protected]")
app.Use("/identity", func(ctx *fiberV2.Ctx) error {
SetAuthenticatedIdentityToContext(ctx, &id)
return ctx.Next()
})
app.Get("/test", func(ctx *fiberV2.Ctx) error {
_, ok := GetAuthenticatedIdentityFromContext(ctx)
require.False(t, ok)
return ctx.SendStatus(fiberV2.StatusOK)
})
app.Get("/identity/test", func(ctx *fiberV2.Ctx) error {
user, ok := GetAuthenticatedIdentityFromContext(ctx)
require.True(t, ok)
require.Equal(t, id.UserName(), user.UserName())
require.Equal(t, id.Domain(), user.Domain())
return ctx.SendStatus(fiberV2.StatusOK)
})
app.Test(httptest.NewRequest("GET", "/test", nil))
app.Test(httptest.NewRequest("GET", "/identity/test", nil))
}
func TestGetAndSetAuthenticatedIdentityFromContextForFiberV2(t *testing.T) {
t.Parallel()
app := fiberV2.New()
id := goidentity.NewUser("[email protected]")
app.Use("/identity", func(ctx *fiberV2.Ctx) error {
SetAuthenticatedIdentityToContext(ctx, &id)
return ctx.Next()
})
app.Get("/test", func(ctx *fiberV2.Ctx) error {
_, ok := GetAuthenticatedIdentityFromContext(ctx)
require.False(t, ok)
return ctx.SendStatus(fiberV2.StatusOK)
})
app.Get("/identity/test", func(ctx *fiberV2.Ctx) error {
user, ok := GetAuthenticatedIdentityFromContext(ctx)
require.True(t, ok)
require.Equal(t, id.UserName(), user.UserName())
require.Equal(t, id.Domain(), user.Domain())
return ctx.SendStatus(fiberV2.StatusOK)
})
resp1, err := app.Test(httptest.NewRequest("GET", "/test", nil))
require.NoError(t, err)
defer resp1.Body.Close()
require.Equal(t, fiberV2.StatusOK, resp1.StatusCode)
resp2, err := app.Test(httptest.NewRequest("GET", "/identity/test", nil))
require.NoError(t, err)
defer resp2.Body.Close()
require.Equal(t, fiberV2.StatusOK, resp2.StatusCode)
}
🤖 Prompt for AI Agents
In spnego/identity_test.go around lines 13 to 34, make the test parallel and
assert the results of app.Test calls: add t.Parallel() at the top of the test,
capture the returned (resp, err) for each app.Test call and use
require.NoError(t, err) and require.Equal(t, fiberV2.StatusOK, resp.StatusCode)
to verify success, and ensure you close each response body (e.g., defer
resp.Body.Close() or resp.Body.Close() after assertions) to avoid leaks; keep
the existing require assertions inside handlers as-is.

Comment on lines +36 to +57
func TestGetAndSetAuthenticatedIdentityFromContextForFiberV3(t *testing.T) {
app := fiberV3.New()
id := goidentity.NewUser("[email protected]")
app.Use("/identity", func(ctx fiberV3.Ctx) error {
SetAuthenticatedIdentityToContext(ctx, &id)
return ctx.Next()
})
app.Get("/test", func(ctx fiberV3.Ctx) error {
_, ok := GetAuthenticatedIdentityFromContext(ctx)
require.False(t, ok)
return ctx.SendStatus(fiberV3.StatusOK)
})
app.Get("/identity/test", func(ctx fiberV3.Ctx) error {
user, ok := GetAuthenticatedIdentityFromContext(ctx)
require.True(t, ok)
require.Equal(t, id.UserName(), user.UserName())
require.Equal(t, id.Domain(), user.Domain())
return ctx.SendStatus(fiberV3.StatusOK)
})
app.Test(httptest.NewRequest("GET", "/test", nil))
app.Test(httptest.NewRequest("GET", "/identity/test", nil))
}
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

Mirror the same robustness for Fiber v3 test.

Parallelize and verify responses.

 func TestGetAndSetAuthenticatedIdentityFromContextForFiberV3(t *testing.T) {
+	t.Parallel()
@@
-	app.Test(httptest.NewRequest("GET", "/test", nil))
-	app.Test(httptest.NewRequest("GET", "/identity/test", nil))
+	resp1, err := app.Test(httptest.NewRequest("GET", "/test", nil))
+	require.NoError(t, err)
+	defer resp1.Body.Close()
+	require.Equal(t, fiberV3.StatusOK, resp1.StatusCode)
+
+	resp2, err := app.Test(httptest.NewRequest("GET", "/identity/test", nil))
+	require.NoError(t, err)
+	defer resp2.Body.Close()
+	require.Equal(t, fiberV3.StatusOK, resp2.StatusCode)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestGetAndSetAuthenticatedIdentityFromContextForFiberV3(t *testing.T) {
app := fiberV3.New()
id := goidentity.NewUser("[email protected]")
app.Use("/identity", func(ctx fiberV3.Ctx) error {
SetAuthenticatedIdentityToContext(ctx, &id)
return ctx.Next()
})
app.Get("/test", func(ctx fiberV3.Ctx) error {
_, ok := GetAuthenticatedIdentityFromContext(ctx)
require.False(t, ok)
return ctx.SendStatus(fiberV3.StatusOK)
})
app.Get("/identity/test", func(ctx fiberV3.Ctx) error {
user, ok := GetAuthenticatedIdentityFromContext(ctx)
require.True(t, ok)
require.Equal(t, id.UserName(), user.UserName())
require.Equal(t, id.Domain(), user.Domain())
return ctx.SendStatus(fiberV3.StatusOK)
})
app.Test(httptest.NewRequest("GET", "/test", nil))
app.Test(httptest.NewRequest("GET", "/identity/test", nil))
}
func TestGetAndSetAuthenticatedIdentityFromContextForFiberV3(t *testing.T) {
t.Parallel()
app := fiberV3.New()
id := goidentity.NewUser("[email protected]")
app.Use("/identity", func(ctx fiberV3.Ctx) error {
SetAuthenticatedIdentityToContext(ctx, &id)
return ctx.Next()
})
app.Get("/test", func(ctx fiberV3.Ctx) error {
_, ok := GetAuthenticatedIdentityFromContext(ctx)
require.False(t, ok)
return ctx.SendStatus(fiberV3.StatusOK)
})
app.Get("/identity/test", func(ctx fiberV3.Ctx) error {
user, ok := GetAuthenticatedIdentityFromContext(ctx)
require.True(t, ok)
require.Equal(t, id.UserName(), user.UserName())
require.Equal(t, id.Domain(), user.Domain())
return ctx.SendStatus(fiberV3.StatusOK)
})
resp1, err := app.Test(httptest.NewRequest("GET", "/test", nil))
require.NoError(t, err)
defer resp1.Body.Close()
require.Equal(t, fiberV3.StatusOK, resp1.StatusCode)
resp2, err := app.Test(httptest.NewRequest("GET", "/identity/test", nil))
require.NoError(t, err)
defer resp2.Body.Close()
require.Equal(t, fiberV3.StatusOK, resp2.StatusCode)
}
🤖 Prompt for AI Agents
In spnego/identity_test.go around lines 36 to 57, the Fiber v3 test should
mirror the more robust Fiber v2 test by running subtests in parallel and
asserting responses; add t.Parallel() at the start of the test, invoke app.Test
for "/test" and "/identity/test" in separate goroutines or use subtests with
t.Run(..., func(t *testing.T){ t.Parallel() ... }), capture and require.NoError
on the returned responses and errors, assert resp.StatusCode == fiberV3.StatusOK
for both requests, and ensure resp.Body is closed after reads; this will
parallelize execution and verify the responses rather than calling app.Test
without checks.

Comment on lines +12 to +14
func SetAuthenticatedIdentityToContext[T FiberContext](ctx T, identity goidentity.Identity) {
ctx.Locals(contextKeyOfIdentity, identity)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify context key definition; otherwise these helpers won’t compile.

Both helpers rely on an unexported contextKeyOfIdentity. Confirm it exists in package spnego and isn’t typed as a plain string to avoid collisions.

Script to confirm presence once in this package:

If missing, add:

// prefer an unexported, unique key type to avoid collisions with user code
type contextKey string
var contextKeyOfIdentity contextKey = "spnego.identity"

🏁 Script executed:

#!/bin/bash
rg -nP '^\s*(type\s+contextKey\b|var\s+contextKeyOfIdentity\b|const\s+contextKeyOfIdentity\b)' spnego -C2

Length of output: 436


Ensure a unique, typed context key to avoid collisions

The contextKeyOfIdentity constant is defined in spnego/config.go:11, but it’s an untyped string, which can collide with other context keys. Update it to use a dedicated key type:

• File: spnego/config.go (around line 11)

  • Current:
    // contextKeyOfIdentity is the key used to store the authenticated identity in the Fiber context
    const contextKeyOfIdentity = "middleware.spnego.Identity"
  • Replace with a private contextKey type and a typed variable:
 // prefer an unexported, unique key type to avoid collisions with user code
+type contextKey string

-// contextKeyOfIdentity is the key used to store the authenticated identity in the Fiber context
-const contextKeyOfIdentity = "middleware.spnego.Identity"
+var contextKeyOfIdentity contextKey = "middleware.spnego.Identity"

This ensures that only values using your contextKey type can collide, preventing accidental overlaps with other packages’ string-based keys.

🤖 Prompt for AI Agents
In spnego/config.go around line 11, replace the untyped string constant
contextKeyOfIdentity with a private typed context key to avoid collisions:
declare a new private type (e.g. type contextKey string) and then define a
package-level variable of that type for the identity key (e.g. var
contextKeyOfIdentity = contextKey("middleware.spnego.Identity")), then update
uses (like in spnego/identity.go lines 12-14) to continue using the variable;
ensure the new type and variable are unexported so only your package can use
that key.

Comment on lines +70 to +72
for item := range maps.Values(keytabMap) {
mk = append(mk, item)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix: range over values (not indices) when converting map to slice

maps.Values returns a slice of values; ranging with a single iteration variable yields the index (int), which then fails to append to MultiKeytabInfo. Use for _, item := range ... to iterate values.

- for item := range maps.Values(keytabMap) {
-   mk = append(mk, item)
- }
+ for _, item := range maps.Values(keytabMap) {
+   mk = append(mk, item)
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for item := range maps.Values(keytabMap) {
mk = append(mk, item)
}
for _, item := range maps.Values(keytabMap) {
mk = append(mk, item)
}
🤖 Prompt for AI Agents
In spnego/utils/keytab.go around lines 70 to 72, the loop uses "for item :=
range maps.Values(keytabMap)" which iterates indices (ints) rather than values;
change the loop to use a two-variable range ("for _, item := range
maps.Values(keytabMap)") so that the actual values are appended to mk
(MultiKeytabInfo) instead of indices, ensuring types match and the slice
contains the map values.

Comment on lines 99 to 120
t.Run("test file created", func(t *testing.T) {
defaultFileOperator = myFileOperator{}
tm := time.Now()
_, clean, err := NewMockKeytab(
WithPrincipal("HTTP/sso.example.com"),
WithRealm("TEST.LOCAL"),
WithPairs(EncryptTypePair{
Version: 3,
EncryptType: 18,
CreateTime: tm,
}),
WithFilename("./temp.keytab"),
)
require.NoError(t, err)
t.Cleanup(clean)
require.FileExists(t, "./temp.keytab")
kt, err := keytab.Load("./temp.keytab")
require.NoError(t, err)
_, kv, err := kt.GetEncryptionKey(types.NewPrincipalName(1, "HTTP/sso.example.com"), "TEST.LOCAL", 3, 18)
require.NoError(t, err)
require.Equal(t, 3, kv)
})
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

Use secure permission and temp path in the positive case too.

Ensure the production path uses mode 0o600 (change in helper required) and avoid ./temp.keytab.

- defaultFileOperator = myFileOperator{}
+ prev := defaultFileOperator
+ defaultFileOperator = myFileOperator{}
+ t.Cleanup(func() { defaultFileOperator = prev })
...
- WithFilename("./temp.keytab"),
+ dir := t.TempDir()
+ WithFilename(filepath.Join(dir, "temp.keytab")),

And in spnego/utils/mock_keytab.go (outside this file):

- defaultFileOperator.OpenFile(opt.Filename, os.O_RDWR|os.O_CREATE, 0o666)
+ defaultFileOperator.OpenFile(opt.Filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)

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

Comment on lines 135 to 147
file, err := defaultFileOperator.OpenFile(opt.Filename, os.O_RDWR|os.O_CREATE, 0o666)
if err != nil {
return nil, nil, fmt.Errorf("error opening file: %w", err)
}
clean = func() {
_ = defaultFileOperator.Remove(opt.Filename)
}
if _, err = kt.Write(file); err != nil {
clean()
return nil, nil, fmt.Errorf("error writing to file: %w", err)
}
_ = file.Close()
return kt, clean, nil
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Close-before-remove and use O_TRUNC for safe, cross-platform cleanup

On write error you remove the file while it may still be open; this can fail on Windows. Also, opening without O_TRUNC may leave stale bytes from a previous run if the new keytab is shorter.

- file, err := defaultFileOperator.OpenFile(opt.Filename, os.O_RDWR|os.O_CREATE, 0o666)
+ file, err := defaultFileOperator.OpenFile(opt.Filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o666)
  if err != nil {
    return nil, nil, fmt.Errorf("error opening file: %w", err)
  }
- clean = func() {
-   _ = defaultFileOperator.Remove(opt.Filename)
- }
- if _, err = kt.Write(file); err != nil {
-   clean()
-   return nil, nil, fmt.Errorf("error writing to file: %w", err)
- }
- _ = file.Close()
+ // cleanup closes and removes the file; safe to call multiple times
+ clean = func() {
+   _ = file.Close()
+   _ = defaultFileOperator.Remove(opt.Filename)
+ }
+ if _, err = kt.Write(file); err != nil {
+   clean() // ensures close-before-remove
+   return nil, nil, fmt.Errorf("error writing to file: %w", err)
+ }
+ // normal path: close the file; leaving remove to the returned cleanup
+ _ = file.Close()
  return kt, clean, nil
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
file, err := defaultFileOperator.OpenFile(opt.Filename, os.O_RDWR|os.O_CREATE, 0o666)
if err != nil {
return nil, nil, fmt.Errorf("error opening file: %w", err)
}
clean = func() {
_ = defaultFileOperator.Remove(opt.Filename)
}
if _, err = kt.Write(file); err != nil {
clean()
return nil, nil, fmt.Errorf("error writing to file: %w", err)
}
_ = file.Close()
return kt, clean, nil
file, err := defaultFileOperator.OpenFile(opt.Filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o666)
if err != nil {
return nil, nil, fmt.Errorf("error opening file: %w", err)
}
// cleanup closes and removes the file; safe to call multiple times
clean = func() {
_ = file.Close()
_ = defaultFileOperator.Remove(opt.Filename)
}
if _, err = kt.Write(file); err != nil {
clean() // ensures close-before-remove
return nil, nil, fmt.Errorf("error writing to file: %w", err)
}
// normal path: close the file; leaving remove to the returned cleanup
_ = file.Close()
return kt, clean, nil
🤖 Prompt for AI Agents
In spnego/utils/mock_keytab.go around lines 135 to 147, opening the temp keytab
uses os.O_RDWR|os.O_CREATE (missing O_TRUNC) which can leave stale bytes, and on
write error the cleanup removes the file while it may still be open (problematic
on Windows). Fix by opening the file with os.O_TRUNC in addition to the existing
flags so the file is truncated on open; on write error call file.Close()
(checking/ignoring its error) before calling the cleanup Remove; also ensure the
normal return path closes the file before returning and that the cleanup closure
only removes the file (not close it).

Comment on lines 43 to 68
tm := time.Now()
_, clean1, err1 := 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: tm,
}),
)
require.NoError(t, err1)
t.Cleanup(clean1)
_, clean2, err2 := utils.NewMockKeytab(
utils.WithPrincipal("HTTP/sso2.example.com"),
utils.WithRealm("EXAMPLE.LOCAL"),
utils.WithFilename("./temp-sso2.keytab"),
utils.WithPairs(utils.EncryptTypePair{
Version: 2,
EncryptType: 18,
CreateTime: tm,
}),
)
require.NoError(t, err2)
t.Cleanup(clean2)
lookupFunc, err := spnego.NewKeytabFileLookupFunc("./temp-sso1.keytab", "./temp-sso2.keytab")
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

Secure keytab files and avoid hardcoded paths.

Persisting keytabs as ./temp-*.keytab and the helper’s 0o666 mode are not appropriate for secret material, even in tests. Use t.TempDir + filepath.Join and ensure creation mode 0o600 in utils.NewMockKeytab.

- utils.WithFilename("./temp-sso1.keytab"),
+ dir := t.TempDir()
+ utils.WithFilename(filepath.Join(dir, "sso1.keytab")),
...
- utils.WithFilename("./temp-sso2.keytab"),
+ utils.WithFilename(filepath.Join(dir, "sso2.keytab")),

And update 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.

🤖 Prompt for AI Agents
In spnego/v3/spnego_test.go around lines 43 to 68, the test writes keytab files
to hardcoded ./temp-*.keytab with permissive 0o666 mode; change the test to
create files inside t.TempDir() (use filepath.Join(t.TempDir(),
"temp-sso1.keytab") etc.) and pass those paths into NewMockKeytab; also update
utils.NewMockKeytab implementation (spnego/utils/mock_keytab.go) to create
keytab files with mode 0o600 (owner-read/write only) instead of 0o666 so test
keytabs are created with secure permissions.

test: Update test cases to use temporary directory instead of current directory

refactor: Remove unused config package
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
spnego/utils/mock_keytab.go (1)

135-147: Close-before-remove on write error; tighten perms; open WRONLY (Windows-safe and credential-safe)
On write error the file is removed while still open; this can fail on Windows and leak a descriptor. Also, using 0o666 for keytab files is overly permissive for credential material; prefer 0o600. Opening WRONLY clarifies intent.

Apply this diff:

-    file, err := defaultFileOperator.OpenFile(opt.Filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o666)
+    file, err := defaultFileOperator.OpenFile(opt.Filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
     if err != nil {
       return nil, nil, fmt.Errorf("error opening file: %w", err)
     }
-    clean = func() {
-      _ = defaultFileOperator.Remove(opt.Filename)
-    }
+    // cleanup closes and removes the file; safe to call multiple times
+    clean = func() {
+      _ = file.Close()
+      _ = defaultFileOperator.Remove(opt.Filename)
+    }
     if _, err = kt.Write(file); err != nil {
-      clean()
+      clean() // ensures close-before-remove on error (important on Windows)
       return nil, nil, fmt.Errorf("error writing to file: %w", err)
     }
     _ = file.Close()
     return kt, clean, nil

Optional verification to catch other overly-permissive creations:

#!/bin/bash
# Find any file creations with 0o666 perms or OpenFile calls using O_RDWR where write-only suffices.
rg -nP '0o666\b' -C2
rg -nP 'OpenFile\([^,]+,\s*os\.O_RDWR\b' -C2
🧹 Nitpick comments (3)
spnego/utils/mock_keytab.go (3)

126-133: Validate required fields when adding entries

If Pairs is non-empty but PrincipalName is empty, gokrb5 may accept an invalid principal or error in surprising ways. Fail fast with a clear error.

Apply this diff:

   kt := keytab.New()
   var err error
+  if len(opt.Pairs) > 0 && opt.PrincipalName == "" {
+    return nil, nil, fmt.Errorf("principal name must be set when adding keytab entries")
+  }
   for _, pair := range opt.Pairs {

Optionally, add a unit test asserting that NewMockKeytab(WithPairs(...)) without WithPrincipal returns an error.


129-131: Wrap underlying error with %w for consistency

Use %w so callers can use errors.Is / errors.As.

Apply this diff:

-      return nil, nil, fmt.Errorf("error adding entry: %v", err)
+      return nil, nil, fmt.Errorf("error adding entry: %w", err)

92-103: Minor naming nit: clarify file operator implementation
Rename myFileOperator to osFileOperator to reflect it delegates to the os package.

Apply this diff:

-type myFileOperator struct{}
+type osFileOperator struct{}

-func (m myFileOperator) OpenFile(filename string, flag int, perm os.FileMode) (*os.File, error) {
+func (m osFileOperator) OpenFile(filename string, flag int, perm os.FileMode) (*os.File, error) {
   return os.OpenFile(filename, flag, perm)
 }

-func (m myFileOperator) Remove(filename string) error {
+func (m osFileOperator) Remove(filename string) error {
   return os.Remove(filename)
 }

-var defaultFileOperator fileOperator = myFileOperator{}
+var defaultFileOperator fileOperator = osFileOperator{}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b795161 and 5197bf6.

📒 Files selected for processing (4)
  • spnego/utils/mock_keytab.go (1 hunks)
  • spnego/utils/mock_keytab_test.go (1 hunks)
  • spnego/v2/spnego_test.go (1 hunks)
  • spnego/v3/spnego_test.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • spnego/utils/mock_keytab_test.go
  • spnego/v3/spnego_test.go
  • spnego/v2/spnego_test.go
🧰 Additional context used
🧬 Code graph analysis (1)
spnego/utils/mock_keytab.go (1)
spnego/utils/keytab.go (1)
  • EncryptTypePair (22-26)
🔇 Additional comments (1)
spnego/utils/mock_keytab.go (1)

10-19: Good use of the Options pattern and solid documentation

Clear option constructors, defaults, and comments. This makes the helper flexible and easy to use in tests.

Also applies to: 21-29, 30-76

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
spnego/utils/mock_keytab.go (1)

135-148: Harden file creation: WRONLY, 0600 perms, idempotent cleanup, and fsync

  • Open with O_WRONLY (not O_RDWR) since we don't read; keep O_TRUNC.
  • Use 0o600 for credential-like files (even in tests) to avoid permissive modes under loose umasks.
  • Make cleanup close+remove to be idempotent and safe if called on error or success.
  • fsync before close to reduce flakiness on slow CI filesystems.
-    file, err := defaultFileOperator.OpenFile(opt.Filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o666)
+    // Write-only, create, and truncate with restrictive perms.
+    file, err := defaultFileOperator.OpenFile(opt.Filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
     if err != nil {
       return nil, nil, fmt.Errorf("error opening file: %w", err)
     }
-    clean = func() {
-      _ = defaultFileOperator.Remove(opt.Filename)
-    }
+    // Idempotent cleanup: close then remove; safe to call multiple times.
+    clean = func() {
+      _ = file.Close()
+      _ = defaultFileOperator.Remove(opt.Filename)
+    }
     if _, err = kt.Write(file); err != nil {
-      file.Close()
-      clean()
+      clean() // ensures close-before-remove on error
       return nil, nil, fmt.Errorf("error writing to file: %w", err)
     }
-    _ = file.Close()
+    // Best effort flush then close; leave remove to returned cleanup.
+    if err := file.Sync(); err != nil {
+      clean()
+      return nil, nil, fmt.Errorf("error syncing file: %w", err)
+    }
+    _ = file.Close()
     return kt, clean, nil

Note: You already addressed O_TRUNC and close-before-remove per the earlier review; this refines flags, perms, and idempotency.

🧹 Nitpick comments (3)
spnego/utils/mock_keytab.go (3)

124-132: Add basic option validation to prevent unusable keytabs

Guard against missing principal/realm and optionally empty pairs to avoid surprising runtime errors.

Apply this diff to introduce validation right after applying options:

 func NewMockKeytab(opts ...MockOption) (*keytab.Keytab, func(), error) {
   opt := newDefaultMockOptions()
   opt.apply(opts...)
+  if opt.PrincipalName == "" {
+    return nil, nil, fmt.Errorf("principal name is required; set WithPrincipal(...)")
+  }
+  if opt.Realm == "" {
+    return nil, nil, fmt.Errorf("realm is required; set WithRealm(...)")
+  }
+  // If an empty keytab is not desirable for your tests, uncomment the guard below:
+  // if len(opt.Pairs) == 0 {
+  //   return nil, nil, fmt.Errorf("at least one EncryptTypePair is required; set WithPairs(...)")
+  // }
   kt := keytab.New()
   var err error
   for _, pair := range opt.Pairs {

If you intentionally rely on empty pairs in some tests, keep the third check commented or toggle it via an option.


128-131: Use %w for proper error wrapping

Preserve the original error in the chain for callers using errors.Is/As.

-    if err = kt.AddEntry(opt.PrincipalName, opt.Realm, opt.Password, pair.CreateTime, pair.Version, pair.EncryptType); err != nil {
-      return nil, nil, fmt.Errorf("error adding entry: %v", err)
+    if err = kt.AddEntry(opt.PrincipalName, opt.Realm, opt.Password, pair.CreateTime, pair.Version, pair.EncryptType); err != nil {
+      return nil, nil, fmt.Errorf("error adding entry: %w", err)
     }

92-103: Name nit: myFileOperator → osFileOperator; consider exposing a setter for tests

Minor clarity nit on the concrete type name. Also, external tests (utils_test package) can’t mutate unexported vars; a tiny setter keeps the seam while maintaining encapsulation.

-type myFileOperator struct{}
+type osFileOperator struct{}
 
-func (m myFileOperator) OpenFile(filename string, flag int, perm os.FileMode) (*os.File, error) {
+func (m osFileOperator) OpenFile(filename string, flag int, perm os.FileMode) (*os.File, error) {
   return os.OpenFile(filename, flag, perm)
 }
 
-func (m myFileOperator) Remove(filename string) error {
+func (m osFileOperator) Remove(filename string) error {
   return os.Remove(filename)
 }
 
-var defaultFileOperator fileOperator = myFileOperator{}
+var defaultFileOperator fileOperator = osFileOperator{}

Optionally add (outside this hunk):

// SetFileOperator allows tests in external packages to inject a fake FS.
func SetFileOperator(op fileOperator) {
  if op != nil {
    defaultFileOperator = op
  }
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5197bf6 and 952aaf6.

📒 Files selected for processing (1)
  • spnego/utils/mock_keytab.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
spnego/utils/mock_keytab.go (1)
spnego/utils/keytab.go (1)
  • EncryptTypePair (22-26)
🔇 Additional comments (2)
spnego/utils/mock_keytab.go (2)

30-76: LGTM: clean, idiomatic option pattern with helpful docs

The option functions are straightforward and composable; docs make intended usage obvious.


104-123: File-backed path testing confirmed

The WithFilename option is exercised in multiple test suites covering both successful and failure scenarios:

  • Happy paths in

    • spnego/v3/spnego_test.go (lines 50–52, 62–64)
    • spnego/utils/mock_keytab_test.go (lines 119–121)
    • spnego/config_test.go (lines 38–40, 66–68, 90–92, 106–108)
    • spnego/v2/spnego_test.go (lines 50–52, 62–64)
  • Failure paths in

    • spnego/utils/mock_keytab_test.go (lines 84–86 expecting os.ErrPermission; lines 103–105 expecting os.ErrClosed)

No additional tests are needed for the file-backed keytab path.

@jarod2011 jarod2011 requested a review from ReneWerner87 August 27, 2025 14:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✏️ Feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

📝 [Proposal]: Add Enterprise-Grade SPNEGO Authentication Middleware

2 participants