Browse Source

spnego service side

Jonathan Turner 9 years ago
parent
commit
f963087c61
3 changed files with 100 additions and 61 deletions
  1. 2 2
      GSSAPI/krb5Token.go
  2. 1 0
      service/cache.go
  3. 97 59
      service/http.go

+ 2 - 2
GSSAPI/krb5Token.go

@@ -111,9 +111,9 @@ func NewKRB5APREQMechToken(c config.Config, cname types.PrincipalName, tkt messa
 }
 
 // Create new kerberos authenticator for kerberos MechToken
-func newAuthenticator(c config.Config, username types.PrincipalName, keyType int) types.Authenticator {
+func newAuthenticator(c config.Config, cname types.PrincipalName, keyType int) types.Authenticator {
 	//RFC 4121 Section 4.1.1
-	auth := types.NewAuthenticator(c.LibDefaults.Default_realm, username)
+	auth := types.NewAuthenticator(c.LibDefaults.Default_realm, cname)
 	auth.Cksum = types.Checksum{
 		CksumType: chksumtype.GSSAPI,
 		Checksum:  newAuthenticatorChksum([]int{GSS_C_INTEG_FLAG, GSS_C_CONF_FLAG}),

+ 1 - 0
service/cache.go

@@ -1,3 +1,4 @@
+// Service integrations for Kerberos authentication.
 package service
 
 import (

+ 97 - 59
service/http.go

@@ -1,8 +1,8 @@
 package service
 
 import (
+	"context"
 	"encoding/base64"
-	"errors"
 	"fmt"
 	"github.com/jcmturner/gokrb5/GSSAPI"
 	"github.com/jcmturner/gokrb5/crypto"
@@ -11,63 +11,88 @@ import (
 	"github.com/jcmturner/gokrb5/keytab"
 	"github.com/jcmturner/gokrb5/messages"
 	"github.com/jcmturner/gokrb5/types"
+	"log"
 	"net/http"
 	"strings"
 	"time"
 )
 
-// Authenticate the request. Returns:
-//
-// boolean: indicates if authenticate succeeded
-//
-// string: client principal name
-//
-// string: client realm
-//
-// error: reason for any authentication failure
-func SPNEGOKRB5Authenticate(w http.ResponseWriter, r *http.Request, ktab keytab.Keytab) (bool, string, string, error) {
-	s := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
-	if len(s) != 2 || s[0] != "Negotiate" {
-		// TODO set the NegTokenResp Negotiate header here on the w
-		return false, "", "", errors.New("No Authorization header with Negotiate content found")
-	}
-	b, err := base64.StdEncoding.DecodeString(s[1])
-	if err != nil {
-		return false, "", "", fmt.Errorf("Authorization header Negotiate content could not be base64 decoded: %v", err)
-	}
-	isInit, nt, err := GSSAPI.UnmarshalNegToken(b)
-	if err != nil || !isInit {
-		return false, "", "", fmt.Errorf("SPNEGO negotiation token is not a NegTokenInit: %v", err)
-	}
-	nInit := nt.(GSSAPI.NegTokenInit)
-	if !nInit.MechTypes[0].Equal(GSSAPI.MechTypeOID_Krb5) {
-		return false, "", "", errors.New("OID of MechToken is not of type KRB5")
-	}
-	var mt GSSAPI.MechToken
-	err = mt.Unmarshal(nInit.MechToken)
-	if err != nil {
-		return false, "", "", fmt.Errorf("Error unmarshalling MechToken: %v", err)
-	}
-	if !mt.IsAPReq() {
-		return false, "", "", errors.New("MechToken does not contain an AP_REQ - KRB_AP_ERR_MSG_TYPE")
-	}
-	err = mt.APReq.Ticket.DecryptEncPart(ktab)
-	if err != nil {
-		return false, "", "", fmt.Errorf("Error decrypting the service ticket provided: %v", err)
-	}
-	ab, err := crypto.DecryptEncPart(mt.APReq.Authenticator, mt.APReq.Ticket.DecryptedEncPart.Key, keyusage.AP_REQ_AUTHENTICATOR)
-	if err != nil {
-		return false, "", "", fmt.Errorf("Error decrypting the authenticator provided: %v", err)
-	}
-	var a types.Authenticator
-	err = a.Unmarshal(ab)
-	if err != nil {
-		return false, "", "", fmt.Errorf("Error unmarshalling the authenticator: %v", err)
+const (
+	// The response on successful authentication always has this header. Capturing as const so we don't have marshaling and encoding overhead.
+	SPNEGO_NegTokenResp_Krb_Accept_Completed = "Negotiate oRQwEqADCgEAoQsGCSqGSIb3EgECAg=="
+	SPNEGO_NegTokenResp_Reject               = "Negotiate oQcwBaADCgEC"
+)
+
+// SPNEGO Kerberos HTTP handler wrapper
+func SPNEGOKRB5Authenticate(f http.HandlerFunc, ktab keytab.Keytab, l *log.Logger) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		s := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
+		if len(s) != 2 || s[0] != "Negotiate" {
+			w.Header().Set("WWW-Authenticate", "Negotiate")
+			w.WriteHeader(401)
+			w.Write([]byte("Unauthorised.\n"))
+			return
+		}
+		b, err := base64.StdEncoding.DecodeString(s[1])
+		if err != nil {
+			rejectSPNEGO(w, l, fmt.Sprintf("%v - SPNEGO error in base64 decoding negotiation header: %v", r.RemoteAddr, err))
+			return
+		}
+		isInit, nt, err := GSSAPI.UnmarshalNegToken(b)
+		if err != nil || !isInit {
+			rejectSPNEGO(w, l, fmt.Sprintf("%v - SPNEGO negotiation token is not a NegTokenInit: %v", r.RemoteAddr, err))
+			return
+		}
+		nInit := nt.(GSSAPI.NegTokenInit)
+		if !nInit.MechTypes[0].Equal(GSSAPI.MechTypeOID_Krb5) {
+			rejectSPNEGO(w, l, fmt.Sprintf("%v - SPNEGO OID of MechToken is not of type KRB5", r.RemoteAddr))
+			return
+		}
+		var mt GSSAPI.MechToken
+		err = mt.Unmarshal(nInit.MechToken)
+		if err != nil {
+			rejectSPNEGO(w, l, fmt.Sprintf("%v - SPNEGO error unmarshaling MechToken: %v", r.RemoteAddr, err))
+			return
+		}
+		if !mt.IsAPReq() {
+			rejectSPNEGO(w, l, fmt.Sprintf("%v - MechToken does not contain an AP_REQ - KRB_AP_ERR_MSG_TYPE", r.RemoteAddr))
+			return
+		}
+		err = mt.APReq.Ticket.DecryptEncPart(ktab)
+		if err != nil {
+			rejectSPNEGO(w, l, fmt.Sprintf("%v - SPNEGO error decrypting the service ticket provided: %v", r.RemoteAddr, err))
+			return
+		}
+		ab, err := crypto.DecryptEncPart(mt.APReq.Authenticator, mt.APReq.Ticket.DecryptedEncPart.Key, keyusage.AP_REQ_AUTHENTICATOR)
+		if err != nil {
+			rejectSPNEGO(w, l, fmt.Sprintf("%v - SPNEGO error decrypting the authenticator provided: %v", r.RemoteAddr, err))
+			return
+		}
+		var a types.Authenticator
+		err = a.Unmarshal(ab)
+		if err != nil {
+			rejectSPNEGO(w, l, fmt.Sprintf("%v - SPNEGO error unmarshalling the authenticator: %v", r.RemoteAddr, err))
+			return
+		}
+		if ok, err := validateAPREQ(a, mt.APReq); ok {
+			ctx := r.Context()
+			ctx = context.WithValue(ctx, "cname", a.CName.GetPrincipalNameString())
+			ctx = context.WithValue(ctx, "crealm", a.CRealm)
+			ctx = context.WithValue(ctx, "authenticated", true)
+			w.Header().Set("WWW-Authenticate", SPNEGO_NegTokenResp_Krb_Accept_Completed)
+			f(w, r.WithContext(ctx))
+		} else {
+			rejectSPNEGO(w, l, fmt.Sprintf("%v - SPNEGO Kerberos authentication failed: %v", r.RemoteAddr, err))
+			return
+		}
 	}
-	// VALIDATIONS
+}
+
+func validateAPREQ(a types.Authenticator, APReq messages.APReq) (bool, error) {
 	// Check CName in Authenticator is the same as that in the ticket
-	if !a.CName.Equal(mt.APReq.Ticket.DecryptedEncPart.CName) {
-		return false, "", "", messages.NewKRBError(mt.APReq.Ticket.SName, mt.APReq.Ticket.Realm, errorcode.KRB_AP_ERR_BADMATCH, "CName in Authenticator does not match that in service 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")
+		return false, err
 	}
 	// TODO client address check
 	//The addresses in the ticket (if any) are then
@@ -82,23 +107,36 @@ func SPNEGOKRB5Authenticate(w http.ResponseWriter, r *http.Request, ktab keytab.
 	// Hardcode 5 min max skew. May want to make this configurable
 	d := time.Duration(5) * time.Minute
 	if t.Sub(ct) > d || ct.Sub(t) > d {
-		return false, "", "", messages.NewKRBError(mt.APReq.Ticket.SName, mt.APReq.Ticket.Realm, errorcode.KRB_AP_ERR_SKEW, fmt.Sprintf("Clock skew with client too large. Greater than %v seconds", d))
+		err := messages.NewKRBError(APReq.Ticket.SName, APReq.Ticket.Realm, errorcode.KRB_AP_ERR_SKEW, fmt.Sprintf("Clock skew with client too large. Greater than %v seconds", d))
+		return false, err
 	}
 
 	// Check for replay
 	rc := GetReplayCache(d)
-	if rc.IsReplay(d, mt.APReq.Ticket.SName, a) {
-		return false, "", "", messages.NewKRBError(mt.APReq.Ticket.SName, mt.APReq.Ticket.Realm, errorcode.KRB_AP_ERR_REPEAT, "Replay detected")
+	if rc.IsReplay(d, APReq.Ticket.SName, a) {
+		err := messages.NewKRBError(APReq.Ticket.SName, APReq.Ticket.Realm, errorcode.KRB_AP_ERR_REPEAT, "Replay detected")
+		return false, err
 	}
 
 	// Check for future tickets or invalid tickets
-	if mt.APReq.Ticket.DecryptedEncPart.StartTime.Sub(t) > d || types.IsFlagSet(&mt.APReq.Ticket.DecryptedEncPart.Flags, types.Invalid) {
-		return false, "", "", messages.NewKRBError(mt.APReq.Ticket.SName, mt.APReq.Ticket.Realm, errorcode.KRB_AP_ERR_TKT_NYV, "Service ticket provided is not yet valid")
+	if APReq.Ticket.DecryptedEncPart.StartTime.Sub(t) > d || types.IsFlagSet(&APReq.Ticket.DecryptedEncPart.Flags, types.Invalid) {
+		err := messages.NewKRBError(APReq.Ticket.SName, APReq.Ticket.Realm, errorcode.KRB_AP_ERR_TKT_NYV, "Service ticket provided is not yet valid")
+		return false, err
 	}
 
 	// Check for expired ticket
-	if t.Sub(mt.APReq.Ticket.DecryptedEncPart.EndTime) > d {
-		return false, "", "", messages.NewKRBError(mt.APReq.Ticket.SName, mt.APReq.Ticket.Realm, errorcode.KRB_AP_ERR_TKT_EXPIRED, "Service ticket provided has expired")
+	if t.Sub(APReq.Ticket.DecryptedEncPart.EndTime) > d {
+		err := messages.NewKRBError(APReq.Ticket.SName, APReq.Ticket.Realm, errorcode.KRB_AP_ERR_TKT_EXPIRED, "Service ticket provided has expired")
+		return false, err
+	}
+	return true
+}
+
+func rejectSPNEGO(w http.ResponseWriter, l *log.Logger, logMsg string) {
+	if l != nil {
+		l.Println(logMsg)
 	}
-	return true, a.CName.GetPrincipalNameString(), a.CRealm, nil
+	w.Header().Set("WWW-Authenticate", SPNEGO_NegTokenResp_Reject)
+	w.WriteHeader(401)
+	w.Write([]byte("Unauthorised.\n"))
 }