github.com/jcmturner/gokrb5/v8@v8.4.4/spnego/http.go (about)

     1  package spnego
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/base64"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net"
    10  	"net/http"
    11  	"net/http/cookiejar"
    12  	"net/url"
    13  	"strings"
    14  
    15  	"github.com/jcmturner/gofork/encoding/asn1"
    16  	"github.com/jcmturner/goidentity/v6"
    17  	"github.com/jcmturner/gokrb5/v8/client"
    18  	"github.com/jcmturner/gokrb5/v8/credentials"
    19  	"github.com/jcmturner/gokrb5/v8/gssapi"
    20  	"github.com/jcmturner/gokrb5/v8/iana/nametype"
    21  	"github.com/jcmturner/gokrb5/v8/keytab"
    22  	"github.com/jcmturner/gokrb5/v8/krberror"
    23  	"github.com/jcmturner/gokrb5/v8/service"
    24  	"github.com/jcmturner/gokrb5/v8/types"
    25  )
    26  
    27  // Client side functionality //
    28  
    29  // Client will negotiate authentication with a server using SPNEGO.
    30  type Client struct {
    31  	*http.Client
    32  	krb5Client *client.Client
    33  	spn        string
    34  	reqs       []*http.Request
    35  }
    36  
    37  type redirectErr struct {
    38  	reqTarget *http.Request
    39  }
    40  
    41  func (e redirectErr) Error() string {
    42  	return fmt.Sprintf("redirect to %v", e.reqTarget.URL)
    43  }
    44  
    45  type teeReadCloser struct {
    46  	io.Reader
    47  	io.Closer
    48  }
    49  
    50  // NewClient returns a SPNEGO enabled HTTP client.
    51  // Be careful when passing in the *http.Client if it is beginning reused in multiple calls to this function.
    52  // Ensure reuse of the provided *http.Client is for the same user as a session cookie may have been added to
    53  // http.Client's cookie jar.
    54  // Incorrect reuse of the provided *http.Client could lead to access to the wrong user's session.
    55  func NewClient(krb5Cl *client.Client, httpCl *http.Client, spn string) *Client {
    56  	if httpCl == nil {
    57  		httpCl = &http.Client{}
    58  	}
    59  	// Add a cookie jar if there isn't one
    60  	if httpCl.Jar == nil {
    61  		httpCl.Jar, _ = cookiejar.New(nil)
    62  	}
    63  	// Add a CheckRedirect function that will execute any functional already defined and then error with a redirectErr
    64  	f := httpCl.CheckRedirect
    65  	httpCl.CheckRedirect = func(req *http.Request, via []*http.Request) error {
    66  		if f != nil {
    67  			err := f(req, via)
    68  			if err != nil {
    69  				return err
    70  			}
    71  		}
    72  		return redirectErr{reqTarget: req}
    73  	}
    74  	return &Client{
    75  		Client:     httpCl,
    76  		krb5Client: krb5Cl,
    77  		spn:        spn,
    78  	}
    79  }
    80  
    81  // Do is the SPNEGO enabled HTTP client's equivalent of the http.Client's Do method.
    82  func (c *Client) Do(req *http.Request) (resp *http.Response, err error) {
    83  	var body bytes.Buffer
    84  	if req.Body != nil {
    85  		// Use a tee reader to capture any body sent in case we have to replay it again
    86  		teeR := io.TeeReader(req.Body, &body)
    87  		teeRC := teeReadCloser{teeR, req.Body}
    88  		req.Body = teeRC
    89  	}
    90  	resp, err = c.Client.Do(req)
    91  	if err != nil {
    92  		if ue, ok := err.(*url.Error); ok {
    93  			if e, ok := ue.Err.(redirectErr); ok {
    94  				// Picked up a redirect
    95  				e.reqTarget.Header.Del(HTTPHeaderAuthRequest)
    96  				c.reqs = append(c.reqs, e.reqTarget)
    97  				if len(c.reqs) >= 10 {
    98  					return resp, errors.New("stopped after 10 redirects")
    99  				}
   100  				if req.Body != nil {
   101  					// Refresh the body reader so the body can be sent again
   102  					e.reqTarget.Body = io.NopCloser(&body)
   103  				}
   104  				return c.Do(e.reqTarget)
   105  			}
   106  		}
   107  		return resp, err
   108  	}
   109  	if respUnauthorizedNegotiate(resp) {
   110  		err := SetSPNEGOHeader(c.krb5Client, req, c.spn)
   111  		if err != nil {
   112  			return resp, err
   113  		}
   114  		if req.Body != nil {
   115  			// Refresh the body reader so the body can be sent again
   116  			req.Body = io.NopCloser(&body)
   117  		}
   118  		io.Copy(io.Discard, resp.Body)
   119  		resp.Body.Close()
   120  		return c.Do(req)
   121  	}
   122  	return resp, err
   123  }
   124  
   125  // Get is the SPNEGO enabled HTTP client's equivalent of the http.Client's Get method.
   126  func (c *Client) Get(url string) (resp *http.Response, err error) {
   127  	req, err := http.NewRequest("GET", url, nil)
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  	return c.Do(req)
   132  }
   133  
   134  // Post is the SPNEGO enabled HTTP client's equivalent of the http.Client's Post method.
   135  func (c *Client) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) {
   136  	req, err := http.NewRequest("POST", url, body)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  	req.Header.Set("Content-Type", contentType)
   141  	return c.Do(req)
   142  }
   143  
   144  // PostForm is the SPNEGO enabled HTTP client's equivalent of the http.Client's PostForm method.
   145  func (c *Client) PostForm(url string, data url.Values) (resp *http.Response, err error) {
   146  	return c.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
   147  }
   148  
   149  // Head is the SPNEGO enabled HTTP client's equivalent of the http.Client's Head method.
   150  func (c *Client) Head(url string) (resp *http.Response, err error) {
   151  	req, err := http.NewRequest("HEAD", url, nil)
   152  	if err != nil {
   153  		return nil, err
   154  	}
   155  	return c.Do(req)
   156  }
   157  
   158  func respUnauthorizedNegotiate(resp *http.Response) bool {
   159  	if resp.StatusCode == http.StatusUnauthorized {
   160  		if resp.Header.Get(HTTPHeaderAuthResponse) == HTTPHeaderAuthResponseValueKey {
   161  			return true
   162  		}
   163  	}
   164  	return false
   165  }
   166  
   167  func setRequestSPN(r *http.Request) (types.PrincipalName, error) {
   168  	h := strings.TrimSuffix(r.URL.Host, ".")
   169  	// This if statement checks if the host includes a port number
   170  	if strings.LastIndex(r.URL.Host, ":") > strings.LastIndex(r.URL.Host, "]") {
   171  		// There is a port number in the URL
   172  		h, p, err := net.SplitHostPort(h)
   173  		if err != nil {
   174  			return types.PrincipalName{}, err
   175  		}
   176  		name, err := net.LookupCNAME(h)
   177  		if name != "" && err == nil {
   178  			// Underlyng canonical name should be used for SPN
   179  			h = strings.ToLower(name)
   180  		}
   181  		h = strings.TrimSuffix(h, ".")
   182  		r.Host = fmt.Sprintf("%s:%s", h, p)
   183  		return types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, "HTTP/"+h), nil
   184  	}
   185  	name, err := net.LookupCNAME(h)
   186  	if name != "" && err == nil {
   187  		// Underlyng canonical name should be used for SPN
   188  		h = strings.ToLower(name)
   189  	}
   190  	h = strings.TrimSuffix(h, ".")
   191  	r.Host = h
   192  	return types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, "HTTP/"+h), nil
   193  }
   194  
   195  // SetSPNEGOHeader gets the service ticket and sets it as the SPNEGO authorization header on HTTP request object.
   196  // To auto generate the SPN from the request object pass a null string "".
   197  func SetSPNEGOHeader(cl *client.Client, r *http.Request, spn string) error {
   198  	if spn == "" {
   199  		pn, err := setRequestSPN(r)
   200  		if err != nil {
   201  			return err
   202  		}
   203  		spn = pn.PrincipalNameString()
   204  	}
   205  	cl.Log("using SPN %s", spn)
   206  	s := SPNEGOClient(cl, spn)
   207  	err := s.AcquireCred()
   208  	if err != nil {
   209  		return fmt.Errorf("could not acquire client credential: %v", err)
   210  	}
   211  	st, err := s.InitSecContext()
   212  	if err != nil {
   213  		return fmt.Errorf("could not initialize context: %v", err)
   214  	}
   215  	nb, err := st.Marshal()
   216  	if err != nil {
   217  		return krberror.Errorf(err, krberror.EncodingError, "could not marshal SPNEGO")
   218  	}
   219  	hs := "Negotiate " + base64.StdEncoding.EncodeToString(nb)
   220  	r.Header.Set(HTTPHeaderAuthRequest, hs)
   221  	return nil
   222  }
   223  
   224  // Service side functionality //
   225  
   226  const (
   227  	// spnegoNegTokenRespKRBAcceptCompleted - The response on successful authentication always has this header. Capturing as const so we don't have marshaling and encoding overhead.
   228  	spnegoNegTokenRespKRBAcceptCompleted = "Negotiate oRQwEqADCgEAoQsGCSqGSIb3EgECAg=="
   229  	// spnegoNegTokenRespReject - The response on a failed authentication always has this rejection header. Capturing as const so we don't have marshaling and encoding overhead.
   230  	spnegoNegTokenRespReject = "Negotiate oQcwBaADCgEC"
   231  	// spnegoNegTokenRespIncompleteKRB5 - Response token specifying incomplete context and KRB5 as the supported mechtype.
   232  	spnegoNegTokenRespIncompleteKRB5 = "Negotiate oRQwEqADCgEBoQsGCSqGSIb3EgECAg=="
   233  	// sessionCredentials is the session value key holding the credentials jcmturner/goidentity/Identity object.
   234  	sessionCredentials = "github.com/jcmturner/gokrb5/v8/sessionCredentials"
   235  	// ctxCredentials is the SPNEGO context key holding the credentials jcmturner/goidentity/Identity object.
   236  	ctxCredentials = "github.com/jcmturner/gokrb5/v8/ctxCredentials"
   237  	// HTTPHeaderAuthRequest is the header that will hold authn/z information.
   238  	HTTPHeaderAuthRequest = "Authorization"
   239  	// HTTPHeaderAuthResponse is the header that will hold SPNEGO data from the server.
   240  	HTTPHeaderAuthResponse = "WWW-Authenticate"
   241  	// HTTPHeaderAuthResponseValueKey is the key in the auth header for SPNEGO.
   242  	HTTPHeaderAuthResponseValueKey = "Negotiate"
   243  	// UnauthorizedMsg is the message returned in the body when authentication fails.
   244  	UnauthorizedMsg = "Unauthorised.\n"
   245  )
   246  
   247  // SPNEGOKRB5Authenticate is a Kerberos SPNEGO authentication HTTP handler wrapper.
   248  func SPNEGOKRB5Authenticate(inner http.Handler, kt *keytab.Keytab, settings ...func(*service.Settings)) http.Handler {
   249  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   250  		// Set up the SPNEGO GSS-API mechanism
   251  		var spnego *SPNEGO
   252  		h, err := types.GetHostAddress(r.RemoteAddr)
   253  		if err == nil {
   254  			// put in this order so that if the user provides a ClientAddress it will override the one here.
   255  			o := append([]func(*service.Settings){service.ClientAddress(h)}, settings...)
   256  			spnego = SPNEGOService(kt, o...)
   257  		} else {
   258  			spnego = SPNEGOService(kt, settings...)
   259  			spnego.Log("%s - SPNEGO could not parse client address: %v", r.RemoteAddr, err)
   260  		}
   261  
   262  		// Check if there is a session manager and if there is an already established session for this client
   263  		id, err := getSessionCredentials(spnego, r)
   264  		if err == nil && id.Authenticated() {
   265  			// There is an established session so bypass auth and serve
   266  			spnego.Log("%s - SPNEGO request served under session %s", r.RemoteAddr, id.SessionID())
   267  			inner.ServeHTTP(w, goidentity.AddToHTTPRequestContext(&id, r))
   268  			return
   269  		}
   270  
   271  		st, err := getAuthorizationNegotiationHeaderAsSPNEGOToken(spnego, r, w)
   272  		if st == nil || err != nil {
   273  			// response to client and logging handled in function above so just return
   274  			return
   275  		}
   276  
   277  		// Validate the context token
   278  		authed, ctx, status := spnego.AcceptSecContext(st)
   279  		if status.Code != gssapi.StatusComplete && status.Code != gssapi.StatusContinueNeeded {
   280  			spnegoResponseReject(spnego, w, "%s - SPNEGO validation error: %v", r.RemoteAddr, status)
   281  			return
   282  		}
   283  		if status.Code == gssapi.StatusContinueNeeded {
   284  			spnegoNegotiateKRB5MechType(spnego, w, "%s - SPNEGO GSS-API continue needed", r.RemoteAddr)
   285  			return
   286  		}
   287  
   288  		if authed {
   289  			// Authentication successful; get user's credentials from the context
   290  			id := ctx.Value(ctxCredentials).(*credentials.Credentials)
   291  			// Create a new session if a session manager has been configured
   292  			err = newSession(spnego, r, w, id)
   293  			if err != nil {
   294  				return
   295  			}
   296  			spnegoResponseAcceptCompleted(spnego, w, "%s %s@%s - SPNEGO authentication succeeded", r.RemoteAddr, id.UserName(), id.Domain())
   297  			// Add the identity to the context and serve the inner/wrapped handler
   298  			inner.ServeHTTP(w, goidentity.AddToHTTPRequestContext(id, r))
   299  			return
   300  		}
   301  		// If we get to here we have not authenticationed so just reject
   302  		spnegoResponseReject(spnego, w, "%s - SPNEGO Kerberos authentication failed", r.RemoteAddr)
   303  		return
   304  	})
   305  }
   306  
   307  func getAuthorizationNegotiationHeaderAsSPNEGOToken(spnego *SPNEGO, r *http.Request, w http.ResponseWriter) (*SPNEGOToken, error) {
   308  	s := strings.SplitN(r.Header.Get(HTTPHeaderAuthRequest), " ", 2)
   309  	if len(s) != 2 || s[0] != HTTPHeaderAuthResponseValueKey {
   310  		// No Authorization header set so return 401 with WWW-Authenticate Negotiate header
   311  		w.Header().Set(HTTPHeaderAuthResponse, HTTPHeaderAuthResponseValueKey)
   312  		http.Error(w, UnauthorizedMsg, http.StatusUnauthorized)
   313  		return nil, errors.New("client did not provide a negotiation authorization header")
   314  	}
   315  
   316  	// Decode the header into an SPNEGO context token
   317  	b, err := base64.StdEncoding.DecodeString(s[1])
   318  	if err != nil {
   319  		err = fmt.Errorf("error in base64 decoding negotiation header: %v", err)
   320  		spnegoNegotiateKRB5MechType(spnego, w, "%s - SPNEGO %v", r.RemoteAddr, err)
   321  		return nil, err
   322  	}
   323  	var st SPNEGOToken
   324  	err = st.Unmarshal(b)
   325  	if err != nil {
   326  		// Check if this is a raw KRB5 context token - issue #347.
   327  		var k5t KRB5Token
   328  		if k5t.Unmarshal(b) != nil {
   329  			err = fmt.Errorf("error in unmarshaling SPNEGO token: %v", err)
   330  			spnegoNegotiateKRB5MechType(spnego, w, "%s - SPNEGO %v", r.RemoteAddr, err)
   331  			return nil, err
   332  		}
   333  		// Wrap it into an SPNEGO context token
   334  		st.Init = true
   335  		st.NegTokenInit = NegTokenInit{
   336  			MechTypes:      []asn1.ObjectIdentifier{k5t.OID},
   337  			MechTokenBytes: b,
   338  		}
   339  	}
   340  	return &st, nil
   341  }
   342  
   343  func getSessionCredentials(spnego *SPNEGO, r *http.Request) (credentials.Credentials, error) {
   344  	var creds credentials.Credentials
   345  	// Check if there is a session manager and if there is an already established session for this client
   346  	if sm := spnego.serviceSettings.SessionManager(); sm != nil {
   347  		cb, err := sm.Get(r, sessionCredentials)
   348  		if err != nil || cb == nil || len(cb) < 1 {
   349  			return creds, fmt.Errorf("%s - SPNEGO error getting session and credentials for request: %v", r.RemoteAddr, err)
   350  		}
   351  		err = creds.Unmarshal(cb)
   352  		if err != nil {
   353  			return creds, fmt.Errorf("%s - SPNEGO credentials malformed in session: %v", r.RemoteAddr, err)
   354  		}
   355  		return creds, nil
   356  	}
   357  	return creds, errors.New("no session manager configured")
   358  }
   359  
   360  func newSession(spnego *SPNEGO, r *http.Request, w http.ResponseWriter, id *credentials.Credentials) error {
   361  	if sm := spnego.serviceSettings.SessionManager(); sm != nil {
   362  		// create new session
   363  		idb, err := id.Marshal()
   364  		if err != nil {
   365  			spnegoInternalServerError(spnego, w, "SPNEGO could not marshal credentials to add to the session: %v", err)
   366  			return err
   367  		}
   368  		err = sm.New(w, r, sessionCredentials, idb)
   369  		if err != nil {
   370  			spnegoInternalServerError(spnego, w, "SPNEGO could not create new session: %v", err)
   371  			return err
   372  		}
   373  		spnego.Log("%s %s@%s - SPNEGO new session (%s) created", r.RemoteAddr, id.UserName(), id.Domain(), id.SessionID())
   374  	}
   375  	return nil
   376  }
   377  
   378  // Log and respond to client for error conditions
   379  
   380  func spnegoNegotiateKRB5MechType(s *SPNEGO, w http.ResponseWriter, format string, v ...interface{}) {
   381  	s.Log(format, v...)
   382  	w.Header().Set(HTTPHeaderAuthResponse, spnegoNegTokenRespIncompleteKRB5)
   383  	http.Error(w, UnauthorizedMsg, http.StatusUnauthorized)
   384  }
   385  
   386  func spnegoResponseReject(s *SPNEGO, w http.ResponseWriter, format string, v ...interface{}) {
   387  	s.Log(format, v...)
   388  	w.Header().Set(HTTPHeaderAuthResponse, spnegoNegTokenRespReject)
   389  	http.Error(w, UnauthorizedMsg, http.StatusUnauthorized)
   390  }
   391  
   392  func spnegoResponseAcceptCompleted(s *SPNEGO, w http.ResponseWriter, format string, v ...interface{}) {
   393  	s.Log(format, v...)
   394  	w.Header().Set(HTTPHeaderAuthResponse, spnegoNegTokenRespKRBAcceptCompleted)
   395  }
   396  
   397  func spnegoInternalServerError(s *SPNEGO, w http.ResponseWriter, format string, v ...interface{}) {
   398  	s.Log(format, v...)
   399  	http.Error(w, "Internal Server Error", http.StatusInternalServerError)
   400  }