Skip to content

Add linked token resolution #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/*
Package websspi provides middleware to require with Windows Integrated Authentication.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be we should extend a bit the comment in the doc.go file. Something like that should do:

Suggested change
Package websspi provides middleware to require with Windows Integrated Authentication.
Package websspi implements a middleware for SSO Kerberos authentication of HTTP requests in Windows environments by SSPI, without the need for any keytab files

*/
package websspi
30 changes: 25 additions & 5 deletions examples/server_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,25 @@ import (
)

var helloTemplate = template.Must(template.New("index.html").Parse(`
{{- if . -}}
<h2>Hello {{ .Username }}!</h2>
{{- if .User -}}
<h2>Hello {{ .User.Username }}!</h2>

{{ if .Groups -}}
{{ if .User.Groups -}}
Groups:
<ul>
{{- range .Groups}}
{{- range .User.Groups}}
<li>{{ . }}</li>
{{end -}}
</ul>
{{- if .Linked}}
<h3>Linked Token: {{ .Linked.Username }}</h3>
Groups:
<ul>
{{- range .Linked.Groups}}
<li>{{ . }}</li>
{{end -}}
</ul>
{{end -}}
{{- end }}
{{- else -}}
<h2>Hello!</h2>
Expand All @@ -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 {
Expand All @@ -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))

Expand Down
2 changes: 2 additions & 0 deletions userinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
73 changes: 71 additions & 2 deletions websspi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net/http/httptest"
"os/user"
"reflect"
"sort"
"strings"
"syscall"
"testing"
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"})
Expand All @@ -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(),
},
},
},
Expand Down Expand Up @@ -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}

Expand Down
115 changes: 110 additions & 5 deletions websspi_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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]))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't really matter, but we could first check the value of err, before setting the value of the tokenuser variable


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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this, but if u.Username is same for both the original Token and the LinkedToken, there won't be need to check for the direct groups again.

GetLinkedUserInfo is a new function, and if we agree that it does not need to be backwards compatible, you can make it completely ignore Config.ServerName value.

}
}

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,
)
Expand All @@ -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,
)
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions win32_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down