├── .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 | [](https://godoc.org/github.com/quasoft/websspi) [](https://app.travis-ci.com/github/quasoft/websspi) [](https://coveralls.io/github/quasoft/websspi?branch=master) [](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 |
23 | {{- range .Groups}}
24 | - {{ . }}
25 | {{end -}}
26 |
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 |
--------------------------------------------------------------------------------