|
|
@@ -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"))
|
|
|
}
|