mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
73776f37eb
commit
e152323a33
@ -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]
|
||||
|
@ -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 =
|
||||
|
@ -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/
|
||||
```
|
||||
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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 =
|
||||
```
|
||||
|
||||
|
@ -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 =
|
||||
```
|
||||
|
@ -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
|
||||
```
|
||||
|
||||
|
@ -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)}}
|
||||
}
|
||||
|
@ -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 ""
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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"`
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user