gwim

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 16, 2026 License: BSD-3-Clause Imports: 11 Imported by: 0

README

gwim

Windows-native Kerberos/NTLM authentication and TLS for Go HTTP servers.

Deploying a Go service inside a corporate Active Directory domain normally means standing up IIS or a reverse proxy just to get Windows integrated authentication, authorization, and TLS. gwim removes that requirement. It wraps any http.Handler with Windows-native Kerberos authentication, enriches the request context with LDAP group memberships, and pulls TLS certificates straight from the Windows certificate store — letting you ship a single self-contained web server .exe that runs seamlessly in a Windows domain environment. With gwim, you can run Go websites securely right where your users are and just how your Windows infrastructure is configured.

Prerequisites

Requirement Needed for
Windows OS All features (library uses Windows SSPI / CryptoAPI)
Active Directory domain membership Kerberos authentication
A registered SPN for the server host (e.g. HTTP/myserver.corp.local) Kerberos authentication
A certificate imported into the Windows certificate store GetWin32Cert / GetCertificateFunc
LDAP-reachable domain controller + a service account SPN NewLDAPProvider

Installation

go get github.com/akennis/gwim

Quick Start

The snippet below is the smallest possible secure server using gwim. It retrieves a TLS certificate from the Windows certificate store, wraps a handler with Kerberos authentication, and starts listening. Error handling is omitted for brevity.

package main

import (
    "crypto/tls"
    "log"
    "net/http"

    "github.com/akennis/gwim"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        username, _ := gwim.User(r)
        w.Write([]byte("Hello, " + username))
    })

    // Create a Kerberos authentication provider
    sspiProvider, _ := gwim.NewSSPIProvider()
    defer sspiProvider.Close()

    // Retrieve a TLS certificate from the Windows certificate store
    certSource, _ := gwim.GetWin32Cert("myserver.corp.local", gwim.CertStoreLocalMachine)
    defer certSource.Close()

    srv := &http.Server{
        Addr:    ":8443",
        Handler: sspiProvider.Middleware(mux),
        TLSConfig: &tls.Config{
            Certificates: []tls.Certificate{certSource.Certificate},
        },
    }

    // Windows authenticates the current domain user transparently —
    // no login prompt, no credentials to manage.
    log.Fatal(srv.ListenAndServeTLS("", ""))
}

How It Works

Requests flow through the middleware chain in the following order:

Client Request
  │
  ▼
SSPI Middleware (Kerberos or NTLM negotiation)
  │  ── sets the authenticated username in the request context
  ▼
LDAP Middleware (optional)
  │  ── looks up the user's group memberships and adds them to the context
  ▼
Your Application Handler
  │  ── reads username via gwim.User(r)
  │  ── reads groups  via gwim.UserGroups(r)

[!IMPORTANT] Middleware Order: SSPIProvider must always be applied before LDAPProvider (i.e. SSPIProvider should be the outermost middleware). This ensures the user's identity is established in the request context before the LDAP provider attempts to look up their group memberships.

Usage Examples

See the examples directory for complete, runnable servers:

  • Minimal secure server — TLS + Kerberos/NTLM authentication with optional LDAP group lookup, in under 200 lines.
  • Session-enabled secure server — adds session management and caching so that authentication and LDAP lookups happen once per session rather than on every request, with graceful shutdown and zero-downtime certificate rotation.

API

Authentication

gwim uses the Middleware Factory Pattern: create a provider once at startup, then register its .Middleware method with any router's Use() call or use it to wrap handlers manually. The .Middleware method satisfies the standard func(http.Handler) http.Handler signature, making it compatible with any router that supports standard Go middleware.

NewSSPIProvider
sspiProvider, err := gwim.NewSSPIProvider(opts ...SSPIOption) (*SSPIProvider, error)

Acquires Windows SSPI credentials and returns a provider. Any credential acquisition error is surfaced here at startup rather than on the first request.

// Kerberos (production default)
sspiProvider, err := gwim.NewSSPIProvider()

// NTLM (local / non-domain development)
sspiProvider, err := gwim.NewSSPIProvider(gwim.WithNTLM())

// With custom error handling
sspiProvider, err := gwim.NewSSPIProvider(
    gwim.WithNTLM(),
    gwim.WithSSPIErrorHandlers(gwim.AuthErrorHandlers{
        OnGeneralError: myErrorHandler,
    }),
)

Use the provider:

// Standard net/http
handler := sspiProvider.Middleware(mux)

// Any router with Use()
router.Use(sspiProvider.Middleware)

SSPIOption functions:

Option Description
WithNTLM() Use NTLM instead of Kerberos. Required for non-domain or localhost scenarios.
WithSSPIErrorHandlers(h AuthErrorHandlers) Override default error responses.

Lifecycle: Call sspiProvider.Close() on server shutdown to release Windows credential handles.

[!NOTE] Kerberos should be used in production as it is significantly more secure. NTLM support is included only to facilitate local development where the developer is hitting the server from a browser on the same host (a scenario where Kerberos loopback authentication does not work).

ConfigureNTLM
gwim.ConfigureNTLM(server *http.Server)

Configures the http.Server with the ConnContext callback required for NTLM connection tracking. NTLM is connection-oriented — each TCP connection carries its own authentication state. This function assigns a unique ID to every connection so the NTLM handler can correlate the two-step token exchange across requests on the same keep-alive connection. Only required when using NTLM; not needed for Kerberos.

LDAP Group Authorization
NewLDAPProvider
ldapProvider, err := gwim.NewLDAPProvider(opts ...LDAPOption) (*LDAPProvider, error)

Returns a provider that enriches an authenticated request's context with the user's Active Directory group memberships (transitively, via the tokenGroups attribute). Groups are returned as LDAP Distinguished Names (DNs), e.g. CN=AppAdmins,OU=Groups,DC=corp,DC=local.

Startup validation: NewLDAPProvider performs a synchronous connectivity check before returning. It opens a TLS (LDAPS) connection to the configured address, authenticates via GSSAPI/Kerberos bind using the server's current-user credentials, and executes a RootDSE search. If any step fails, an error is returned here at startup rather than on the first request.

At runtime, the provider maintains an internal connection pool (capacity: 10). On each request a pooled connection is health-checked with a lightweight RootDSE probe before use; connections that fail the probe or exceed their TTL are discarded and a new one is created.

ldapProvider, err := gwim.NewLDAPProvider(
    gwim.WithLDAPAddress("dc01.corp.local:636"),
    gwim.WithLDAPUsersDN("OU=Users,DC=corp,DC=local"),
    gwim.WithLDAPServiceAccountSPN("LDAP/DC1.corp.local"),
)
if err != nil {
    log.Fatalf("failed to create LDAP provider: %v", err)
}

// Wrap the LDAP-wrapped handler with SSPI (SSPI runs first)
handler = sspiProvider.Middleware(ldapProvider.Middleware(mux))

// Or with a router
router.Use(sspiProvider.Middleware)
router.Use(ldapProvider.Middleware)

LDAPOption functions:

Option Description
WithLDAPAddress(addr string) Host and port of the LDAP / domain controller (LDAPS is always used)
WithLDAPUsersDN(dn string) Distinguished Name of the OU containing user accounts
WithLDAPServiceAccountSPN(spn string) SPN of the LDAP service account used for GSSAPI bind
WithLDAPTimeout(d time.Duration) Per-operation timeout; defaults to DefaultLdapTimeout (5s)
WithLDAPConnectionTTL(d time.Duration) Max lifetime of a pooled connection; defaults to DefaultLdapTTL (1h)
WithLDAPErrorHandlers(h AuthErrorHandlers) Override default error responses

Lifecycle: Call ldapProvider.Close() on server shutdown to drain the connection pool and release Windows credentials used for the LDAP bind.

Request Context Helpers
  • User(r *http.Request) (string, bool) — Returns the authenticated username from the request context.
  • SetUser(r *http.Request, username string) *http.Request — Injects a username into the request context. If a username is already present when the SSPI middleware runs, authentication is skipped — use this to restore a session without re-running SSPI.
  • UserGroups(r *http.Request) ([]string, bool) — Returns the user's group memberships from the request context as LDAP Distinguished Names (DNs), e.g. CN=AppAdmins,OU=Groups,DC=corp,DC=local.
  • SetUserGroups(r *http.Request, groups []string) *http.Request — Injects group memberships into the request context. If groups are already present when the LDAP middleware runs, the LDAP lookup is skipped — use this to restore cached groups from a session.

Use SetUser and SetUserGroups together to restore a previously authenticated identity from a session store, avoiding re-authentication and LDAP lookups on every request:

// In your session middleware, before the SSPI middleware runs:
if sessionUser, ok := getSession(r); ok {
    r = gwim.SetUser(r, sessionUser)
    r = gwim.SetUserGroups(r, sessionUser.Groups)
}

See sec-win-server/main.go for a full working example of this pattern.

TLS Certificate
  • GetWin32Cert(certSubject string, store CertStore) (*CertificateSource, error) — Retrieves a TLS certificate from the Windows certificate store by Common Name. Use CertStoreLocalMachine or CertStoreCurrentUser for store. The certificate is validated before being returned: it must not be expired, must carry ExtKeyUsageServerAuth, and its full issuer chain must pass verification via the Windows CryptoAPI. The returned CertificateSource.Certificate is a tls.Certificate ready for use in tls.Config.Certificates. Call Close() on the source when it is no longer needed (e.g. on server shutdown) to release Windows store handles.

  • GetCertificateFunc(certSubject string, store CertStore, refreshThreshold, retryInterval time.Duration) (func(*tls.ClientHelloInfo) (*tls.Certificate, error), io.Closer, error) — Like GetWin32Cert but returns a tls.Config.GetCertificate callback that transparently refreshes the certificate in the background when it is within refreshThreshold of expiry, enabling zero-downtime rotation. Pass DefaultRefreshThreshold and DefaultRetryInterval for standard values. Call Close() on the returned io.Closer after http.Server.Shutdown returns.

Error Handling (AuthErrorHandlers)

Both WithSSPIErrorHandlers and WithLDAPErrorHandlers accept an AuthErrorHandlers struct to customize error responses. Each field is an AuthErrorHandler func(w http.ResponseWriter, r *http.Request, err error):

Field Triggered when…
OnUnauthorized The Authorization header is missing or invalid
OnInvalidToken The base64 token from the client is malformed
OnAuthFailed An error occurs during the SSPI/GSSAPI token exchange
OnIdentityError The username cannot be retrieved after successful auth
OnLdapConnectionError A connection to the LDAP server cannot be established
OnLdapLookupError An error occurs during an LDAP search or lookup
OnGeneralError Catch-all: fills in for any of the above handlers that is not explicitly set

If no options are provided, sensible defaults are used (plain-text HTTP error responses).

Requests do not progress to the next request hander in the chain once an error has occurred. The error handler is executed and control is sent back up the request hander chain (i.e. the earlier handlers).

Integration Testing

The integration_tests package contains client/server tests for both NTLM and Kerberos authentication.

Running NTLM Tests

NTLM tests can be run locally by spawning the test server and the test runner on the same machine.

  1. Build the test server:
    go build -o testserver.exe ./integration_tests/cmd/testserver/main.go
    
  2. Run the test server:
    .\testserver.exe --addr 127.0.0.1:8080 --use-ntlm=true
    
  3. Run the tests:
    go test -tags=integration -v ./integration_tests -server-url http://127.0.0.1:8080 -auth-mode ntlm
    
Running Kerberos Tests

Kerberos tests require the test server and the test runner to be on separate machines within the same Active Directory domain. This is because Windows handles local Kerberos authentication (loopback) differently than remote authentication.

  1. Deploy and run testserver.exe on Machine A (the "server").
  2. Run the tests from Machine B (the "client") pointing to Machine A:
    go test -tags=integration -v ./integration_tests -server-url http://<machine-a-hostname>:8080 -auth-mode kerberos
    

Code Coverage

gwim supports code coverage collection for out-of-process integration tests using Go 1.22's -cover instrumentation.

  1. Build an instrumented test server:
    go build -cover -o testserver.exe ./integration_tests/cmd/testserver/main.go
    
  2. Run the server with GOCOVERDIR set to an output directory:
    mkdir coverage_data
    $env:GOCOVERDIR="coverage_data"
    .\testserver.exe --addr 127.0.0.1:8080 --use-ntlm=true
    
  3. Run your integration tests as usual.
  4. Stop the server (Ctrl+C). The server will gracefully shut down and flush coverage data to the coverage_data directory.
  5. View the coverage percentage:
    go tool covdata percent -i=coverage_data
    
  6. Generate an HTML report:
    go tool covdata textfmt -i=coverage_data -o coverage.out
    go tool cover '-html=coverage.out'
    

License

This project is licensed under the BSD 3-Clause License — see the LICENSE file for details.

Documentation

Rendered for windows/amd64

Index

Constants

View Source
const (
	// CertStoreLocalMachine searches the LocalMachine certificate store (default).
	CertStoreLocalMachine CertStore = icert.StoreLocalMachine
	// CertStoreCurrentUser searches the CurrentUser certificate store.
	CertStoreCurrentUser CertStore = icert.StoreCurrentUser

	// DefaultRefreshThreshold is the window before certificate expiry at which
	// GetCertificateFunc triggers a background refresh. Pass this value to
	// GetCertificateFunc when you do not need a custom refresh window.
	DefaultRefreshThreshold = 7 * 24 * time.Hour

	// DefaultRetryInterval is the minimum time between background refresh
	// attempts. If a refresh fails (e.g. the renewed certificate is not yet in
	// the store), subsequent requests within the refresh window are served from
	// the cache without spawning new goroutines until this interval elapses.
	DefaultRetryInterval = 5 * time.Minute

	// DefaultLdapTimeout is the per-operation timeout applied to every LDAP
	// call (searches, health-check probes, etc.). In a corporate Active
	// Directory environment LDAP round-trips are typically sub-100 ms; five
	// seconds is generous while still failing fast against a hung server.
	DefaultLdapTimeout = 5 * time.Second

	// DefaultLdapTTL is the default maximum lifetime for a pooled LDAP connection.
	// In Active Directory, Kerberos tickets typically expire after 10 hours.
	// Rotating connections every 1 hour ensures they never encounter an expired ticket.
	DefaultLdapTTL = 1 * time.Hour
)

Variables

This section is empty.

Functions

func ConfigureNTLM

func ConfigureNTLM(server *http.Server)

ConfigureNTLM sets the ConnContext on server so that each connection is assigned a unique ID. This ID is required by the NTLM handler to correlate the two-round token exchange across separate HTTP requests on the same keep-alive connection. Only required when using NTLM authentication.

func GetCertificateFunc

func GetCertificateFunc(certSubject string, store CertStore, refreshThreshold, retryInterval time.Duration) (func(*tls.ClientHelloInfo) (*tls.Certificate, error), io.Closer, error)

GetCertificateFunc fetches the named certificate from the Windows store immediately — surfacing any configuration error at startup rather than on the first TLS handshake — and returns a tls.Config.GetCertificate callback that transparently refreshes the certificate in a background goroutine when it is within refreshThreshold of expiry, enabling zero-downtime rotation. Pass DefaultRefreshThreshold for the standard 7-day window.

retryInterval is the minimum time between background refresh attempts. If the store is temporarily unavailable (e.g. the renewed certificate has not been deployed yet), requests that arrive within the refresh window would otherwise each spawn a new goroutine. retryInterval rate-limits that behaviour so that at most one attempt runs per interval. Pass DefaultRetryInterval for the standard 5-minute window.

The returned io.Closer releases the Windows store handles for the currently-cached certificate. Call it after http.Server.Shutdown returns to ensure all active connections have already finished.

func SetUser

func SetUser(r *http.Request, username string) *http.Request

SetUser injects a username into the request context, normalising it first. Use this to resume a session without re-running SSPI authentication. An empty username is stored as-is; User(r) will return ("", false) for it since empty strings are treated as "no authenticated user."

func SetUserGroups

func SetUserGroups(r *http.Request, groups []string) *http.Request

SetUserGroups injects group memberships into the request context. Use this to resume a session with cached groups without re-running LDAP.

func User

func User(r *http.Request) (string, bool)

User returns the authenticated username from the request context. The second return value is false if no user has been set.

func UserGroups

func UserGroups(r *http.Request) ([]string, bool)

UserGroups returns the authenticated user's group memberships from the request context. The second return value is false if no groups are present. After the LDAP middleware runs, it returns ([]string{}, true) for users with no group memberships, distinguishing "no groups" from "LDAP didn't run."

Types

type AuthErrorHandler

type AuthErrorHandler = iauth.AuthErrorHandler

AuthErrorHandler is a function type for handling an authentication or authorisation error. Assign one to any field of AuthErrorHandlers to override the default behaviour for that specific error category.

type AuthErrorHandlers

type AuthErrorHandlers = iauth.AuthErrorHandlers

AuthErrorHandlers configures the error-handling behaviour of the authentication middleware. Pass one to WithSSPIErrorHandlers or WithLDAPErrorHandlers. Any field left nil falls back to the built-in default for that category; set OnGeneralError as a single catch-all.

type CertStore

type CertStore = icert.CertStore

CertStore identifies which Windows certificate store to search. Use CertStoreLocalMachine or CertStoreCurrentUser.

type CertificateSource

type CertificateSource = icert.CertificateSource

CertificateSource holds a TLS certificate retrieved from the Windows store. Call Close when the certificate is no longer needed (e.g. on server shutdown).

func GetWin32Cert

func GetWin32Cert(subject string, store CertStore) (*CertificateSource, error)

GetWin32Cert retrieves a certificate from the Windows certificate store by Common Name and returns a CertificateSource. The certificate is validated before being returned: it must not be expired and must carry the ExtKeyUsageServerAuth extended key usage.

The caller must call Close on the returned CertificateSource when it is no longer needed to release Windows store handles.

For servers that need zero-downtime certificate rotation, use GetCertificateFunc instead.

type LDAPOption added in v0.2.0

type LDAPOption func(*ldapConfig)

LDAPOption configures an LDAPProvider.

func WithLDAPAddress added in v0.2.0

func WithLDAPAddress(addr string) LDAPOption

WithLDAPAddress sets the address of the LDAP server (host:port).

func WithLDAPConnectionTTL added in v0.2.0

func WithLDAPConnectionTTL(d time.Duration) LDAPOption

WithLDAPConnectionTTL sets the maximum lifetime of a pooled LDAP connection. This prevents stale Kerberos tickets from causing failures on long-lived connections. Zero disables the TTL.

func WithLDAPErrorHandlers added in v0.2.0

func WithLDAPErrorHandlers(h AuthErrorHandlers) LDAPOption

WithLDAPErrorHandlers overrides the default error-handling behaviour of the LDAP middleware. Any field left nil falls back to the built-in default.

func WithLDAPServiceAccountSPN added in v0.2.0

func WithLDAPServiceAccountSPN(spn string) LDAPOption

WithLDAPServiceAccountSPN sets the Service Principal Name of the account used to bind to the LDAP server via GSSAPI/Kerberos.

func WithLDAPTimeout added in v0.2.0

func WithLDAPTimeout(d time.Duration) LDAPOption

WithLDAPTimeout sets the per-operation timeout applied to every LDAP call on each connection (searches, health-check probes, etc.). Zero is treated as DefaultLdapTimeout.

func WithLDAPUsersDN added in v0.2.0

func WithLDAPUsersDN(dn string) LDAPOption

WithLDAPUsersDN sets the Distinguished Name under which users are searched.

type LDAPProvider added in v0.2.0

type LDAPProvider struct {
	// contains filtered or unexported fields
}

LDAPProvider enriches an authenticated request's context with the user's Active Directory group memberships. Create one with NewLDAPProvider, then register its Middleware method with your router or wrap handlers manually. It must be placed after SSPIProvider in the middleware chain.

func NewLDAPProvider added in v0.2.0

func NewLDAPProvider(opts ...LDAPOption) (*LDAPProvider, error)

NewLDAPProvider returns an LDAPProvider configured by the given options. LDAP connections are established lazily per request after initial validation.

func (*LDAPProvider) Close added in v0.2.0

func (p *LDAPProvider) Close() error

Close drains the LDAP connection pool, closing all idle connections. Call this on server shutdown after the HTTP server has stopped accepting new requests.

func (*LDAPProvider) Middleware added in v0.2.0

func (p *LDAPProvider) Middleware(next http.Handler) http.Handler

Middleware satisfies func(http.Handler) http.Handler and can be passed directly to any router's Use() method or used to wrap a handler manually:

router.Use(ldapProvider.Middleware)
handler := ldapProvider.Middleware(myHandler)

type SSPIOption added in v0.2.0

type SSPIOption func(*sspiConfig)

SSPIOption configures an SSPIProvider.

func WithNTLM added in v0.2.0

func WithNTLM() SSPIOption

WithNTLM configures the SSPIProvider to use NTLM instead of Kerberos. Required for non-domain or localhost scenarios.

func WithSSPIErrorHandlers added in v0.2.0

func WithSSPIErrorHandlers(h AuthErrorHandlers) SSPIOption

WithSSPIErrorHandlers overrides the default error-handling behaviour of the SSPI middleware. Any field left nil falls back to the built-in default.

type SSPIProvider added in v0.2.0

type SSPIProvider struct {
	// contains filtered or unexported fields
}

SSPIProvider authenticates requests using Windows SSPI (Kerberos or NTLM). Create one with NewSSPIProvider, then register its Middleware method with your router's Use() method or wrap handlers manually.

func NewSSPIProvider added in v0.2.0

func NewSSPIProvider(opts ...SSPIOption) (*SSPIProvider, error)

NewSSPIProvider acquires the required Windows SSPI credentials and returns a provider whose Middleware method satisfies func(http.Handler) http.Handler. Credential acquisition happens once here so that any configuration error is surfaced at startup rather than on the first request.

func (*SSPIProvider) Close added in v0.2.0

func (p *SSPIProvider) Close() error

Close releases the Windows SSPI credentials held by this provider. Call this on server shutdown.

func (*SSPIProvider) Middleware added in v0.2.0

func (p *SSPIProvider) Middleware(next http.Handler) http.Handler

Middleware satisfies func(http.Handler) http.Handler and can be passed directly to any router's Use() method or used to wrap a handler manually:

router.Use(sspiProvider.Middleware)
handler := sspiProvider.Middleware(myHandler)

Directories

Path Synopsis
examples
min-win-server command
quick-start command
sec-win-server command
integration_tests
cmd/testserver command
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL