handler.go (11860B)
1 package login 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io/ioutil" 7 "net/http" 8 "strings" 9 "time" 10 11 "github.com/dgrijalva/jwt-go" 12 "github.com/pkg/errors" 13 "github.com/tarent/loginsrv/logging" 14 "github.com/tarent/loginsrv/model" 15 "github.com/tarent/loginsrv/oauth2" 16 ) 17 18 const contentTypeHTML = "text/html; charset=utf-8" 19 const contentTypeJWT = "application/jwt" 20 const contentTypeJSON = "application/json" 21 const contentTypePlain = "text/plain" 22 23 type userClaimsFunc func(userInfo model.UserInfo) (jwt.Claims, error) 24 25 // Handler is the mail login handler. 26 // It serves the login ressource and does the authentication against the backends or oauth provider. 27 type Handler struct { 28 backends []Backend 29 oauth oauthManager 30 config *Config 31 signingMethod jwt.SigningMethod 32 signingKey interface{} 33 signingVerifyKey interface{} 34 userClaims userClaimsFunc 35 } 36 37 // NewHandler creates a login handler based on the supplied configuration. 38 func NewHandler(config *Config) (*Handler, error) { 39 if len(config.Backends) == 0 && len(config.Oauth) == 0 { 40 return nil, errors.New("No login backends or oauth provider configured") 41 } 42 43 backends := []Backend{} 44 for pName, opts := range config.Backends { 45 p, exist := GetProvider(pName) 46 if !exist { 47 return nil, fmt.Errorf("No such provider: %v", pName) 48 } 49 b, err := p(opts) 50 if err != nil { 51 return nil, err 52 } 53 backends = append(backends, b) 54 } 55 56 oauth := oauth2.NewManager() 57 for providerName, opts := range config.Oauth { 58 err := oauth.AddConfig(providerName, opts) 59 if err != nil { 60 return nil, err 61 } 62 } 63 64 userClaims, err := NewUserClaims(config) 65 if err != nil { 66 return nil, err 67 } 68 69 return &Handler{ 70 backends: backends, 71 config: config, 72 oauth: oauth, 73 userClaims: userClaims.Claims, 74 }, nil 75 } 76 77 func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 78 if !strings.HasPrefix(r.URL.Path, h.config.LoginPath) { 79 h.respondNotFound(w, r) 80 return 81 } 82 83 h.setRedirectCookie(w, r) 84 85 _, err := h.oauth.GetConfigFromRequest(r) 86 if err == nil { 87 h.handleOauth(w, r) 88 return 89 } 90 91 h.handleLogin(w, r) 92 } 93 94 func (h *Handler) handleOauth(w http.ResponseWriter, r *http.Request) { 95 startedFlow, authenticated, userInfo, err := h.oauth.Handle(w, r) 96 97 if startedFlow { 98 // the oauth flow started 99 return 100 } 101 102 if err != nil { 103 logging.Application(r.Header).WithError(err).Error() 104 h.respondError(w, r) 105 return 106 } 107 108 if authenticated { 109 logging.Application(r.Header). 110 WithField("username", userInfo.Sub).Info("successfully authenticated") 111 h.respondAuthenticated(w, r, userInfo) 112 return 113 } 114 logging.Application(r.Header). 115 WithField("username", userInfo.Sub).Info("failed authentication") 116 117 h.respondAuthFailure(w, r) 118 } 119 120 func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) { 121 contentType := r.Header.Get("Content-Type") 122 if !(r.Method == "GET" || r.Method == "DELETE" || 123 (r.Method == "POST" && 124 (strings.HasPrefix(contentType, contentTypeJSON) || 125 strings.HasPrefix(contentType, "application/x-www-form-urlencoded") || 126 strings.HasPrefix(contentType, "multipart/form-data") || 127 contentType == ""))) { 128 h.respondBadRequest(w, r) 129 return 130 } 131 132 r.ParseForm() 133 if r.Method == "DELETE" || r.FormValue("logout") == "true" { 134 h.deleteToken(w) 135 if h.config.LogoutURL != "" { 136 w.Header().Set("Location", h.config.LogoutURL) 137 w.WriteHeader(303) 138 return 139 } 140 writeLoginForm(w, 141 loginFormData{ 142 Config: h.config, 143 }) 144 return 145 } 146 147 if r.Method == "GET" { 148 userInfo, valid := h.GetToken(r) 149 if wantJSON(r) { 150 if valid { 151 w.Header().Set("Content-Type", contentTypeJSON) 152 enc := json.NewEncoder(w) 153 enc.Encode(userInfo) // ignore error of encoding 154 } else { 155 h.respondAuthFailure(w, r) 156 } 157 return 158 } 159 writeLoginForm(w, 160 loginFormData{ 161 Config: h.config, 162 Authenticated: valid, 163 UserInfo: userInfo, 164 }) 165 return 166 } 167 168 if r.Method == "POST" { 169 username, password, err := getCredentials(r) 170 if err != nil { 171 h.respondBadRequest(w, r) 172 return 173 } 174 if username != "" { 175 // No token found or credentials found, assuming new authentication 176 h.handleAuthentication(w, r, username, password) 177 return 178 } 179 userInfo, valid := h.GetToken(r) 180 if valid { 181 h.handleRefresh(w, r, userInfo) 182 return 183 } 184 if username == "" { 185 h.respondAuthFailure(w, r) 186 return 187 } 188 189 h.respondBadRequest(w, r) 190 return 191 } 192 } 193 194 func (h *Handler) handleAuthentication(w http.ResponseWriter, r *http.Request, username string, password string) { 195 authenticated, userInfo, err := h.authenticate(username, password) 196 if err != nil { 197 logging.Application(r.Header).WithError(err).Error() 198 h.respondError(w, r) 199 return 200 } 201 202 if authenticated { 203 logging.Application(r.Header). 204 WithField("username", username).Info("successfully authenticated") 205 h.respondAuthenticated(w, r, userInfo) 206 return 207 } 208 logging.Application(r.Header). 209 WithField("username", username).Info("failed authentication") 210 211 h.respondAuthFailure(w, r) 212 } 213 214 func (h *Handler) handleRefresh(w http.ResponseWriter, r *http.Request, userInfo model.UserInfo) { 215 if userInfo.Refreshes >= h.config.JwtRefreshes { 216 h.respondMaxRefreshesReached(w, r) 217 } else { 218 userInfo.Refreshes++ 219 h.respondAuthenticated(w, r, userInfo) 220 logging.Application(r.Header).WithField("username", userInfo.Sub).Info("refreshed jwt") 221 } 222 } 223 224 func (h *Handler) deleteToken(w http.ResponseWriter) { 225 cookie := &http.Cookie{ 226 Name: h.config.CookieName, 227 Value: "delete", 228 HttpOnly: true, 229 Expires: time.Unix(0, 0), 230 Path: "/", 231 } 232 if h.config.CookieDomain != "" { 233 cookie.Domain = h.config.CookieDomain 234 } 235 http.SetCookie(w, cookie) 236 } 237 238 func (h *Handler) respondAuthenticated(w http.ResponseWriter, r *http.Request, userInfo model.UserInfo) { 239 userInfo.Expiry = time.Now().Add(h.config.JwtExpiry).Unix() 240 token, err := h.createToken(userInfo) 241 if err != nil { 242 logging.Application(r.Header).WithError(err).Error() 243 h.respondError(w, r) 244 return 245 } 246 247 if wantHTML(r) { 248 h.respondAuthenticatedHTML(w, r, token) 249 return 250 } 251 252 w.Header().Set("Content-Type", contentTypeJWT) 253 w.WriteHeader(200) 254 fmt.Fprint(w, token) 255 } 256 257 func (h *Handler) respondAuthenticatedHTML(w http.ResponseWriter, r *http.Request, token string) { 258 cookie := &http.Cookie{ 259 Name: h.config.CookieName, 260 Value: token, 261 HttpOnly: h.config.CookieHTTPOnly, 262 Path: "/", 263 } 264 if h.config.CookieExpiry != 0 { 265 cookie.Expires = time.Now().Add(h.config.CookieExpiry) 266 } 267 if h.config.CookieDomain != "" { 268 cookie.Domain = h.config.CookieDomain 269 } 270 cookie.Secure = h.config.CookieSecure 271 http.SetCookie(w, cookie) 272 w.Header().Set("Location", h.redirectURL(r, w)) 273 h.deleteRedirectCookie(w, r) 274 w.WriteHeader(303) 275 } 276 277 func (h *Handler) createToken(userInfo model.UserInfo) (string, error) { 278 var claims jwt.Claims = userInfo 279 if h.userClaims != nil { 280 var err error 281 claims, err = h.userClaims(userInfo) 282 if err != nil { 283 return "", err 284 } 285 } 286 287 signingMethod, key, _, err := h.signingInfo() 288 if err != nil { 289 return "", err 290 } 291 token := jwt.NewWithClaims(signingMethod, claims) 292 return token.SignedString(key) 293 } 294 295 func (h *Handler) GetToken(r *http.Request) (userInfo model.UserInfo, valid bool) { 296 c, err := r.Cookie(h.config.CookieName) 297 if err != nil { 298 return model.UserInfo{}, false 299 } 300 301 token, err := jwt.ParseWithClaims(c.Value, &model.UserInfo{}, func(*jwt.Token) (interface{}, error) { 302 _, _, verifyKey, err := h.signingInfo() 303 return verifyKey, err 304 }) 305 if err != nil { 306 return model.UserInfo{}, false 307 } 308 309 u, ok := token.Claims.(*model.UserInfo) 310 if !ok { 311 return model.UserInfo{}, false 312 } 313 314 return *u, u.Valid() == nil 315 } 316 317 func (h *Handler) signingInfo() (signingMethod jwt.SigningMethod, key, verifyKey interface{}, err error) { 318 if h.signingMethod == nil || h.signingKey == nil || h.signingVerifyKey == nil { 319 h.signingMethod = jwt.GetSigningMethod(h.config.JwtAlgo) 320 if h.signingMethod == nil { 321 return nil, nil, nil, errors.New("invalid signing method: " + h.config.JwtAlgo) 322 } 323 324 keyString := h.config.JwtSecret 325 switch h.config.JwtAlgo { 326 case "ES256", "ES384", "ES512": 327 if !strings.Contains(keyString, "-----") { 328 keyString = "-----BEGIN EC PRIVATE KEY-----\n" + keyString + "\n-----END EC PRIVATE KEY-----" 329 } 330 331 key, err := jwt.ParseECPrivateKeyFromPEM([]byte(keyString)) 332 if err != nil { 333 return nil, nil, nil, errors.Wrap(err, "can not parse PEM formated EC private key") 334 } 335 h.signingKey = key 336 h.signingVerifyKey = key.Public() 337 case "RS256", "RS384", "RS512": 338 if !strings.Contains(keyString, "-----") { 339 keyString = "-----BEGIN RSA PRIVATE KEY-----\n" + keyString + "\n-----END RSA PRIVATE KEY-----" 340 } 341 342 key, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(keyString)) 343 if err != nil { 344 return nil, nil, nil, errors.Wrap(err, "can not parse PEM formated RSA private key") 345 } 346 h.signingKey = key 347 h.signingVerifyKey = key.Public() 348 default: 349 h.signingKey = []byte(keyString) 350 h.signingVerifyKey = h.signingKey 351 } 352 } 353 return h.signingMethod, h.signingKey, h.signingVerifyKey, nil 354 } 355 356 func (h *Handler) respondError(w http.ResponseWriter, r *http.Request) { 357 if wantHTML(r) { 358 username, _, _ := getCredentials(r) 359 writeLoginForm(w, 360 loginFormData{ 361 Error: true, 362 Config: h.config, 363 UserInfo: model.UserInfo{Sub: username}, 364 }) 365 return 366 } 367 w.Header().Set("Content-Type", contentTypePlain) 368 w.WriteHeader(500) 369 fmt.Fprintf(w, "Internal Server Error") 370 } 371 372 func (h *Handler) respondBadRequest(w http.ResponseWriter, r *http.Request) { 373 w.WriteHeader(400) 374 fmt.Fprintf(w, "Bad Request: Method or content-type not supported") 375 } 376 377 func (h *Handler) respondNotFound(w http.ResponseWriter, r *http.Request) { 378 w.WriteHeader(404) 379 fmt.Fprintf(w, "Not Found: The requested page does not exist") 380 } 381 382 func (h *Handler) respondMaxRefreshesReached(w http.ResponseWriter, r *http.Request) { 383 w.WriteHeader(403) 384 fmt.Fprint(w, "Max JWT refreshes reached") 385 } 386 387 func (h *Handler) respondAuthFailure(w http.ResponseWriter, r *http.Request) { 388 if wantHTML(r) { 389 w.Header().Set("Content-Type", contentTypeHTML) 390 w.WriteHeader(403) 391 username, _, _ := getCredentials(r) 392 writeLoginForm(w, 393 loginFormData{ 394 Failure: true, 395 Config: h.config, 396 UserInfo: model.UserInfo{Sub: username}, 397 }) 398 return 399 } 400 401 if wantJSON(r) { 402 w.Header().Set("Content-Type", contentTypeJSON) 403 w.WriteHeader(403) 404 fmt.Fprintf(w, `{"error": "Wrong credentials"}`) 405 } else { 406 w.Header().Set("Content-Type", contentTypePlain) 407 w.WriteHeader(403) 408 fmt.Fprintf(w, "Wrong credentials") 409 } 410 411 } 412 413 func wantHTML(r *http.Request) bool { 414 return strings.Contains(r.Header.Get("Accept"), "text/html") 415 } 416 417 func wantJSON(r *http.Request) bool { 418 return strings.Contains(r.Header.Get("Accept"), contentTypeJSON) 419 } 420 421 func getCredentials(r *http.Request) (string, string, error) { 422 if strings.HasPrefix(r.Header.Get("Content-Type"), contentTypeJSON) { 423 m := map[string]string{} 424 body, err := ioutil.ReadAll(r.Body) 425 if err != nil { 426 return "", "", err 427 } 428 err = json.Unmarshal(body, &m) 429 if err != nil { 430 return "", "", err 431 } 432 return m["username"], m["password"], nil 433 } 434 return r.PostForm.Get("username"), r.PostForm.Get("password"), nil 435 } 436 437 func (h *Handler) authenticate(username, password string) (bool, model.UserInfo, error) { 438 for _, b := range h.backends { 439 authenticated, userInfo, err := b.Authenticate(username, password) 440 if err != nil { 441 return false, model.UserInfo{}, err 442 } 443 if authenticated { 444 return authenticated, userInfo, nil 445 } 446 } 447 return false, model.UserInfo{}, nil 448 } 449 450 type oauthManager interface { 451 Handle(w http.ResponseWriter, r *http.Request) ( 452 startedFlow bool, 453 authenticated bool, 454 userInfo model.UserInfo, 455 err error) 456 AddConfig(providerName string, opts map[string]string) error 457 GetConfigFromRequest(r *http.Request) (oauth2.Config, error) 458 }