From fc58a6982ac11a463805b0706c5325f34404eed6 Mon Sep 17 00:00:00 2001 From: Simon Biewald Date: Fri, 15 Oct 2021 16:32:34 +0200 Subject: [PATCH 1/8] Add TokenLinkedToken structure definition. --- win32_windows.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/win32_windows.go b/win32_windows.go index e96b92f..cc86915 100644 --- a/win32_windows.go +++ b/win32_windows.go @@ -14,6 +14,10 @@ type TokenGroups struct { Groups syscall.SIDAndAttributes // *SIDAndAttributes[] } +type TokenLinkedToken struct { + LinkedToken syscall.Handle +} + // secur32.dll type SECURITY_STATUS syscall.Errno From 25a6c4b5eb87efe028fc666f797bea071ba69e18 Mon Sep 17 00:00:00 2001 From: Simon Biewald Date: Fri, 15 Oct 2021 16:35:40 +0200 Subject: [PATCH 2/8] Move token group resolutution to new function. --- websspi_windows.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/websspi_windows.go b/websspi_windows.go index b1a9df3..de353cd 100644 --- a/websspi_windows.go +++ b/websspi_windows.go @@ -63,7 +63,8 @@ func (c contextKey) String() string { } var ( - UserInfoKey = contextKey("UserInfo") + UserInfoKey = contextKey("UserInfo") + LinkedTokenUserInfo = contextKey("LinkedTokenUserInfo") ) // The Authenticator type provides middleware methods for authentication of http requests. @@ -349,11 +350,17 @@ func (a *Authenticator) GetGroups(context *CtxtHandle) (groups []string, err err err = fmt.Errorf("QueryContextAttributes failed with status 0x%x", status) return } + + return a.GetGroupsFromToken(syscall.Token(token.AccessToken)) +} + +// GetGroupsFromToken returns the active groups of a Windows token +func (a *Authenticator) GetGroupsFromToken(token syscall.Token) (groups []string, err error) { var requiredMemory uint32 // 1. Get buffer size ec := a.Config.authAPI.GetTokenInformation( - syscall.Token(token.AccessToken), + syscall.Token(token), syscall.TokenGroups, nil, 0, &requiredMemory, ) @@ -366,7 +373,7 @@ func (a *Authenticator) GetGroups(context *CtxtHandle) (groups []string, err err tokenInformation := make([]byte, requiredMemory) // 2. Get data ec = a.Config.authAPI.GetTokenInformation( - syscall.Token(token.AccessToken), + syscall.Token(token), syscall.TokenGroups, &tokenInformation[0], uint32(len(tokenInformation)), &requiredMemory, ) From 2affb469bbbe95b5caaa47f9e54ba1d247ffb211 Mon Sep 17 00:00:00 2001 From: Simon Biewald Date: Fri, 15 Oct 2021 18:50:56 +0200 Subject: [PATCH 3/8] Add documentation file. --- doc.go | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 doc.go diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..27d18ce --- /dev/null +++ b/doc.go @@ -0,0 +1,4 @@ +/* +Package websspi provides middleware to require with Windows Integrated Authentication. +*/ +package websspi From 75ed2e3e389b1150c10f4bf2b58593ddef038dbf Mon Sep 17 00:00:00 2001 From: Simon Biewald Date: Fri, 15 Oct 2021 18:51:26 +0200 Subject: [PATCH 4/8] Add private linked field to the UserInfo struct. This allows to pass the linked token without changing function signatures. --- userinfo.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/userinfo.go b/userinfo.go index 359bfb5..0f5d465 100644 --- a/userinfo.go +++ b/userinfo.go @@ -4,4 +4,6 @@ package websspi type UserInfo struct { Username string // Name of user, usually in the form DOMAIN\User Groups []string // The global groups the user is a member of + + linked *UserInfo } From 322790ed1360eb46b54a82f85c8a2b62f93ed2ac Mon Sep 17 00:00:00 2001 From: Simon Biewald Date: Fri, 15 Oct 2021 18:53:35 +0200 Subject: [PATCH 5/8] Add resolution of linked token. It can be retrieved with (*http.Request).Context().Value(websspi.LinkedTokenUserInfoKey). --- websspi_test.go | 15 +++++-- websspi_windows.go | 97 +++++++++++++++++++++++++++++++++++++++++++--- win32_windows.go | 4 ++ 3 files changed, 108 insertions(+), 8 deletions(-) diff --git a/websspi_test.go b/websspi_test.go index f80fe6b..1015f74 100644 --- a/websspi_test.go +++ b/websspi_test.go @@ -23,11 +23,20 @@ var sidRemoteDesktopUsers *syscall.SID var resolvedGroups []string var resolvedGroupsWoAdmin []string +var sidAdministrator *syscall.SID +var resolvedAdministrator string + func init() { + me, _ := user.Current() + parts := strings.Split(me.Uid, "-") + administrator, _ := user.LookupId(strings.Join(parts[0:len(parts)-2], "-") + "-500") + resolvedAdministrator = administrator.Username + for stringSid, binPtr := range map[string]**syscall.SID{ - "S-1-5-32-544": &sidAdministrators, // BUILTIN\Administrators - "S-1-5-32-545": &sidUsers, // BUILTIN\Users - "S-1-5-32-555": &sidRemoteDesktopUsers, // BUILTIN\Remote Desktop Users + administrator.Uid: &sidAdministrator, // ...\Administrator + "S-1-5-32-544": &sidAdministrators, // BUILTIN\Administrators + "S-1-5-32-545": &sidUsers, // BUILTIN\Users + "S-1-5-32-555": &sidRemoteDesktopUsers, // BUILTIN\Remote Desktop Users } { *binPtr, _ = syscall.StringToSid(stringSid) } diff --git a/websspi_windows.go b/websspi_windows.go index de353cd..8983251 100644 --- a/websspi_windows.go +++ b/websspi_windows.go @@ -63,8 +63,8 @@ func (c contextKey) String() string { } var ( - UserInfoKey = contextKey("UserInfo") - LinkedTokenUserInfo = contextKey("LinkedTokenUserInfo") + UserInfoKey = contextKey("UserInfo") + LinkedTokenUserInfoKey = contextKey("LinkedTokenUserInfo") ) // The Authenticator type provides middleware methods for authentication of http requests. @@ -342,16 +342,93 @@ func (a *Authenticator) GetUsername(context *CtxtHandle) (username string, err e return } -// GetGroups returns the groups assosiated with the specified security context -func (a *Authenticator) GetGroups(context *CtxtHandle) (groups []string, err error) { +// GetAccessToken returns the access token of a context handle. +func (a *Authenticator) GetAccessToken(context *CtxtHandle) (t syscall.Token, err error) { var token SecPkgContext_AccessToken status := a.Config.authAPI.QueryContextAttributes(context, SECPKG_ATTR_ACCESS_TOKEN, (*byte)(unsafe.Pointer(&token))) if status != SEC_E_OK { err = fmt.Errorf("QueryContextAttributes failed with status 0x%x", status) return } + return syscall.Token(token.AccessToken), err +} + +// GetLinkedUserInfo returns the user info of a linked token e.g. the full token when using the UAC +func (a *Authenticator) GetLinkedUserInfo(context *CtxtHandle) (u *UserInfo, err error) { + var token syscall.Token + token, err = a.GetAccessToken(context) + if err != nil { + return + } + + linkedUserInfo := TokenLinkedToken{} + var usedMemory uint32 + + err = a.Config.authAPI.GetTokenInformation( + token, + uint32(syscall.TokenLinkedToken), + (*byte)(unsafe.Pointer(&linkedUserInfo)), + uint32(reflect.TypeOf(linkedUserInfo).Size()), + &usedMemory, + ) + if err != nil { + return + } + + defer syscall.CloseHandle(linkedUserInfo.LinkedToken) + // The buffer will also store the SID, therefore more than sizeof(TokenUser) bytes are required. + buffer := make([]byte, 50) + linkedToken := syscall.Token(linkedUserInfo.LinkedToken) + + err = a.Config.authAPI.GetTokenInformation( + token, + uint32(syscall.TokenUser), + &buffer[0], + uint32(len(buffer)), + &usedMemory, + ) + + tokenuser := (*TokenUser)(unsafe.Pointer(&buffer[0])) + + if err != nil { + return + } + + var stringsid string + stringsid, err = tokenuser.User.Sid.String() + if err != nil { + return + + } - return a.GetGroupsFromToken(syscall.Token(token.AccessToken)) + var lookedup *user.User + lookedup, err = user.LookupId(stringsid) + if err != nil { + return + } + + u = &UserInfo{} + u.Username = lookedup.Username + + if a.Config.EnumerateGroups { + if a.Config.ServerName == "" { + u.Groups, err = a.GetGroupsFromToken(linkedToken) + } else { + u.Groups, err = a.GetUserGroups(u.Username) + } + } + + return +} + +// GetGroups returns the groups assosiated with the specified security context +func (a *Authenticator) GetGroups(context *CtxtHandle) (groups []string, err error) { + var token syscall.Token + token, err = a.GetAccessToken(context) + if err != nil { + return + } + return a.GetGroupsFromToken(token) } // GetGroupsFromToken returns the active groups of a Windows token @@ -487,6 +564,13 @@ func (a *Authenticator) GetUserInfo(context *CtxtHandle) (*UserInfo, error) { } } + if a.Config.ResolveLinked { + info.linked, err = a.GetLinkedUserInfo(context) + if err != nil { + return nil, err + } + } + return &info, nil } @@ -671,6 +755,9 @@ func (a *Authenticator) WithAuth(next http.Handler) http.Handler { log.Print("Authenticated\n") // Add the UserInfo value to the reqest's context r = r.WithContext(context.WithValue(r.Context(), UserInfoKey, user)) + if user.linked != nil { + r = r.WithContext(context.WithValue(r.Context(), LinkedTokenUserInfoKey, user.linked)) + } // and to the request header with key Config.AuthUserKey if a.Config.AuthUserKey != "" { r.Header.Set(a.Config.AuthUserKey, user.Username) diff --git a/win32_windows.go b/win32_windows.go index cc86915..c05abbc 100644 --- a/win32_windows.go +++ b/win32_windows.go @@ -18,6 +18,10 @@ type TokenLinkedToken struct { LinkedToken syscall.Handle } +type TokenUser struct { + User syscall.SIDAndAttributes +} + // secur32.dll type SECURITY_STATUS syscall.Errno From 76e52899bad9910970e2d44ba3020a9d70dc4051 Mon Sep 17 00:00:00 2001 From: Simon Biewald Date: Fri, 15 Oct 2021 19:13:27 +0200 Subject: [PATCH 6/8] Add linked token to example. --- examples/server_windows.go | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/examples/server_windows.go b/examples/server_windows.go index f83c7d1..54a546e 100644 --- a/examples/server_windows.go +++ b/examples/server_windows.go @@ -14,16 +14,25 @@ import ( ) var helloTemplate = template.Must(template.New("index.html").Parse(` -{{- if . -}} -

Hello {{ .Username }}!

+{{- if .User -}} +

Hello {{ .User.Username }}!

-{{ if .Groups -}} +{{ if .User.Groups -}} Groups:
    -{{- range .Groups}} +{{- range .User.Groups}}
  • {{ . }}
  • {{end -}}
+{{- if .Linked}} +

Linked Token: {{ .Linked.Username }}

+Groups: +
    +{{- range .Linked.Groups}} +
  • {{ . }}
  • +{{end -}} +
+{{end -}} {{- end }} {{- else -}}

Hello!

@@ -34,6 +43,10 @@ func main() { config := websspi.NewConfig() config.EnumerateGroups = true // If groups should be resolved // config.ServerName = "..." // If static instead of dynamic group membership should be resolved + config.ResolveLinked = true + // If a linked token should be resolved. + // For UAC restricted admin the linked user info will have the "all" groups. + // For UAC elevated user the linked user info will have the restricted ones. auth, err := websspi.New(config) if err != nil { @@ -43,9 +56,16 @@ func main() { server := &http.Server{Addr: "0.0.0.0:9000"} handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { info := r.Context().Value(websspi.UserInfoKey) + linked := r.Context().Value(websspi.LinkedTokenUserInfoKey) userInfo, _ := info.(*websspi.UserInfo) + linkedTokenUserInfo, _ := linked.(*websspi.UserInfo) w.Header().Add("Content-Type", "text/html; encoding=utf-8") - helloTemplate.Execute(w, userInfo) + helloTemplate.Execute(w, struct { + User *websspi.UserInfo + Linked *websspi.UserInfo + }{ + userInfo, linkedTokenUserInfo, + }) }) http.Handle("/", auth.WithAuth(handler)) From b4e2d29601a962fab5f82067d359afa64df3c219 Mon Sep 17 00:00:00 2001 From: Simon Biewald Date: Fri, 15 Oct 2021 21:11:19 +0200 Subject: [PATCH 7/8] Properly allocate buffer for linked token's username. --- websspi_windows.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/websspi_windows.go b/websspi_windows.go index 8983251..7c70728 100644 --- a/websspi_windows.go +++ b/websspi_windows.go @@ -376,15 +376,26 @@ func (a *Authenticator) GetLinkedUserInfo(context *CtxtHandle) (u *UserInfo, err } defer syscall.CloseHandle(linkedUserInfo.LinkedToken) - // The buffer will also store the SID, therefore more than sizeof(TokenUser) bytes are required. - buffer := make([]byte, 50) linkedToken := syscall.Token(linkedUserInfo.LinkedToken) + // The buffer will also store the SID, therefore more than sizeof(TokenUser) bytes are required. err = a.Config.authAPI.GetTokenInformation( - token, + linkedToken, + uint32(syscall.TokenUser), + nil, + 0, + &usedMemory, + ) + if err != syscall.ERROR_INSUFFICIENT_BUFFER { + return + } + + buffer := make([]byte, int(usedMemory)) + err = a.Config.authAPI.GetTokenInformation( + linkedToken, uint32(syscall.TokenUser), &buffer[0], - uint32(len(buffer)), + usedMemory, &usedMemory, ) From d1f1c416f22128e2791d42fe7f5b629adacaa685 Mon Sep 17 00:00:00 2001 From: Simon Biewald Date: Fri, 15 Oct 2021 22:12:14 +0200 Subject: [PATCH 8/8] Add test for (*Authenticator).GetLinkedUserInfo(). --- websspi_test.go | 82 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 11 deletions(-) diff --git a/websspi_test.go b/websspi_test.go index 1015f74..c3c5163 100644 --- a/websspi_test.go +++ b/websspi_test.go @@ -10,6 +10,7 @@ import ( "net/http/httptest" "os/user" "reflect" + "sort" "strings" "syscall" "testing" @@ -23,20 +24,19 @@ var sidRemoteDesktopUsers *syscall.SID var resolvedGroups []string var resolvedGroupsWoAdmin []string -var sidAdministrator *syscall.SID -var resolvedAdministrator string +var sidThisUser *syscall.SID +var thisUser string func init() { me, _ := user.Current() - parts := strings.Split(me.Uid, "-") - administrator, _ := user.LookupId(strings.Join(parts[0:len(parts)-2], "-") + "-500") - resolvedAdministrator = administrator.Username + normalized, _ := user.LookupId(me.Uid) + thisUser = normalized.Username for stringSid, binPtr := range map[string]**syscall.SID{ - administrator.Uid: &sidAdministrator, // ...\Administrator - "S-1-5-32-544": &sidAdministrators, // BUILTIN\Administrators - "S-1-5-32-545": &sidUsers, // BUILTIN\Users - "S-1-5-32-555": &sidRemoteDesktopUsers, // BUILTIN\Remote Desktop Users + me.Uid: &sidThisUser, // ...\Administrator + "S-1-5-32-544": &sidAdministrators, // BUILTIN\Administrators + "S-1-5-32-545": &sidUsers, // BUILTIN\Users + "S-1-5-32-555": &sidRemoteDesktopUsers, // BUILTIN\Remote Desktop Users } { *binPtr, _ = syscall.StringToSid(stringSid) } @@ -165,7 +165,7 @@ func (s *stubAPI) GetTokenInformation(t syscall.Token, infoClass uint32, info *b temp2, ok := temp1[int(infoClass)] if !ok { - return syscall.Errno(998) + return syscall.Errno(999) } length := len(temp2) @@ -253,6 +253,38 @@ func newGroups(limited bool) []byte { return out } +func newUser() []byte { + u := TokenUser{ + syscall.SIDAndAttributes{ + Sid: sidThisUser, + Attributes: 0, + }, + } + + in := make([]byte, reflect.TypeOf(u).Size()) + out := make([]byte, reflect.TypeOf(u).Size()) + var inHdr *reflect.SliceHeader + inHdr = (*reflect.SliceHeader)(unsafe.Pointer(&in)) + inHdr.Data = uintptr(unsafe.Pointer(&u)) + + copy(out, in) + return out +} + +func newToken() []byte { + u := TokenLinkedToken{ + LinkedToken: 2, + } + in := make([]byte, reflect.TypeOf(u).Size()) + out := make([]byte, reflect.TypeOf(u).Size()) + var inHdr *reflect.SliceHeader + inHdr = (*reflect.SliceHeader)(unsafe.Pointer(&in)) + inHdr.Data = uintptr(unsafe.Pointer(&u)) + + copy(out, in) + return out +} + // newTestAuthenticator creates an Authenticator for use in tests. func newTestAuthenticator(t *testing.T) *Authenticator { entries, total, groupsBuf := newGroupUsersInfo0([]string{"group1", "group2", "group3"}) @@ -278,10 +310,12 @@ func newTestAuthenticator(t *testing.T) *Authenticator { getTokenInformation: map[int]map[int][]byte{ 1: { - syscall.TokenGroups: newGroups(true), + syscall.TokenGroups: newGroups(true), + syscall.TokenLinkedToken: newToken(), }, 2: { syscall.TokenGroups: newGroups(false), + syscall.TokenUser: newUser(), }, }, }, @@ -651,6 +685,32 @@ func TestGetUserGroups_PartialRead(t *testing.T) { } } +func TestGetLinkedUserInfo(t *testing.T) { + token1 := SecPkgContext_AccessToken{1} + + auth := newTestAuthenticator(t) + auth.Config.ServerName = "" + auth.Config.authAPI.(*stubAPI).queryStatus = 0 + auth.Config.authAPI.(*stubAPI).queryOutBuf = (*byte)(unsafe.Pointer(&token1)) + + linked, err := auth.GetLinkedUserInfo(nil) + if err != nil { + t.Fatal("GetLinkedUserInfo() returns an error.", err) + } + + if linked.Username != thisUser { + t.Fatal("GetLinkedUserInfo() returns the wrong user", linked.Username, "instead of", thisUser) + } + + expectedGroups := resolvedGroups + sort.Strings(linked.Groups) + sort.Strings(expectedGroups) + + if len(linked.Groups) != len(expectedGroups) || !reflect.DeepEqual(linked.Groups, expectedGroups) { + t.Fatal("GetLinkedUserInfo() returns the wrong groups", linked.Groups, "instead of", expectedGroups) + } +} + func TestGetGroups(t *testing.T) { token1 := SecPkgContext_AccessToken{1}