2014-12-29 13:36:08 +01:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
2019-01-23 17:01:09 +01:00
|
|
|
"encoding/hex"
|
2020-03-23 13:37:53 +01:00
|
|
|
"errors"
|
2020-09-04 14:54:59 +02:00
|
|
|
"net/http"
|
2015-01-27 12:05:23 +01:00
|
|
|
"net/url"
|
2019-12-12 17:08:34 +02:00
|
|
|
"strings"
|
2015-01-27 12:05:23 +01:00
|
|
|
|
2015-02-05 10:37:13 +01:00
|
|
|
"github.com/grafana/grafana/pkg/api/dtos"
|
|
|
|
"github.com/grafana/grafana/pkg/bus"
|
2019-05-13 14:45:54 +08:00
|
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
2019-02-23 23:35:26 +01:00
|
|
|
"github.com/grafana/grafana/pkg/infra/metrics"
|
2020-11-25 03:55:22 -03:00
|
|
|
"github.com/grafana/grafana/pkg/infra/network"
|
2015-07-15 10:08:23 +02:00
|
|
|
"github.com/grafana/grafana/pkg/login"
|
2020-12-11 11:44:44 +01:00
|
|
|
"github.com/grafana/grafana/pkg/middleware/cookies"
|
2019-07-05 15:24:52 +02:00
|
|
|
"github.com/grafana/grafana/pkg/models"
|
2015-02-05 10:37:13 +01:00
|
|
|
"github.com/grafana/grafana/pkg/setting"
|
2019-01-23 17:01:09 +01:00
|
|
|
"github.com/grafana/grafana/pkg/util"
|
2020-03-23 13:37:53 +01:00
|
|
|
"github.com/grafana/grafana/pkg/util/errutil"
|
2014-12-29 13:36:08 +01:00
|
|
|
)
|
|
|
|
|
2015-01-27 12:05:23 +01:00
|
|
|
const (
|
2019-01-23 17:01:09 +01:00
|
|
|
ViewIndex = "index"
|
|
|
|
LoginErrorCookieName = "login_error"
|
2015-01-27 12:05:23 +01:00
|
|
|
)
|
|
|
|
|
2019-07-09 09:37:24 +03:00
|
|
|
var setIndexViewData = (*HTTPServer).setIndexViewData
|
|
|
|
|
|
|
|
var getViewIndex = func() string {
|
|
|
|
return ViewIndex
|
|
|
|
}
|
|
|
|
|
2020-04-17 10:48:37 +03:00
|
|
|
func (hs *HTTPServer) ValidateRedirectTo(redirectTo string) error {
|
2019-12-12 17:08:34 +02:00
|
|
|
to, err := url.Parse(redirectTo)
|
|
|
|
if err != nil {
|
|
|
|
return login.ErrInvalidRedirectTo
|
|
|
|
}
|
|
|
|
if to.IsAbs() {
|
|
|
|
return login.ErrAbsoluteRedirectTo
|
|
|
|
}
|
2020-06-16 16:33:44 +03:00
|
|
|
|
|
|
|
if to.Host != "" {
|
|
|
|
return login.ErrForbiddenRedirectTo
|
|
|
|
}
|
|
|
|
|
|
|
|
// path should have exactly one leading slash
|
|
|
|
if !strings.HasPrefix(to.Path, "/") {
|
|
|
|
return login.ErrForbiddenRedirectTo
|
|
|
|
}
|
|
|
|
if strings.HasPrefix(to.Path, "//") {
|
|
|
|
return login.ErrForbiddenRedirectTo
|
|
|
|
}
|
|
|
|
|
2020-03-11 11:04:48 +02:00
|
|
|
// when using a subUrl, the redirect_to should start with the subUrl (which contains the leading slash), otherwise the redirect
|
2020-02-14 08:51:35 -05:00
|
|
|
// will send the user to the wrong location
|
2020-11-13 09:52:38 +01:00
|
|
|
if hs.Cfg.AppSubURL != "" && !strings.HasPrefix(to.Path, hs.Cfg.AppSubURL+"/") {
|
2019-12-12 17:08:34 +02:00
|
|
|
return login.ErrInvalidRedirectTo
|
|
|
|
}
|
2020-06-16 16:33:44 +03:00
|
|
|
|
2019-12-12 17:08:34 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-12-11 11:44:44 +01:00
|
|
|
func (hs *HTTPServer) CookieOptionsFromCfg() cookies.CookieOptions {
|
2020-04-06 16:56:19 +02:00
|
|
|
path := "/"
|
2020-11-13 09:52:38 +01:00
|
|
|
if len(hs.Cfg.AppSubURL) > 0 {
|
|
|
|
path = hs.Cfg.AppSubURL
|
2020-04-06 16:56:19 +02:00
|
|
|
}
|
2020-12-11 11:44:44 +01:00
|
|
|
return cookies.CookieOptions{
|
2020-04-06 16:56:19 +02:00
|
|
|
Path: path,
|
2020-01-14 17:41:54 +01:00
|
|
|
Secure: hs.Cfg.CookieSecure,
|
|
|
|
SameSiteDisabled: hs.Cfg.CookieSameSiteDisabled,
|
|
|
|
SameSiteMode: hs.Cfg.CookieSameSiteMode,
|
2020-01-10 14:55:30 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-05 15:24:52 +02:00
|
|
|
func (hs *HTTPServer) LoginView(c *models.ReqContext) {
|
2019-07-09 09:37:24 +03:00
|
|
|
viewData, err := setIndexViewData(hs, c)
|
2015-11-20 09:43:10 +01:00
|
|
|
if err != nil {
|
2015-01-27 10:09:54 +01:00
|
|
|
c.Handle(500, "Failed to get settings", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-10-20 14:51:06 +03:00
|
|
|
urlParams := c.Req.URL.Query()
|
|
|
|
if _, disableAutoLogin := urlParams["disableAutoLogin"]; disableAutoLogin {
|
|
|
|
hs.log.Debug("Auto login manually disabled")
|
|
|
|
c.HTML(200, getViewIndex(), viewData)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-09-28 15:10:50 +02:00
|
|
|
enabledOAuths := make(map[string]interface{})
|
|
|
|
for key, oauth := range setting.OAuthService.OAuthInfos {
|
|
|
|
enabledOAuths[key] = map[string]string{"name": oauth.Name}
|
|
|
|
}
|
|
|
|
|
|
|
|
viewData.Settings["oauth"] = enabledOAuths
|
2019-11-01 14:56:12 +01:00
|
|
|
viewData.Settings["samlEnabled"] = hs.License.HasValidLicense() && hs.Cfg.SAMLEnabled
|
2015-01-28 10:26:13 +01:00
|
|
|
|
2019-01-23 17:01:09 +01:00
|
|
|
if loginError, ok := tryGetEncryptedCookie(c, LoginErrorCookieName); ok {
|
2020-09-22 16:22:19 +02:00
|
|
|
// this cookie is only set whenever an OAuth login fails
|
|
|
|
// therefore the loginError should be passed to the view data
|
|
|
|
// and the view should return immediately before attempting
|
|
|
|
// to login again via OAuth and enter to a redirect loop
|
2020-12-11 11:44:44 +01:00
|
|
|
cookies.DeleteCookie(c.Resp, LoginErrorCookieName, hs.CookieOptionsFromCfg)
|
2017-02-01 16:32:51 +03:00
|
|
|
viewData.Settings["loginError"] = loginError
|
2019-07-09 09:37:24 +03:00
|
|
|
c.HTML(200, getViewIndex(), viewData)
|
|
|
|
return
|
2017-02-01 16:32:51 +03:00
|
|
|
}
|
|
|
|
|
2018-05-28 16:16:48 +02:00
|
|
|
if tryOAuthAutoLogin(c) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-11-07 17:48:56 +01:00
|
|
|
if c.IsSignedIn {
|
|
|
|
// Assign login token to auth proxy users if enable_login_token = true
|
2020-12-11 11:44:44 +01:00
|
|
|
if hs.Cfg.AuthProxyEnabled && hs.Cfg.AuthProxyEnableLoginToken {
|
2020-03-23 13:37:53 +01:00
|
|
|
user := &models.User{Id: c.SignedInUser.UserId, Email: c.SignedInUser.Email, Login: c.SignedInUser.Login}
|
|
|
|
err := hs.loginUserWithUser(user, c)
|
|
|
|
if err != nil {
|
|
|
|
c.Handle(500, "Failed to sign in user", err)
|
|
|
|
return
|
|
|
|
}
|
2019-11-07 17:48:56 +01:00
|
|
|
}
|
|
|
|
|
2020-11-24 07:27:08 +01:00
|
|
|
if redirectTo := c.GetCookie("redirect_to"); len(redirectTo) > 0 {
|
2020-04-17 10:48:37 +03:00
|
|
|
if err := hs.ValidateRedirectTo(redirectTo); err != nil {
|
2020-03-11 11:04:48 +02:00
|
|
|
// the user is already logged so instead of rendering the login page with error
|
|
|
|
// it should be redirected to the home page.
|
2020-07-23 08:14:39 +02:00
|
|
|
log.Debugf("Ignored invalid redirect_to cookie value: %v", redirectTo)
|
2020-11-13 09:52:38 +01:00
|
|
|
redirectTo = hs.Cfg.AppSubURL + "/"
|
2019-12-12 17:08:34 +02:00
|
|
|
}
|
2020-12-11 11:44:44 +01:00
|
|
|
cookies.DeleteCookie(c.Resp, "redirect_to", hs.CookieOptionsFromCfg)
|
2019-11-07 17:48:56 +01:00
|
|
|
c.Redirect(redirectTo)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c.Redirect(setting.AppSubUrl + "/")
|
2019-01-23 15:28:33 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-12-12 17:08:34 +02:00
|
|
|
c.HTML(200, getViewIndex(), viewData)
|
2019-11-07 17:48:56 +01:00
|
|
|
}
|
2019-01-23 15:28:33 +01:00
|
|
|
|
2019-07-05 15:24:52 +02:00
|
|
|
func tryOAuthAutoLogin(c *models.ReqContext) bool {
|
2018-05-28 16:16:48 +02:00
|
|
|
if !setting.OAuthAutoLogin {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
oauthInfos := setting.OAuthService.OAuthInfos
|
|
|
|
if len(oauthInfos) != 1 {
|
2020-07-23 08:14:39 +02:00
|
|
|
log.Warnf("Skipping OAuth auto login because multiple OAuth providers are configured")
|
2018-05-28 16:16:48 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
for key := range setting.OAuthService.OAuthInfos {
|
|
|
|
redirectUrl := setting.AppSubUrl + "/login/" + key
|
2020-07-23 08:14:39 +02:00
|
|
|
log.Infof("OAuth auto login enabled. Redirecting to " + redirectUrl)
|
2018-05-28 16:16:48 +02:00
|
|
|
c.Redirect(redirectUrl, 307)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2019-07-05 15:24:52 +02:00
|
|
|
func (hs *HTTPServer) LoginAPIPing(c *models.ReqContext) Response {
|
2019-01-24 15:17:09 +01:00
|
|
|
if c.IsSignedIn || c.IsAnonymous {
|
|
|
|
return JSON(200, "Logged in")
|
2015-01-27 12:05:23 +01:00
|
|
|
}
|
|
|
|
|
2019-01-24 15:17:09 +01:00
|
|
|
return Error(401, "Unauthorized", nil)
|
2015-01-27 10:09:54 +01:00
|
|
|
}
|
|
|
|
|
2019-07-05 15:24:52 +02:00
|
|
|
func (hs *HTTPServer) LoginPost(c *models.ReqContext, cmd dtos.LoginCommand) Response {
|
2020-11-06 10:01:13 +01:00
|
|
|
authModule := ""
|
2020-09-04 14:54:59 +02:00
|
|
|
var user *models.User
|
|
|
|
var response *NormalResponse
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
err := response.err
|
|
|
|
if err == nil && response.errMessage != "" {
|
|
|
|
err = errors.New(response.errMessage)
|
|
|
|
}
|
2020-11-06 10:01:13 +01:00
|
|
|
hs.HooksService.RunLoginHook(&models.LoginInfo{
|
|
|
|
AuthModule: authModule,
|
2020-10-26 15:47:01 +01:00
|
|
|
User: user,
|
|
|
|
LoginUsername: cmd.User,
|
|
|
|
HTTPStatus: response.status,
|
|
|
|
Error: err,
|
2020-11-06 10:01:13 +01:00
|
|
|
}, c)
|
2020-09-04 14:54:59 +02:00
|
|
|
}()
|
|
|
|
|
2017-03-17 16:35:05 -04:00
|
|
|
if setting.DisableLoginForm {
|
2020-09-04 14:54:59 +02:00
|
|
|
response = Error(http.StatusUnauthorized, "Login is disabled", nil)
|
|
|
|
return response
|
2017-03-17 16:35:05 -04:00
|
|
|
}
|
|
|
|
|
2019-07-05 15:24:52 +02:00
|
|
|
authQuery := &models.LoginUserQuery{
|
2018-03-23 15:50:07 -04:00
|
|
|
ReqContext: c,
|
|
|
|
Username: cmd.User,
|
|
|
|
Password: cmd.Password,
|
|
|
|
IpAddress: c.Req.RemoteAddr,
|
2020-12-11 11:44:44 +01:00
|
|
|
Cfg: hs.Cfg,
|
2014-12-29 13:36:08 +01:00
|
|
|
}
|
|
|
|
|
2020-09-04 14:54:59 +02:00
|
|
|
err := bus.Dispatch(authQuery)
|
2020-11-06 10:01:13 +01:00
|
|
|
authModule = authQuery.AuthModule
|
2020-09-04 14:54:59 +02:00
|
|
|
if err != nil {
|
|
|
|
response = Error(401, "Invalid username or password", err)
|
2020-11-19 13:34:28 +01:00
|
|
|
if errors.Is(err, login.ErrInvalidCredentials) || errors.Is(err, login.ErrTooManyLoginAttempts) || errors.Is(err,
|
|
|
|
models.ErrUserNotFound) {
|
2020-09-04 14:54:59 +02:00
|
|
|
return response
|
2015-06-04 09:34:42 +02:00
|
|
|
}
|
|
|
|
|
2019-07-23 13:12:55 +03:00
|
|
|
// Do not expose disabled status,
|
|
|
|
// just show incorrect user credentials error (see #17947)
|
2020-11-19 13:34:28 +01:00
|
|
|
if errors.Is(err, login.ErrUserDisabled) {
|
2019-07-23 13:12:55 +03:00
|
|
|
hs.log.Warn("User is disabled", "user", cmd.User)
|
2020-09-04 14:54:59 +02:00
|
|
|
return response
|
2019-05-21 14:52:49 +03:00
|
|
|
}
|
|
|
|
|
2020-09-04 14:54:59 +02:00
|
|
|
response = Error(500, "Error while trying to authenticate user", err)
|
|
|
|
return response
|
2014-12-29 13:36:08 +01:00
|
|
|
}
|
|
|
|
|
2020-09-04 14:54:59 +02:00
|
|
|
user = authQuery.User
|
2015-07-10 11:10:48 +02:00
|
|
|
|
2020-09-04 14:54:59 +02:00
|
|
|
err = hs.loginUserWithUser(user, c)
|
2020-03-23 13:37:53 +01:00
|
|
|
if err != nil {
|
2020-09-04 14:54:59 +02:00
|
|
|
response = Error(http.StatusInternalServerError, "Error while signing in user", err)
|
|
|
|
return response
|
2020-03-23 13:37:53 +01:00
|
|
|
}
|
2014-12-29 13:36:08 +01:00
|
|
|
|
2015-01-27 12:05:23 +01:00
|
|
|
result := map[string]interface{}{
|
|
|
|
"message": "Logged in",
|
|
|
|
}
|
|
|
|
|
2020-11-24 07:27:08 +01:00
|
|
|
if redirectTo := c.GetCookie("redirect_to"); len(redirectTo) > 0 {
|
2020-04-17 10:48:37 +03:00
|
|
|
if err := hs.ValidateRedirectTo(redirectTo); err == nil {
|
2019-12-12 17:08:34 +02:00
|
|
|
result["redirectUrl"] = redirectTo
|
|
|
|
} else {
|
2020-07-23 08:14:39 +02:00
|
|
|
log.Infof("Ignored invalid redirect_to cookie value: %v", redirectTo)
|
2019-12-12 17:08:34 +02:00
|
|
|
}
|
2020-12-11 11:44:44 +01:00
|
|
|
cookies.DeleteCookie(c.Resp, "redirect_to", hs.CookieOptionsFromCfg)
|
2015-01-27 12:05:23 +01:00
|
|
|
}
|
|
|
|
|
2019-07-16 17:58:46 +03:00
|
|
|
metrics.MApiLoginPost.Inc()
|
2020-09-04 14:54:59 +02:00
|
|
|
response = JSON(http.StatusOK, result)
|
|
|
|
return response
|
2015-06-04 09:34:42 +02:00
|
|
|
}
|
|
|
|
|
2020-03-23 13:37:53 +01:00
|
|
|
func (hs *HTTPServer) loginUserWithUser(user *models.User, c *models.ReqContext) error {
|
2015-01-19 18:01:04 +01:00
|
|
|
if user == nil {
|
2020-03-23 13:37:53 +01:00
|
|
|
return errors.New("could not login user")
|
2014-12-29 13:36:08 +01:00
|
|
|
}
|
|
|
|
|
2020-11-25 03:55:22 -03:00
|
|
|
addr := c.RemoteAddr()
|
|
|
|
ip, err := network.GetIPFromAddress(addr)
|
|
|
|
if err != nil {
|
|
|
|
hs.log.Debug("Failed to get IP from client address", "addr", addr)
|
|
|
|
ip = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
hs.log.Debug("Got IP address from client address", "addr", addr, "ip", ip)
|
|
|
|
userToken, err := hs.AuthTokenService.CreateToken(c.Req.Context(), user.Id, ip, c.Req.UserAgent())
|
2019-01-15 15:15:52 +01:00
|
|
|
if err != nil {
|
2020-03-23 13:37:53 +01:00
|
|
|
return errutil.Wrap("failed to create auth token", err)
|
2016-03-07 17:26:31 +01:00
|
|
|
}
|
2020-03-23 13:37:53 +01:00
|
|
|
|
2019-07-03 09:16:00 -04:00
|
|
|
hs.log.Info("Successful Login", "User", user.Email)
|
2020-12-11 11:44:44 +01:00
|
|
|
cookies.WriteSessionCookie(c, hs.Cfg, userToken.UnhashedToken, hs.Cfg.LoginMaxLifetime)
|
2020-03-23 13:37:53 +01:00
|
|
|
return nil
|
2014-12-29 13:36:08 +01:00
|
|
|
}
|
|
|
|
|
2019-07-05 15:24:52 +02:00
|
|
|
func (hs *HTTPServer) Logout(c *models.ReqContext) {
|
2020-10-08 17:42:55 +03:00
|
|
|
if hs.Cfg.SAMLEnabled && hs.Cfg.SAMLSingleLogoutEnabled {
|
|
|
|
c.Redirect(setting.AppSubUrl + "/logout/saml")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-11-19 13:34:28 +01:00
|
|
|
err := hs.AuthTokenService.RevokeToken(c.Req.Context(), c.UserToken)
|
|
|
|
if err != nil && !errors.Is(err, models.ErrUserTokenNotFound) {
|
2019-02-04 23:44:28 +01:00
|
|
|
hs.log.Error("failed to revoke auth token", "error", err)
|
|
|
|
}
|
|
|
|
|
2020-12-11 11:44:44 +01:00
|
|
|
cookies.WriteSessionCookie(c, hs.Cfg, "", -1)
|
2019-01-15 15:15:52 +01:00
|
|
|
|
2018-05-27 14:52:50 +02:00
|
|
|
if setting.SignoutRedirectUrl != "" {
|
|
|
|
c.Redirect(setting.SignoutRedirectUrl)
|
|
|
|
} else {
|
2019-07-03 09:16:00 -04:00
|
|
|
hs.log.Info("Successful Logout", "User", c.Email)
|
2018-05-27 14:52:50 +02:00
|
|
|
c.Redirect(setting.AppSubUrl + "/login")
|
|
|
|
}
|
2014-12-29 13:36:08 +01:00
|
|
|
}
|
2019-01-23 17:01:09 +01:00
|
|
|
|
2019-07-05 15:24:52 +02:00
|
|
|
func tryGetEncryptedCookie(ctx *models.ReqContext, cookieName string) (string, bool) {
|
2019-01-23 17:01:09 +01:00
|
|
|
cookie := ctx.GetCookie(cookieName)
|
|
|
|
if cookie == "" {
|
|
|
|
return "", false
|
|
|
|
}
|
|
|
|
|
|
|
|
decoded, err := hex.DecodeString(cookie)
|
|
|
|
if err != nil {
|
|
|
|
return "", false
|
|
|
|
}
|
|
|
|
|
2019-04-16 10:27:07 +02:00
|
|
|
decryptedError, err := util.Decrypt(decoded, setting.SecretKey)
|
2019-01-23 17:01:09 +01:00
|
|
|
return string(decryptedError), err == nil
|
|
|
|
}
|
|
|
|
|
2019-07-05 15:24:52 +02:00
|
|
|
func (hs *HTTPServer) trySetEncryptedCookie(ctx *models.ReqContext, cookieName string, value string, maxAge int) error {
|
2019-01-23 17:01:09 +01:00
|
|
|
encryptedError, err := util.Encrypt([]byte(value), setting.SecretKey)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-12-11 11:44:44 +01:00
|
|
|
cookies.WriteCookie(ctx.Resp, cookieName, hex.EncodeToString(encryptedError), 60, hs.CookieOptionsFromCfg)
|
2019-01-23 17:01:09 +01:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2020-04-17 10:48:37 +03:00
|
|
|
|
|
|
|
func (hs *HTTPServer) redirectWithError(ctx *models.ReqContext, err error, v ...interface{}) {
|
|
|
|
ctx.Logger.Error(err.Error(), v...)
|
|
|
|
if err := hs.trySetEncryptedCookie(ctx, LoginErrorCookieName, err.Error(), 60); err != nil {
|
|
|
|
hs.log.Error("Failed to set encrypted cookie", "err", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx.Redirect(setting.AppSubUrl + "/login")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (hs *HTTPServer) RedirectResponseWithError(ctx *models.ReqContext, err error, v ...interface{}) *RedirectResponse {
|
|
|
|
ctx.Logger.Error(err.Error(), v...)
|
|
|
|
if err := hs.trySetEncryptedCookie(ctx, LoginErrorCookieName, err.Error(), 60); err != nil {
|
|
|
|
hs.log.Error("Failed to set encrypted cookie", "err", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return Redirect(setting.AppSubUrl + "/login")
|
|
|
|
}
|