1
0
Fork 0
forked from forgejo/forgejo

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
This commit is contained in:
QuaSoft 2019-11-23 01:33:31 +02:00 committed by Lauris BH
parent eb1b225d9a
commit 7b4d2f7a2a
174 changed files with 6363 additions and 1306 deletions

12
vendor/github.com/quasoft/websspi/.gitignore generated vendored Normal file
View file

@ -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

13
vendor/github.com/quasoft/websspi/.travis.yml generated vendored Normal file
View file

@ -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

21
vendor/github.com/quasoft/websspi/LICENSE generated vendored Normal file
View file

@ -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.

41
vendor/github.com/quasoft/websspi/README.md generated vendored Normal file
View file

@ -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!

9
vendor/github.com/quasoft/websspi/go.mod generated vendored Normal file
View file

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

6
vendor/github.com/quasoft/websspi/go.sum generated vendored Normal file
View file

@ -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=

36
vendor/github.com/quasoft/websspi/secctx/session.go generated vendored Normal file
View file

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

13
vendor/github.com/quasoft/websspi/secctx/store.go generated vendored Normal file
View file

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

7
vendor/github.com/quasoft/websspi/userinfo.go generated vendored Normal file
View file

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

22
vendor/github.com/quasoft/websspi/utf16.go generated vendored Normal file
View file

@ -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 ""
}

615
vendor/github.com/quasoft/websspi/websspi_windows.go generated vendored Normal file
View file

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

312
vendor/github.com/quasoft/websspi/win32_windows.go generated vendored Normal file
View file

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