gwim

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Mar 31, 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. gwim removes that requirement. It wraps any http.Handler with 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 .exe.

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 NewLdapGroupProvider

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))
    })

    // Wrap the handler with Kerberos authentication (useNTLM = false)
    handler, _ := gwim.NewSSPIHandler(mux, false)

    // 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: handler,
        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 Handler (Kerberos or NTLM negotiation)
  │  ── sets the authenticated username in the request context
  ▼
LDAP Group Provider (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 must be applied in reverse execution order. Wrap your router with the LDAP provider first, then wrap the result with the SSPI handler. The LDAP provider depends on the username that the SSPI handler places in the context.

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
  • NewSSPIHandler(next http.Handler, useNTLM bool, options ...AuthErrorHandlers) (http.Handler, error) — Creates a Windows native authentication middleware that wraps an existing http.Handler. The useNTLM boolean selects NTLM or Kerberos. Optional AuthErrorHandlers allow customizing error responses. Returns an error if Windows SSPI credentials cannot be acquired (e.g. the Negotiate security package is unavailable). On success, the authenticated username is stored in the request context and readable via gwim.User(r). On failure, the appropriate error handler is invoked (default: 401 Unauthorized).

[!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(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
  • NewLdapGroupProvider(next http.Handler, ldapAddress, ldapUsersDN, ldapServiceAccountSPN string, ldapTimeout, ldapTTL time.Duration, options ...AuthErrorHandlers) http.Handler — Returns a middleware that enriches the request context with the authenticated user's LDAP group memberships (transitively).

    Parameter Example Description
    ldapAddress dc01.corp.local:636 Host and port of the LDAP / domain controller (LDAPS is always used)
    ldapUsersDN OU=Users,DC=corp,DC=local Distinguished Name of the OU containing user accounts
    ldapServiceAccountSPN HTTP/myserver.corp.local SPN of the LDAP server
    ldapTimeout gwim.DefaultLdapTimeout Per-operation timeout for every LDAP call; pass gwim.DefaultLdapTimeout for the standard 5-second value
    ldapTTL gwim.DefaultLdapTTL Maximum lifetime of a connection to prevent stale Kerberos tickets; pass gwim.DefaultLdapTTL for 1 hour
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 handler 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.
  • SetUserGroups(r *http.Request, groups []string) *http.Request — Injects group memberships into the request context. If groups are already present when the LDAP provider 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 handler runs:
if sessionUser, ok := getSession(r); ok {
    r = gwim.SetUser(r, sessionUser)
    r = gwim.SetUserGroups(r, sessionUser.Groups)
}

See sec-win-server.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 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 NewSSPIHandler and NewLdapGroupProvider accept a variadic 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).

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 -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 -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.
	// Pass this value to NewLdapGroupProvider when you do not need a custom
	// timeout.
	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.
	// Pass this value to NewLdapGroupProvider when you do not need a custom TTL.
	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 NewLdapGroupProvider

func NewLdapGroupProvider(next http.Handler, ldapAddress, ldapUsersDN, ldapServiceAccountSPN string, ldapTimeout, ldapTTL time.Duration, options ...AuthErrorHandlers) http.Handler

NewLdapGroupProvider returns an http.Handler that looks up the authenticated user's Active Directory group memberships via LDAP and stores them in the request context, then delegates to next.

ldapTimeout is applied to every LDAP operation on each connection (searches, health-check probes, and the initial GSSAPI bind). Zero disables the timeout. Pass DefaultLdapTimeout when you do not need a custom value.

ldapTTL is the maximum lifetime of a pooled LDAP connection. This prevents stale Kerberos tickets from causing authentication failures on long-lived connections. Pass DefaultLdapTTL for a standard 1-hour lifetime. Zero disables the TTL.

func NewSSPIHandler

func NewSSPIHandler(next http.Handler, useNTLM bool, options ...AuthErrorHandlers) (http.Handler, error)

NewSSPIHandler returns an http.Handler that authenticates each request using Kerberos (useNTLM=false) or NTLM (useNTLM=true) via Windows SSPI, then delegates to next. Pass an AuthErrorHandlers value to customise error responses; any unset fields fall back to sensible defaults.

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.

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.

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 as a variadic option to NewSSPIHandler or NewLdapGroupProvider. 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.

Directories

Path Synopsis
cmd/testserver command
internal

Jump to

Keyboard shortcuts

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