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 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: +{{- if .Linked}} +

Linked Token: {{ .Linked.Username }}

+Groups: + +{{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)) 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 } diff --git a/websspi_test.go b/websspi_test.go index f80fe6b..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,8 +24,16 @@ var sidRemoteDesktopUsers *syscall.SID var resolvedGroups []string var resolvedGroupsWoAdmin []string +var sidThisUser *syscall.SID +var thisUser string + func init() { + me, _ := user.Current() + normalized, _ := user.LookupId(me.Uid) + thisUser = normalized.Username + for stringSid, binPtr := range map[string]**syscall.SID{ + 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 @@ -156,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) @@ -244,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"}) @@ -269,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(), }, }, }, @@ -642,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} diff --git a/websspi_windows.go b/websspi_windows.go index b1a9df3..7c70728 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") + LinkedTokenUserInfoKey = contextKey("LinkedTokenUserInfo") ) // The Authenticator type provides middleware methods for authentication of http requests. @@ -341,19 +342,113 @@ 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) + 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( + 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], + usedMemory, + &usedMemory, + ) + + tokenuser := (*TokenUser)(unsafe.Pointer(&buffer[0])) + + if err != nil { + return + } + + var stringsid string + stringsid, err = tokenuser.User.Sid.String() + if err != nil { + return + + } + + 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 +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 +461,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, ) @@ -480,6 +575,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 } @@ -664,6 +766,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 e96b92f..c05abbc 100644 --- a/win32_windows.go +++ b/win32_windows.go @@ -14,6 +14,14 @@ type TokenGroups struct { Groups syscall.SIDAndAttributes // *SIDAndAttributes[] } +type TokenLinkedToken struct { + LinkedToken syscall.Handle +} + +type TokenUser struct { + User syscall.SIDAndAttributes +} + // secur32.dll type SECURITY_STATUS syscall.Errno