Auth: Split signout_redirect_url into per provider settings (#75269)

* Split signout_redirect_url into per provider settings

* Split signout_redirect_url into per provider settings

* Update docs/sources/setup-grafana/configure-security/configure-authentication/grafana/index.md

Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>

* Split signout_redirect_url into per provider settings

* Split signout_redirect_url into per provider settings

* Split signout_redirect_url into per provider settings

* Split signout_redirect_url into per provider settings

* Split signout_redirect_url into per provider settings

* Split signout_redirect_url into per provider settings

* update docs

* update devenvs

* add missing struct tag

---------

Co-authored-by: Rao, B V Chalapathi <b_v_chalapathi.rao@nokia.com>
Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>
Co-authored-by: jguer <me@jguer.space>
This commit is contained in:
venkatbvc
2023-11-29 14:50:21 +01:00
committed by GitHub
co-authored by Christopher Moyer Rao, B V Chalapathi jguer
parent 73776f37eb
commit e152323a33
15 changed files with 128 additions and 24 deletions
+35
View File
@@ -11,6 +11,7 @@ import (
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/response"
@@ -214,6 +215,40 @@ func setupScenarioContext(t *testing.T, url string) *scenarioContext {
return sc
}
func setupScenarioContextSamlLogout(t *testing.T, url string) *scenarioContext {
cfg := setting.NewCfg()
//seed sections and keys
cfg.Raw.DeleteSection("DEFAULT")
saml, err := cfg.Raw.NewSection("auth.saml")
assert.NoError(t, err)
_, err = saml.NewKey("enabled", "true")
assert.NoError(t, err)
_, err = saml.NewKey("allow_idp_initiated", "false")
assert.NoError(t, err)
_, err = saml.NewKey("single_logout", "true")
assert.NoError(t, err)
ctxHdlr := getContextHandler(t, cfg)
sc := &scenarioContext{
url: url,
t: t,
cfg: cfg,
ctxHdlr: ctxHdlr,
}
viewsPath, err := filepath.Abs("../../public/views")
require.NoError(t, err)
exists, err := fs.Exists(viewsPath)
require.NoError(t, err)
require.Truef(t, exists, "Views should be in %q", viewsPath)
sc.m = web.New()
sc.m.UseMiddleware(web.Renderer(viewsPath, "[[", "]]"))
sc.m.Use(ctxHdlr.Middleware)
return sc
}
// FIXME: This user should not be anonymous
func authedUserWithPermissions(userID, orgID int64, permissions []accesscontrol.Permission) *user.SignedInUser {
return &user.SignedInUser{UserID: userID, OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(permissions)}}
}
+27 -8
View File
@@ -248,19 +248,29 @@ func (hs *HTTPServer) Logout(c *contextmodel.ReqContext) {
hs.log.Error("failed to retrieve user ID", "error", errID)
}
// If SAML is enabled and this is a SAML user use saml logout
if hs.samlSingleLogoutEnabled() {
getAuthQuery := loginservice.GetAuthInfoQuery{UserId: userID}
if authInfo, err := hs.authInfoService.GetAuthInfo(c.Req.Context(), &getAuthQuery); err == nil {
oauthProviderSignoutRedirectUrl := ""
getAuthQuery := loginservice.GetAuthInfoQuery{UserId: userID}
authInfo, err := hs.authInfoService.GetAuthInfo(c.Req.Context(), &getAuthQuery)
if err == nil {
// If SAML is enabled and this is a SAML user use saml logout
if hs.samlSingleLogoutEnabled() {
if authInfo.AuthModule == loginservice.SAMLAuthModule {
c.Redirect(hs.Cfg.AppSubURL + "/logout/saml")
return
}
}
oauthProvider := hs.SocialService.GetOAuthInfoProvider(strings.TrimPrefix(authInfo.AuthModule, "oauth_"))
oauthProviderSignoutRedirectUrl = oauthProvider.SignoutRedirectUrl
}
hs.log.Debug("Logout Redirect url", "auth.SignoutRedirectUrl:", hs.Cfg.SignoutRedirectUrl)
hs.log.Debug("Logout Redirect url", "oauth provider redirect url:", oauthProviderSignoutRedirectUrl)
signOutRedirectUrl := getSignOutRedirectUrl(hs.Cfg.SignoutRedirectUrl, oauthProviderSignoutRedirectUrl)
hs.log.Debug("Logout Redirect url", "signOurRedirectUrl:", signOutRedirectUrl)
idTokenHint := ""
oidcLogout := isPostLogoutRedirectConfigured(hs.Cfg.SignoutRedirectUrl)
oidcLogout := isPostLogoutRedirectConfigured(signOutRedirectUrl)
// Invalidate the OAuth tokens in case the User logged in with OAuth or the last external AuthEntry is an OAuth one
if entry, exists, _ := hs.oauthTokenService.HasOAuthEntry(c.Req.Context(), c.SignedInUser); exists {
@@ -278,17 +288,17 @@ func (hs *HTTPServer) Logout(c *contextmodel.ReqContext) {
}
}
err := hs.AuthTokenService.RevokeToken(c.Req.Context(), c.UserToken, false)
err = hs.AuthTokenService.RevokeToken(c.Req.Context(), c.UserToken, false)
if err != nil && !errors.Is(err, auth.ErrUserTokenNotFound) {
hs.log.Error("failed to revoke auth token", "error", err)
}
authn.DeleteSessionCookie(c.Resp, hs.Cfg)
rdUrl := hs.Cfg.SignoutRedirectUrl
rdUrl := signOutRedirectUrl
if rdUrl != "" {
if oidcLogout {
rdUrl = getPostRedirectUrl(hs.Cfg.SignoutRedirectUrl, idTokenHint)
rdUrl = getPostRedirectUrl(signOutRedirectUrl, idTokenHint)
}
c.Redirect(rdUrl)
} else {
@@ -443,3 +453,12 @@ func getPostRedirectUrl(rdUrl string, tokenHint string) string {
return u.String()
}
func getSignOutRedirectUrl(gRdUrl string, oauthProviderUrl string) string {
if oauthProviderUrl != "" {
return oauthProviderUrl
} else if gRdUrl != "" {
return gRdUrl
}
return ""
}
+33
View File
@@ -29,7 +29,9 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/licensing/licensingtest"
loginservice "github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/login/authinfotest"
"github.com/grafana/grafana/pkg/services/navtree"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
@@ -644,6 +646,37 @@ func setupAuthProxyLoginTest(t *testing.T, enableLoginToken bool) *scenarioConte
return sc
}
func TestLogoutSaml(t *testing.T) {
fakeSetIndexViewData(t)
fakeViewIndex(t)
sc := setupScenarioContextSamlLogout(t, "/logout")
license := licensingtest.NewFakeLicensing()
license.On("FeatureEnabled", "saml").Return(true)
hs := &HTTPServer{
Cfg: sc.cfg,
SettingsProvider: &setting.OSSImpl{Cfg: sc.cfg},
License: license,
SocialService: &mockSocialService{},
Features: featuremgmt.WithFeatures(),
authInfoService: &authinfotest.FakeService{
ExpectedUserAuth: &loginservice.UserAuth{AuthModule: loginservice.SAMLAuthModule},
},
}
assert.Equal(t, true, hs.samlSingleLogoutEnabled())
sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response {
c.SignedInUser = &user.SignedInUser{
UserID: 1,
}
hs.Logout(c)
return response.Empty(http.StatusOK)
})
sc.m.Get(sc.url, sc.defaultHandler)
sc.fakeReqNoAssertions("GET", sc.url).exec()
require.Equal(t, 302, sc.resp.Code)
}
type mockSocialService struct {
oAuthInfo *social.OAuthInfo
oAuthInfos map[string]*social.OAuthInfo