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 }