Procházet zdrojové kódy

implementations for jcmturner/goidentity and approach for username/password basic auth check with krb5

Jonathan Turner před 8 roky
rodič
revize
ac369b8922

+ 126 - 6
credentials/credentials.go

@@ -2,6 +2,7 @@
 package credentials
 
 import (
+	"github.com/hashicorp/go-uuid"
 	"github.com/jcmturner/gokrb5/iana/nametype"
 	"github.com/jcmturner/gokrb5/keytab"
 	"github.com/jcmturner/gokrb5/types"
@@ -16,12 +17,17 @@ const (
 // Contains either a keytab, password or both.
 // Keytabs are used over passwords if both are defined.
 type Credentials struct {
-	Username   string
-	Realm      string
-	CName      types.PrincipalName
-	Keytab     keytab.Keytab
-	Password   string
-	Attributes map[int]interface{}
+	Username        string
+	Realm           string
+	CName           types.PrincipalName
+	Keytab          keytab.Keytab
+	Password        string
+	Attributes      map[int]interface{}
+	authenticated   bool
+	human           bool
+	authTime        time.Time
+	groupMembership map[string]bool
+	sessionID       string
 }
 
 // ADCredentials contains information obtained from the PAC.
@@ -41,6 +47,10 @@ type ADCredentials struct {
 
 // NewCredentials creates a new Credentials instance.
 func NewCredentials(username string, realm string) Credentials {
+	uid, err := uuid.GenerateUUID()
+	if err != nil {
+		uid = "00unique-sess-ions-uuid-unavailable0"
+	}
 	return Credentials{
 		Username: username,
 		Realm:    realm,
@@ -50,17 +60,23 @@ func NewCredentials(username string, realm string) Credentials {
 		},
 		Keytab:     keytab.NewKeytab(),
 		Attributes: make(map[int]interface{}),
+		sessionID:  uid,
 	}
 }
 
 // NewCredentialsFromPrincipal creates a new Credentials instance with the user details provides as a PrincipalName type.
 func NewCredentialsFromPrincipal(cname types.PrincipalName, realm string) Credentials {
+	uid, err := uuid.GenerateUUID()
+	if err != nil {
+		uid = "00unique-sess-ions-uuid-unavailable0"
+	}
 	return Credentials{
 		Username:   cname.GetPrincipalNameString(),
 		Realm:      realm,
 		CName:      cname,
 		Keytab:     keytab.NewKeytab(),
 		Attributes: make(map[int]interface{}),
+		sessionID:  uid,
 	}
 }
 
@@ -91,3 +107,107 @@ func (c *Credentials) HasPassword() bool {
 	}
 	return false
 }
+
+func (c *Credentials) SetADCredentials(a ADCredentials) {
+	c.Attributes[AttributeKey_ADCredentials] = a
+	if a.FullName != "" {
+		c.SetDisplayName(a.FullName)
+	}
+	for i := range a.GroupMembershipSIDs {
+		c.AddAuthzAttribute(a.GroupMembershipSIDs[i])
+	}
+}
+
+// Methods to implement goidentity.Identity interface
+
+func (c *Credentials) UserName() string {
+	return c.Username
+}
+
+func (c *Credentials) SetUserName(s string) {
+	c.Username = s
+}
+
+func (c *Credentials) Domain() string {
+	return c.Realm
+}
+
+func (c *Credentials) SetDomain(s string) {
+	c.Realm = s
+}
+
+func (c *Credentials) DisplayName() string {
+	return c.Username
+}
+
+func (c *Credentials) SetDisplayName(s string) {
+	c.Username = s
+}
+
+func (c *Credentials) Human() bool {
+	return c.human
+}
+
+func (c *Credentials) SetHuman(b bool) {
+	c.human = b
+}
+
+func (c *Credentials) AuthTime() time.Time {
+	return c.authTime
+}
+
+func (c *Credentials) SetAuthTime(t time.Time) {
+	c.authTime = t
+}
+
+func (c *Credentials) AuthzAttributes() []string {
+	s := make([]string, len(c.groupMembership))
+	i := 0
+	for a := range c.groupMembership {
+		s[i] = a
+		i++
+	}
+	return s
+}
+
+func (c *Credentials) Authenticated() bool {
+	return c.authenticated
+}
+
+func (c *Credentials) SetAuthenticated(b bool) {
+	c.authenticated = b
+}
+
+func (c *Credentials) AddAuthzAttribute(a string) {
+	c.groupMembership[a] = true
+}
+
+func (c *Credentials) RemoveAuthzAttribute(a string) {
+	if _, ok := c.groupMembership[a]; !ok {
+		return
+	}
+	delete(c.groupMembership, a)
+}
+
+func (c *Credentials) EnableAuthzAttribute(a string) {
+	if enabled, ok := c.groupMembership[a]; ok && !enabled {
+		c.groupMembership[a] = true
+	}
+}
+
+func (c *Credentials) DisableAuthzAttribute(a string) {
+	if enabled, ok := c.groupMembership[a]; ok && enabled {
+		c.groupMembership[a] = false
+	}
+}
+
+func (c *Credentials) Authorized(a string) bool {
+	if enabled, ok := c.groupMembership[a]; ok && enabled {
+		return true
+	}
+	return false
+}
+
+func (c *Credentials) SessionID() string {
+	return c.sessionID
+}

+ 13 - 0
credentials/credentials_test.go

@@ -0,0 +1,13 @@
+package credentials
+
+import (
+	"github.com/jcmturner/goidentity"
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestImplementsInterface(t *testing.T) {
+	u := new(Credentials)
+	i := new(goidentity.Identity)
+	assert.Implements(t, i, u, "Credentials type does not implement the Identity interface")
+}

+ 7 - 2
examples/example-AD.go

@@ -75,8 +75,13 @@ func testAppHandler(w http.ResponseWriter, r *http.Request) {
 	fmt.Fprint(w, "<html>\n<p><h1>TEST.GOKRB5 Handler</h1></p>\n")
 	if validuser, ok := ctx.Value(service.CTXKey_Authenticated).(bool); ok && validuser {
 		if creds, ok := ctx.Value(service.CTXKey_Credentials).(credentials.Credentials); ok {
-			fmt.Fprintf(w, "<ul><li>Authenticed user: %s</li>\n", creds.Username)
-			fmt.Fprintf(w, "<li>User's realm: %s</li>\n", creds.Realm)
+			fmt.Fprintf(w, "<ul><li>Authenticed user: %s</li>\n", creds.UserName())
+			fmt.Fprintf(w, "<li>User's realm: %s</li>\n", creds.Domain())
+			fmt.Fprint(w, "<li>Authz Attributes (Group Memberships):</li><ul>\n")
+			for _, s := range creds.AuthzAttributes() {
+				fmt.Fprintf(w, "<li>%v</li>\n", s)
+			}
+			fmt.Fprint(w, "</ul>\n")
 			if ADCreds, ok := creds.Attributes[credentials.AttributeKey_ADCredentials].(credentials.ADCredentials); ok {
 				// Now access the fields of the ADCredentials struct. For example:
 				fmt.Fprintf(w, "<li>EffectiveName: %v</li>\n", ADCreds.EffectiveName)

+ 4 - 3
service/APExchange.go

@@ -30,7 +30,6 @@ func ValidateAPREQ(APReq messages.APReq, kt keytab.Keytab, sa string, cAddr stri
 	if err != nil {
 		return false, creds, krberror.Errorf(err, krberror.ENCODING_ERROR, "Error unmarshaling authenticator")
 	}
-
 	// Check CName in Authenticator is the same as that in the ticket
 	if !a.CName.Equal(APReq.Ticket.DecryptedEncPart.CName) {
 		err := messages.NewKRBError(APReq.Ticket.SName, APReq.Ticket.Realm, errorcode.KRB_AP_ERR_BADMATCH, "CName in Authenticator does not match that in service ticket")
@@ -82,13 +81,15 @@ func ValidateAPREQ(APReq messages.APReq, kt keytab.Keytab, sa string, cAddr stri
 		return false, creds, err
 	}
 	creds = credentials.NewCredentialsFromPrincipal(a.CName, a.CRealm)
+	creds.SetAuthTime(t)
+	creds.SetAuthenticated(true)
 	isPAC, pac, err := APReq.Ticket.GetPACType(kt, sa)
 	if isPAC && err != nil {
 		return false, creds, err
 	}
 	if isPAC {
 		// There is a valid PAC. Adding attributes to creds
-		creds.Attributes[credentials.AttributeKey_ADCredentials] = credentials.ADCredentials{
+		creds.SetADCredentials(credentials.ADCredentials{
 			GroupMembershipSIDs: pac.KerbValidationInfo.GetGroupMembershipSIDs(),
 			LogOnTime:           pac.KerbValidationInfo.LogOnTime.Time(),
 			LogOffTime:          pac.KerbValidationInfo.LogOffTime.Time(),
@@ -100,7 +101,7 @@ func ValidateAPREQ(APReq messages.APReq, kt keytab.Keytab, sa string, cAddr stri
 			LogonServer:         pac.KerbValidationInfo.LogonServer.Value,
 			LogonDomainName:     pac.KerbValidationInfo.LogonDomainName.Value,
 			LogonDomainID:       pac.KerbValidationInfo.LogonDomainID.ToString(),
-		}
+		})
 	}
 	return true, creds, nil
 }

+ 159 - 0
service/authenticator.go

@@ -0,0 +1,159 @@
+package service
+
+import (
+	"encoding/base64"
+	"errors"
+	"fmt"
+	"github.com/jcmturner/goidentity"
+	"github.com/jcmturner/gokrb5/client"
+	"github.com/jcmturner/gokrb5/config"
+	"github.com/jcmturner/gokrb5/credentials"
+	"github.com/jcmturner/gokrb5/gssapi"
+	"github.com/jcmturner/gokrb5/keytab"
+	"strings"
+	"time"
+)
+
+// SPNEGOAuthenticator implements github.com/jcmturner/goidentity.Authenticator interface
+type SPNEGOAuthenticator struct {
+	SPNEGOHeaderValue string
+	Keytab            *keytab.Keytab
+	ServiceAccount    string
+	ClientAddr        string
+}
+
+// Authenticate and retrieve a goidentity.Identity. In this case it is a pointer to a credentials.Credentials
+func (a SPNEGOAuthenticator) Authenticate() (i goidentity.Identity, ok bool, err error) {
+	b, err := base64.StdEncoding.DecodeString(a.SPNEGOHeaderValue)
+	if err != nil {
+		err = fmt.Errorf("SPNEGO error in base64 decoding negotiation header: %v", err)
+		return
+	}
+	var spnego gssapi.SPNEGO
+	err = spnego.Unmarshal(b)
+	if !spnego.Init {
+		err = fmt.Errorf("SPNEGO negotiation token is not a NegTokenInit: %v", err)
+		return
+	}
+	if !spnego.NegTokenInit.MechTypes[0].Equal(gssapi.MechTypeOID_Krb5) {
+		err = errors.New("SPNEGO OID of MechToken is not of type KRB5")
+		return
+	}
+	var mt gssapi.MechToken
+	err = mt.Unmarshal(spnego.NegTokenInit.MechToken)
+	if err != nil {
+		err = fmt.Errorf("SPNEGO error unmarshaling MechToken: %v", err)
+		return
+	}
+	if !mt.IsAPReq() {
+		err = errors.New("MechToken does not contain an AP_REQ - KRB_AP_ERR_MSG_TYPE")
+		return
+	}
+
+	ok, c, err := ValidateAPREQ(mt.APReq, *a.Keytab, a.ServiceAccount, a.ClientAddr)
+	if err != nil {
+		err = fmt.Errorf("SPNEGO validation error: %v", err)
+		return
+	}
+	i = &c
+	return
+}
+
+func (a SPNEGOAuthenticator) Mechanism() string {
+	return "SPNEGO Kerberos"
+}
+
+// KRB5BasicAuthenticator implements github.com/jcmturner/goidentity.Authenticator interface.
+// It takes username and password so can be used for basic authentication.
+type KRB5BasicAuthenticator struct {
+	BasicHeaderValue string
+	realm            string
+	username         string
+	password         string
+	ServiceKeytab    *keytab.Keytab
+	ServiceAccount   string
+	Config           *config.Config
+	SPN              string
+}
+
+func (a KRB5BasicAuthenticator) Authenticate() (i goidentity.Identity, ok bool, err error) {
+	a.realm, a.username, a.password, err = parseBasicHeaderValue(a.BasicHeaderValue)
+	if err != nil {
+		err = fmt.Errorf("could not parse basic authentication header: %v", err)
+		return
+	}
+	cl := client.NewClientWithPassword(a.username, a.realm, a.password)
+	cl.WithConfig(a.Config)
+	err = cl.Login()
+	if err != nil {
+		// Username and/or password could be wrong
+		err = fmt.Errorf("Error with user credentials during login: %v", err)
+		return
+	}
+	tkt, _, err := cl.GetServiceTicket(a.SPN)
+	if err != nil {
+		err = fmt.Errorf("Could not get service ticket: %v", err)
+		return
+	}
+	err = tkt.DecryptEncPart(*a.ServiceKeytab, a.ServiceAccount)
+	if err != nil {
+		err = fmt.Errorf("Could not decrypt service ticket: %v", err)
+		return
+	}
+	cl.Credentials.SetAuthTime(time.Now().UTC())
+	cl.Credentials.SetAuthenticated(true)
+	isPAC, pac, err := tkt.GetPACType(*a.ServiceKeytab, a.ServiceAccount)
+	if isPAC && err != nil {
+		err = fmt.Errorf("Error processing PAC: %v", err)
+		return
+	}
+	if isPAC {
+		// There is a valid PAC. Adding attributes to creds
+		cl.Credentials.SetADCredentials(credentials.ADCredentials{
+			GroupMembershipSIDs: pac.KerbValidationInfo.GetGroupMembershipSIDs(),
+			LogOnTime:           pac.KerbValidationInfo.LogOnTime.Time(),
+			LogOffTime:          pac.KerbValidationInfo.LogOffTime.Time(),
+			PasswordLastSet:     pac.KerbValidationInfo.PasswordLastSet.Time(),
+			EffectiveName:       pac.KerbValidationInfo.EffectiveName.Value,
+			FullName:            pac.KerbValidationInfo.FullName.Value,
+			UserID:              int(pac.KerbValidationInfo.UserID),
+			PrimaryGroupID:      int(pac.KerbValidationInfo.PrimaryGroupID),
+			LogonServer:         pac.KerbValidationInfo.LogonServer.Value,
+			LogonDomainName:     pac.KerbValidationInfo.LogonDomainName.Value,
+			LogonDomainID:       pac.KerbValidationInfo.LogonDomainID.ToString(),
+		})
+	}
+	ok = true
+	i = cl.Credentials
+	return
+}
+
+func (a KRB5BasicAuthenticator) Mechanism() string {
+	return "Kerberos Basic"
+}
+
+func parseBasicHeaderValue(s string) (domain, username, password string, err error) {
+	b, err := base64.StdEncoding.DecodeString(s)
+	if err != nil {
+		return
+	}
+	v := string(b)
+	vc := strings.SplitN(v, ":", 2)
+	password = vc[1]
+	// Domain and username can be specified in 2 formats:
+	// <Username> - no domain specified
+	// <Domain>\<Username>
+	// <Username>@<Domain>
+	if strings.Contains(vc[0], `\`) {
+		u := strings.SplitN(vc[0], `\`, 2)
+		domain = u[0]
+		username = u[1]
+	} else if strings.Contains(vc[0], `@`) {
+		u := strings.SplitN(vc[0], `@`, 2)
+		domain = u[1]
+		username = u[0]
+	} else {
+		username = vc[0]
+	}
+	return
+}

+ 14 - 0
service/authenticator_test.go

@@ -0,0 +1,14 @@
+package service
+
+import (
+	"github.com/jcmturner/goidentity"
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestImplementsInterface(t *testing.T) {
+	//s := new(SPNEGOAuthenticator)
+	var s SPNEGOAuthenticator
+	a := new(goidentity.Authenticator)
+	assert.Implements(t, a, s, "SPNEGOAuthenticator type does not implement the goidentity.Authenticator interface")
+}

+ 9 - 1
service/http.go

@@ -71,7 +71,7 @@ func SPNEGOKRB5Authenticate(f http.Handler, kt keytab.Keytab, sa string, l *log.
 			if l != nil {
 				l.Printf("%v %s@%s - SPNEGO authentication succeeded", r.RemoteAddr, creds.Username, creds.Realm)
 			}
-			w.Header().Set("WWW-Authenticate", SPNEGO_NegTokenResp_Krb_Accept_Completed)
+			SPNEGOResponseAcceptCompleted(w)
 			f.ServeHTTP(w, r.WithContext(ctx))
 		} else {
 			rejectSPNEGO(w, l, fmt.Sprintf("%v - SPNEGO Kerberos authentication failed: %v", r.RemoteAddr, err))
@@ -85,7 +85,15 @@ func rejectSPNEGO(w http.ResponseWriter, l *log.Logger, logMsg string) {
 	if l != nil {
 		l.Println(logMsg)
 	}
+	SPNEGOResponseReject(w)
+}
+
+func SPNEGOResponseReject(w http.ResponseWriter) {
 	w.Header().Set("WWW-Authenticate", SPNEGO_NegTokenResp_Reject)
 	w.WriteHeader(http.StatusUnauthorized)
 	w.Write([]byte("Unauthorised.\n"))
 }
+
+func SPNEGOResponseAcceptCompleted(w http.ResponseWriter) {
+	w.Header().Set("WWW-Authenticate", SPNEGO_NegTokenResp_Krb_Accept_Completed)
+}