├── .gitignore ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── examples └── server_windows.go ├── go.mod ├── go.sum ├── secctx ├── session.go ├── session_test.go └── store.go ├── userinfo.go ├── utf16.go ├── utf16_test.go ├── websspi_test.go ├── websspi_windows.go └── win32_windows.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | include: 3 | - os: windows 4 | script: $GOPATH/bin/goveralls -service=travis-ci 5 | language: go 6 | sudo: false 7 | go: 1.13.x 8 | before_install: go get github.com/mattn/goveralls 9 | - os: linux 10 | script: go build 11 | language: go 12 | sudo: false 13 | go: 1.13.x 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.lintFlags": [ 3 | "[\"--exclude=\\\"\\bwin32_windows.go\\b\\\"\"]" 4 | ], 5 | "zenMode.centerLayout": false, 6 | "zenMode.hideLineNumbers": false, 7 | "zenMode.hideTabs": false 8 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 QuaSoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # websspi 2 | 3 | [![GoDoc](https://godoc.org/github.com/quasoft/websspi?status.svg)](https://godoc.org/github.com/quasoft/websspi) [![Build Status](https://app.travis-ci.com/quasoft/websspi.svg?branch=master)](https://app.travis-ci.com/github/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) 4 | 5 | `websspi` is an HTTP middleware for Golang that uses Kerberos/NTLM for single sign-on (SSO) authentication of browser based clients in a Windows environment. 6 | 7 | It performs authentication of HTTP requests without the need to create or use keytab files. 8 | 9 | The middleware implements the scheme defined by RFC4559 (SPNEGO-based HTTP Authentication in Microsoft Windows) to exchange security tokens via HTTP headers and uses SSPI (Security Support Provider Interface) to authenticate HTTP requests. 10 | 11 | ## How to use 12 | 13 | The [examples directory](https://github.com/quasoft/websspi/tree/master/examples) contains a [simple web server](https://github.com/quasoft/websspi/blob/master/examples/server_windows.go) that demonstrates how to use the package. 14 | Before trying it, you need to prepare your environment: 15 | 16 | 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) 17 | 18 | 2. Create a service principal name for the host with class HTTP: 19 | - Start Command prompt or PowerShell as domain administrator 20 | - 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.: 21 | 22 | setspn -A HTTP/host.domain.local domain\user 23 | 24 | 3. Start the web server app under the account created in step 1. 25 | 26 | 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`) 27 | 28 | 5. Start Chrome, Edge or Internet Explorer and navigate to the URL of the web app (eg. `http://host.domain.local:9000`) 29 | 30 | 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: 31 | 32 | - 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. If you do run the web browser at the same server SSPI package will fallback to NTLM protocol and Kerberos will not be used. 33 | - There is only one HTTP/... SPN for the host 34 | - The SPN contains only the hostname, without the port 35 | - You have added the URL of the web app to the `Local intranet` zone 36 | - The clocks of the server and client should not differ with more than 5 minutes 37 | - `Integrated Windows Authentication` should be enabled in Internet Explorer (under `Advanced settings`) 38 | 39 | ## Security requirements 40 | 41 | - SPNEGO over HTTP provides no facilities for protection of the authroization data contained in HTTP headers (the `Authorization` and `WWW-Authenticate` headers), which means that the web server **MUST** enforce use of HTTPS to provide confidentiality for the data in those headers! 42 | -------------------------------------------------------------------------------- /examples/server_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "html/template" 6 | "log" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/quasoft/websspi" 14 | ) 15 | 16 | var helloTemplate = template.Must(template.New("index.html").Parse(` 17 | {{- if . -}} 18 |

Hello {{ .Username }}!

19 | 20 | {{ if .Groups -}} 21 | Groups: 22 | 27 | {{- end }} 28 | {{- else -}} 29 |

Hello!

30 | {{- end -}} 31 | `)) 32 | 33 | func main() { 34 | config := websspi.NewConfig() 35 | config.EnumerateGroups = true // If groups should be resolved 36 | // config.ServerName = "..." // If static instead of dynamic group membership should be resolved 37 | 38 | auth, err := websspi.New(config) 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | server := &http.Server{Addr: "0.0.0.0:9000"} 44 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 45 | info := r.Context().Value(websspi.UserInfoKey) 46 | userInfo, _ := info.(*websspi.UserInfo) 47 | w.Header().Add("Content-Type", "text/html; encoding=utf-8") 48 | helloTemplate.Execute(w, userInfo) 49 | }) 50 | http.Handle("/", auth.WithAuth(handler)) 51 | 52 | stop := make(chan os.Signal, 2) 53 | signal.Notify(stop, os.Interrupt, syscall.SIGTERM) 54 | 55 | go func() { 56 | if err := server.ListenAndServe(); err != http.ErrServerClosed { 57 | log.Fatalf("ListenAndServe(): %s\n", err) 58 | } 59 | }() 60 | 61 | // Wait for SIGINT or SIGTERM 62 | <-stop 63 | log.Print("Shutting down and releasing resources...\n") 64 | err = auth.Free() 65 | if err != nil { 66 | log.Printf("Error while releasing resources: %v\n", err) 67 | } 68 | 69 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 70 | cancel() 71 | if err := server.Shutdown(ctx); err != nil { 72 | log.Fatalf("Graceful shutdown timed out: %s\n", err) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/quasoft/websspi 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/gorilla/securecookie v1.1.1 7 | github.com/gorilla/sessions v1.2.0 8 | golang.org/x/sys v0.0.0-20191010194322-b09406accb47 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 2 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 3 | github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ= 4 | github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 5 | golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY= 6 | golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 7 | -------------------------------------------------------------------------------- /secctx/session.go: -------------------------------------------------------------------------------- 1 | package secctx 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gorilla/securecookie" 7 | "github.com/gorilla/sessions" 8 | ) 9 | 10 | // CookieStore can store and retrieve SSPI context handles to/from an encrypted Cookie. 11 | type CookieStore struct { 12 | store *sessions.CookieStore 13 | } 14 | 15 | // NewCookieStore creates a new CookieStore for storing and retrieving of SSPI context handles 16 | // to/from encrypted Cookies 17 | func NewCookieStore() *CookieStore { 18 | s := &CookieStore{} 19 | s.store = sessions.NewCookieStore([]byte(securecookie.GenerateRandomKey(32))) 20 | return s 21 | } 22 | 23 | // GetHandle retrieves a *websspi.CtxtHandle value from the store 24 | func (s *CookieStore) GetHandle(r *http.Request) (interface{}, error) { 25 | session, _ := s.store.Get(r, "websspi") 26 | contextHandle := session.Values["contextHandle"] 27 | return contextHandle, nil 28 | } 29 | 30 | // SetHandle saves a *websspi.CtxtHandle value to the store 31 | func (s *CookieStore) SetHandle(r *http.Request, w http.ResponseWriter, contextHandle interface{}) error { 32 | session, _ := s.store.Get(r, "websspi") 33 | session.Values["contextHandle"] = contextHandle 34 | err := session.Save(r, w) 35 | return err 36 | } 37 | -------------------------------------------------------------------------------- /secctx/session_test.go: -------------------------------------------------------------------------------- 1 | package secctx 2 | 3 | import ( 4 | "net/http/httptest" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestSetHandle(t *testing.T) { 10 | store := NewCookieStore() 11 | r := httptest.NewRequest("GET", "http://example.local/", nil) 12 | r.Header.Set("Authorization", "Negotiate a87421000492aa874209af8bc028") 13 | w := httptest.NewRecorder() 14 | 15 | ctx := 314 16 | err := store.SetHandle(r, w, &ctx) 17 | if err != nil { 18 | t.Fatalf("SetHandle() failed with error %q, wanted no error", err) 19 | } 20 | gotCookie := w.Header().Get("Set-Cookie") 21 | wantCookie := "websspi=" 22 | if !strings.HasPrefix(gotCookie, wantCookie) { 23 | t.Errorf("SetHandle() failed to set encrypted websspi cookie, got = %q, want %q", gotCookie, wantCookie) 24 | } 25 | } 26 | 27 | func TestGetHandle(t *testing.T) { 28 | store := NewCookieStore() 29 | r := httptest.NewRequest("GET", "http://example.local/", nil) 30 | r.Header.Set("Authorization", "Negotiate a87421000492aa874209af8bc028") 31 | w := httptest.NewRecorder() 32 | 33 | var wantCtxHandle uint32 = 314 34 | err := store.SetHandle(r, w, wantCtxHandle) 35 | if err != nil { 36 | t.Fatalf("SetHandle() failed with error %q, wanted no error", err) 37 | } 38 | 39 | r.Header.Set("Cookie", w.Header().Get("Set-Cookie")) 40 | handle, err := store.GetHandle(r) 41 | if err != nil { 42 | t.Fatalf("GetHandle() failed with error %q, wanted no error", err) 43 | } 44 | gotCtxHandle, ok := handle.(uint32) 45 | if !ok { 46 | t.Fatal("SetHandle() followed by GetHandle() returned value of different type") 47 | } 48 | if gotCtxHandle != wantCtxHandle { 49 | t.Errorf("GetHandle() returned wrong context handle, got = %v, want %v", gotCtxHandle, wantCtxHandle) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /secctx/store.go: -------------------------------------------------------------------------------- 1 | package secctx 2 | 3 | import "net/http" 4 | 5 | // Store is an interface for storage of SSPI context handles. 6 | // SSPI context handles are Windows API handles and have nothing to do 7 | // with the "context" package in Go. 8 | type Store interface { 9 | // GetHandle retrieves a *websspi.CtxtHandle value from the store 10 | GetHandle(r *http.Request) (interface{}, error) 11 | // SetHandle saves a *websspi.CtxtHandle value to the store 12 | SetHandle(r *http.Request, w http.ResponseWriter, contextHandle interface{}) error 13 | } 14 | -------------------------------------------------------------------------------- /userinfo.go: -------------------------------------------------------------------------------- 1 | package websspi 2 | 3 | // UserInfo represents an authenticated user. 4 | type UserInfo struct { 5 | Username string // Name of user, usually in the form DOMAIN\User 6 | Groups []string // The global groups the user is a member of 7 | } 8 | -------------------------------------------------------------------------------- /utf16.go: -------------------------------------------------------------------------------- 1 | package websspi 2 | 3 | import ( 4 | "unicode/utf16" 5 | "unsafe" 6 | ) 7 | 8 | // UTF16PtrToString converts a pointer to a UTF-16 sequence to a string. 9 | // The UTF-16 sequence must null terminated or shorter than maxLen. 10 | // 11 | // If the UTF-16 sequence is longer than maxlen, an empty string is returned. 12 | func UTF16PtrToString(ptr *uint16, maxLen int) (s string) { 13 | if ptr == nil { 14 | return "" 15 | } 16 | 17 | buf := make([]uint16, 0, maxLen) 18 | for i, p := 0, unsafe.Pointer(ptr); i < maxLen; i, p = i+1, unsafe.Pointer(uintptr(p)+2) { 19 | char := *(*uint16)(p) 20 | if char == 0 { 21 | // Decode sequence and return 22 | return string(utf16.Decode(buf)) 23 | } 24 | 25 | buf = append(buf, char) 26 | } 27 | // Return empty string once maxLen is reached. 28 | return "" 29 | } 30 | -------------------------------------------------------------------------------- /utf16_test.go: -------------------------------------------------------------------------------- 1 | package websspi_test 2 | 3 | import ( 4 | "syscall" 5 | "testing" 6 | 7 | "github.com/quasoft/websspi" 8 | ) 9 | 10 | const maxmaxlen = 1024 11 | 12 | var testcases = []struct { 13 | utf16 []uint16 14 | result string 15 | maxlen int 16 | }{ 17 | { 18 | utf16: syscall.StringToUTF16("Hello World!"), 19 | result: "", // empty string if sequence longer than maxlen 20 | maxlen: 5, 21 | }, 22 | { 23 | utf16: syscall.StringToUTF16("Hello World!"), 24 | result: "Hello World!", 25 | maxlen: maxmaxlen, 26 | }, 27 | { 28 | utf16: []uint16{0}, 29 | result: "", 30 | maxlen: maxmaxlen, 31 | }, 32 | { 33 | utf16: nil, 34 | result: "", 35 | maxlen: maxmaxlen, 36 | }, 37 | } 38 | 39 | func TestUTF16PtrToString(t *testing.T) { 40 | for i, c := range testcases { 41 | var ptr *uint16 42 | if c.utf16 != nil { 43 | ptr = &c.utf16[0] 44 | } 45 | 46 | if r := websspi.UTF16PtrToString(ptr, c.maxlen); r != c.result { 47 | t.Errorf("#%d: Got %q instead of %q", i, r, c.result) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /websspi_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package websspi 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "net/http" 10 | "net/http/httptest" 11 | "os/user" 12 | "reflect" 13 | "strings" 14 | "syscall" 15 | "testing" 16 | "unsafe" 17 | ) 18 | 19 | var sidUsers *syscall.SID 20 | var sidAdministrators *syscall.SID 21 | var sidRemoteDesktopUsers *syscall.SID 22 | 23 | var resolvedGroups []string 24 | var resolvedGroupsWoAdmin []string 25 | 26 | func init() { 27 | for stringSid, binPtr := range map[string]**syscall.SID{ 28 | "S-1-5-32-544": &sidAdministrators, // BUILTIN\Administrators 29 | "S-1-5-32-545": &sidUsers, // BUILTIN\Users 30 | "S-1-5-32-555": &sidRemoteDesktopUsers, // BUILTIN\Remote Desktop Users 31 | } { 32 | *binPtr, _ = syscall.StringToSid(stringSid) 33 | } 34 | 35 | // Groups are localized... 36 | for _, sid := range []string{"S-1-5-32-544", "S-1-5-32-545", "S-1-5-32-555"} { 37 | g, _ := user.LookupGroupId(sid) 38 | resolvedGroups = append(resolvedGroups, g.Name) 39 | if sid != "S-1-5-32-544" { 40 | resolvedGroupsWoAdmin = append(resolvedGroupsWoAdmin, g.Name) 41 | } 42 | } 43 | } 44 | 45 | type stubAPI struct { 46 | acquireStatus SECURITY_STATUS // the status code that should be returned in simulated calls to AcquireCredentialsHandle 47 | acceptStatus SECURITY_STATUS // the status code that should be returned in simulated calls to AcceptSecurityContext 48 | acceptNewCtx *CtxtHandle // the context handle to be returned in simulated calls to AcceptSecurityContext 49 | acceptOutBuf *SecBuffer // the output buffer to be returned in simulated calls to AcceptSecurityContext 50 | deleteStatus SECURITY_STATUS // the status code that should be returned in simulated calls to DeleteSecurityContext 51 | deleteCalled bool // true if DeleteSecurityContext has been called 52 | queryStatus SECURITY_STATUS // the status code that should be returned in simulated calls to QueryContextAttributes 53 | queryOutBuf *byte // the buffer to be returned in simulated calls to QueryContextAttributes 54 | freeBufferStatus SECURITY_STATUS // the status code that should be returned in simulated calls to FreeContextBuffer 55 | freeCredsStatus SECURITY_STATUS // the status code that should be returned in simulated calls to FreeCredentialsHandle 56 | validToken string // value that will be asumed to be a valid token 57 | getGroupsErr error // error to be returned in simulated calls to NetUserGetGroups 58 | getGroupsBuf *byte // the buffer to be returned in simulated calls to NetUserGetGroups 59 | getGroupsEntries uint32 // entriesRead to be returned in simulated calls to NetUserGetGroups 60 | getGroupsTotal uint32 // totalEntries to be returned in simulated calls to NetUserGetGroups 61 | netBufferFreeErr error // error to be returned in simulated calls to NetApiBufferFree 62 | getTokenInformation map[int]map[int][]byte // map[Token] map[TokenInformationClass] 63 | } 64 | 65 | func (s *stubAPI) AcquireCredentialsHandle( 66 | principal *uint16, 67 | _package *uint16, 68 | credentialUse uint32, 69 | logonId *LUID, 70 | authData *byte, 71 | getKeyFn uintptr, 72 | getKeyArgument uintptr, 73 | credHandle *CredHandle, 74 | expiry *syscall.Filetime, 75 | ) SECURITY_STATUS { 76 | if s.acquireStatus != SEC_E_OK { 77 | return s.acquireStatus 78 | } 79 | *credHandle = CredHandle{} 80 | *expiry = syscall.Filetime{} 81 | return SEC_E_OK 82 | } 83 | 84 | func (s *stubAPI) AcceptSecurityContext( 85 | credential *CredHandle, 86 | context *CtxtHandle, 87 | input *SecBufferDesc, 88 | contextReq uint32, 89 | targDataRep uint32, 90 | newContext *CtxtHandle, 91 | output *SecBufferDesc, 92 | contextAttr *uint32, 93 | expiry *syscall.Filetime, 94 | ) SECURITY_STATUS { 95 | if s.acceptNewCtx != nil { 96 | *newContext = *s.acceptNewCtx 97 | } 98 | if s.acceptOutBuf != nil { 99 | *output.Buffers = *s.acceptOutBuf 100 | } 101 | return s.acceptStatus 102 | } 103 | 104 | func (s *stubAPI) QueryContextAttributes(context *CtxtHandle, attribute uint32, buffer *byte) SECURITY_STATUS { 105 | if s.queryOutBuf != nil { 106 | if attribute == SECPKG_ATTR_NAMES { 107 | inNames := (*SecPkgContext_Names)(unsafe.Pointer(buffer)) 108 | outNames := (*SecPkgContext_Names)(unsafe.Pointer(s.queryOutBuf)) 109 | *inNames = *outNames 110 | } else if attribute == SECPKG_ATTR_ACCESS_TOKEN { 111 | inToken := (*SecPkgContext_AccessToken)(unsafe.Pointer(buffer)) 112 | outToken := (*SecPkgContext_AccessToken)(unsafe.Pointer(s.queryOutBuf)) 113 | *inToken = *outToken 114 | } 115 | } 116 | return s.queryStatus 117 | } 118 | 119 | func (s *stubAPI) DeleteSecurityContext(context *CtxtHandle) SECURITY_STATUS { 120 | s.deleteCalled = true 121 | return s.deleteStatus 122 | } 123 | 124 | func (s *stubAPI) FreeContextBuffer(buffer *byte) SECURITY_STATUS { 125 | return s.freeBufferStatus 126 | } 127 | 128 | func (s *stubAPI) FreeCredentialsHandle(handle *CredHandle) SECURITY_STATUS { 129 | return s.freeCredsStatus 130 | } 131 | 132 | func (s *stubAPI) NetUserGetGroups( 133 | serverName *uint16, 134 | userName *uint16, 135 | level uint32, 136 | buf **byte, 137 | prefmaxlen uint32, 138 | entriesread *uint32, 139 | totalentries *uint32, 140 | ) (neterr error) { 141 | *buf = s.getGroupsBuf 142 | *entriesread = s.getGroupsEntries 143 | *totalentries = s.getGroupsTotal 144 | return s.getGroupsErr 145 | } 146 | 147 | func (s *stubAPI) NetApiBufferFree(buf *byte) (neterr error) { 148 | return s.netBufferFreeErr 149 | } 150 | 151 | func (s *stubAPI) GetTokenInformation(t syscall.Token, infoClass uint32, info *byte, infoLen uint32, returnedLen *uint32) (err error) { 152 | temp1, ok := s.getTokenInformation[int(t)] 153 | if !ok { 154 | return syscall.Errno(998) 155 | } 156 | 157 | temp2, ok := temp1[int(infoClass)] 158 | if !ok { 159 | return syscall.Errno(998) 160 | } 161 | 162 | length := len(temp2) 163 | *returnedLen = uint32(length) 164 | if infoLen < *returnedLen { 165 | return syscall.ERROR_INSUFFICIENT_BUFFER 166 | } 167 | 168 | out := make([]byte, length) 169 | var outHdr *reflect.SliceHeader 170 | outHdr = (*reflect.SliceHeader)(unsafe.Pointer(&out)) 171 | outHdr.Data = uintptr(unsafe.Pointer(info)) 172 | 173 | // dst, src 174 | copy(out, temp2) 175 | return nil 176 | } 177 | 178 | type stubContextStore struct { 179 | contextHandle interface{} // local storage for the last value set by SetHandle 180 | getError error // Error value that should be returned on calls to GetHandle 181 | setError error // Error value that should be returned on calls to SetHandle 182 | } 183 | 184 | func (s *stubContextStore) GetHandle(r *http.Request) (interface{}, error) { 185 | return s.contextHandle, s.getError 186 | } 187 | 188 | func (s *stubContextStore) SetHandle(r *http.Request, w http.ResponseWriter, contextHandle interface{}) error { 189 | s.contextHandle = contextHandle 190 | return s.setError 191 | } 192 | 193 | func newSecPkgContextNames(username string) *byte { 194 | namePtr, err := syscall.UTF16PtrFromString(username) 195 | if err != nil { 196 | panic(err) 197 | } 198 | return (*byte)(unsafe.Pointer(&SecPkgContext_Names{UserName: namePtr})) 199 | } 200 | 201 | func newGroupUsersInfo0(groupNames []string) (entires uint32, total uint32, buf *byte) { 202 | info := []GroupUsersInfo0{} 203 | for _, name := range groupNames { 204 | namePtr, err := syscall.UTF16PtrFromString(name) 205 | if err != nil { 206 | panic(err) 207 | } 208 | info = append(info, GroupUsersInfo0{Grui0_name: namePtr}) 209 | } 210 | entires = uint32(len(groupNames)) 211 | total = entires 212 | buf = (*byte)(unsafe.Pointer(&info[0])) 213 | return 214 | } 215 | 216 | func newGroups(limited bool) []byte { 217 | info := struct { 218 | GroupCount uint32 219 | Groups [3]syscall.SIDAndAttributes 220 | }{} 221 | info.GroupCount = 3 222 | 223 | info.Groups[0].Sid = sidUsers 224 | info.Groups[0].Attributes = 4 225 | info.Groups[1].Sid = sidRemoteDesktopUsers 226 | info.Groups[1].Attributes = 4 227 | info.Groups[2].Sid = sidAdministrators 228 | info.Groups[2].Attributes = 4 229 | 230 | if limited { 231 | info.Groups[2].Attributes = 0 232 | } 233 | 234 | in := make([]byte, reflect.TypeOf(info).Size()) 235 | out := make([]byte, reflect.TypeOf(info).Size()) 236 | 237 | var inHdr *reflect.SliceHeader 238 | inHdr = (*reflect.SliceHeader)(unsafe.Pointer(&in)) 239 | inHdr.Data = uintptr(unsafe.Pointer(&info)) 240 | 241 | // Copy to prevent accidential garbage collection. 242 | copy(out, in) // dst[], src[] 243 | 244 | return out 245 | } 246 | 247 | // newTestAuthenticator creates an Authenticator for use in tests. 248 | func newTestAuthenticator(t *testing.T) *Authenticator { 249 | entries, total, groupsBuf := newGroupUsersInfo0([]string{"group1", "group2", "group3"}) 250 | 251 | config := Config{ 252 | contextStore: &stubContextStore{}, 253 | authAPI: &stubAPI{ 254 | acquireStatus: SEC_E_OK, 255 | acceptStatus: SEC_E_OK, 256 | acceptNewCtx: nil, 257 | acceptOutBuf: nil, 258 | deleteStatus: SEC_E_OK, 259 | queryStatus: SEC_E_OK, 260 | queryOutBuf: newSecPkgContextNames("testuser"), 261 | freeBufferStatus: SEC_E_OK, 262 | freeCredsStatus: SEC_E_OK, 263 | validToken: "a87421000492aa874209af8bc028", 264 | getGroupsErr: nil, 265 | getGroupsBuf: groupsBuf, 266 | getGroupsEntries: entries, 267 | getGroupsTotal: total, 268 | netBufferFreeErr: nil, 269 | 270 | getTokenInformation: map[int]map[int][]byte{ 271 | 1: { 272 | syscall.TokenGroups: newGroups(true), 273 | }, 274 | 2: { 275 | syscall.TokenGroups: newGroups(false), 276 | }, 277 | }, 278 | }, 279 | KrbPrincipal: "service@test.local", 280 | EnumerateGroups: true, 281 | ServerName: "server.test.local", 282 | } 283 | auth, err := New(&config) 284 | if err != nil { 285 | t.Errorf("could not create new authenticator: %s", err) 286 | } 287 | return auth 288 | } 289 | 290 | func TestConfigValidate_NoContextStore(t *testing.T) { 291 | config := NewConfig() 292 | config.contextStore = nil 293 | err := config.Validate() 294 | if err == nil { 295 | t.Errorf("Config.Validate() returned nil (no error) when contextStore was nil, wanted error") 296 | } 297 | } 298 | 299 | func TestConfigValidate_NoAuthAPI(t *testing.T) { 300 | config := NewConfig() 301 | config.authAPI = nil 302 | err := config.Validate() 303 | if err == nil { 304 | t.Errorf("Config.Validate() returned nil (no error) when authAPI was nil, wanted error") 305 | } 306 | } 307 | 308 | func TestConfigValidate_Complete(t *testing.T) { 309 | config := NewConfig() 310 | config.KrbPrincipal = "service@test.local" 311 | err := config.Validate() 312 | if err != nil { 313 | t.Errorf("Config.Validate() returned error for a valid config, wanted nil (no error)") 314 | } 315 | } 316 | 317 | func TestNewAuthenticator_InvalidConfig(t *testing.T) { 318 | _, err := New(&Config{}) 319 | if err == nil { 320 | t.Errorf("New() returns nil (no error) when Config was not valid, wanted error") 321 | } 322 | } 323 | 324 | func TestNewAuthenticator_ErrorOnAcquire(t *testing.T) { 325 | config := Config{ 326 | contextStore: &stubContextStore{}, 327 | authAPI: &stubAPI{acquireStatus: SEC_E_INSUFFICIENT_MEMORY}, 328 | KrbPrincipal: "service@test.local", 329 | } 330 | _, err := New(&config) 331 | if err == nil { 332 | t.Errorf("New() returns nil (no error) when AcquireCredentialHandle fails, wanted error") 333 | } 334 | } 335 | 336 | func TestFree_ErrorOnFreeCredentials(t *testing.T) { 337 | config := Config{ 338 | contextStore: &stubContextStore{}, 339 | authAPI: &stubAPI{freeCredsStatus: SEC_E_INVALID_HANDLE}, 340 | KrbPrincipal: "service@test.local", 341 | } 342 | auth, err := New(&config) 343 | if err != nil { 344 | t.Fatalf("New() failed with valid config, error: %v", err) 345 | } 346 | err = auth.Free() 347 | if err == nil { 348 | t.Error("Free() returns nil (no error) when FreeCredentialsHandle fails, wanted error") 349 | } 350 | } 351 | 352 | func TestFree_DeleteContexts(t *testing.T) { 353 | auth := newTestAuthenticator(t) 354 | ctx := CtxtHandle{42, 314} 355 | auth.StoreCtxHandle(&ctx) 356 | err := auth.Free() 357 | if err != nil { 358 | t.Fatalf("Free() failed with error %s, wanted no error", err) 359 | } 360 | if !auth.Config.authAPI.(*stubAPI).deleteCalled { 361 | t.Errorf("Free() did NOT call DeleteSecurityContext, wanted at least one call") 362 | } 363 | if len(auth.ctxList) > 0 { 364 | t.Errorf("Free() did not delete all contexts. Have %d contexts, wanted 0", len(auth.ctxList)) 365 | } 366 | } 367 | 368 | func TestFree_ErrorOnDeleteContexts(t *testing.T) { 369 | auth := newTestAuthenticator(t) 370 | auth.Config.authAPI.(*stubAPI).deleteStatus = SEC_E_INTERNAL_ERROR 371 | ctx := CtxtHandle{42, 314} 372 | auth.StoreCtxHandle(&ctx) 373 | err := auth.Free() 374 | if err == nil { 375 | t.Errorf("Free() returns no error when DeleteSecurityContext fails, wanted an error") 376 | } 377 | } 378 | 379 | func TestStoreCtxHandle(t *testing.T) { 380 | auth := newTestAuthenticator(t) 381 | ctx := CtxtHandle{42, 314} 382 | auth.StoreCtxHandle(&ctx) 383 | if len(auth.ctxList) == 0 || auth.ctxList[0] != ctx { 384 | t.Error("StoreCtxHandle() does not store the context handle") 385 | } 386 | } 387 | 388 | func TestStoreCtxHandle_NilHandle(t *testing.T) { 389 | auth := newTestAuthenticator(t) 390 | auth.StoreCtxHandle(nil) 391 | if len(auth.ctxList) > 0 { 392 | t.Errorf("StoreCtxHandle() stored a nil handle, got %v, wanted empty list", auth.ctxList) 393 | } 394 | } 395 | 396 | func TestStoreCtxHandle_EmptyHandle(t *testing.T) { 397 | auth := newTestAuthenticator(t) 398 | ctx := CtxtHandle{0, 0} 399 | auth.StoreCtxHandle(&ctx) 400 | if len(auth.ctxList) > 0 { 401 | t.Errorf("StoreCtxHandle() stored an empty handle, got %v, wanted empty list", auth.ctxList) 402 | } 403 | } 404 | 405 | func TestReleaseCtxHandle(t *testing.T) { 406 | auth := newTestAuthenticator(t) 407 | ctx := CtxtHandle{42, 314} 408 | auth.ctxList = append(auth.ctxList, ctx) 409 | err := auth.ReleaseCtxHandle(&ctx) 410 | if err != nil { 411 | t.Fatalf("ReleaseCtxHandle() returned error %s, wanted no error", err) 412 | } 413 | if len(auth.ctxList) > 0 { 414 | t.Errorf("ReleaseCtxHandle() did not clear the context list, got %v, wanted an empty list", auth.ctxList) 415 | } 416 | } 417 | 418 | func TestReleaseCtxHandle_ErrorOnDeleteContexts(t *testing.T) { 419 | auth := newTestAuthenticator(t) 420 | auth.Config.authAPI.(*stubAPI).deleteStatus = SEC_E_INTERNAL_ERROR 421 | ctx := CtxtHandle{42, 314} 422 | auth.ctxList = append(auth.ctxList, ctx) 423 | err := auth.ReleaseCtxHandle(&ctx) 424 | if err == nil { 425 | t.Errorf("ReleaseCtxHandle() returns no error when DeleteSecurityContext fails, wanted an error") 426 | } 427 | } 428 | 429 | func TestAcceptOrContinue_SetCtxHandle(t *testing.T) { 430 | auth := newTestAuthenticator(t) 431 | r := httptest.NewRequest("GET", "http://localhost:9000/", nil) 432 | w := httptest.NewRecorder() 433 | 434 | wantCtx := &CtxtHandle{42, 314} 435 | err := auth.SetCtxHandle(r, w, wantCtx) 436 | if err != nil { 437 | t.Fatalf("SetCtxHandle() failed with error %s, wanted no error", err) 438 | } 439 | value := auth.Config.contextStore.(*stubContextStore).contextHandle 440 | gotCtx, ok := value.(*CtxtHandle) 441 | if !ok || *gotCtx != *wantCtx { 442 | t.Errorf("SetCtxHandle() did not save the context value to the store, got = %v, want = %v", gotCtx, wantCtx) 443 | } 444 | } 445 | 446 | func TestAcceptOrContinue_ClearCtxHandle(t *testing.T) { 447 | auth := newTestAuthenticator(t) 448 | r := httptest.NewRequest("GET", "http://localhost:9000/", nil) 449 | w := httptest.NewRecorder() 450 | 451 | err := auth.SetCtxHandle(r, w, nil) 452 | if err != nil { 453 | t.Fatalf("SetCtxHandle() failed with error %s, wanted no error", err) 454 | } 455 | value := auth.Config.contextStore.(*stubContextStore).contextHandle 456 | gotCtx, ok := value.(*CtxtHandle) 457 | wantCtx := &CtxtHandle{0, 0} 458 | if !ok || *gotCtx != *wantCtx { 459 | t.Errorf("SetCtxHandle() did not clear context value in store, got = %v, want = %v", gotCtx, wantCtx) 460 | } 461 | } 462 | 463 | func TestAcceptOrContinue_GetCtxHandle(t *testing.T) { 464 | auth := newTestAuthenticator(t) 465 | r := httptest.NewRequest("GET", "http://localhost:9000/", nil) 466 | 467 | wantCtx := &CtxtHandle{42, 314} 468 | auth.Config.contextStore.(*stubContextStore).contextHandle = wantCtx 469 | gotCtx, err := auth.GetCtxHandle(r) 470 | if err != nil { 471 | t.Fatalf("GetCtxHandle() failed with error %s, wanted no error", err) 472 | } 473 | if gotCtx == nil { 474 | t.Fatalf("GetCtxHandle() returned nil context, wanted %v", *wantCtx) 475 | } 476 | if *gotCtx != *wantCtx { 477 | t.Errorf("GetCtxHandle() returned wrong context handle, got = %v, want = %v", gotCtx, wantCtx) 478 | } 479 | } 480 | 481 | func TestAcceptOrContinue_GetEmptyCtxHandle(t *testing.T) { 482 | auth := newTestAuthenticator(t) 483 | r := httptest.NewRequest("GET", "http://localhost:9000/", nil) 484 | 485 | wantCtx := &CtxtHandle{0, 0} 486 | auth.Config.contextStore.(*stubContextStore).contextHandle = wantCtx 487 | gotCtx, err := auth.GetCtxHandle(r) 488 | if err != nil { 489 | t.Fatalf("GetCtxHandle() failed with error %s, wanted no error", err) 490 | } 491 | if gotCtx != nil { 492 | t.Errorf("GetCtxHandle() returned %v for empty context handle, wanted nil", *gotCtx) 493 | } 494 | } 495 | 496 | func TestAcceptOrContinue_WithEmptyInput(t *testing.T) { 497 | auth := newTestAuthenticator(t) 498 | _, _, _, err := auth.AcceptOrContinue(nil, nil) 499 | // AcceptOrContinue should not panic on nil arguments 500 | if err == nil { 501 | t.Error("AcceptOrContinue(nil, nil) returned no error, should have returned an error") 502 | } 503 | } 504 | 505 | func TestAcceptOrContinue_WithOutputBuffer(t *testing.T) { 506 | wantData := [5]byte{2, 4, 8, 16, 32} 507 | buf := SecBuffer{uint32(len(wantData)), SECBUFFER_TOKEN, &wantData[0]} 508 | auth := newTestAuthenticator(t) 509 | auth.Config.authAPI.(*stubAPI).acceptOutBuf = &buf 510 | _, gotOut, _, _ := auth.AcceptOrContinue(nil, []byte{0}) 511 | if gotOut == nil { 512 | t.Fatalf("AcceptOrContinue() returned no output data, wanted %v", wantData) 513 | } 514 | if !bytes.Equal(gotOut, wantData[:]) { 515 | t.Errorf("AcceptOrContinue() got %v for output data, wanted %v", gotOut, wantData) 516 | } 517 | } 518 | 519 | func TestAcceptOrContinue_ErrorOnFreeBuffer(t *testing.T) { 520 | data := [1]byte{0} 521 | buf := SecBuffer{uint32(len(data)), SECBUFFER_TOKEN, &data[0]} 522 | auth := newTestAuthenticator(t) 523 | auth.Config.authAPI.(*stubAPI).acceptOutBuf = &buf 524 | auth.Config.authAPI.(*stubAPI).freeBufferStatus = SEC_E_INVALID_HANDLE 525 | _, _, _, err := auth.AcceptOrContinue(nil, []byte{0}) 526 | if err == nil { 527 | t.Error("AcceptOrContinue() returns no error when FreeContextBuffer fails, should have returned an error") 528 | } 529 | } 530 | 531 | func TestAcceptOrContinue_WithoutNewContext(t *testing.T) { 532 | auth := newTestAuthenticator(t) 533 | auth.Config.authAPI.(*stubAPI).acceptNewCtx = &CtxtHandle{0, 0} 534 | newCtx, _, _, _ := auth.AcceptOrContinue(nil, []byte{0}) 535 | if newCtx != nil { 536 | t.Error("AcceptOrContinue() returned a new context handle for a simulated call to AcceptSecurityContext that returns NULL") 537 | } 538 | } 539 | 540 | func TestAcceptOrContinue_WithNewContext(t *testing.T) { 541 | auth := newTestAuthenticator(t) 542 | auth.Config.authAPI.(*stubAPI).acceptNewCtx = &CtxtHandle{42, 314} 543 | gotNewCtx, _, _, _ := auth.AcceptOrContinue(nil, []byte{0}) 544 | if gotNewCtx == nil { 545 | t.Fatal("AcceptOrContinue() returned nil for new context handle for a simulated call to AcceptSecurityContext that returns a valid handle") 546 | } 547 | wantNewCtx := &CtxtHandle{42, 314} 548 | if *gotNewCtx != *wantNewCtx { 549 | t.Errorf("AcceptOrContinue() got new context handle = %v, want %v (returned by AcceptSecurityContext)", *gotNewCtx, *wantNewCtx) 550 | } 551 | } 552 | 553 | func TestAcceptOrContinue_OnErrorStatus(t *testing.T) { 554 | auth := newTestAuthenticator(t) 555 | tests := []struct { 556 | name string 557 | errorStatus SECURITY_STATUS 558 | }{ 559 | {"SEC_E_INCOMPLETE_MESSAGE", SEC_E_INCOMPLETE_MESSAGE}, 560 | {"SEC_E_INSUFFICIENT_MEMORY", SEC_E_INSUFFICIENT_MEMORY}, 561 | {"SEC_E_INTERNAL_ERROR", SEC_E_INTERNAL_ERROR}, 562 | {"SEC_E_INVALID_HANDLE", SEC_E_INVALID_HANDLE}, 563 | {"SEC_E_INVALID_TOKEN", SEC_E_INVALID_TOKEN}, 564 | {"SEC_E_LOGON_DENIED", SEC_E_LOGON_DENIED}, 565 | {"SEC_E_NOT_OWNER", SEC_E_NOT_OWNER}, 566 | {"SEC_E_NO_AUTHENTICATING_AUTHORITY", SEC_E_NO_AUTHENTICATING_AUTHORITY}, 567 | {"SEC_E_NO_CREDENTIALS", SEC_E_NO_CREDENTIALS}, 568 | {"SEC_E_SECPKG_NOT_FOUND", SEC_E_SECPKG_NOT_FOUND}, 569 | {"SEC_E_UNKNOWN_CREDENTIALS", SEC_E_UNKNOWN_CREDENTIALS}, 570 | {"SEC_E_UNSUPPORTED_FUNCTION", SEC_E_UNSUPPORTED_FUNCTION}, 571 | } 572 | for _, tt := range tests { 573 | t.Run(tt.name, func(t *testing.T) { 574 | auth.Config.authAPI.(*stubAPI).acceptStatus = tt.errorStatus 575 | _, _, _, err := auth.AcceptOrContinue(nil, []byte{0}) 576 | if err == nil { 577 | t.Errorf("AcceptOrContinue() returns no error when AcceptSecurityContext fails with %s", tt.name) 578 | } 579 | }) 580 | } 581 | } 582 | 583 | func TestGetFlags_ErrorOnQueryAttributes(t *testing.T) { 584 | auth := newTestAuthenticator(t) 585 | auth.Config.authAPI.(*stubAPI).queryStatus = SEC_E_INTERNAL_ERROR 586 | _, err := auth.GetFlags(&CtxtHandle{0, 0}) 587 | if err == nil { 588 | t.Errorf("GetFlags() returns no error when QueryContextAttributes fails, wanted an error") 589 | } 590 | } 591 | 592 | func TestGetUsername_ErrorOnQueryAttributes(t *testing.T) { 593 | auth := newTestAuthenticator(t) 594 | auth.Config.authAPI.(*stubAPI).queryStatus = SEC_E_INTERNAL_ERROR 595 | _, err := auth.GetUsername(&CtxtHandle{0, 0}) 596 | if err == nil { 597 | t.Errorf("GetUsername() returns no error when QueryContextAttributes fails, wanted an error") 598 | } 599 | } 600 | 601 | func TestGetUsername_ErrorOnFreeBuffer(t *testing.T) { 602 | auth := newTestAuthenticator(t) 603 | auth.Config.authAPI.(*stubAPI).freeBufferStatus = SEC_E_INTERNAL_ERROR 604 | _, err := auth.GetUsername(&CtxtHandle{0, 0}) 605 | if err == nil { 606 | t.Errorf("GetUsername() returns no error when FreeContextBuffer fails, wanted an error") 607 | } 608 | } 609 | 610 | func TestGetUsername_Valid(t *testing.T) { 611 | auth := newTestAuthenticator(t) 612 | got, err := auth.GetUsername(&CtxtHandle{0, 0}) 613 | if err != nil { 614 | t.Fatalf("GetUsername() failed with error %q, wanted no error", err) 615 | } 616 | if got != "testuser" { 617 | t.Errorf("GetUsername() got %s, want %s", got, "testuser") 618 | } 619 | } 620 | 621 | func TestGetUserGroups_NilBuf(t *testing.T) { 622 | auth := newTestAuthenticator(t) 623 | auth.Config.authAPI.(*stubAPI).getGroupsBuf = nil 624 | auth.Config.authAPI.(*stubAPI).getGroupsEntries = 0 625 | auth.Config.authAPI.(*stubAPI).getGroupsTotal = 0 626 | 627 | _, err := auth.GetUserGroups("testuser") 628 | 629 | if err == nil { 630 | t.Errorf("GetUserGroups() returns no error when bufptr is nil, wanted an error") 631 | } 632 | } 633 | 634 | func TestGetUserGroups_PartialRead(t *testing.T) { 635 | auth := newTestAuthenticator(t) 636 | auth.Config.authAPI.(*stubAPI).getGroupsEntries = 1 637 | 638 | _, err := auth.GetUserGroups("testuser") 639 | 640 | if err == nil { 641 | t.Errorf("GetUserGroups() returns no error when entries read (%d) < total entries (%d), wanted an error", 1, 3) 642 | } 643 | } 644 | 645 | func TestGetGroups(t *testing.T) { 646 | token1 := SecPkgContext_AccessToken{1} 647 | 648 | auth := newTestAuthenticator(t) 649 | auth.Config.authAPI.(*stubAPI).queryStatus = 0 650 | auth.Config.authAPI.(*stubAPI).queryOutBuf = (*byte)(unsafe.Pointer(&token1)) 651 | 652 | groups, err := auth.GetGroups(nil) 653 | if err != nil { 654 | t.Fatal("GetGroups() returns an error.") 655 | } 656 | 657 | equals := true 658 | shouldBe := resolvedGroupsWoAdmin 659 | if len(groups) == len(shouldBe) { 660 | equals = reflect.DeepEqual(shouldBe, groups) 661 | } else { 662 | equals = false 663 | } 664 | 665 | if !equals { 666 | t.Fatalf("GetGroups() returns %+v instead of %+v", groups, shouldBe) 667 | } 668 | } 669 | 670 | func TestGetUserGroups_ErrorOnGetGroups(t *testing.T) { 671 | auth := newTestAuthenticator(t) 672 | auth.Config.authAPI.(*stubAPI).getGroupsErr = errors.New("simulated error") 673 | 674 | _, err := auth.GetUserGroups("testuser") 675 | 676 | if err == nil { 677 | t.Error("GetUserGroups() returns no error when NetUserGetGroups fails, wanted an error") 678 | } 679 | } 680 | 681 | func TestGetUserGroups_ErrorOnBufferFree(t *testing.T) { 682 | auth := newTestAuthenticator(t) 683 | auth.Config.authAPI.(*stubAPI).netBufferFreeErr = errors.New("simulated error") 684 | 685 | _, err := auth.GetUserGroups("testuser") 686 | 687 | if err == nil { 688 | t.Error("GetUserGroups() returns no error when NetApiBufferFree fails, wanted an error") 689 | } 690 | } 691 | 692 | func TestGetUserGroups_Valid(t *testing.T) { 693 | want := []string{"group1", "group2", "group3"} 694 | auth := newTestAuthenticator(t) 695 | 696 | got, err := auth.GetUserGroups("testuser") 697 | 698 | if err != nil { 699 | t.Fatalf("GetUserGroups() failed with error %q, wanted no error", err) 700 | } 701 | if !reflect.DeepEqual(got, want) { 702 | t.Errorf("GetUserGroups() got %v, want %v", got, want) 703 | } 704 | } 705 | 706 | func TestReturn401_Headers(t *testing.T) { 707 | auth := newTestAuthenticator(t) 708 | w := httptest.NewRecorder() 709 | 710 | auth.Return401(w, "") 711 | 712 | got := w.Header().Get("WWW-Authenticate") 713 | if !strings.HasPrefix(got, "Negotiate") { 714 | t.Errorf("Return401() returned a WWW-Authenticate header that does not start with Negotiate, got = %q", got) 715 | } 716 | } 717 | 718 | func TestReturn401_WithOutputData(t *testing.T) { 719 | auth := newTestAuthenticator(t) 720 | w := httptest.NewRecorder() 721 | 722 | auth.Return401(w, "output-token") 723 | 724 | got := w.Header().Get("WWW-Authenticate") 725 | if !strings.Contains(got, "output-token") { 726 | t.Errorf("The header returned by Return401() does not contain the output token, got = %q", got) 727 | } 728 | } 729 | 730 | func TestAuthenticate_NoAuthHeader(t *testing.T) { 731 | auth := newTestAuthenticator(t) 732 | 733 | r := httptest.NewRequest("GET", "http://example.local/", nil) 734 | 735 | _, _, err := auth.Authenticate(r, nil) 736 | if err == nil { 737 | t.Error("Authenticate() returned nil (no error) for request without Authorization header, wanted an error") 738 | } 739 | } 740 | 741 | func TestAuthenticate_MultipleAuthHeaders(t *testing.T) { 742 | auth := newTestAuthenticator(t) 743 | 744 | r := httptest.NewRequest("GET", "http://example.local/", nil) 745 | r.Header.Add("Authorization", "Negotiate a87421000492aa874209af8bc028") 746 | r.Header.Add("Authorization", "Negotiate a87421000492aa874209af8bc028") 747 | 748 | _, _, err := auth.Authenticate(r, nil) 749 | if err == nil { 750 | t.Error("Authenticate() returned nil (no error) for request with multiple Authorization headers, wanted an error") 751 | } 752 | } 753 | 754 | func TestAuthenticate_EmptyAuthHeader(t *testing.T) { 755 | auth := newTestAuthenticator(t) 756 | 757 | r := httptest.NewRequest("GET", "http://example.local/", nil) 758 | r.Header.Set("Authorization", "") 759 | 760 | _, _, err := auth.Authenticate(r, nil) 761 | if err == nil { 762 | t.Error("Authenticate() returned nil (no error) for request with empty Authorization header, wanted an error") 763 | } 764 | } 765 | 766 | func TestAuthenticate_BadAuthPrefix(t *testing.T) { 767 | auth := newTestAuthenticator(t) 768 | 769 | r := httptest.NewRequest("GET", "http://example.local/", nil) 770 | r.Header.Set("Authorization", "auth: neg") 771 | 772 | _, _, err := auth.Authenticate(r, nil) 773 | if err == nil { 774 | t.Error("Authenticate() returned nil (no error) for request with bad Authorization header, wanted an error") 775 | } 776 | } 777 | 778 | func TestAuthenticate_EmptyToken(t *testing.T) { 779 | auth := newTestAuthenticator(t) 780 | 781 | tests := []struct { 782 | name string 783 | value string 784 | }{ 785 | {"No space delimiter and no token", "Negotiate"}, 786 | {"Space delimiter, but no token", "Negotiate "}, 787 | {"Double space delimiter, but no token", "Negotiate "}, 788 | } 789 | for _, tt := range tests { 790 | t.Run(tt.name, func(t *testing.T) { 791 | r := httptest.NewRequest("GET", "http://example.local/", nil) 792 | r.Header.Set("Authorization", tt.value) 793 | 794 | _, _, err := auth.Authenticate(r, nil) 795 | if err == nil { 796 | t.Errorf( 797 | "Authenticate() returned nil (no error) for request with bad Authorization header (%v), wanted an error", 798 | tt.name, 799 | ) 800 | } 801 | }) 802 | } 803 | } 804 | 805 | func TestAuthenticate_BadBase64(t *testing.T) { 806 | auth := newTestAuthenticator(t) 807 | 808 | r := httptest.NewRequest("GET", "http://example.local/", nil) 809 | r.Header.Set("Authorization", "Negotiate a874-210004-92aa8742-09af8-bc028") 810 | 811 | _, _, err := auth.Authenticate(r, nil) 812 | if err == nil { 813 | t.Error("Authenticate() returned nil (no error) for request with token that is not valid base64 string, wanted an error") 814 | } 815 | } 816 | 817 | func TestAuthenticate_ErrorGetCtxHandle(t *testing.T) { 818 | auth := newTestAuthenticator(t) 819 | auth.Config.contextStore.(*stubContextStore).getError = errors.New("internal error") 820 | r := httptest.NewRequest("GET", "http://example.local/", nil) 821 | r.Header.Set("Authorization", "Negotiate a87421000492aa874209af8bc028") 822 | _, _, err := auth.Authenticate(r, nil) 823 | if err == nil { 824 | t.Error("Authenticate() returns nil (no error) when GetCtxHandle fails, wanted an error") 825 | } 826 | } 827 | 828 | func TestAuthenticate_ErrorSetCtxHandle(t *testing.T) { 829 | auth := newTestAuthenticator(t) 830 | auth.Config.authAPI.(*stubAPI).acceptNewCtx = &CtxtHandle{42, 314} 831 | auth.Config.contextStore.(*stubContextStore).setError = errors.New("internal error") 832 | r := httptest.NewRequest("GET", "http://example.local/", nil) 833 | r.Header.Set("Authorization", "Negotiate a87421000492aa874209af8bc028") 834 | _, _, err := auth.Authenticate(r, nil) 835 | if err == nil { 836 | t.Error("Authenticate() returns nil (no error) when SetCtxHandle fails, wanted an error") 837 | } 838 | } 839 | 840 | func TestAuthenticate_WithContinueAndOutputToken(t *testing.T) { 841 | wantData := [5]byte{2, 4, 8, 16, 32} 842 | buf := SecBuffer{uint32(len(wantData)), SECBUFFER_TOKEN, &wantData[0]} 843 | auth := newTestAuthenticator(t) 844 | auth.Config.authAPI.(*stubAPI).acceptStatus = SEC_I_CONTINUE_NEEDED 845 | auth.Config.authAPI.(*stubAPI).acceptOutBuf = &buf 846 | r := httptest.NewRequest("GET", "http://example.local/", nil) 847 | r.Header.Set("Authorization", "Negotiate a87421000492aa874209af8bc028") 848 | _, gotTokenB64, err := auth.Authenticate(r, nil) 849 | if err == nil { 850 | t.Fatal("Authenticate() returns nil (no error) on SEC_I_CONTINUE_NEEDED") 851 | } 852 | if !strings.Contains(err.Error(), "continue") { 853 | t.Errorf("Authenticate() returned wrong error value on SEC_I_CONTINUE_NEEDED, got = %q, want = %q", err, "Negotiation should continue") 854 | } 855 | if gotTokenB64 == "" { 856 | t.Error("Authenticate() returns no output token on SEC_I_CONTINUE_NEEDED") 857 | } 858 | wantTokenB64 := "AgQIECA=" 859 | if gotTokenB64 != wantTokenB64 { 860 | t.Errorf("Authenticate() got output token = %q, want %q", gotTokenB64, wantTokenB64) 861 | } 862 | } 863 | 864 | func TestAuthenticate_OnErrorStatus(t *testing.T) { 865 | auth := newTestAuthenticator(t) 866 | r := httptest.NewRequest("GET", "http://example.local/", nil) 867 | r.Header.Set("Authorization", "Negotiate a87421000492aa874209af8bc028") 868 | 869 | tests := []struct { 870 | name string 871 | errorStatus SECURITY_STATUS 872 | }{ 873 | {"SEC_E_INCOMPLETE_MESSAGE", SEC_E_INCOMPLETE_MESSAGE}, 874 | {"SEC_E_INSUFFICIENT_MEMORY", SEC_E_INSUFFICIENT_MEMORY}, 875 | {"SEC_E_INTERNAL_ERROR", SEC_E_INTERNAL_ERROR}, 876 | {"SEC_E_INVALID_HANDLE", SEC_E_INVALID_HANDLE}, 877 | {"SEC_E_INVALID_TOKEN", SEC_E_INVALID_TOKEN}, 878 | {"SEC_E_LOGON_DENIED", SEC_E_LOGON_DENIED}, 879 | {"SEC_E_NOT_OWNER", SEC_E_NOT_OWNER}, 880 | {"SEC_E_NO_AUTHENTICATING_AUTHORITY", SEC_E_NO_AUTHENTICATING_AUTHORITY}, 881 | {"SEC_E_NO_CREDENTIALS", SEC_E_NO_CREDENTIALS}, 882 | {"SEC_E_SECPKG_NOT_FOUND", SEC_E_SECPKG_NOT_FOUND}, 883 | {"SEC_E_UNKNOWN_CREDENTIALS", SEC_E_UNKNOWN_CREDENTIALS}, 884 | {"SEC_E_UNSUPPORTED_FUNCTION", SEC_E_UNSUPPORTED_FUNCTION}, 885 | } 886 | for _, tt := range tests { 887 | t.Run(tt.name, func(t *testing.T) { 888 | auth.Config.authAPI.(*stubAPI).acceptStatus = tt.errorStatus 889 | _, _, err := auth.Authenticate(r, nil) 890 | if err == nil { 891 | t.Errorf("Authenticate() returns no error when AcceptSecurityContext fails with %s", tt.name) 892 | } 893 | }) 894 | } 895 | } 896 | 897 | func TestAuthenticate_ValidBase64(t *testing.T) { 898 | auth := newTestAuthenticator(t) 899 | 900 | r := httptest.NewRequest("GET", "http://example.local/", nil) 901 | r.Header.Set("Authorization", "Negotiate a87421000492aa874209af8bc028") 902 | 903 | _, _, err := auth.Authenticate(r, nil) 904 | if err != nil { 905 | t.Errorf( 906 | "Authenticate() returned error %q for request with valid base64 string, wanted nil (no error)", 907 | err, 908 | ) 909 | } 910 | } 911 | 912 | func TestAuthenticate_ValidToken(t *testing.T) { 913 | auth := newTestAuthenticator(t) 914 | 915 | r := httptest.NewRequest("GET", "http://example.local/", nil) 916 | r.Header.Set("Authorization", "Negotiate a87421000492aa874209af8bc028") 917 | 918 | _, _, err := auth.Authenticate(r, nil) 919 | if err != nil { 920 | t.Errorf( 921 | "Authenticate() with valid token returned error %q, wanted nil (no error)", 922 | err, 923 | ) 924 | } 925 | } 926 | 927 | func TestAuthenticate_ReturnOutputOnSecEOK(t *testing.T) { 928 | data := [1]byte{0} 929 | buf := SecBuffer{uint32(len(data)), SECBUFFER_TOKEN, &data[0]} 930 | auth := newTestAuthenticator(t) 931 | auth.Config.authAPI.(*stubAPI).acceptStatus = SEC_E_OK 932 | auth.Config.authAPI.(*stubAPI).acceptOutBuf = &buf 933 | 934 | r := httptest.NewRequest("GET", "http://example.local/", nil) 935 | r.Header.Set("Authorization", "Negotiate a87421000492aa874209af8bc028") 936 | 937 | want := "AA==" 938 | _, output, _ := auth.Authenticate(r, nil) 939 | if output == "" { 940 | t.Errorf("Authenticate() returns empty output token when AcceptSecurityContext returns SEC_E_OK with output buffer, wanted %q", want) 941 | } 942 | } 943 | 944 | func TestWithAuth_ValidToken(t *testing.T) { 945 | data := [1]byte{0} 946 | buf := SecBuffer{uint32(len(data)), SECBUFFER_TOKEN, &data[0]} 947 | auth := newTestAuthenticator(t) 948 | auth.Config.authAPI.(*stubAPI).acceptOutBuf = &buf 949 | auth.Config.AuthUserKey = "REMOTE_USER" 950 | 951 | r := httptest.NewRequest("GET", "http://example.local/", nil) 952 | r.Header.Set("Authorization", "Negotiate a87421000492aa874209af8bc028") 953 | w := httptest.NewRecorder() 954 | 955 | handlerCalled := false 956 | gotUsername := "" 957 | gotRemoteUser := "" 958 | gotGroups := []string{} 959 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 960 | handlerCalled = true 961 | info := r.Context().Value(UserInfoKey) 962 | userInfo, ok := info.(*UserInfo) 963 | if ok && userInfo != nil { 964 | gotUsername = userInfo.Username 965 | gotGroups = userInfo.Groups 966 | } 967 | gotRemoteUser = r.Header.Get("REMOTE_USER") 968 | }) 969 | protectedHandler := auth.WithAuth(handler) 970 | protectedHandler.ServeHTTP(w, r) 971 | 972 | _ = auth.Free() 973 | 974 | code := w.Result().StatusCode 975 | if code != http.StatusOK { 976 | t.Errorf( 977 | "Got status %v for request with valid token, wanted StatusOK (%v)", 978 | code, 979 | http.StatusOK, 980 | ) 981 | } 982 | 983 | if code != http.StatusOK && handlerCalled { 984 | t.Error("Handler was called, when status code was not OK. Handler should not be called for error status codes.") 985 | } else if code == http.StatusOK && !handlerCalled { 986 | t.Error("Handler was not called, even though token was valid") 987 | } 988 | 989 | wantUsername := "testuser" 990 | if gotUsername != wantUsername { 991 | t.Errorf("Username stored in request context is %q, want %q", gotUsername, wantUsername) 992 | } 993 | 994 | wantGroups := []string{"group1", "group2", "group3"} 995 | if !reflect.DeepEqual(gotGroups, wantGroups) { 996 | t.Errorf("Groups stored in request context are %q, want %q", gotGroups, wantGroups) 997 | } 998 | 999 | wantRemoteUser := "testuser" 1000 | if gotRemoteUser != wantRemoteUser { 1001 | t.Errorf("Username in REMOTE_USER header is %q, want %q", gotRemoteUser, wantRemoteUser) 1002 | } 1003 | 1004 | wantHeader := "AA==" 1005 | gotHeader := w.Header().Get("WWW-Authenticate") 1006 | if !strings.Contains(gotHeader, wantHeader) { 1007 | t.Errorf("WithAuth() does not return output token when AcceptSecurityContext returns SEC_E_OK with output buffer, wanted token %q", wantHeader) 1008 | } 1009 | } 1010 | 1011 | func TestWithAuth_OnErrorStatus(t *testing.T) { 1012 | auth := newTestAuthenticator(t) 1013 | r := httptest.NewRequest("GET", "http://example.local/", nil) 1014 | r.Header.Set("Authorization", "Negotiate a87421000492aa874209af8bc028") 1015 | 1016 | tests := []struct { 1017 | name string 1018 | errorStatus SECURITY_STATUS 1019 | }{ 1020 | {"SEC_E_INCOMPLETE_MESSAGE", SEC_E_INCOMPLETE_MESSAGE}, 1021 | {"SEC_E_INSUFFICIENT_MEMORY", SEC_E_INSUFFICIENT_MEMORY}, 1022 | {"SEC_E_INTERNAL_ERROR", SEC_E_INTERNAL_ERROR}, 1023 | {"SEC_E_INVALID_HANDLE", SEC_E_INVALID_HANDLE}, 1024 | {"SEC_E_INVALID_TOKEN", SEC_E_INVALID_TOKEN}, 1025 | {"SEC_E_LOGON_DENIED", SEC_E_LOGON_DENIED}, 1026 | {"SEC_E_NOT_OWNER", SEC_E_NOT_OWNER}, 1027 | {"SEC_E_NO_AUTHENTICATING_AUTHORITY", SEC_E_NO_AUTHENTICATING_AUTHORITY}, 1028 | {"SEC_E_NO_CREDENTIALS", SEC_E_NO_CREDENTIALS}, 1029 | {"SEC_E_SECPKG_NOT_FOUND", SEC_E_SECPKG_NOT_FOUND}, 1030 | {"SEC_E_UNKNOWN_CREDENTIALS", SEC_E_UNKNOWN_CREDENTIALS}, 1031 | {"SEC_E_UNSUPPORTED_FUNCTION", SEC_E_UNSUPPORTED_FUNCTION}, 1032 | } 1033 | for _, tt := range tests { 1034 | t.Run(tt.name, func(t *testing.T) { 1035 | auth.Config.authAPI.(*stubAPI).acceptStatus = tt.errorStatus 1036 | w := httptest.NewRecorder() 1037 | 1038 | handlerCalled := false 1039 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1040 | handlerCalled = true 1041 | }) 1042 | protectedHandler := auth.WithAuth(handler) 1043 | protectedHandler.ServeHTTP(w, r) 1044 | 1045 | _ = auth.Free() 1046 | 1047 | code := w.Result().StatusCode 1048 | if code != http.StatusUnauthorized { 1049 | t.Errorf( 1050 | "Got HTTP status %v for unauthorized request (when AcceptSecurityContext = 0x%x), wanted http.StatusUnauthorized (%v)", 1051 | code, 1052 | tt.errorStatus, 1053 | http.StatusUnauthorized, 1054 | ) 1055 | } 1056 | 1057 | if code == http.StatusUnauthorized && handlerCalled { 1058 | t.Error("Handler was called, when status code was StatusUnauthorized. Handler should not be called for error status codes.") 1059 | } 1060 | }) 1061 | } 1062 | } 1063 | -------------------------------------------------------------------------------- /websspi_windows.go: -------------------------------------------------------------------------------- 1 | package websspi 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "encoding/gob" 7 | "errors" 8 | "fmt" 9 | "log" 10 | "net/http" 11 | "os/user" 12 | "reflect" 13 | "strings" 14 | "sync" 15 | "syscall" 16 | "time" 17 | "unsafe" 18 | 19 | "github.com/quasoft/websspi/secctx" 20 | ) 21 | 22 | // The Config object determines the behaviour of the Authenticator. 23 | // 24 | // To resolve group membership of authenticated principals, set EnumerateGroups to true. 25 | // Currently there are two options to resolve group membership - both return different results. 26 | // To resolve the "static" local or AD group membership, additionally set "ServerName" to a Windows server or Active Directory. 27 | // 28 | type Config struct { 29 | contextStore secctx.Store 30 | authAPI API 31 | KrbPrincipal string // Name of Kerberos principle used by the service (optional). 32 | AuthUserKey string // Key of header to fill with authenticated username, eg. "X-Authenticated-User" or "REMOTE_USER" (optional). 33 | EnumerateGroups bool // If true, groups the user is a member of are enumerated and stored in request context (default false) 34 | ServerName string // Specifies the DNS or NetBIOS name of the remote server which to query about user groups. Use an empty value to query the groups granted on a real login. Ignored if EnumerateGroups is false. 35 | ResolveLinked bool // Resolve a linked token. 36 | } 37 | 38 | // NewConfig creates a configuration object with default values. 39 | func NewConfig() *Config { 40 | return &Config{ 41 | contextStore: secctx.NewCookieStore(), 42 | authAPI: &Win32{}, 43 | } 44 | } 45 | 46 | // Validate makes basic validation of configuration to make sure that important and required fields 47 | // have been set with values in expected format. 48 | func (c *Config) Validate() error { 49 | if c.contextStore == nil { 50 | return errors.New("Store for context handles not specified in Config") 51 | } 52 | if c.authAPI == nil { 53 | return errors.New("Authentication API not specified in Config") 54 | } 55 | return nil 56 | } 57 | 58 | // contextKey represents a custom key for values stored in context.Context 59 | type contextKey string 60 | 61 | func (c contextKey) String() string { 62 | return "websspi-key-" + string(c) 63 | } 64 | 65 | var ( 66 | UserInfoKey = contextKey("UserInfo") 67 | ) 68 | 69 | // The Authenticator type provides middleware methods for authentication of http requests. 70 | // A single authenticator object can be shared by concurrent goroutines. 71 | type Authenticator struct { 72 | Config Config 73 | serverCred *CredHandle 74 | credExpiry *time.Time 75 | ctxList []CtxtHandle 76 | ctxListMux *sync.Mutex 77 | } 78 | 79 | // New creates a new Authenticator object with the given configuration options. 80 | func New(config *Config) (*Authenticator, error) { 81 | err := config.Validate() 82 | if err != nil { 83 | return nil, fmt.Errorf("invalid config: %v", err) 84 | } 85 | 86 | var auth = &Authenticator{ 87 | Config: *config, 88 | ctxListMux: &sync.Mutex{}, 89 | } 90 | 91 | err = auth.PrepareCredentials(config.KrbPrincipal) 92 | if err != nil { 93 | return nil, fmt.Errorf("could not acquire credentials handle for the service: %v", err) 94 | } 95 | log.Printf("Credential handle expiry: %v\n", *auth.credExpiry) 96 | 97 | return auth, nil 98 | } 99 | 100 | // PrepareCredentials method acquires a credentials handle for the specified principal 101 | // for use during the live of the application. 102 | // On success stores the handle in the serverCred field and its expiry time in the 103 | // credExpiry field. 104 | // This method must be called once - when the application is starting or when the first 105 | // request from a client is received. 106 | func (a *Authenticator) PrepareCredentials(principal string) error { 107 | var principalPtr *uint16 108 | if principal != "" { 109 | var err error 110 | principalPtr, err = syscall.UTF16PtrFromString(principal) 111 | if err != nil { 112 | return err 113 | } 114 | } 115 | credentialUsePtr, err := syscall.UTF16PtrFromString(NEGOSSP_NAME) 116 | if err != nil { 117 | return err 118 | } 119 | var handle CredHandle 120 | var expiry syscall.Filetime 121 | status := a.Config.authAPI.AcquireCredentialsHandle( 122 | principalPtr, 123 | credentialUsePtr, 124 | SECPKG_CRED_INBOUND, 125 | nil, // logonId 126 | nil, // authData 127 | 0, // getKeyFn 128 | 0, // getKeyArgument 129 | &handle, 130 | &expiry, 131 | ) 132 | if status != SEC_E_OK { 133 | return fmt.Errorf("call to AcquireCredentialsHandle failed with code 0x%x", status) 134 | } 135 | expiryTime := time.Unix(0, expiry.Nanoseconds()) 136 | a.credExpiry = &expiryTime 137 | a.serverCred = &handle 138 | return nil 139 | } 140 | 141 | // Free method should be called before shutting down the server to let 142 | // it release allocated Win32 resources 143 | func (a *Authenticator) Free() error { 144 | var status SECURITY_STATUS 145 | a.ctxListMux.Lock() 146 | for _, ctx := range a.ctxList { 147 | // TODO: Also check for stale security contexts and delete them periodically 148 | status = a.Config.authAPI.DeleteSecurityContext(&ctx) 149 | if status != SEC_E_OK { 150 | return fmt.Errorf("call to DeleteSecurityContext failed with code 0x%x", status) 151 | } 152 | } 153 | a.ctxList = nil 154 | a.ctxListMux.Unlock() 155 | if a.serverCred != nil { 156 | status = a.Config.authAPI.FreeCredentialsHandle(a.serverCred) 157 | if status != SEC_E_OK { 158 | return fmt.Errorf("call to FreeCredentialsHandle failed with code 0x%x", status) 159 | } 160 | a.serverCred = nil 161 | } 162 | return nil 163 | } 164 | 165 | // StoreCtxHandle stores the specified context to the internal list (ctxList) 166 | func (a *Authenticator) StoreCtxHandle(handle *CtxtHandle) { 167 | if handle == nil || *handle == (CtxtHandle{}) { 168 | // Should not add nil or empty handle 169 | return 170 | } 171 | a.ctxListMux.Lock() 172 | defer a.ctxListMux.Unlock() 173 | a.ctxList = append(a.ctxList, *handle) 174 | } 175 | 176 | // ReleaseCtxHandle deletes a context handle and removes it from the internal list (ctxList) 177 | func (a *Authenticator) ReleaseCtxHandle(handle *CtxtHandle) error { 178 | if handle == nil || *handle == (CtxtHandle{}) { 179 | // Removing a nil or empty handle is not an error condition 180 | return nil 181 | } 182 | a.ctxListMux.Lock() 183 | defer a.ctxListMux.Unlock() 184 | 185 | // First, try to delete the handle 186 | status := a.Config.authAPI.DeleteSecurityContext(handle) 187 | if status != SEC_E_OK { 188 | return fmt.Errorf("call to DeleteSecurityContext failed with code 0x%x", status) 189 | } 190 | 191 | // Then remove it from the internal list 192 | foundAt := -1 193 | for i, ctx := range a.ctxList { 194 | if ctx == *handle { 195 | foundAt = i 196 | break 197 | } 198 | } 199 | if foundAt > -1 { 200 | a.ctxList[foundAt] = a.ctxList[len(a.ctxList)-1] 201 | a.ctxList = a.ctxList[:len(a.ctxList)-1] 202 | } 203 | return nil 204 | } 205 | 206 | // AcceptOrContinue tries to validate the auth-data token by calling the AcceptSecurityContext 207 | // function and returns and error if validation failed or continuation of the negotiation is needed. 208 | // No error is returned if the token was validated (user was authenticated). 209 | func (a *Authenticator) AcceptOrContinue(context *CtxtHandle, authData []byte) (newCtx *CtxtHandle, out []byte, exp *time.Time, err error) { 210 | if authData == nil { 211 | err = errors.New("input token cannot be nil") 212 | return 213 | } 214 | 215 | var inputDesc SecBufferDesc 216 | var inputBuf SecBuffer 217 | inputDesc.BuffersCount = 1 218 | inputDesc.Version = SECBUFFER_VERSION 219 | inputDesc.Buffers = &inputBuf 220 | inputBuf.BufferSize = uint32(len(authData)) 221 | inputBuf.BufferType = SECBUFFER_TOKEN 222 | inputBuf.Buffer = &authData[0] 223 | 224 | var outputDesc SecBufferDesc 225 | var outputBuf SecBuffer 226 | outputDesc.BuffersCount = 1 227 | outputDesc.Version = SECBUFFER_VERSION 228 | outputDesc.Buffers = &outputBuf 229 | outputBuf.BufferSize = 0 230 | outputBuf.BufferType = SECBUFFER_TOKEN 231 | outputBuf.Buffer = nil 232 | 233 | var expiry syscall.Filetime 234 | var contextAttr uint32 235 | var newContextHandle CtxtHandle 236 | 237 | var status = a.Config.authAPI.AcceptSecurityContext( 238 | a.serverCred, 239 | context, 240 | &inputDesc, 241 | ASC_REQ_ALLOCATE_MEMORY|ASC_REQ_MUTUAL_AUTH|ASC_REQ_CONFIDENTIALITY| 242 | ASC_REQ_INTEGRITY|ASC_REQ_REPLAY_DETECT|ASC_REQ_SEQUENCE_DETECT, // contextReq uint32, 243 | SECURITY_NATIVE_DREP, // targDataRep uint32, 244 | &newContextHandle, 245 | &outputDesc, // *SecBufferDesc 246 | &contextAttr, // contextAttr *uint32, 247 | &expiry, // *syscall.Filetime 248 | ) 249 | if newContextHandle.Lower != 0 || newContextHandle.Upper != 0 { 250 | newCtx = &newContextHandle 251 | } 252 | tm := time.Unix(0, expiry.Nanoseconds()) 253 | exp = &tm 254 | if status == SEC_E_OK || status == SEC_I_CONTINUE_NEEDED { 255 | // Copy outputBuf.Buffer to out and free the outputBuf.Buffer 256 | out = make([]byte, outputBuf.BufferSize) 257 | var bufPtr = unsafe.Pointer(outputBuf.Buffer) 258 | for i := 0; i < len(out); i++ { 259 | out[i] = *(*byte)(bufPtr) 260 | bufPtr = unsafe.Pointer(uintptr(bufPtr) + 1) 261 | } 262 | } 263 | if outputBuf.Buffer != nil { 264 | freeStatus := a.Config.authAPI.FreeContextBuffer(outputBuf.Buffer) 265 | if freeStatus != SEC_E_OK { 266 | status = freeStatus 267 | err = fmt.Errorf("could not free output buffer; FreeContextBuffer() failed with code: 0x%x", freeStatus) 268 | return 269 | } 270 | } 271 | if status == SEC_I_CONTINUE_NEEDED { 272 | err = errors.New("Negotiation should continue") 273 | return 274 | } else if status != SEC_E_OK { 275 | err = fmt.Errorf("call to AcceptSecurityContext failed with code 0x%x", status) 276 | return 277 | } 278 | // TODO: Check contextAttr? 279 | return 280 | } 281 | 282 | // GetCtxHandle retrieves the context handle for this client from request's cookies 283 | func (a *Authenticator) GetCtxHandle(r *http.Request) (*CtxtHandle, error) { 284 | sessionHandle, err := a.Config.contextStore.GetHandle(r) 285 | if err != nil { 286 | return nil, fmt.Errorf("could not get context handle from session: %s", err) 287 | } 288 | if contextHandle, ok := sessionHandle.(*CtxtHandle); ok { 289 | log.Printf("CtxHandle: 0x%x\n", *contextHandle) 290 | if contextHandle.Lower == 0 && contextHandle.Upper == 0 { 291 | return nil, nil 292 | } 293 | return contextHandle, nil 294 | } 295 | log.Printf("CtxHandle: nil\n") 296 | return nil, nil 297 | } 298 | 299 | // SetCtxHandle stores the context handle for this client to cookie of response 300 | func (a *Authenticator) SetCtxHandle(r *http.Request, w http.ResponseWriter, newContext *CtxtHandle) error { 301 | // Store can't store nil value, so if newContext is nil, store an empty CtxHandle 302 | ctx := &CtxtHandle{} 303 | if newContext != nil { 304 | ctx = newContext 305 | } 306 | err := a.Config.contextStore.SetHandle(r, w, ctx) 307 | if err != nil { 308 | return fmt.Errorf("could not save context to cookie: %s", err) 309 | } 310 | log.Printf("New context: 0x%x\n", *ctx) 311 | return nil 312 | } 313 | 314 | // GetFlags returns the negotiated context flags 315 | func (a *Authenticator) GetFlags(context *CtxtHandle) (uint32, error) { 316 | var flags SecPkgContext_Flags 317 | status := a.Config.authAPI.QueryContextAttributes(context, SECPKG_ATTR_FLAGS, (*byte)(unsafe.Pointer(&flags))) 318 | if status != SEC_E_OK { 319 | return 0, fmt.Errorf("QueryContextAttributes failed with status 0x%x", status) 320 | } 321 | return flags.Flags, nil 322 | } 323 | 324 | // GetUsername returns the name of the user associated with the specified security context 325 | func (a *Authenticator) GetUsername(context *CtxtHandle) (username string, err error) { 326 | var names SecPkgContext_Names 327 | status := a.Config.authAPI.QueryContextAttributes(context, SECPKG_ATTR_NAMES, (*byte)(unsafe.Pointer(&names))) 328 | if status != SEC_E_OK { 329 | err = fmt.Errorf("QueryContextAttributes failed with status 0x%x", status) 330 | return 331 | } 332 | if names.UserName != nil { 333 | username = UTF16PtrToString(names.UserName, 2048) 334 | status = a.Config.authAPI.FreeContextBuffer((*byte)(unsafe.Pointer(names.UserName))) 335 | if status != SEC_E_OK { 336 | err = fmt.Errorf("FreeContextBuffer failed with status 0x%x", status) 337 | } 338 | return 339 | } 340 | err = errors.New("QueryContextAttributes returned empty name") 341 | return 342 | } 343 | 344 | // GetGroups returns the groups assosiated with the specified security context 345 | func (a *Authenticator) GetGroups(context *CtxtHandle) (groups []string, err error) { 346 | var token SecPkgContext_AccessToken 347 | status := a.Config.authAPI.QueryContextAttributes(context, SECPKG_ATTR_ACCESS_TOKEN, (*byte)(unsafe.Pointer(&token))) 348 | if status != SEC_E_OK { 349 | err = fmt.Errorf("QueryContextAttributes failed with status 0x%x", status) 350 | return 351 | } 352 | var requiredMemory uint32 353 | 354 | // 1. Get buffer size 355 | ec := a.Config.authAPI.GetTokenInformation( 356 | syscall.Token(token.AccessToken), 357 | syscall.TokenGroups, 358 | nil, 0, &requiredMemory, 359 | ) 360 | 361 | if ec != syscall.ERROR_INSUFFICIENT_BUFFER { 362 | err = fmt.Errorf("GetTokenInformation failed with %+v (while getting required memory)", ec) 363 | return 364 | } 365 | 366 | tokenInformation := make([]byte, requiredMemory) 367 | // 2. Get data 368 | ec = a.Config.authAPI.GetTokenInformation( 369 | syscall.Token(token.AccessToken), 370 | syscall.TokenGroups, 371 | &tokenInformation[0], uint32(len(tokenInformation)), &requiredMemory, 372 | ) 373 | 374 | if ec != nil { 375 | err = fmt.Errorf("GetTokenInformation failed with %+v (when looking up group membership)", ec) 376 | return 377 | } 378 | 379 | // The struct ends with a variable amount of SIDAndAttributes structs. 380 | var tokens *TokenGroups = (*TokenGroups)(unsafe.Pointer(&tokenInformation[0])) 381 | var allSidAndAttributes []syscall.SIDAndAttributes 382 | hdr := (*reflect.SliceHeader)(unsafe.Pointer(&allSidAndAttributes)) 383 | hdr.Data = uintptr(unsafe.Pointer(&tokens.Groups)) 384 | hdr.Len = int(tokens.GroupCount) 385 | hdr.Cap = int(tokens.GroupCount) 386 | 387 | for _, sidAndAttributes := range allSidAndAttributes { 388 | // SE_GROUP_ENABLED 389 | if sidAndAttributes.Attributes&4 == 4 { 390 | str, _ := sidAndAttributes.Sid.String() 391 | group, err := user.LookupGroupId(str) 392 | if err != nil { // Non-group SIDs - can happen sometimes. 393 | continue 394 | } 395 | groups = append(groups, group.Name) 396 | } 397 | } 398 | 399 | return 400 | } 401 | 402 | // GetUserGroups returns the groups the user is a member of 403 | func (a *Authenticator) GetUserGroups(userName string) (groups []string, err error) { 404 | var serverNamePtr *uint16 405 | if a.Config.ServerName != "" { 406 | serverNamePtr, err = syscall.UTF16PtrFromString(a.Config.ServerName) 407 | if err != nil { 408 | return 409 | } 410 | } 411 | 412 | userNamePtr, err := syscall.UTF16PtrFromString(userName) 413 | if err != nil { 414 | return 415 | } 416 | var buf *byte 417 | var entriesRead uint32 418 | var totalEntries uint32 419 | err = a.Config.authAPI.NetUserGetGroups( 420 | serverNamePtr, 421 | userNamePtr, 422 | 0, 423 | &buf, 424 | MAX_PREFERRED_LENGTH, 425 | &entriesRead, 426 | &totalEntries, 427 | ) 428 | if buf == nil { 429 | err = fmt.Errorf("NetUserGetGroups(): returned nil buffer, error: %s", err) 430 | return 431 | } 432 | defer func() { 433 | freeErr := a.Config.authAPI.NetApiBufferFree(buf) 434 | if freeErr != nil { 435 | err = freeErr 436 | } 437 | }() 438 | if err != nil { 439 | return 440 | } 441 | if entriesRead < totalEntries { 442 | err = fmt.Errorf("NetUserGetGroups(): could not read all entries, read only %d entries of %d", entriesRead, totalEntries) 443 | return 444 | } 445 | 446 | ptr := unsafe.Pointer(buf) 447 | for i := uint32(0); i < entriesRead; i++ { 448 | groupInfo := (*GroupUsersInfo0)(ptr) 449 | groupName := UTF16PtrToString(groupInfo.Grui0_name, MAX_GROUP_NAME_LENGTH) 450 | if groupName != "" { 451 | groups = append(groups, groupName) 452 | } 453 | ptr = unsafe.Pointer(uintptr(ptr) + unsafe.Sizeof(GroupUsersInfo0{})) 454 | } 455 | return 456 | } 457 | 458 | // GetUserInfo returns a structure containing the name of the user associated with the 459 | // specified security context and the groups to which they are a member of (if Config.EnumerateGroups) 460 | // is enabled 461 | func (a *Authenticator) GetUserInfo(context *CtxtHandle) (*UserInfo, error) { 462 | // Get username 463 | username, err := a.GetUsername(context) 464 | if err != nil { 465 | return nil, err 466 | } 467 | info := UserInfo{ 468 | Username: username, 469 | } 470 | 471 | // Get groups 472 | if a.Config.EnumerateGroups { 473 | if a.Config.ServerName != "" { 474 | info.Groups, err = a.GetUserGroups(username) 475 | } else { 476 | info.Groups, err = a.GetGroups(context) 477 | } 478 | if err != nil { 479 | return nil, err 480 | } 481 | } 482 | 483 | return &info, nil 484 | } 485 | 486 | // GetAuthData parses the "Authorization" header received from the client, 487 | // extracts the auth-data token (input token) and decodes it to []byte 488 | func (a *Authenticator) GetAuthData(r *http.Request, w http.ResponseWriter) (authData []byte, err error) { 489 | // 1. Check if Authorization header is present 490 | headers := r.Header["Authorization"] 491 | if len(headers) == 0 { 492 | err = errors.New("the Authorization header is not provided") 493 | return 494 | } 495 | if len(headers) > 1 { 496 | err = errors.New("received multiple Authorization headers, but expected only one") 497 | return 498 | } 499 | 500 | authzHeader := strings.TrimSpace(headers[0]) 501 | if authzHeader == "" { 502 | err = errors.New("the Authorization header is empty") 503 | return 504 | } 505 | // 1.1. Make sure header starts with "Negotiate" 506 | if !strings.HasPrefix(strings.ToLower(authzHeader), "negotiate") { 507 | err = errors.New("the Authorization header does not start with 'Negotiate'") 508 | return 509 | } 510 | 511 | // 2. Extract token from Authorization header 512 | authzParts := strings.Split(authzHeader, " ") 513 | if len(authzParts) < 2 { 514 | err = errors.New("the Authorization header does not contain token (gssapi-data)") 515 | return 516 | } 517 | token := authzParts[len(authzParts)-1] 518 | if token == "" { 519 | err = errors.New("the token (gssapi-data) in the Authorization header is empty") 520 | return 521 | } 522 | 523 | // 3. Decode token 524 | authData, err = base64.StdEncoding.DecodeString(token) 525 | if err != nil { 526 | err = errors.New("could not decode token as base64 string") 527 | return 528 | } 529 | 530 | return 531 | } 532 | 533 | // Authenticate tries to authenticate the HTTP request and returns nil 534 | // if authentication was successful. 535 | // Returns error and data for continuation if authentication was not successful. 536 | func (a *Authenticator) Authenticate(r *http.Request, w http.ResponseWriter) (userInfo *UserInfo, outToken string, err error) { 537 | // 1. Extract auth-data from Authorization header 538 | authData, err := a.GetAuthData(r, w) 539 | if err != nil { 540 | err = fmt.Errorf("could not get auth data: %s", err) 541 | return 542 | } 543 | 544 | // 2. Authenticate user with provided token 545 | contextHandle, err := a.GetCtxHandle(r) 546 | if err != nil { 547 | return 548 | } 549 | newCtx, output, _, err := a.AcceptOrContinue(contextHandle, authData) 550 | 551 | // If a new context was created, make sure to delete it or store it 552 | // both in internal list and response Cookie 553 | defer func() { 554 | // Negotiation is ending if we don't expect further responses from the client 555 | // (authentication was successful or no output token is going to be sent back), 556 | // clear client cookie 557 | endOfNegotiation := err == nil || len(output) == 0 558 | 559 | // Current context (contextHandle) is not needed anymore and should be deleted if: 560 | // - we don't expect further responses from the client 561 | // - a new context has been returned by AcceptSecurityContext 562 | currCtxNotNeeded := endOfNegotiation || newCtx != nil 563 | if !currCtxNotNeeded { 564 | // Release current context only if its different than the new context 565 | if contextHandle != nil && *contextHandle != *newCtx { 566 | remErr := a.ReleaseCtxHandle(contextHandle) 567 | if remErr != nil { 568 | err = remErr 569 | return 570 | } 571 | } 572 | } 573 | 574 | if endOfNegotiation { 575 | // Clear client cookie 576 | setErr := a.SetCtxHandle(r, w, nil) 577 | if setErr != nil { 578 | err = fmt.Errorf("could not clear context, error: %s", setErr) 579 | return 580 | } 581 | 582 | // Delete any new context handle 583 | remErr := a.ReleaseCtxHandle(newCtx) 584 | if remErr != nil { 585 | err = remErr 586 | return 587 | } 588 | 589 | // Exit defer func 590 | return 591 | } 592 | 593 | if newCtx != nil { 594 | // Store new context handle to internal list and response Cookie 595 | a.StoreCtxHandle(newCtx) 596 | setErr := a.SetCtxHandle(r, w, newCtx) 597 | if setErr != nil { 598 | err = setErr 599 | return 600 | } 601 | } 602 | }() 603 | 604 | outToken = base64.StdEncoding.EncodeToString(output) 605 | if err != nil { 606 | err = fmt.Errorf("AcceptOrContinue failed: %s", err) 607 | return 608 | } 609 | 610 | // 3. Get username and user groups 611 | currentCtx := newCtx 612 | if currentCtx == nil { 613 | currentCtx = contextHandle 614 | } 615 | userInfo, err = a.GetUserInfo(currentCtx) 616 | if err != nil { 617 | err = fmt.Errorf("could not get username, error: %s", err) 618 | return 619 | } 620 | 621 | return 622 | } 623 | 624 | // AppendAuthenticateHeader populates WWW-Authenticate header, 625 | // indicating to client that authentication is required and returns a 401 (Unauthorized) 626 | // response code. 627 | // The data parameter can be empty for the first 401 response from the server. 628 | // For subsequent 401 responses the data parameter should contain the gssapi-data, 629 | // which is required for continuation of the negotiation. 630 | func (a *Authenticator) AppendAuthenticateHeader(w http.ResponseWriter, data string) { 631 | value := "Negotiate" 632 | if data != "" { 633 | value += " " + data 634 | } 635 | w.Header().Set("WWW-Authenticate", value) 636 | } 637 | 638 | // Return401 populates WWW-Authenticate header, indicating to client that authentication 639 | // is required and returns a 401 (Unauthorized) response code. 640 | // The data parameter can be empty for the first 401 response from the server. 641 | // For subsequent 401 responses the data parameter should contain the gssapi-data, 642 | // which is required for continuation of the negotiation. 643 | func (a *Authenticator) Return401(w http.ResponseWriter, data string) { 644 | a.AppendAuthenticateHeader(w, data) 645 | http.Error(w, "Error!", http.StatusUnauthorized) 646 | } 647 | 648 | // WithAuth authenticates the request. On successful authentication the request 649 | // is passed down to the next http handler. The next handler can access information 650 | // about the authenticated user via the GetUserName method. 651 | // If authentication was not successful, the server returns 401 response code with 652 | // a WWW-Authenticate, indicating that authentication is required. 653 | func (a *Authenticator) WithAuth(next http.Handler) http.Handler { 654 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 655 | log.Printf("Authenticating request to %s\n", r.RequestURI) 656 | 657 | user, data, err := a.Authenticate(r, w) 658 | if err != nil { 659 | log.Printf("Authentication failed with error: %v\n", err) 660 | a.Return401(w, data) 661 | return 662 | } 663 | 664 | log.Print("Authenticated\n") 665 | // Add the UserInfo value to the reqest's context 666 | r = r.WithContext(context.WithValue(r.Context(), UserInfoKey, user)) 667 | // and to the request header with key Config.AuthUserKey 668 | if a.Config.AuthUserKey != "" { 669 | r.Header.Set(a.Config.AuthUserKey, user.Username) 670 | } 671 | 672 | // The WWW-Authenticate header might need to be sent back even 673 | // on successful authentication (eg. in order to let the client complete 674 | // mutual authentication). 675 | if data != "" { 676 | a.AppendAuthenticateHeader(w, data) 677 | } 678 | next.ServeHTTP(w, r) 679 | }) 680 | } 681 | 682 | func init() { 683 | gob.Register(&CtxtHandle{}) 684 | gob.Register(&UserInfo{}) 685 | } 686 | -------------------------------------------------------------------------------- /win32_windows.go: -------------------------------------------------------------------------------- 1 | package websspi 2 | 3 | import ( 4 | "syscall" 5 | "unsafe" 6 | 7 | "golang.org/x/sys/windows" 8 | ) 9 | 10 | // advapi32.dll 11 | 12 | type TokenGroups struct { 13 | GroupCount uint32 // DWORD 14 | Groups syscall.SIDAndAttributes // *SIDAndAttributes[] 15 | } 16 | 17 | // secur32.dll 18 | 19 | type SECURITY_STATUS syscall.Errno 20 | 21 | const ( 22 | SEC_E_OK = SECURITY_STATUS(0) 23 | 24 | SEC_E_INCOMPLETE_MESSAGE = SECURITY_STATUS(0x80090318) 25 | SEC_E_INSUFFICIENT_MEMORY = SECURITY_STATUS(0x80090300) 26 | SEC_E_INTERNAL_ERROR = SECURITY_STATUS(0x80090304) 27 | SEC_E_INVALID_HANDLE = SECURITY_STATUS(0x80090301) 28 | SEC_E_INVALID_TOKEN = SECURITY_STATUS(0x80090308) 29 | SEC_E_LOGON_DENIED = SECURITY_STATUS(0x8009030C) 30 | SEC_E_NO_AUTHENTICATING_AUTHORITY = SECURITY_STATUS(0x80090311) 31 | SEC_E_NO_CREDENTIALS = SECURITY_STATUS(0x8009030E) 32 | SEC_E_UNSUPPORTED_FUNCTION = SECURITY_STATUS(0x80090302) 33 | SEC_I_COMPLETE_AND_CONTINUE = SECURITY_STATUS(0x00090314) 34 | SEC_I_COMPLETE_NEEDED = SECURITY_STATUS(0x00090313) 35 | SEC_I_CONTINUE_NEEDED = SECURITY_STATUS(0x00090312) 36 | SEC_E_NOT_OWNER = SECURITY_STATUS(0x80090306) 37 | SEC_E_SECPKG_NOT_FOUND = SECURITY_STATUS(0x80090305) 38 | SEC_E_UNKNOWN_CREDENTIALS = SECURITY_STATUS(0x8009030D) 39 | 40 | NEGOSSP_NAME = "Negotiate" 41 | SECPKG_CRED_INBOUND = 1 42 | SECURITY_NATIVE_DREP = 16 43 | 44 | ASC_REQ_DELEGATE = 1 45 | ASC_REQ_MUTUAL_AUTH = 2 46 | ASC_REQ_REPLAY_DETECT = 4 47 | ASC_REQ_SEQUENCE_DETECT = 8 48 | ASC_REQ_CONFIDENTIALITY = 16 49 | ASC_REQ_USE_SESSION_KEY = 32 50 | ASC_REQ_ALLOCATE_MEMORY = 256 51 | ASC_REQ_USE_DCE_STYLE = 512 52 | ASC_REQ_DATAGRAM = 1024 53 | ASC_REQ_CONNECTION = 2048 54 | ASC_REQ_EXTENDED_ERROR = 32768 55 | ASC_REQ_STREAM = 65536 56 | ASC_REQ_INTEGRITY = 131072 57 | 58 | SECPKG_ATTR_SIZES = 0 59 | SECPKG_ATTR_NAMES = 1 60 | SECPKG_ATTR_LIFESPAN = 2 61 | SECPKG_ATTR_DCE_INFO = 3 62 | SECPKG_ATTR_STREAM_SIZES = 4 63 | SECPKG_ATTR_KEY_INFO = 5 64 | SECPKG_ATTR_AUTHORITY = 6 65 | SECPKG_ATTR_PROTO_INFO = 7 66 | SECPKG_ATTR_PASSWORD_EXPIRY = 8 67 | SECPKG_ATTR_SESSION_KEY = 9 68 | SECPKG_ATTR_PACKAGE_INFO = 10 69 | SECPKG_ATTR_USER_FLAGS = 11 70 | SECPKG_ATTR_NEGOTIATION_INFO = 12 71 | SECPKG_ATTR_NATIVE_NAMES = 13 72 | SECPKG_ATTR_FLAGS = 14 73 | SECPKG_ATTR_ACCESS_TOKEN = 18 74 | 75 | SECBUFFER_VERSION = 0 76 | SECBUFFER_TOKEN = 2 77 | ) 78 | 79 | type CredHandle struct { 80 | Lower uintptr 81 | Upper uintptr 82 | } 83 | 84 | type CtxtHandle struct { 85 | Lower uintptr 86 | Upper uintptr 87 | } 88 | 89 | type SecBuffer struct { 90 | BufferSize uint32 91 | BufferType uint32 92 | Buffer *byte 93 | } 94 | 95 | type SecBufferDesc struct { 96 | Version uint32 97 | BuffersCount uint32 98 | Buffers *SecBuffer 99 | } 100 | 101 | type LUID struct { 102 | LowPart uint32 103 | HighPart int32 104 | } 105 | 106 | type SecPkgContext_Names struct { 107 | UserName *uint16 108 | } 109 | 110 | type SecPkgContext_Flags struct { 111 | Flags uint32 112 | } 113 | 114 | type SecPkgContext_AccessToken struct { 115 | AccessToken uintptr 116 | } 117 | 118 | // netapi32.dll 119 | 120 | const ( 121 | NERR_Success = 0x0 122 | NERR_InternalError = 0x85C 123 | NERR_UserNotFound = 0x8AD 124 | 125 | ERROR_ACCESS_DENIED = 0x5 126 | ERROR_BAD_NETPATH = 0x35 127 | ERROR_INVALID_LEVEL = 0x7C 128 | ERROR_INVALID_NAME = 0x7B 129 | ERROR_MORE_DATA = 0xEA 130 | ERROR_NOT_ENOUGH_MEMORY = 0x8 131 | 132 | MAX_PREFERRED_LENGTH = 0xFFFFFFFF 133 | MAX_GROUP_NAME_LENGTH = 256 134 | 135 | SE_GROUP_MANDATORY = 0x1 136 | SE_GROUP_ENABLED_BY_DEFAULT = 0x2 137 | SE_GROUP_ENABLED = 0x4 138 | SE_GROUP_OWNER = 0x8 139 | SE_GROUP_USE_FOR_DENY_ONLY = 0x10 140 | SE_GROUP_INTEGRITY = 0x20 141 | SE_GROUP_INTEGRITY_ENABLED = 0x40 142 | SE_GROUP_LOGON_ID = 0xC0000000 143 | SE_GROUP_RESOURCE = 0x20000000 144 | ) 145 | 146 | type GroupUsersInfo0 struct { 147 | Grui0_name *uint16 148 | } 149 | 150 | type GroupUsersInfo1 struct { 151 | Grui1_name *uint16 152 | Grui1_attributes uint32 153 | } 154 | 155 | // The API interface describes the Win32 functions used in this package and 156 | // its primary purpose is to allow replacing them with stub functions in unit tests. 157 | type API interface { 158 | AcquireCredentialsHandle( 159 | principal *uint16, 160 | _package *uint16, 161 | credentialUse uint32, 162 | logonID *LUID, 163 | authData *byte, 164 | getKeyFn uintptr, 165 | getKeyArgument uintptr, 166 | credHandle *CredHandle, 167 | expiry *syscall.Filetime, 168 | ) SECURITY_STATUS 169 | AcceptSecurityContext( 170 | credential *CredHandle, 171 | context *CtxtHandle, 172 | input *SecBufferDesc, 173 | contextReq uint32, 174 | targDataRep uint32, 175 | newContext *CtxtHandle, 176 | output *SecBufferDesc, 177 | contextAttr *uint32, 178 | expiry *syscall.Filetime, 179 | ) SECURITY_STATUS 180 | QueryContextAttributes(context *CtxtHandle, attribute uint32, buffer *byte) SECURITY_STATUS 181 | DeleteSecurityContext(context *CtxtHandle) SECURITY_STATUS 182 | FreeContextBuffer(buffer *byte) SECURITY_STATUS 183 | FreeCredentialsHandle(handle *CredHandle) SECURITY_STATUS 184 | NetUserGetGroups( 185 | serverName *uint16, 186 | userName *uint16, 187 | level uint32, 188 | buf **byte, 189 | prefmaxlen uint32, 190 | entriesread *uint32, 191 | totalentries *uint32, 192 | ) (neterr error) 193 | NetApiBufferFree(buf *byte) (neterr error) 194 | 195 | GetTokenInformation(t syscall.Token, infoClass uint32, info *byte, infoLen uint32, returnedLen *uint32) (err error) 196 | } 197 | 198 | // Win32 implements the API interface by calling the relevant system functions 199 | // from secur32.dll and netapi32.dll 200 | type Win32 struct{} 201 | 202 | var ( 203 | secur32dll = windows.NewLazySystemDLL("secur32.dll") 204 | netapi32dll = windows.NewLazySystemDLL("netapi32.dll") 205 | 206 | procAcquireCredentialsHandleW = secur32dll.NewProc("AcquireCredentialsHandleW") 207 | procAcceptSecurityContext = secur32dll.NewProc("AcceptSecurityContext") 208 | procQueryContextAttributesW = secur32dll.NewProc("QueryContextAttributesW") 209 | procDeleteSecurityContext = secur32dll.NewProc("DeleteSecurityContext") 210 | procFreeContextBuffer = secur32dll.NewProc("FreeContextBuffer") 211 | procFreeCredentialsHandle = secur32dll.NewProc("FreeCredentialsHandle") 212 | procNetUserGetGroups = netapi32dll.NewProc("NetUserGetGroups") 213 | ) 214 | 215 | func (w *Win32) AcquireCredentialsHandle( 216 | principal *uint16, 217 | _package *uint16, 218 | credentialUse uint32, 219 | logonId *LUID, 220 | authData *byte, 221 | getKeyFn uintptr, 222 | getKeyArgument uintptr, 223 | credHandle *CredHandle, 224 | expiry *syscall.Filetime, 225 | ) SECURITY_STATUS { 226 | r1, _, _ := syscall.Syscall9( 227 | procAcquireCredentialsHandleW.Addr(), 9, 228 | uintptr(unsafe.Pointer(principal)), 229 | uintptr(unsafe.Pointer(_package)), 230 | uintptr(credentialUse), 231 | uintptr(unsafe.Pointer(logonId)), 232 | uintptr(unsafe.Pointer(authData)), 233 | uintptr(getKeyFn), 234 | uintptr(getKeyArgument), 235 | uintptr(unsafe.Pointer(credHandle)), 236 | uintptr(unsafe.Pointer(expiry)), 237 | ) 238 | return SECURITY_STATUS(r1) 239 | } 240 | 241 | func (w *Win32) AcceptSecurityContext( 242 | credential *CredHandle, 243 | context *CtxtHandle, 244 | input *SecBufferDesc, 245 | contextReq uint32, 246 | targDataRep uint32, 247 | newContext *CtxtHandle, 248 | output *SecBufferDesc, 249 | contextAttr *uint32, 250 | expiry *syscall.Filetime, 251 | ) SECURITY_STATUS { 252 | r1, _, _ := syscall.Syscall9( 253 | procAcceptSecurityContext.Addr(), 9, 254 | uintptr(unsafe.Pointer(credential)), 255 | uintptr(unsafe.Pointer(context)), 256 | uintptr(unsafe.Pointer(input)), 257 | uintptr(contextReq), 258 | uintptr(targDataRep), 259 | uintptr(unsafe.Pointer(newContext)), 260 | uintptr(unsafe.Pointer(output)), 261 | uintptr(unsafe.Pointer(contextAttr)), 262 | uintptr(unsafe.Pointer(expiry)), 263 | ) 264 | return SECURITY_STATUS(r1) 265 | } 266 | 267 | func (w *Win32) QueryContextAttributes( 268 | context *CtxtHandle, 269 | attribute uint32, 270 | buffer *byte, 271 | ) SECURITY_STATUS { 272 | r1, _, _ := syscall.Syscall( 273 | procQueryContextAttributesW.Addr(), 3, 274 | uintptr(unsafe.Pointer(context)), 275 | uintptr(attribute), 276 | uintptr(unsafe.Pointer(buffer)), 277 | ) 278 | return SECURITY_STATUS(r1) 279 | } 280 | 281 | func (w *Win32) DeleteSecurityContext(context *CtxtHandle) SECURITY_STATUS { 282 | r1, _, _ := syscall.Syscall( 283 | procDeleteSecurityContext.Addr(), 1, 284 | uintptr(unsafe.Pointer(context)), 285 | 0, 0, 286 | ) 287 | return SECURITY_STATUS(r1) 288 | } 289 | 290 | func (w *Win32) FreeContextBuffer(buffer *byte) SECURITY_STATUS { 291 | r1, _, _ := syscall.Syscall( 292 | procFreeContextBuffer.Addr(), 1, 293 | uintptr(unsafe.Pointer(buffer)), 294 | 0, 0, 295 | ) 296 | return SECURITY_STATUS(r1) 297 | } 298 | 299 | func (w *Win32) FreeCredentialsHandle(handle *CredHandle) SECURITY_STATUS { 300 | r1, _, _ := syscall.Syscall( 301 | procFreeCredentialsHandle.Addr(), 1, 302 | uintptr(unsafe.Pointer(handle)), 303 | 0, 0, 304 | ) 305 | return SECURITY_STATUS(r1) 306 | } 307 | 308 | func (w *Win32) NetUserGetGroups( 309 | serverName *uint16, 310 | userName *uint16, 311 | level uint32, 312 | buf **byte, 313 | prefmaxlen uint32, 314 | entriesread *uint32, 315 | totalentries *uint32, 316 | ) (neterr error) { 317 | 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) 318 | if r0 != 0 { 319 | neterr = syscall.Errno(r0) 320 | } 321 | return 322 | } 323 | 324 | func (w *Win32) NetApiBufferFree(buf *byte) (neterr error) { 325 | return syscall.NetApiBufferFree(buf) 326 | } 327 | 328 | func (w *Win32) GetTokenInformation(t syscall.Token, infoClass uint32, info *byte, infoLen uint32, returnedLen *uint32) (err error) { 329 | return syscall.GetTokenInformation(t, infoClass, info, infoLen, returnedLen) 330 | } 331 | --------------------------------------------------------------------------------