Auth: Fix redirection when auto_login is enabled (#94311)

* Fix for SAML auto login

* Fix for OAuth auto login
This commit is contained in:
Misi 2024-10-07 14:59:00 +02:00 committed by GitHub
parent 6bd2f9f3ea
commit 0539ccf10d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 62 additions and 20 deletions

View File

@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/infra/network"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/middleware/cookies"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/authn"
@ -181,6 +182,9 @@ func (hs *HTTPServer) tryAutoLogin(c *contextmodel.ReqContext) bool {
for providerName, provider := range oauthInfos {
if provider.AutoLogin || hs.Cfg.OAuthAutoLogin {
redirectUrl := hs.Cfg.AppSubURL + "/login/" + providerName
if hs.Features.IsEnabledGlobally(featuremgmt.FlagUseSessionStorageForRedirection) {
redirectUrl += hs.getRedirectToForAutoLogin(c)
}
c.Logger.Info("OAuth auto login enabled. Redirecting to " + redirectUrl)
c.Redirect(redirectUrl, 307)
return true
@ -189,6 +193,9 @@ func (hs *HTTPServer) tryAutoLogin(c *contextmodel.ReqContext) bool {
if samlAutoLogin {
redirectUrl := hs.Cfg.AppSubURL + "/login/saml"
if hs.Features.IsEnabledGlobally(featuremgmt.FlagUseSessionStorageForRedirection) {
redirectUrl += hs.getRedirectToForAutoLogin(c)
}
c.Logger.Info("SAML auto login enabled. Redirecting to " + redirectUrl)
c.Redirect(redirectUrl, 307)
return true
@ -197,6 +204,21 @@ func (hs *HTTPServer) tryAutoLogin(c *contextmodel.ReqContext) bool {
return false
}
func (hs *HTTPServer) getRedirectToForAutoLogin(c *contextmodel.ReqContext) string {
redirectTo := c.Req.FormValue("redirectTo")
if hs.Cfg.AppSubURL != "" && strings.HasPrefix(redirectTo, hs.Cfg.AppSubURL) {
redirectTo = strings.TrimPrefix(redirectTo, hs.Cfg.AppSubURL)
}
if redirectTo == "/" {
return ""
}
// remove any forceLogin=true params
redirectTo = middleware.RemoveForceLoginParams(redirectTo)
return "?redirectTo=" + url.QueryEscape(redirectTo)
}
func (hs *HTTPServer) LoginAPIPing(c *contextmodel.ReqContext) response.Response {
if c.IsSignedIn || c.IsAnonymous {
return response.JSON(http.StatusOK, util.DynMap{"message": "Logged in"})

View File

@ -6,6 +6,7 @@ import (
"github.com/grafana/grafana/pkg/middleware/cookies"
"github.com/grafana/grafana/pkg/services/authn"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/web"
)
@ -25,6 +26,7 @@ func (hs *HTTPServer) OAuthLogin(reqCtx *contextmodel.ReqContext) {
}
code := reqCtx.Query("code")
redirectTo := reqCtx.Query("redirectTo")
req := &authn.Request{HTTPRequest: reqCtx.Req}
if code == "" {
@ -36,6 +38,9 @@ func (hs *HTTPServer) OAuthLogin(reqCtx *contextmodel.ReqContext) {
cookies.WriteCookie(reqCtx.Resp, OauthStateCookieName, redirect.Extra[authn.KeyOAuthState], hs.Cfg.OAuthCookieMaxAge, hs.CookieOptionsFromCfg)
if hs.Features.IsEnabledGlobally(featuremgmt.FlagUseSessionStorageForRedirection) {
cookies.WriteCookie(reqCtx.Resp, "redirectTo", redirectTo, hs.Cfg.OAuthCookieMaxAge, hs.CookieOptionsFromCfg)
}
if pkce := redirect.Extra[authn.KeyOAuthPKCE]; pkce != "" {
cookies.WriteCookie(reqCtx.Resp, OauthPKCECookieName, pkce, hs.Cfg.OAuthCookieMaxAge, hs.CookieOptionsFromCfg)
}

View File

@ -98,7 +98,7 @@ func writeRedirectCookie(c *contextmodel.ReqContext) {
}
// remove any forceLogin=true params
redirectTo = removeForceLoginParams(redirectTo)
redirectTo = RemoveForceLoginParams(redirectTo)
cookies.WriteCookie(c.Resp, "redirect_to", url.QueryEscape(redirectTo), 0, nil)
}
@ -113,13 +113,13 @@ func getRedirectToQueryParam(c *contextmodel.ReqContext) string {
}
// remove any forceLogin=true params
redirectTo = removeForceLoginParams(redirectTo)
redirectTo = RemoveForceLoginParams(redirectTo)
return "?redirectTo=" + url.QueryEscape(redirectTo)
}
var forceLoginParamsRegexp = regexp.MustCompile(`&?forceLogin=true`)
func removeForceLoginParams(str string) string {
func RemoveForceLoginParams(str string) string {
return forceLoginParamsRegexp.ReplaceAllString(str, "")
}
@ -138,7 +138,8 @@ func CanAdminPlugins(cfg *setting.Cfg, accessControl ac.AccessControl) func(c *c
}
func RoleAppPluginAuth(accessControl ac.AccessControl, ps pluginstore.Store, features featuremgmt.FeatureToggles,
logger log.Logger) func(c *contextmodel.ReqContext) {
logger log.Logger,
) func(c *contextmodel.ReqContext) {
return func(c *contextmodel.ReqContext) {
pluginID := web.Params(c.Req)[":id"]
p, exists := ps.Plugin(c.Req.Context(), pluginID)

View File

@ -352,7 +352,7 @@ func TestRemoveForceLoginparams(t *testing.T) {
}
for i, tc := range tcs {
t.Run(fmt.Sprintf("testcase %d", i), func(t *testing.T) {
require.Equal(t, tc.exp, removeForceLoginParams(tc.inp))
require.Equal(t, tc.exp, RemoveForceLoginParams(tc.inp))
})
}
}

View File

@ -32,9 +32,10 @@ const (
)
const (
MetaKeyUsername = "username"
MetaKeyAuthModule = "authModule"
MetaKeyIsLogin = "isLogin"
MetaKeyUsername = "username"
MetaKeyAuthModule = "authModule"
MetaKeyIsLogin = "isLogin"
defaultRedirectToCookieKey = "redirect_to"
)
// ClientParams are hints to the auth service about how to handle the identity management
@ -74,9 +75,11 @@ type FetchPermissionsParams struct {
Roles []string
}
type PostAuthHookFn func(ctx context.Context, identity *Identity, r *Request) error
type PostLoginHookFn func(ctx context.Context, identity *Identity, r *Request, err error)
type PreLogoutHookFn func(ctx context.Context, requester identity.Requester, sessionToken *usertoken.UserToken) error
type (
PostAuthHookFn func(ctx context.Context, identity *Identity, r *Request) error
PostLoginHookFn func(ctx context.Context, identity *Identity, r *Request, err error)
PreLogoutHookFn func(ctx context.Context, requester identity.Requester, sessionToken *usertoken.UserToken) error
)
type Authenticator interface {
// Authenticate authenticates a request
@ -233,41 +236,52 @@ type RedirectValidator func(url string) error
// HandleLoginResponse is a utility function to perform common operations after a successful login and returns response.NormalResponse
func HandleLoginResponse(r *http.Request, w http.ResponseWriter, cfg *setting.Cfg, identity *Identity, validator RedirectValidator, features featuremgmt.FeatureToggles) *response.NormalResponse {
result := map[string]any{"message": "Logged in"}
result["redirectUrl"] = handleLogin(r, w, cfg, identity, validator, features)
result["redirectUrl"] = handleLogin(r, w, cfg, identity, validator, features, "")
return response.JSON(http.StatusOK, result)
}
// HandleLoginRedirect is a utility function to perform common operations after a successful login and redirects
func HandleLoginRedirect(r *http.Request, w http.ResponseWriter, cfg *setting.Cfg, identity *Identity, validator RedirectValidator, features featuremgmt.FeatureToggles) {
redirectURL := handleLogin(r, w, cfg, identity, validator, features)
redirectURL := handleLogin(r, w, cfg, identity, validator, features, "redirectTo")
http.Redirect(w, r, redirectURL, http.StatusFound)
}
// HandleLoginRedirectResponse is a utility function to perform common operations after a successful login and return a response.RedirectResponse
func HandleLoginRedirectResponse(r *http.Request, w http.ResponseWriter, cfg *setting.Cfg, identity *Identity, validator RedirectValidator, features featuremgmt.FeatureToggles) *response.RedirectResponse {
return response.Redirect(handleLogin(r, w, cfg, identity, validator, features))
func HandleLoginRedirectResponse(r *http.Request, w http.ResponseWriter, cfg *setting.Cfg, identity *Identity, validator RedirectValidator, features featuremgmt.FeatureToggles, redirectToCookieName string) *response.RedirectResponse {
return response.Redirect(handleLogin(r, w, cfg, identity, validator, features, redirectToCookieName))
}
func handleLogin(r *http.Request, w http.ResponseWriter, cfg *setting.Cfg, identity *Identity, validator RedirectValidator, features featuremgmt.FeatureToggles) string {
func handleLogin(r *http.Request, w http.ResponseWriter, cfg *setting.Cfg, identity *Identity, validator RedirectValidator, features featuremgmt.FeatureToggles, redirectToCookieName string) string {
WriteSessionCookie(w, cfg, identity.SessionToken)
redirectURL := cfg.AppSubURL + "/"
if features.IsEnabledGlobally(featuremgmt.FlagUseSessionStorageForRedirection) {
return cfg.AppSubURL + "/"
if redirectToCookieName != "" {
scopedRedirectToCookie, err := r.Cookie(redirectToCookieName)
if err == nil {
redirectTo, _ := url.QueryUnescape(scopedRedirectToCookie.Value)
if redirectTo != "" && validator(redirectTo) == nil {
redirectURL = cfg.AppSubURL + redirectTo
}
cookies.DeleteCookie(w, redirectToCookieName, cookieOptions(cfg))
}
}
return redirectURL
}
redirectURL := cfg.AppSubURL + "/"
redirectURL = cfg.AppSubURL + "/"
if redirectTo := getRedirectURL(r); len(redirectTo) > 0 {
if validator(redirectTo) == nil {
redirectURL = redirectTo
}
cookies.DeleteCookie(w, "redirect_to", cookieOptions(cfg))
cookies.DeleteCookie(w, defaultRedirectToCookieKey, cookieOptions(cfg))
}
return redirectURL
}
func getRedirectURL(r *http.Request) string {
cookie, err := r.Cookie("redirect_to")
cookie, err := r.Cookie(defaultRedirectToCookieKey)
if err != nil {
return ""
}