http.go 14 KB

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