diff options
author | QuaSoft <info@quasoft.net> | 2019-11-23 01:33:31 +0200 |
---|---|---|
committer | Lauris BH <lauris@nix.lv> | 2019-11-23 01:33:31 +0200 |
commit | 7b4d2f7a2aa3af093571628f979bdc939f10890c (patch) | |
tree | f9abd74c01d006892f5474557cb545e4f60dfe71 /vendor/github.com/quasoft | |
parent | eb1b225d9a920e68ce415b9732b4ec1d9527a2a2 (diff) | |
download | gitea-7b4d2f7a2aa3af093571628f979bdc939f10890c.tar.gz gitea-7b4d2f7a2aa3af093571628f979bdc939f10890c.zip |
Add single sign-on support via SSPI on Windows (#8463)
* Add single sign-on support via SSPI on Windows
* Ensure plugins implement interface
* Ensure plugins implement interface
* Move functions used only by the SSPI auth method to sspi_windows.go
* Field SSPISeparatorReplacement of AuthenticationForm should not be required via binding, as binding will insist the field is non-empty even if another login type is selected
* Fix breaking of oauth authentication on download links. Do not create new session with SSPI authentication on download links.
* Update documentation for the new 'SPNEGO with SSPI' login source
* Mention in documentation that ROOT_URL should contain the FQDN of the server
* Make sure that Contexter is not checking for active login sources when the ORM engine is not initialized (eg. when installing)
* Always initialize and free SSO methods, even if they are not enabled, as a method can be activated while the app is running (from Authentication sources)
* Add option in SSPIConfig for removing of domains from logon names
* Update helper text for StripDomainNames option
* Make sure handleSignIn() is called after a new user object is created by SSPI auth method
* Remove default value from text of form field helper
Co-Authored-By: Lauris BH <lauris@nix.lv>
* Remove default value from text of form field helper
Co-Authored-By: Lauris BH <lauris@nix.lv>
* Remove default value from text of form field helper
Co-Authored-By: Lauris BH <lauris@nix.lv>
* Only make a query to the DB to check if SSPI is enabled on handlers that need that information for templates
* Remove code duplication
* Log errors in ActiveLoginSources
Co-Authored-By: Lauris BH <lauris@nix.lv>
* Revert suffix of randomly generated E-mails for Reverse proxy authentication
Co-Authored-By: Lauris BH <lauris@nix.lv>
* Revert unneeded white-space change in template
Co-Authored-By: Lauris BH <lauris@nix.lv>
* Add copyright comments at the top of new files
* Use loopback name for randomly generated emails
* Add locale tag for the SSPISeparatorReplacement field with proper casing
* Revert casing of SSPISeparatorReplacement field in locale file, moving it up, next to other form fields
* Update docs/content/doc/features/authentication.en-us.md
Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>
* Remove Priority() method and define the order in which SSO auth methods should be executed in one place
* Log authenticated username only if it's not empty
* Rephrase helper text for automatic creation of users
* Return error if more than one active SSPI auth source is found
* Change newUser() function to return error, letting caller log/handle the error
* Move isPublicResource, isPublicPage and handleSignIn functions outside SSPI auth method to allow other SSO methods to reuse them if needed
* Refactor initialization of the list containing SSO auth methods
* Validate SSPI settings on POST
* Change SSPI to only perform authentication on its own login page, API paths and download links. Leave Toggle middleware to redirect non authenticated users to login page
* Make 'Default language' in SSPI config empty, unless changed by admin
* Show error if admin tries to add a second authentication source of type SSPI
* Simplify declaration of global variable
* Rebuild gitgraph.js on Linux
* Make sure config values containing only whitespace are not accepted
Diffstat (limited to 'vendor/github.com/quasoft')
-rw-r--r-- | vendor/github.com/quasoft/websspi/.gitignore | 12 | ||||
-rw-r--r-- | vendor/github.com/quasoft/websspi/.travis.yml | 13 | ||||
-rw-r--r-- | vendor/github.com/quasoft/websspi/LICENSE | 21 | ||||
-rw-r--r-- | vendor/github.com/quasoft/websspi/README.md | 41 | ||||
-rw-r--r-- | vendor/github.com/quasoft/websspi/go.mod | 9 | ||||
-rw-r--r-- | vendor/github.com/quasoft/websspi/go.sum | 6 | ||||
-rw-r--r-- | vendor/github.com/quasoft/websspi/secctx/session.go | 36 | ||||
-rw-r--r-- | vendor/github.com/quasoft/websspi/secctx/store.go | 13 | ||||
-rw-r--r-- | vendor/github.com/quasoft/websspi/userinfo.go | 7 | ||||
-rw-r--r-- | vendor/github.com/quasoft/websspi/utf16.go | 22 | ||||
-rw-r--r-- | vendor/github.com/quasoft/websspi/websspi_windows.go | 615 | ||||
-rw-r--r-- | vendor/github.com/quasoft/websspi/win32_windows.go | 312 |
12 files changed, 1107 insertions, 0 deletions
diff --git a/vendor/github.com/quasoft/websspi/.gitignore b/vendor/github.com/quasoft/websspi/.gitignore new file mode 100644 index 0000000000..f1c181ec9c --- /dev/null +++ b/vendor/github.com/quasoft/websspi/.gitignore @@ -0,0 +1,12 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out diff --git a/vendor/github.com/quasoft/websspi/.travis.yml b/vendor/github.com/quasoft/websspi/.travis.yml new file mode 100644 index 0000000000..1aaa667e90 --- /dev/null +++ b/vendor/github.com/quasoft/websspi/.travis.yml @@ -0,0 +1,13 @@ +jobs: + include: + - os: windows + script: $GOPATH/bin/goveralls -service=travis-ci + language: go + sudo: false + go: 1.13.x + before_install: go get github.com/mattn/goveralls + - os: linux + script: go build + language: go + sudo: false + go: 1.13.x diff --git a/vendor/github.com/quasoft/websspi/LICENSE b/vendor/github.com/quasoft/websspi/LICENSE new file mode 100644 index 0000000000..e1520f7d6d --- /dev/null +++ b/vendor/github.com/quasoft/websspi/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 QuaSoft + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/quasoft/websspi/README.md b/vendor/github.com/quasoft/websspi/README.md new file mode 100644 index 0000000000..4f7b12167b --- /dev/null +++ b/vendor/github.com/quasoft/websspi/README.md @@ -0,0 +1,41 @@ +# websspi + +[![GoDoc](https://godoc.org/github.com/quasoft/websspi?status.svg)](https://godoc.org/github.com/quasoft/websspi) [![Build Status](https://travis-ci.org/quasoft/websspi.png?branch=master)](https://travis-ci.org/quasoft/websspi) [![Coverage Status](https://coveralls.io/repos/github/quasoft/websspi/badge.svg?branch=master)](https://coveralls.io/github/quasoft/websspi?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/quasoft/websspi)](https://goreportcard.com/report/github.com/quasoft/websspi) + +`websspi` will be an HTTP middleware for Golang that uses Kerberos for single sign-on (SSO) authentication of browser based clients in a Windows environment. + +The main goal is to create a middleware that performs authentication of HTTP requests without the need to create or use keytab files. + +The middleware will implement the scheme defined by RFC4559 (SPNEGO-based HTTP Authentication in Microsoft Windows) to exchange security tokens via HTTP headers and will use SSPI (Security Support Provider Interface) to authenticate HTTP requests. + +## How to use + +The examples directory contains a simple web server that demonstrates how to use the package. +Before trying it, you need to prepare your environment: + +1. Create a separate user account in active directory, under which the web server process will be running (eg. `user` under the `domain.local` domain) + +2. Create a service principal name for the host with class HTTP: + - Start Command prompt or PowerShell as domain administrator + - Run the command below, replacing `host.domain.local` with the fully qualified domain name of the server where the web application will be running, and `domain\user` with the name of the account created in step 1.: + + setspn -A HTTP/host.domain.local domain\user + +3. Start the web server app under the account created in step 1. + +4. If you are using Chrome, Edge or Internet Explorer, add the URL of the web app to the Local intranet sites (`Internet Options -> Security -> Local intranet -> Sites`) + +5. Start Chrome, Edge or Internet Explorer and navigate to the URL of the web app (eg. `http://host.domain.local:9000`) + +6. The web app should greet you with the name of your AD account without asking you to login. In case it doesn't, make sure that: + + - You are not running the web browser on the same server where the web app is running. You should be running the web browser on a domain joined computer (client) that is different from the server + - There is only one HTTP/... SPN for the host + - The SPN contains only the hostname, without the port + - You have added the URL of the web app to the `Local intranet` zone + - The clocks of the server and client should not differ with more than 5 minutes + - `Integrated Windows Authentication` should be enabled in Internet Explorer (under `Advanced settings`) + +## Security requirements + +- SPNEGO HTTP provides no facilities for protecting the HTTP headers or data including the Authorization and WWW-Authenticate headers, which means that the HTTP server **MUST** enforce use of SSL to provide confidentiality to data in these headers! diff --git a/vendor/github.com/quasoft/websspi/go.mod b/vendor/github.com/quasoft/websspi/go.mod new file mode 100644 index 0000000000..0b53f166a4 --- /dev/null +++ b/vendor/github.com/quasoft/websspi/go.mod @@ -0,0 +1,9 @@ +module github.com/quasoft/websspi + +go 1.13 + +require ( + github.com/gorilla/securecookie v1.1.1 + github.com/gorilla/sessions v1.2.0 + golang.org/x/sys v0.0.0-20191010194322-b09406accb47 +) diff --git a/vendor/github.com/quasoft/websspi/go.sum b/vendor/github.com/quasoft/websspi/go.sum new file mode 100644 index 0000000000..d6b6b7d000 --- /dev/null +++ b/vendor/github.com/quasoft/websspi/go.sum @@ -0,0 +1,6 @@ +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ= +github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/vendor/github.com/quasoft/websspi/secctx/session.go b/vendor/github.com/quasoft/websspi/secctx/session.go new file mode 100644 index 0000000000..6f8ce29c42 --- /dev/null +++ b/vendor/github.com/quasoft/websspi/secctx/session.go @@ -0,0 +1,36 @@ +package secctx + +import ( + "net/http" + + "github.com/gorilla/securecookie" + "github.com/gorilla/sessions" +) + +// CookieStore can store and retrieve SSPI context handles to/from an encrypted Cookie. +type CookieStore struct { + store *sessions.CookieStore +} + +// NewCookieStore creates a new CookieStore for storing and retrieving of SSPI context handles +// to/from encrypted Cookies +func NewCookieStore() *CookieStore { + s := &CookieStore{} + s.store = sessions.NewCookieStore([]byte(securecookie.GenerateRandomKey(32))) + return s +} + +// GetHandle retrieves a *websspi.CtxtHandle value from the store +func (s *CookieStore) GetHandle(r *http.Request) (interface{}, error) { + session, _ := s.store.Get(r, "websspi") + contextHandle := session.Values["contextHandle"] + return contextHandle, nil +} + +// SetHandle saves a *websspi.CtxtHandle value to the store +func (s *CookieStore) SetHandle(r *http.Request, w http.ResponseWriter, contextHandle interface{}) error { + session, _ := s.store.Get(r, "websspi") + session.Values["contextHandle"] = contextHandle + err := session.Save(r, w) + return err +} diff --git a/vendor/github.com/quasoft/websspi/secctx/store.go b/vendor/github.com/quasoft/websspi/secctx/store.go new file mode 100644 index 0000000000..9c336c3097 --- /dev/null +++ b/vendor/github.com/quasoft/websspi/secctx/store.go @@ -0,0 +1,13 @@ +package secctx + +import "net/http" + +// Store is an interface for storage of SSPI context handles. +// SSPI context handles are Windows API handles and have nothing to do +// with the "context" package in Go. +type Store interface { + // GetHandle retrieves a *websspi.CtxtHandle value from the store + GetHandle(r *http.Request) (interface{}, error) + // SetHandle saves a *websspi.CtxtHandle value to the store + SetHandle(r *http.Request, w http.ResponseWriter, contextHandle interface{}) error +} diff --git a/vendor/github.com/quasoft/websspi/userinfo.go b/vendor/github.com/quasoft/websspi/userinfo.go new file mode 100644 index 0000000000..359bfb5c11 --- /dev/null +++ b/vendor/github.com/quasoft/websspi/userinfo.go @@ -0,0 +1,7 @@ +package websspi + +// UserInfo represents an authenticated user. +type UserInfo struct { + Username string // Name of user, usually in the form DOMAIN\User + Groups []string // The global groups the user is a member of +} diff --git a/vendor/github.com/quasoft/websspi/utf16.go b/vendor/github.com/quasoft/websspi/utf16.go new file mode 100644 index 0000000000..7c85c883ed --- /dev/null +++ b/vendor/github.com/quasoft/websspi/utf16.go @@ -0,0 +1,22 @@ +package websspi + +import ( + "unicode/utf16" + "unsafe" +) + +// UTF16PtrToString converts a pointer to a UTF16 string to a string +func UTF16PtrToString(ptr *uint16, maxLen int) (s string) { + if ptr == nil { + return "" + } + buf := make([]uint16, 0, maxLen) + for i, p := 0, uintptr(unsafe.Pointer(ptr)); i < maxLen; i, p = i+1, p+2 { + char := *(*uint16)(unsafe.Pointer(p)) + if char == 0 { + return string(utf16.Decode(buf)) + } + buf = append(buf, char) + } + return "" +} diff --git a/vendor/github.com/quasoft/websspi/websspi_windows.go b/vendor/github.com/quasoft/websspi/websspi_windows.go new file mode 100644 index 0000000000..1605a6cbcc --- /dev/null +++ b/vendor/github.com/quasoft/websspi/websspi_windows.go @@ -0,0 +1,615 @@ +package websspi + +import ( + "context" + "encoding/base64" + "encoding/gob" + "errors" + "fmt" + "log" + "net/http" + "strings" + "sync" + "syscall" + "time" + "unsafe" + + "github.com/quasoft/websspi/secctx" +) + +// The Config object determines the behaviour of the Authenticator. +type Config struct { + contextStore secctx.Store + authAPI API + KrbPrincipal string // Name of Kerberos principle used by the service (optional). + AuthUserKey string // Key of header to fill with authenticated username, eg. "X-Authenticated-User" or "REMOTE_USER" (optional). + EnumerateGroups bool // If true, groups the user is a member of are enumerated and stored in request context (default false) + ServerName string // Specifies the DNS or NetBIOS name of the remote server which to query about user groups. Ignored if EnumerateGroups is false. +} + +// NewConfig creates a configuration object with default values. +func NewConfig() *Config { + return &Config{ + contextStore: secctx.NewCookieStore(), + authAPI: &Win32{}, + } +} + +// Validate makes basic validation of configuration to make sure that important and required fields +// have been set with values in expected format. +func (c *Config) Validate() error { + if c.contextStore == nil { + return errors.New("Store for context handles not specified in Config") + } + if c.authAPI == nil { + return errors.New("Authentication API not specified in Config") + } + return nil +} + +// contextKey represents a custom key for values stored in context.Context +type contextKey string + +func (c contextKey) String() string { + return "websspi-key-" + string(c) +} + +var ( + UserInfoKey = contextKey("UserInfo") +) + +// The Authenticator type provides middleware methods for authentication of http requests. +// A single authenticator object can be shared by concurrent goroutines. +type Authenticator struct { + Config Config + serverCred *CredHandle + credExpiry *time.Time + ctxList []CtxtHandle + ctxListMux *sync.Mutex +} + +// New creates a new Authenticator object with the given configuration options. +func New(config *Config) (*Authenticator, error) { + err := config.Validate() + if err != nil { + return nil, fmt.Errorf("invalid config: %v", err) + } + + var auth = &Authenticator{ + Config: *config, + ctxListMux: &sync.Mutex{}, + } + + err = auth.PrepareCredentials(config.KrbPrincipal) + if err != nil { + return nil, fmt.Errorf("could not acquire credentials handle for the service: %v", err) + } + log.Printf("Credential handle expiry: %v\n", *auth.credExpiry) + + return auth, nil +} + +// PrepareCredentials method acquires a credentials handle for the specified principal +// for use during the live of the application. +// On success stores the handle in the serverCred field and its expiry time in the +// credExpiry field. +// This method must be called once - when the application is starting or when the first +// request from a client is received. +func (a *Authenticator) PrepareCredentials(principal string) error { + var principalPtr *uint16 + if principal != "" { + var err error + principalPtr, err = syscall.UTF16PtrFromString(principal) + if err != nil { + return err + } + } + credentialUsePtr, err := syscall.UTF16PtrFromString(NEGOSSP_NAME) + if err != nil { + return err + } + var handle CredHandle + var expiry syscall.Filetime + status := a.Config.authAPI.AcquireCredentialsHandle( + principalPtr, + credentialUsePtr, + SECPKG_CRED_INBOUND, + nil, // logonId + nil, // authData + 0, // getKeyFn + 0, // getKeyArgument + &handle, + &expiry, + ) + if status != SEC_E_OK { + return fmt.Errorf("call to AcquireCredentialsHandle failed with code 0x%x", status) + } + expiryTime := time.Unix(0, expiry.Nanoseconds()) + a.credExpiry = &expiryTime + a.serverCred = &handle + return nil +} + +// Free method should be called before shutting down the server to let +// it release allocated Win32 resources +func (a *Authenticator) Free() error { + var status SECURITY_STATUS + a.ctxListMux.Lock() + for _, ctx := range a.ctxList { + // TODO: Also check for stale security contexts and delete them periodically + status = a.Config.authAPI.DeleteSecurityContext(&ctx) + if status != SEC_E_OK { + return fmt.Errorf("call to DeleteSecurityContext failed with code 0x%x", status) + } + } + a.ctxList = nil + a.ctxListMux.Unlock() + if a.serverCred != nil { + status = a.Config.authAPI.FreeCredentialsHandle(a.serverCred) + if status != SEC_E_OK { + return fmt.Errorf("call to FreeCredentialsHandle failed with code 0x%x", status) + } + a.serverCred = nil + } + return nil +} + +// StoreCtxHandle stores the specified context to the internal list (ctxList) +func (a *Authenticator) StoreCtxHandle(handle *CtxtHandle) { + if handle == nil || *handle == (CtxtHandle{}) { + // Should not add nil or empty handle + return + } + a.ctxListMux.Lock() + defer a.ctxListMux.Unlock() + a.ctxList = append(a.ctxList, *handle) +} + +// ReleaseCtxHandle deletes a context handle and removes it from the internal list (ctxList) +func (a *Authenticator) ReleaseCtxHandle(handle *CtxtHandle) error { + if handle == nil || *handle == (CtxtHandle{}) { + // Removing a nil or empty handle is not an error condition + return nil + } + a.ctxListMux.Lock() + defer a.ctxListMux.Unlock() + + // First, try to delete the handle + status := a.Config.authAPI.DeleteSecurityContext(handle) + if status != SEC_E_OK { + return fmt.Errorf("call to DeleteSecurityContext failed with code 0x%x", status) + } + + // Then remove it from the internal list + foundAt := -1 + for i, ctx := range a.ctxList { + if ctx == *handle { + foundAt = i + break + } + } + if foundAt > -1 { + a.ctxList[foundAt] = a.ctxList[len(a.ctxList)-1] + a.ctxList = a.ctxList[:len(a.ctxList)-1] + } + return nil +} + +// AcceptOrContinue tries to validate the auth-data token by calling the AcceptSecurityContext +// function and returns and error if validation failed or continuation of the negotiation is needed. +// No error is returned if the token was validated (user was authenticated). +func (a *Authenticator) AcceptOrContinue(context *CtxtHandle, authData []byte) (newCtx *CtxtHandle, out []byte, exp *time.Time, err error) { + if authData == nil { + err = errors.New("input token cannot be nil") + return + } + + var inputDesc SecBufferDesc + var inputBuf SecBuffer + inputDesc.BuffersCount = 1 + inputDesc.Version = SECBUFFER_VERSION + inputDesc.Buffers = &inputBuf + inputBuf.BufferSize = uint32(len(authData)) + inputBuf.BufferType = SECBUFFER_TOKEN + inputBuf.Buffer = &authData[0] + + var outputDesc SecBufferDesc + var outputBuf SecBuffer + outputDesc.BuffersCount = 1 + outputDesc.Version = SECBUFFER_VERSION + outputDesc.Buffers = &outputBuf + outputBuf.BufferSize = 0 + outputBuf.BufferType = SECBUFFER_TOKEN + outputBuf.Buffer = nil + + var expiry syscall.Filetime + var contextAttr uint32 + var newContextHandle CtxtHandle + + var status = a.Config.authAPI.AcceptSecurityContext( + a.serverCred, + context, + &inputDesc, + ASC_REQ_ALLOCATE_MEMORY|ASC_REQ_MUTUAL_AUTH|ASC_REQ_CONFIDENTIALITY| + ASC_REQ_INTEGRITY|ASC_REQ_REPLAY_DETECT|ASC_REQ_SEQUENCE_DETECT, // contextReq uint32, + SECURITY_NATIVE_DREP, // targDataRep uint32, + &newContextHandle, + &outputDesc, // *SecBufferDesc + &contextAttr, // contextAttr *uint32, + &expiry, // *syscall.Filetime + ) + if newContextHandle.Lower != 0 || newContextHandle.Upper != 0 { + newCtx = &newContextHandle + } + tm := time.Unix(0, expiry.Nanoseconds()) + exp = &tm + if status == SEC_E_OK || status == SEC_I_CONTINUE_NEEDED { + // Copy outputBuf.Buffer to out and free the outputBuf.Buffer + out = make([]byte, outputBuf.BufferSize) + var bufPtr = uintptr(unsafe.Pointer(outputBuf.Buffer)) + for i := 0; i < len(out); i++ { + out[i] = *(*byte)(unsafe.Pointer(bufPtr)) + bufPtr++ + } + } + if outputBuf.Buffer != nil { + freeStatus := a.Config.authAPI.FreeContextBuffer(outputBuf.Buffer) + if freeStatus != SEC_E_OK { + status = freeStatus + err = fmt.Errorf("could not free output buffer; FreeContextBuffer() failed with code: 0x%x", freeStatus) + return + } + } + if status == SEC_I_CONTINUE_NEEDED { + err = errors.New("Negotiation should continue") + return + } else if status != SEC_E_OK { + err = fmt.Errorf("call to AcceptSecurityContext failed with code 0x%x", status) + return + } + // TODO: Check contextAttr? + return +} + +// GetCtxHandle retrieves the context handle for this client from request's cookies +func (a *Authenticator) GetCtxHandle(r *http.Request) (*CtxtHandle, error) { + sessionHandle, err := a.Config.contextStore.GetHandle(r) + if err != nil { + return nil, fmt.Errorf("could not get context handle from session: %s", err) + } + if contextHandle, ok := sessionHandle.(*CtxtHandle); ok { + log.Printf("CtxHandle: 0x%x\n", *contextHandle) + if contextHandle.Lower == 0 && contextHandle.Upper == 0 { + return nil, nil + } + return contextHandle, nil + } + log.Printf("CtxHandle: nil\n") + return nil, nil +} + +// SetCtxHandle stores the context handle for this client to cookie of response +func (a *Authenticator) SetCtxHandle(r *http.Request, w http.ResponseWriter, newContext *CtxtHandle) error { + // Store can't store nil value, so if newContext is nil, store an empty CtxHandle + ctx := &CtxtHandle{} + if newContext != nil { + ctx = newContext + } + err := a.Config.contextStore.SetHandle(r, w, ctx) + if err != nil { + return fmt.Errorf("could not save context to cookie: %s", err) + } + log.Printf("New context: 0x%x\n", *ctx) + return nil +} + +// GetFlags returns the negotiated context flags +func (a *Authenticator) GetFlags(context *CtxtHandle) (uint32, error) { + var flags SecPkgContext_Flags + status := a.Config.authAPI.QueryContextAttributes(context, SECPKG_ATTR_FLAGS, (*byte)(unsafe.Pointer(&flags))) + if status != SEC_E_OK { + return 0, fmt.Errorf("QueryContextAttributes failed with status 0x%x", status) + } + return flags.Flags, nil +} + +// GetUsername returns the name of the user associated with the specified security context +func (a *Authenticator) GetUsername(context *CtxtHandle) (username string, err error) { + var names SecPkgContext_Names + status := a.Config.authAPI.QueryContextAttributes(context, SECPKG_ATTR_NAMES, (*byte)(unsafe.Pointer(&names))) + if status != SEC_E_OK { + err = fmt.Errorf("QueryContextAttributes failed with status 0x%x", status) + return + } + if names.UserName != nil { + username = UTF16PtrToString(names.UserName, 2048) + status = a.Config.authAPI.FreeContextBuffer((*byte)(unsafe.Pointer(names.UserName))) + if status != SEC_E_OK { + err = fmt.Errorf("FreeContextBuffer failed with status 0x%x", status) + } + return + } + err = errors.New("QueryContextAttributes returned empty name") + return +} + +// GetUserGroups returns the groups the user is a member of +func (a *Authenticator) GetUserGroups(userName string) (groups []string, err error) { + var serverNamePtr *uint16 + if a.Config.ServerName != "" { + serverNamePtr, err = syscall.UTF16PtrFromString(a.Config.ServerName) + if err != nil { + return + } + } + + userNamePtr, err := syscall.UTF16PtrFromString(userName) + if err != nil { + return + } + var buf *byte + var entriesRead uint32 + var totalEntries uint32 + err = a.Config.authAPI.NetUserGetGroups( + serverNamePtr, + userNamePtr, + 0, + &buf, + MAX_PREFERRED_LENGTH, + &entriesRead, + &totalEntries, + ) + if buf == nil { + err = fmt.Errorf("NetUserGetGroups(): returned nil buffer, error: %s", err) + return + } + defer func() { + freeErr := a.Config.authAPI.NetApiBufferFree(buf) + if freeErr != nil { + err = freeErr + } + }() + if err != nil { + return + } + if entriesRead < totalEntries { + err = fmt.Errorf("NetUserGetGroups(): could not read all entries, read only %d entries of %d", entriesRead, totalEntries) + return + } + + ptr := uintptr(unsafe.Pointer(buf)) + for i := uint32(0); i < entriesRead; i++ { + groupInfo := (*GroupUsersInfo0)(unsafe.Pointer(ptr)) + groupName := UTF16PtrToString(groupInfo.Grui0_name, MAX_GROUP_NAME_LENGTH) + if groupName != "" { + groups = append(groups, groupName) + } + ptr += unsafe.Sizeof(GroupUsersInfo0{}) + } + return +} + +// GetUserInfo returns a structure containing the name of the user associated with the +// specified security context and the groups to which they are a member of (if Config.EnumerateGroups) +// is enabled +func (a *Authenticator) GetUserInfo(context *CtxtHandle) (*UserInfo, error) { + // Get username + username, err := a.GetUsername(context) + if err != nil { + return nil, err + } + info := UserInfo{ + Username: username, + } + + // Get groups + if a.Config.EnumerateGroups { + info.Groups, err = a.GetUserGroups(username) + if err != nil { + return nil, err + } + } + + return &info, nil +} + +// GetAuthData parses the "Authorization" header received from the client, +// extracts the auth-data token (input token) and decodes it to []byte +func (a *Authenticator) GetAuthData(r *http.Request, w http.ResponseWriter) (authData []byte, err error) { + // 1. Check if Authorization header is present + headers := r.Header["Authorization"] + if len(headers) == 0 { + err = errors.New("the Authorization header is not provided") + return + } + if len(headers) > 1 { + err = errors.New("received multiple Authorization headers, but expected only one") + return + } + + authzHeader := strings.TrimSpace(headers[0]) + if authzHeader == "" { + err = errors.New("the Authorization header is empty") + return + } + // 1.1. Make sure header starts with "Negotiate" + if !strings.HasPrefix(strings.ToLower(authzHeader), "negotiate") { + err = errors.New("the Authorization header does not start with 'Negotiate'") + return + } + + // 2. Extract token from Authorization header + authzParts := strings.Split(authzHeader, " ") + if len(authzParts) < 2 { + err = errors.New("the Authorization header does not contain token (gssapi-data)") + return + } + token := authzParts[len(authzParts)-1] + if token == "" { + err = errors.New("the token (gssapi-data) in the Authorization header is empty") + return + } + + // 3. Decode token + authData, err = base64.StdEncoding.DecodeString(token) + if err != nil { + err = errors.New("could not decode token as base64 string") + return + } + + return +} + +// Authenticate tries to authenticate the HTTP request and returns nil +// if authentication was successful. +// Returns error and data for continuation if authentication was not successful. +func (a *Authenticator) Authenticate(r *http.Request, w http.ResponseWriter) (userInfo *UserInfo, outToken string, err error) { + // 1. Extract auth-data from Authorization header + authData, err := a.GetAuthData(r, w) + if err != nil { + err = fmt.Errorf("could not get auth data: %s", err) + return + } + + // 2. Authenticate user with provided token + contextHandle, err := a.GetCtxHandle(r) + if err != nil { + return + } + newCtx, output, _, err := a.AcceptOrContinue(contextHandle, authData) + + // If a new context was created, make sure to delete it or store it + // both in internal list and response Cookie + defer func() { + // Negotiation is ending if we don't expect further responses from the client + // (authentication was successful or no output token is going to be sent back), + // clear client cookie + endOfNegotiation := err == nil || len(output) == 0 + + // Current context (contextHandle) is not needed anymore and should be deleted if: + // - we don't expect further responses from the client + // - a new context has been returned by AcceptSecurityContext + currCtxNotNeeded := endOfNegotiation || newCtx != nil + if !currCtxNotNeeded { + // Release current context only if its different than the new context + if contextHandle != nil && *contextHandle != *newCtx { + remErr := a.ReleaseCtxHandle(contextHandle) + if remErr != nil { + err = remErr + return + } + } + } + + if endOfNegotiation { + // Clear client cookie + setErr := a.SetCtxHandle(r, w, nil) + if setErr != nil { + err = fmt.Errorf("could not clear context, error: %s", setErr) + return + } + + // Delete any new context handle + remErr := a.ReleaseCtxHandle(newCtx) + if remErr != nil { + err = remErr + return + } + + // Exit defer func + return + } + + if newCtx != nil { + // Store new context handle to internal list and response Cookie + a.StoreCtxHandle(newCtx) + setErr := a.SetCtxHandle(r, w, newCtx) + if setErr != nil { + err = setErr + return + } + } + }() + + outToken = base64.StdEncoding.EncodeToString(output) + if err != nil { + err = fmt.Errorf("AcceptOrContinue failed: %s", err) + return + } + + // 3. Get username and user groups + currentCtx := newCtx + if currentCtx == nil { + currentCtx = contextHandle + } + userInfo, err = a.GetUserInfo(currentCtx) + if err != nil { + err = fmt.Errorf("could not get username, error: %s", err) + return + } + + return +} + +// AppendAuthenticateHeader populates WWW-Authenticate header, +// indicating to client that authentication is required and returns a 401 (Unauthorized) +// response code. +// The data parameter can be empty for the first 401 response from the server. +// For subsequent 401 responses the data parameter should contain the gssapi-data, +// which is required for continuation of the negotiation. +func (a *Authenticator) AppendAuthenticateHeader(w http.ResponseWriter, data string) { + value := "Negotiate" + if data != "" { + value += " " + data + } + w.Header().Set("WWW-Authenticate", value) +} + +// Return401 populates WWW-Authenticate header, indicating to client that authentication +// is required and returns a 401 (Unauthorized) response code. +// The data parameter can be empty for the first 401 response from the server. +// For subsequent 401 responses the data parameter should contain the gssapi-data, +// which is required for continuation of the negotiation. +func (a *Authenticator) Return401(w http.ResponseWriter, data string) { + a.AppendAuthenticateHeader(w, data) + http.Error(w, "Error!", http.StatusUnauthorized) +} + +// WithAuth authenticates the request. On successful authentication the request +// is passed down to the next http handler. The next handler can access information +// about the authenticated user via the GetUserName method. +// If authentication was not successful, the server returns 401 response code with +// a WWW-Authenticate, indicating that authentication is required. +func (a *Authenticator) WithAuth(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Printf("Authenticating request to %s\n", r.RequestURI) + + user, data, err := a.Authenticate(r, w) + if err != nil { + log.Printf("Authentication failed with error: %v\n", err) + a.Return401(w, data) + return + } + + log.Print("Authenticated\n") + // Add the UserInfo value to the reqest's context + r = r.WithContext(context.WithValue(r.Context(), UserInfoKey, user)) + // and to the request header with key Config.AuthUserKey + if a.Config.AuthUserKey != "" { + r.Header.Set(a.Config.AuthUserKey, user.Username) + } + + // The WWW-Authenticate header might need to be sent back even + // on successful authentication (eg. in order to let the client complete + // mutual authentication). + if data != "" { + a.AppendAuthenticateHeader(w, data) + } + next.ServeHTTP(w, r) + }) +} + +func init() { + gob.Register(&CtxtHandle{}) + gob.Register(&UserInfo{}) +} diff --git a/vendor/github.com/quasoft/websspi/win32_windows.go b/vendor/github.com/quasoft/websspi/win32_windows.go new file mode 100644 index 0000000000..7359a0909a --- /dev/null +++ b/vendor/github.com/quasoft/websspi/win32_windows.go @@ -0,0 +1,312 @@ +package websspi + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +// secur32.dll + +type SECURITY_STATUS syscall.Errno + +const ( + SEC_E_OK = SECURITY_STATUS(0) + + SEC_E_INCOMPLETE_MESSAGE = SECURITY_STATUS(0x80090318) + SEC_E_INSUFFICIENT_MEMORY = SECURITY_STATUS(0x80090300) + SEC_E_INTERNAL_ERROR = SECURITY_STATUS(0x80090304) + SEC_E_INVALID_HANDLE = SECURITY_STATUS(0x80090301) + SEC_E_INVALID_TOKEN = SECURITY_STATUS(0x80090308) + SEC_E_LOGON_DENIED = SECURITY_STATUS(0x8009030C) + SEC_E_NO_AUTHENTICATING_AUTHORITY = SECURITY_STATUS(0x80090311) + SEC_E_NO_CREDENTIALS = SECURITY_STATUS(0x8009030E) + SEC_E_UNSUPPORTED_FUNCTION = SECURITY_STATUS(0x80090302) + SEC_I_COMPLETE_AND_CONTINUE = SECURITY_STATUS(0x00090314) + SEC_I_COMPLETE_NEEDED = SECURITY_STATUS(0x00090313) + SEC_I_CONTINUE_NEEDED = SECURITY_STATUS(0x00090312) + SEC_E_NOT_OWNER = SECURITY_STATUS(0x80090306) + SEC_E_SECPKG_NOT_FOUND = SECURITY_STATUS(0x80090305) + SEC_E_UNKNOWN_CREDENTIALS = SECURITY_STATUS(0x8009030D) + + NEGOSSP_NAME = "Negotiate" + SECPKG_CRED_INBOUND = 1 + SECURITY_NATIVE_DREP = 16 + + ASC_REQ_DELEGATE = 1 + ASC_REQ_MUTUAL_AUTH = 2 + ASC_REQ_REPLAY_DETECT = 4 + ASC_REQ_SEQUENCE_DETECT = 8 + ASC_REQ_CONFIDENTIALITY = 16 + ASC_REQ_USE_SESSION_KEY = 32 + ASC_REQ_ALLOCATE_MEMORY = 256 + ASC_REQ_USE_DCE_STYLE = 512 + ASC_REQ_DATAGRAM = 1024 + ASC_REQ_CONNECTION = 2048 + ASC_REQ_EXTENDED_ERROR = 32768 + ASC_REQ_STREAM = 65536 + ASC_REQ_INTEGRITY = 131072 + + SECPKG_ATTR_SIZES = 0 + SECPKG_ATTR_NAMES = 1 + SECPKG_ATTR_LIFESPAN = 2 + SECPKG_ATTR_DCE_INFO = 3 + SECPKG_ATTR_STREAM_SIZES = 4 + SECPKG_ATTR_KEY_INFO = 5 + SECPKG_ATTR_AUTHORITY = 6 + SECPKG_ATTR_PROTO_INFO = 7 + SECPKG_ATTR_PASSWORD_EXPIRY = 8 + SECPKG_ATTR_SESSION_KEY = 9 + SECPKG_ATTR_PACKAGE_INFO = 10 + SECPKG_ATTR_USER_FLAGS = 11 + SECPKG_ATTR_NEGOTIATION_INFO = 12 + SECPKG_ATTR_NATIVE_NAMES = 13 + SECPKG_ATTR_FLAGS = 14 + + SECBUFFER_VERSION = 0 + SECBUFFER_TOKEN = 2 +) + +type CredHandle struct { + Lower uintptr + Upper uintptr +} + +type CtxtHandle struct { + Lower uintptr + Upper uintptr +} + +type SecBuffer struct { + BufferSize uint32 + BufferType uint32 + Buffer *byte +} + +type SecBufferDesc struct { + Version uint32 + BuffersCount uint32 + Buffers *SecBuffer +} + +type LUID struct { + LowPart uint32 + HighPart int32 +} + +type SecPkgContext_Names struct { + UserName *uint16 +} + +type SecPkgContext_Flags struct { + Flags uint32 +} + +// netapi32.dll + +const ( + NERR_Success = 0x0 + NERR_InternalError = 0x85C + NERR_UserNotFound = 0x8AD + + ERROR_ACCESS_DENIED = 0x5 + ERROR_BAD_NETPATH = 0x35 + ERROR_INVALID_LEVEL = 0x7C + ERROR_INVALID_NAME = 0x7B + ERROR_MORE_DATA = 0xEA + ERROR_NOT_ENOUGH_MEMORY = 0x8 + + MAX_PREFERRED_LENGTH = 0xFFFFFFFF + MAX_GROUP_NAME_LENGTH = 256 + + SE_GROUP_MANDATORY = 0x1 + SE_GROUP_ENABLED_BY_DEFAULT = 0x2 + SE_GROUP_ENABLED = 0x4 + SE_GROUP_OWNER = 0x8 + SE_GROUP_USE_FOR_DENY_ONLY = 0x10 + SE_GROUP_INTEGRITY = 0x20 + SE_GROUP_INTEGRITY_ENABLED = 0x40 + SE_GROUP_LOGON_ID = 0xC0000000 + SE_GROUP_RESOURCE = 0x20000000 +) + +type GroupUsersInfo0 struct { + Grui0_name *uint16 +} + +type GroupUsersInfo1 struct { + Grui1_name *uint16 + Grui1_attributes uint32 +} + +// The API interface describes the Win32 functions used in this package and +// its primary purpose is to allow replacing them with stub functions in unit tests. +type API interface { + AcquireCredentialsHandle( + principal *uint16, + _package *uint16, + credentialUse uint32, + logonID *LUID, + authData *byte, + getKeyFn uintptr, + getKeyArgument uintptr, + credHandle *CredHandle, + expiry *syscall.Filetime, + ) SECURITY_STATUS + AcceptSecurityContext( + credential *CredHandle, + context *CtxtHandle, + input *SecBufferDesc, + contextReq uint32, + targDataRep uint32, + newContext *CtxtHandle, + output *SecBufferDesc, + contextAttr *uint32, + expiry *syscall.Filetime, + ) SECURITY_STATUS + QueryContextAttributes(context *CtxtHandle, attribute uint32, buffer *byte) SECURITY_STATUS + DeleteSecurityContext(context *CtxtHandle) SECURITY_STATUS + FreeContextBuffer(buffer *byte) SECURITY_STATUS + FreeCredentialsHandle(handle *CredHandle) SECURITY_STATUS + NetUserGetGroups( + serverName *uint16, + userName *uint16, + level uint32, + buf **byte, + prefmaxlen uint32, + entriesread *uint32, + totalentries *uint32, + ) (neterr error) + NetApiBufferFree(buf *byte) (neterr error) +} + +// Win32 implements the API interface by calling the relevant system functions +// from secur32.dll and netapi32.dll +type Win32 struct{} + +var ( + secur32dll = windows.NewLazySystemDLL("secur32.dll") + netapi32dll = windows.NewLazySystemDLL("netapi32.dll") + + procAcquireCredentialsHandleW = secur32dll.NewProc("AcquireCredentialsHandleW") + procAcceptSecurityContext = secur32dll.NewProc("AcceptSecurityContext") + procQueryContextAttributesW = secur32dll.NewProc("QueryContextAttributesW") + procDeleteSecurityContext = secur32dll.NewProc("DeleteSecurityContext") + procFreeContextBuffer = secur32dll.NewProc("FreeContextBuffer") + procFreeCredentialsHandle = secur32dll.NewProc("FreeCredentialsHandle") + procNetUserGetGroups = netapi32dll.NewProc("NetUserGetGroups") +) + +func (w *Win32) AcquireCredentialsHandle( + principal *uint16, + _package *uint16, + credentialUse uint32, + logonId *LUID, + authData *byte, + getKeyFn uintptr, + getKeyArgument uintptr, + credHandle *CredHandle, + expiry *syscall.Filetime, +) SECURITY_STATUS { + r1, _, _ := syscall.Syscall9( + procAcquireCredentialsHandleW.Addr(), 9, + uintptr(unsafe.Pointer(principal)), + uintptr(unsafe.Pointer(_package)), + uintptr(credentialUse), + uintptr(unsafe.Pointer(logonId)), + uintptr(unsafe.Pointer(authData)), + uintptr(getKeyFn), + uintptr(getKeyArgument), + uintptr(unsafe.Pointer(credHandle)), + uintptr(unsafe.Pointer(expiry)), + ) + return SECURITY_STATUS(r1) +} + +func (w *Win32) AcceptSecurityContext( + credential *CredHandle, + context *CtxtHandle, + input *SecBufferDesc, + contextReq uint32, + targDataRep uint32, + newContext *CtxtHandle, + output *SecBufferDesc, + contextAttr *uint32, + expiry *syscall.Filetime, +) SECURITY_STATUS { + r1, _, _ := syscall.Syscall9( + procAcceptSecurityContext.Addr(), 9, + uintptr(unsafe.Pointer(credential)), + uintptr(unsafe.Pointer(context)), + uintptr(unsafe.Pointer(input)), + uintptr(contextReq), + uintptr(targDataRep), + uintptr(unsafe.Pointer(newContext)), + uintptr(unsafe.Pointer(output)), + uintptr(unsafe.Pointer(contextAttr)), + uintptr(unsafe.Pointer(expiry)), + ) + return SECURITY_STATUS(r1) +} + +func (w *Win32) QueryContextAttributes( + context *CtxtHandle, + attribute uint32, + buffer *byte, +) SECURITY_STATUS { + r1, _, _ := syscall.Syscall( + procQueryContextAttributesW.Addr(), 3, + uintptr(unsafe.Pointer(context)), + uintptr(attribute), + uintptr(unsafe.Pointer(buffer)), + ) + return SECURITY_STATUS(r1) +} + +func (w *Win32) DeleteSecurityContext(context *CtxtHandle) SECURITY_STATUS { + r1, _, _ := syscall.Syscall( + procDeleteSecurityContext.Addr(), 1, + uintptr(unsafe.Pointer(context)), + 0, 0, + ) + return SECURITY_STATUS(r1) +} + +func (w *Win32) FreeContextBuffer(buffer *byte) SECURITY_STATUS { + r1, _, _ := syscall.Syscall( + procFreeContextBuffer.Addr(), 1, + uintptr(unsafe.Pointer(buffer)), + 0, 0, + ) + return SECURITY_STATUS(r1) +} + +func (w *Win32) FreeCredentialsHandle(handle *CredHandle) SECURITY_STATUS { + r1, _, _ := syscall.Syscall( + procFreeCredentialsHandle.Addr(), 1, + uintptr(unsafe.Pointer(handle)), + 0, 0, + ) + return SECURITY_STATUS(r1) +} + +func (w *Win32) NetUserGetGroups( + serverName *uint16, + userName *uint16, + level uint32, + buf **byte, + prefmaxlen uint32, + entriesread *uint32, + totalentries *uint32, +) (neterr error) { + r0, _, _ := syscall.Syscall9(procNetUserGetGroups.Addr(), 7, uintptr(unsafe.Pointer(serverName)), uintptr(unsafe.Pointer(userName)), uintptr(level), uintptr(unsafe.Pointer(buf)), uintptr(prefmaxlen), uintptr(unsafe.Pointer(entriesread)), uintptr(unsafe.Pointer(totalentries)), 0, 0) + if r0 != 0 { + neterr = syscall.Errno(r0) + } + return +} + +func (w *Win32) NetApiBufferFree(buf *byte) (neterr error) { + return syscall.NetApiBufferFree(buf) +} |