http.go 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. package spnego
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/base64"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "io/ioutil"
  10. "net"
  11. "net/http"
  12. "net/http/cookiejar"
  13. "net/url"
  14. "strings"
  15. "gopkg.in/jcmturner/goidentity.v3"
  16. "gopkg.in/jcmturner/gokrb5.v7/client"
  17. "gopkg.in/jcmturner/gokrb5.v7/gssapi"
  18. "gopkg.in/jcmturner/gokrb5.v7/keytab"
  19. "gopkg.in/jcmturner/gokrb5.v7/krberror"
  20. "gopkg.in/jcmturner/gokrb5.v7/service"
  21. "gopkg.in/jcmturner/gokrb5.v7/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. type ctxKey string
  178. const (
  179. // spnegoNegTokenRespKRBAcceptCompleted - The response on successful authentication always has this header. Capturing as const so we don't have marshaling and encoding overhead.
  180. spnegoNegTokenRespKRBAcceptCompleted = "Negotiate oRQwEqADCgEAoQsGCSqGSIb3EgECAg=="
  181. // spnegoNegTokenRespReject - The response on a failed authentication always has this rejection header. Capturing as const so we don't have marshaling and encoding overhead.
  182. spnegoNegTokenRespReject = "Negotiate oQcwBaADCgEC"
  183. // spnegoNegTokenRespIncompleteKRB5 - Response token specifying incomplete context and KRB5 as the supported mechtype.
  184. spnegoNegTokenRespIncompleteKRB5 = "Negotiate oRQwEqADCgEBoQsGCSqGSIb3EgECAg=="
  185. // CTXKeyAuthenticated is the request context key holding a boolean indicating if the request has been authenticated.
  186. CTXKeyAuthenticated ctxKey = "github.com/jcmturner/gokrb5/CTXKeyAuthenticated"
  187. // CTXKeyCredentials is the request context key holding the credentials gopkg.in/jcmturner/goidentity.v2/Identity object.
  188. CTXKeyCredentials ctxKey = "github.com/jcmturner/gokrb5/CTXKeyCredentials"
  189. // HTTPHeaderAuthRequest is the header that will hold authn/z information.
  190. HTTPHeaderAuthRequest = "Authorization"
  191. // HTTPHeaderAuthResponse is the header that will hold SPNEGO data from the server.
  192. HTTPHeaderAuthResponse = "WWW-Authenticate"
  193. // HTTPHeaderAuthResponseValueKey is the key in the auth header for SPNEGO.
  194. HTTPHeaderAuthResponseValueKey = "Negotiate"
  195. // UnauthorizedMsg is the message returned in the body when authentication fails.
  196. UnauthorizedMsg = "Unauthorised.\n"
  197. )
  198. // SPNEGOKRB5Authenticate is a Kerberos SPNEGO authentication HTTP handler wrapper.
  199. func SPNEGOKRB5Authenticate(inner http.Handler, kt *keytab.Keytab, settings ...func(*service.Settings)) http.Handler {
  200. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  201. // Get the auth header
  202. s := strings.SplitN(r.Header.Get(HTTPHeaderAuthRequest), " ", 2)
  203. if len(s) != 2 || s[0] != HTTPHeaderAuthResponseValueKey {
  204. // No Authorization header set so return 401 with WWW-Authenticate Negotiate header
  205. w.Header().Set(HTTPHeaderAuthResponse, HTTPHeaderAuthResponseValueKey)
  206. http.Error(w, UnauthorizedMsg, http.StatusUnauthorized)
  207. return
  208. }
  209. // Set up the SPNEGO GSS-API mechanism
  210. var spnego *SPNEGO
  211. h, err := types.GetHostAddress(r.RemoteAddr)
  212. if err == nil {
  213. // put in this order so that if the user provides a ClientAddress it will override the one here.
  214. o := append([]func(*service.Settings){service.ClientAddress(h)}, settings...)
  215. spnego = SPNEGOService(kt, o...)
  216. } else {
  217. spnego = SPNEGOService(kt, settings...)
  218. spnego.Log("%s - SPNEGO could not parse client address: %v", r.RemoteAddr, err)
  219. }
  220. // Decode the header into an SPNEGO context token
  221. b, err := base64.StdEncoding.DecodeString(s[1])
  222. if err != nil {
  223. spnegoNegotiateKRB5MechType(spnego, w, "%s - SPNEGO error in base64 decoding negotiation header: %v", r.RemoteAddr, err)
  224. return
  225. }
  226. var st SPNEGOToken
  227. err = st.Unmarshal(b)
  228. if err != nil {
  229. spnegoNegotiateKRB5MechType(spnego, w, "%s - SPNEGO error in unmarshaling SPNEGO token: %v", r.RemoteAddr, err)
  230. return
  231. }
  232. // Validate the context token
  233. authed, ctx, status := spnego.AcceptSecContext(&st)
  234. if status.Code != gssapi.StatusComplete && status.Code != gssapi.StatusContinueNeeded {
  235. spnegoResponseReject(spnego, w, "%s - SPNEGO validation error: %v", r.RemoteAddr, status)
  236. return
  237. }
  238. if status.Code == gssapi.StatusContinueNeeded {
  239. spnegoNegotiateKRB5MechType(spnego, w, "%s - SPNEGO GSS-API continue needed", r.RemoteAddr)
  240. return
  241. }
  242. if authed {
  243. id := ctx.Value(CTXKeyCredentials).(goidentity.Identity)
  244. requestCtx := r.Context()
  245. requestCtx = context.WithValue(requestCtx, CTXKeyCredentials, id)
  246. requestCtx = context.WithValue(requestCtx, CTXKeyAuthenticated, ctx.Value(CTXKeyAuthenticated))
  247. spnegoResponseAcceptCompleted(spnego, w, "%s %s@%s - SPNEGO authentication succeeded", r.RemoteAddr, id.UserName(), id.Domain())
  248. inner.ServeHTTP(w, r.WithContext(requestCtx))
  249. } else {
  250. spnegoResponseReject(spnego, w, "%s - SPNEGO Kerberos authentication failed", r.RemoteAddr)
  251. return
  252. }
  253. })
  254. }
  255. func spnegoNegotiateKRB5MechType(s *SPNEGO, w http.ResponseWriter, format string, v ...interface{}) {
  256. s.Log(format, v...)
  257. w.Header().Set(HTTPHeaderAuthResponse, spnegoNegTokenRespIncompleteKRB5)
  258. http.Error(w, UnauthorizedMsg, http.StatusUnauthorized)
  259. }
  260. func spnegoResponseReject(s *SPNEGO, w http.ResponseWriter, format string, v ...interface{}) {
  261. s.Log(format, v...)
  262. w.Header().Set(HTTPHeaderAuthResponse, spnegoNegTokenRespReject)
  263. http.Error(w, UnauthorizedMsg, http.StatusUnauthorized)
  264. }
  265. func spnegoResponseAcceptCompleted(s *SPNEGO, w http.ResponseWriter, format string, v ...interface{}) {
  266. s.Log(format, v...)
  267. w.Header().Set(HTTPHeaderAuthResponse, spnegoNegTokenRespKRBAcceptCompleted)
  268. }