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 }