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 19:20:21 +05:30 committed by GitHub
parent 73776f37eb
commit e152323a33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 128 additions and 24 deletions

View File

@ -592,6 +592,7 @@ scopes = user:email,read:org
auth_url = https://github.com/login/oauth/authorize
token_url = https://github.com/login/oauth/access_token
api_url = https://api.github.com/user
signout_redirect_url =
allowed_domains =
team_ids =
allowed_organizations =
@ -619,6 +620,7 @@ scopes = openid email profile
auth_url = https://gitlab.com/oauth/authorize
token_url = https://gitlab.com/oauth/token
api_url = https://gitlab.com/api/v4
signout_redirect_url =
allowed_domains =
allowed_groups =
role_attribute_path =
@ -645,6 +647,7 @@ scopes = openid email profile
auth_url = https://accounts.google.com/o/oauth2/v2/auth
token_url = https://oauth2.googleapis.com/token
api_url = https://openidconnect.googleapis.com/v1/userinfo
signout_redirect_url =
allowed_domains =
hosted_domain =
allowed_groups =
@ -695,6 +698,7 @@ client_secret =
scopes = openid email profile
auth_url = https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorize
token_url = https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
signout_redirect_url =
allowed_domains =
allowed_groups =
allowed_organizations =
@ -722,6 +726,7 @@ scopes = openid profile email groups
auth_url = https://<tenant-id>.okta.com/oauth2/v1/authorize
token_url = https://<tenant-id>.okta.com/oauth2/v1/token
api_url = https://<tenant-id>.okta.com/oauth2/v1/userinfo
signout_redirect_url =
allowed_domains =
allowed_groups =
role_attribute_path =
@ -758,6 +763,7 @@ team_ids_attribute_path =
auth_url =
token_url =
api_url =
signout_redirect_url =
teams_url =
allowed_domains =
allowed_groups =
@ -808,6 +814,7 @@ auto_sign_up = false
url_login = false
allow_assign_grafana_admin = false
skip_org_role_sync = false
signout_redirect_url =
#################################### Auth LDAP ###########################
[auth.ldap]

View File

@ -581,6 +581,7 @@
;auth_url = https://github.com/login/oauth/authorize
;token_url = https://github.com/login/oauth/access_token
;api_url = https://api.github.com/user
;signout_redirect_url =
;allowed_domains =
;team_ids =
;allowed_organizations =
@ -602,6 +603,7 @@
;auth_url = https://gitlab.com/oauth/authorize
;token_url = https://gitlab.com/oauth/token
;api_url = https://gitlab.com/api/v4
;signout_redirect_url =
;allowed_domains =
;allowed_groups =
;role_attribute_path =
@ -627,6 +629,7 @@
;auth_url = https://accounts.google.com/o/oauth2/v2/auth
;token_url = https://oauth2.googleapis.com/token
;api_url = https://openidconnect.googleapis.com/v1/userinfo
;signout_redirect_url =
;allowed_domains =
;hosted_domain =
;allowed_groups =
@ -661,6 +664,7 @@
;scopes = openid email profile
;auth_url = https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorize
;token_url = https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
;signout_redirect_url =
;allowed_domains =
;allowed_groups =
;allowed_organizations =
@ -682,6 +686,7 @@
;auth_url = https://<tenant-id>.okta.com/oauth2/v1/authorize
;token_url = https://<tenant-id>.okta.com/oauth2/v1/token
;api_url = https://<tenant-id>.okta.com/oauth2/v1/userinfo
;signout_redirect_url =
;allowed_domains =
;allowed_groups =
;role_attribute_path =
@ -708,6 +713,7 @@
;auth_url = https://foo.bar/login/oauth/authorize
;token_url = https://foo.bar/login/oauth/access_token
;api_url = https://foo.bar/user
;signout_redirect_url =
;teams_url =
;allowed_domains =
;team_ids =

View File

@ -85,8 +85,6 @@ auth_url = http://localhost:9000/application/o/authorize/
token_url = http://localhost:9000/application/o/token/
api_url = http://localhost:9000/application/o/userinfo/
role_attribute_path = contains(groups[*], 'admin') && 'Admin' || contains(groups[*], 'editor') && 'Editor' || 'Viewer'
[auth]
signout_redirect_url = http://localhost:9000/application/o/grafana-oidc/end-session/
```

View File

@ -1,5 +1,5 @@
authentikdb:
image: docker.io/library/postgres:12-alpine
image: docker.io/library/postgres:16-alpine
restart: unless-stopped
container_name: authentikdb
environment:
@ -39,7 +39,7 @@
- "authentik:authentik"
authentik:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.5.1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.4}
restart: unless-stopped
container_name: authentik
command: server
@ -66,7 +66,7 @@
- "authentikredis:authentikredis"
authentik-worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.5.1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.4}
restart: unless-stopped
container_name: authentik-worker
command: worker

View File

@ -1,5 +1,5 @@
oauthkeycloakdb:
image: docker.io/library/postgres:12-alpine
image: docker.io/library/postgres:16-alpine
container_name: oauthkeycloakdb
environment:
POSTGRES_DB: keycloak
@ -10,7 +10,7 @@
restart: unless-stopped
oauthkeycloak:
image: quay.io/keycloak/keycloak:21.1
image: quay.io/keycloak/keycloak:23.0
container_name: oauthkeycloak
command: start-dev
environment:

View File

@ -1,5 +1,5 @@
oauthkeycloakdb:
image: docker.io/library/postgres:12-alpine
image: docker.io/library/postgres:16-alpine
container_name: oauthkeycloakdb
environment:
POSTGRES_DB: keycloak
@ -10,7 +10,7 @@
restart: unless-stopped
oauthkeycloak:
image: quay.io/keycloak/keycloak:22.0
image: quay.io/keycloak/keycloak:23.0
container_name: oauthkeycloak
command: start-dev
environment:

View File

@ -10,9 +10,6 @@ make devenv sources="auth/oauth"
Here is the conf you need to add to your configuration file (conf/custom.ini):
```ini
[auth]
signout_redirect_url = http://localhost:8087/realms/grafana/protocol/openid-connect/logout?post_logout_redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Flogin
[auth.generic_oauth]
enabled = true
name = Keycloak-OAuth
@ -28,6 +25,7 @@ auth_url = http://localhost:8087/realms/grafana/protocol/openid-connect/auth
token_url = http://localhost:8087/realms/grafana/protocol/openid-connect/token
role_attribute_path = contains(roles[*], 'grafanaadmin') && 'GrafanaAdmin' || contains(roles[*], 'admin') && 'Admin' || contains(roles[*], 'editor') && 'Editor' || 'Viewer'
allow_assign_grafana_admin = true
signout_redirect_url = http://localhost:8087/realms/grafana/protocol/openid-connect/logout?post_logout_redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Flogin
```
## Devenv setup jwt auth

View File

@ -205,10 +205,12 @@ disable_signout_menu = true
### URL redirect after signing out
URL to redirect the user to after signing out from Grafana. This can for example be used to enable signout from OAuth provider.
URL to redirect the user to after signing out from Grafana. This can for example be used to enable signout from an OAuth provider.
Example for Generic OAuth:
```bash
[auth]
[auth.generic_oauth]
signout_redirect_url =
```

View File

@ -121,9 +121,12 @@ disable_signout_menu = true
### URL redirect after signing out
URL to redirect the user to after signing out from Grafana. This can for example be used to enable signout from oauth provider.
The URL to redirect the user to after signing out from Grafana can be configured under `[auth]` or under a specific OAuth provider section (for example, `[auth.generic_oauth]`). The URL configured under a specific OAuth provider section takes precedence over the URL configured in `[auth]` section. This can, for example, enable signout from the OAuth provider.
```bash
[auth.generic_oauth]
signout_redirect_url =
[auth]
signout_redirect_url =
```

View File

@ -136,7 +136,7 @@ groups_attribute_path = reverse("Global:department")
To enable Single Logout, you need to add the following option to the configuration of Grafana:
```ini
[auth]
[auth.generic_oauth]
signout_redirect_url = https://<PROVIDER_DOMAIN>/auth/realms/<REALM_NAME>/protocol/openid-connect/logout?post_logout_redirect_uri=https%3A%2F%2<GRAFANA_DOMAIN>%2Flogin
```

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)}}
}

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 ""
}

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

View File

@ -47,6 +47,7 @@ skip_org_role_sync = true
use_refresh_token = true
empty_scopes =
hosted_domain = test_hosted_domain
signout_redirect_url = https://oauth.com/signout?post_logout_redirect_uri=https://grafana.com
`
iniFile, err := ini.Load([]byte(iniContent))
@ -83,6 +84,7 @@ hosted_domain = test_hosted_domain
AllowAssignGrafanaAdmin: true,
UseRefreshToken: true,
HostedDomain: "test_hosted_domain",
SignoutRedirectUrl: "https://oauth.com/signout?post_logout_redirect_uri=https://grafana.com",
Extra: map[string]string{
"allowed_organizations": "org1, org2",
"id_token_attribute_name": "id_token",

View File

@ -74,6 +74,7 @@ type OAuthInfo struct {
TlsSkipVerify bool `mapstructure:"tls_skip_verify_insecure" toml:"tls_skip_verify_insecure"`
UsePKCE bool `mapstructure:"use_pkce" toml:"use_pkce"`
UseRefreshToken bool `mapstructure:"use_refresh_token" toml:"use_refresh_token"`
SignoutRedirectUrl string `mapstructure:"signout_redirect_url" toml:"signout_redirect_url"`
Extra map[string]string `mapstructure:",remain" toml:"extra,omitempty"`
}