github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/server/login.go (about)

     1  package server
     2  
     3  import (
     4  	"crypto/rand"
     5  	"encoding/hex"
     6  	"encoding/json"
     7  	"errors"
     8  	"net/http"
     9  	"strings"
    10  
    11  	"github.com/pyroscope-io/pyroscope/pkg/api"
    12  	"github.com/pyroscope-io/pyroscope/pkg/model"
    13  	"github.com/pyroscope-io/pyroscope/pkg/server/httputils"
    14  )
    15  
    16  // TODO(kolesnikovae): This part should be moved from
    17  //  Controller to a separate handler/service (Login).
    18  
    19  // TODO(kolesnikovae): Instead of rendering Login and Signup templates
    20  //  on the server side in order to provide available auth options,
    21  //  we should expose a dedicated endpoint, so that the client could
    22  //  figure out all the necessary info on its own.
    23  
    24  func (ctrl *Controller) loginHandler(w http.ResponseWriter, r *http.Request) {
    25  	switch r.Method {
    26  	case http.MethodGet:
    27  		ctrl.indexHandler()(w, r)
    28  	case http.MethodPost:
    29  		ctrl.loginPost(w, r)
    30  	default:
    31  		ctrl.httpUtils.WriteInvalidMethodError(r, w)
    32  	}
    33  }
    34  
    35  func (ctrl *Controller) loginPost(w http.ResponseWriter, r *http.Request) {
    36  	if !ctrl.isLoginWithPasswordAllowed(r) {
    37  		ctrl.redirectPreservingBaseURL(w, r, "/", http.StatusTemporaryRedirect)
    38  		return
    39  	}
    40  	type loginCredentials struct {
    41  		Username string `json:"username"`
    42  		Password []byte `json:"password"`
    43  	}
    44  	var req loginCredentials
    45  	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
    46  		ctrl.log.WithError(err).Error("failed to parse user credentials")
    47  		ctrl.httpUtils.HandleError(r, w, httputils.JSONError{Err: err})
    48  		return
    49  	}
    50  	u, err := ctrl.authService.AuthenticateUser(r.Context(), req.Username, string(req.Password))
    51  	if err != nil {
    52  		ctrl.httpUtils.HandleError(r, w, err)
    53  		return
    54  	}
    55  	token, err := ctrl.jwtTokenService.Sign(ctrl.jwtTokenService.GenerateUserJWTToken(u.Name, u.Role))
    56  	if err != nil {
    57  		ctrl.httpUtils.HandleError(r, w, err)
    58  		return
    59  	}
    60  	ctrl.createCookie(w, api.JWTCookieName, token)
    61  	w.WriteHeader(http.StatusNoContent)
    62  }
    63  
    64  func (ctrl *Controller) signupHandler(w http.ResponseWriter, r *http.Request) {
    65  	switch r.Method {
    66  	case http.MethodGet:
    67  		ctrl.indexHandler()(w, r)
    68  	case http.MethodPost:
    69  		ctrl.signupPost(w, r)
    70  	default:
    71  		ctrl.httpUtils.WriteInvalidMethodError(r, w)
    72  	}
    73  }
    74  
    75  func (ctrl *Controller) signupPost(w http.ResponseWriter, r *http.Request) {
    76  	if !ctrl.isSignupAllowed(r) {
    77  		ctrl.redirectPreservingBaseURL(w, r, "/", http.StatusTemporaryRedirect)
    78  		return
    79  	}
    80  	type signupRequest struct {
    81  		Name     string  `json:"name"`
    82  		Email    *string `json:"email,omitempty"`
    83  		FullName *string `json:"fullName,omitempty"`
    84  		Password []byte  `json:"password"`
    85  	}
    86  	var req signupRequest
    87  	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
    88  		ctrl.httpUtils.HandleError(r, w, err)
    89  		return
    90  	}
    91  	_, err := ctrl.userService.CreateUser(r.Context(), model.CreateUserParams{
    92  		Name:     req.Name,
    93  		Email:    req.Email,
    94  		FullName: req.FullName,
    95  		Password: string(req.Password),
    96  		Role:     ctrl.signupDefaultRole,
    97  	})
    98  	ctrl.httpUtils.HandleError(r, w, err)
    99  }
   100  
   101  func (ctrl *Controller) isAuthRequired() bool {
   102  	return ctrl.config.Auth.Internal.Enabled ||
   103  		ctrl.config.Auth.Google.Enabled ||
   104  		ctrl.config.Auth.Github.Enabled ||
   105  		ctrl.config.Auth.Gitlab.Enabled
   106  }
   107  
   108  func (ctrl *Controller) isLoginFormEnabled(r *http.Request) bool {
   109  	return !ctrl.isUserAuthenticated(r) && ctrl.isAuthRequired()
   110  }
   111  
   112  func (ctrl *Controller) isLoginWithPasswordAllowed(r *http.Request) bool {
   113  	return !ctrl.isUserAuthenticated(r) &&
   114  		ctrl.config.Auth.Internal.Enabled
   115  }
   116  
   117  func (ctrl *Controller) isSignupAllowed(r *http.Request) bool {
   118  	return !ctrl.isUserAuthenticated(r) &&
   119  		ctrl.config.Auth.Internal.Enabled &&
   120  		ctrl.config.Auth.Internal.SignupEnabled
   121  }
   122  
   123  func (ctrl *Controller) isUserAuthenticated(r *http.Request) bool {
   124  	if v, err := r.Cookie(api.JWTCookieName); err == nil {
   125  		if _, err = ctrl.authService.UserFromJWTToken(r.Context(), v.Value); err == nil {
   126  			return true
   127  		}
   128  	}
   129  	return false
   130  }
   131  
   132  func (ctrl *Controller) isCookieSecureRequired() bool {
   133  	return ctrl.config.Auth.CookieSecure ||
   134  		ctrl.config.Auth.CookieSameSite == http.SameSiteNoneMode
   135  }
   136  
   137  func (ctrl *Controller) createCookie(w http.ResponseWriter, name, value string) {
   138  	http.SetCookie(w, &http.Cookie{
   139  		Name:     name,
   140  		Path:     "/",
   141  		Value:    value,
   142  		HttpOnly: true,
   143  		MaxAge:   0,
   144  		SameSite: ctrl.config.Auth.CookieSameSite,
   145  		Secure:   ctrl.isCookieSecureRequired(),
   146  	})
   147  }
   148  
   149  func (ctrl *Controller) invalidateCookie(w http.ResponseWriter, name string) {
   150  	http.SetCookie(w, &http.Cookie{
   151  		Name:     name,
   152  		Path:     "/",
   153  		Value:    "",
   154  		HttpOnly: true,
   155  		// MaxAge -1 request cookie be deleted immediately
   156  		MaxAge:   -1,
   157  		SameSite: ctrl.config.Auth.CookieSameSite,
   158  		Secure:   ctrl.isCookieSecureRequired(),
   159  	})
   160  }
   161  
   162  func (ctrl *Controller) logoutHandler(w http.ResponseWriter, r *http.Request) {
   163  	switch r.Method {
   164  	case http.MethodPost, http.MethodGet:
   165  		ctrl.invalidateCookie(w, api.JWTCookieName)
   166  		ctrl.loginRedirect(w, r)
   167  	default:
   168  		ctrl.httpUtils.WriteInvalidMethodError(r, w)
   169  	}
   170  }
   171  
   172  // can be replaced with a faster solution if cryptographic randomness isn't a priority
   173  func generateStateToken(length int) (string, error) {
   174  	b := make([]byte, length)
   175  	if _, err := rand.Read(b); err != nil {
   176  		return "", err
   177  	}
   178  	return hex.EncodeToString(b), nil
   179  }
   180  
   181  func (ctrl *Controller) oauthLoginHandler(oh oauthHandler) http.HandlerFunc {
   182  	return func(w http.ResponseWriter, r *http.Request) {
   183  		authURL, state, err := oh.getOauthBase().buildAuthQuery(r, w)
   184  		if err != nil {
   185  			ctrl.log.WithError(err).Error("problem generating state token")
   186  			return
   187  		}
   188  		ctrl.createCookie(w, stateCookieName, state)
   189  		http.Redirect(w, r, authURL, http.StatusTemporaryRedirect)
   190  	}
   191  }
   192  
   193  // Instead of this handler that just redirects, Javascript code can be added to load the state and send it to backend
   194  // this is done so that the state cookie would be send back from browser
   195  func (ctrl *Controller) callbackHandler(redirectPath string) http.HandlerFunc {
   196  	return func(w http.ResponseWriter, r *http.Request) {
   197  		// Well I know that's kinda kludge, but here is rationale:
   198  		// We need to know if the url uses https or not
   199  		// Previous way of doing this was to use the html template and detect the protocol
   200  		// This way doesn't require any templates, but rely on referer header
   201  		// The other way around would be to include the protocol in the baseURL
   202  		hasTLS := "false"
   203  		if strings.HasPrefix(r.Header.Get("Referer"), "https:") {
   204  			hasTLS = "true"
   205  		}
   206  
   207  		ctrl.redirectPreservingBaseURL(w, r, redirectPath+"?"+r.URL.RawQuery+"&tls="+hasTLS, http.StatusPermanentRedirect)
   208  	}
   209  }
   210  
   211  func (ctrl *Controller) logErrorAndRedirect(w http.ResponseWriter, r *http.Request, msg string, err error) {
   212  	if err != nil {
   213  		ctrl.log.WithError(err).Error(msg)
   214  	} else {
   215  		ctrl.log.Error(msg)
   216  	}
   217  	ctrl.invalidateCookie(w, stateCookieName)
   218  	ctrl.redirectPreservingBaseURL(w, r, "/forbidden", http.StatusTemporaryRedirect)
   219  }
   220  
   221  func (ctrl *Controller) callbackRedirectHandler(oh oauthHandler) http.HandlerFunc {
   222  	return func(w http.ResponseWriter, r *http.Request) {
   223  		cookie, err := r.Cookie(stateCookieName)
   224  		if err != nil {
   225  			ctrl.logErrorAndRedirect(w, r, "missing state cookie", err)
   226  			return
   227  		}
   228  		if cookie.Value != r.FormValue("state") {
   229  			ctrl.logErrorAndRedirect(w, r, "invalid oauth state", nil)
   230  			return
   231  		}
   232  
   233  		client, err := oh.getOauthBase().generateOauthClient(r)
   234  		if err != nil {
   235  			ctrl.logErrorAndRedirect(w, r, "failed to generate oauth client", err)
   236  			return
   237  		}
   238  
   239  		u, err := oh.userAuth(client)
   240  		if err != nil {
   241  			ctrl.logErrorAndRedirect(w, r, "failed to get user auth info", err)
   242  			return
   243  		}
   244  
   245  		user, err := ctrl.userService.FindUserByName(r.Context(), u.Name)
   246  		switch {
   247  		default:
   248  			ctrl.logErrorAndRedirect(w, r, "failed to find user", err)
   249  			return
   250  		case err == nil:
   251  			// TODO(kolesnikovae): Update found user with the new user info, if applicable.
   252  		case errors.Is(err, model.ErrUserNotFound):
   253  			user, err = ctrl.userService.CreateUser(r.Context(), model.CreateUserParams{
   254  				Name:       u.Name,
   255  				Email:      model.String(u.Email),
   256  				Role:       ctrl.signupDefaultRole,
   257  				Password:   model.MustRandomPassword(),
   258  				IsExternal: true,
   259  				// TODO(kolesnikovae): Specify the user source (oauth-provider, ldap, etc).
   260  			})
   261  			if err != nil {
   262  				ctrl.logErrorAndRedirect(w, r, "failed to create external user", err)
   263  				return
   264  			}
   265  		}
   266  		if model.IsUserDisabled(user) {
   267  			ctrl.logErrorAndRedirect(w, r, "user disabled", err)
   268  			return
   269  		}
   270  		token, err := ctrl.jwtTokenService.Sign(ctrl.jwtTokenService.GenerateUserJWTToken(user.Name, user.Role))
   271  		if err != nil {
   272  			ctrl.logErrorAndRedirect(w, r, "signing jwt failed", err)
   273  			return
   274  		}
   275  
   276  		// delete state cookie and add jwt cookie
   277  		ctrl.invalidateCookie(w, stateCookieName)
   278  		ctrl.createCookie(w, api.JWTCookieName, token)
   279  		ctrl.redirectPreservingBaseURL(w, r, "/", http.StatusPermanentRedirect)
   280  	}
   281  }