loginsrv

Unnamed repository; edit this file 'description' to name the repository.
git clone git@jamesshield.xyz:repos/loginsrv.git
Log | Files | Refs | README | LICENSE

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 }