http.go 13 KB

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