diff --git a/devenv/docker/blocks/auth/jwt_proxy/docker-compose.yaml b/devenv/docker/blocks/auth/jwt_proxy/docker-compose.yaml index 5f3e45055f5..cc18a38126c 100644 --- a/devenv/docker/blocks/auth/jwt_proxy/docker-compose.yaml +++ b/devenv/docker/blocks/auth/jwt_proxy/docker-compose.yaml @@ -12,7 +12,7 @@ oauthkeycloak: image: quay.io/keycloak/keycloak:21.1 container_name: oauthkeycloak - command: --spi-login-protocol-openid-connect-legacy-logout-redirect-uri=true start-dev + command: start-dev environment: KC_DB: postgres KC_DB_URL: jdbc:postgresql://oauthkeycloakdb/keycloak diff --git a/devenv/docker/blocks/auth/oauth/docker-compose.yaml b/devenv/docker/blocks/auth/oauth/docker-compose.yaml index f322a879757..4efaa56d114 100644 --- a/devenv/docker/blocks/auth/oauth/docker-compose.yaml +++ b/devenv/docker/blocks/auth/oauth/docker-compose.yaml @@ -12,7 +12,7 @@ oauthkeycloak: image: quay.io/keycloak/keycloak:22.0 container_name: oauthkeycloak - command: --spi-login-protocol-openid-connect-legacy-logout-redirect-uri=true start-dev + command: start-dev environment: KC_DB: postgres KC_DB_URL: jdbc:postgresql://oauthkeycloakdb/keycloak diff --git a/devenv/docker/blocks/auth/oauth/readme.md b/devenv/docker/blocks/auth/oauth/readme.md index e9bdab5e1ba..be529273937 100644 --- a/devenv/docker/blocks/auth/oauth/readme.md +++ b/devenv/docker/blocks/auth/oauth/readme.md @@ -11,7 +11,7 @@ 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?redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Flogin +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 diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 23bd7940857..82a00092ede 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -901,7 +901,11 @@ Set to `true` to disable the signout link in the side menu. This is useful if yo ### signout_redirect_url -URL to redirect the user to after they sign out. +The URL the user is redirected to upon signing out. To support [OpenID Connect RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html), the user must add `post_logout_redirect_uri` to the `signout_redirect_url`. + +Example: + +signout_redirect_url = http://localhost:8087/realms/grafana/protocol/openid-connect/logout?post_logout_redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Flogin ### oauth_auto_login diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/keycloak/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/keycloak/index.md index 0b1427742b6..322df6c50a3 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/keycloak/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/keycloak/index.md @@ -137,7 +137,7 @@ To enable Single Logout, you need to add the following option to the configurati ```ini [auth] -signout_redirect_url = https:///auth/realms//protocol/openid-connect/logout?redirect_uri=https%3A%2F%2F%2Flogin +signout_redirect_url = https:///auth/realms//protocol/openid-connect/logout?post_logout_redirect_uri=https%3A%2F%2%2Flogin ``` As an example, `` can be `keycloak-demo.grafana.org`, diff --git a/pkg/api/login.go b/pkg/api/login.go index bf02fa71396..0d3e76ad445 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -21,7 +21,6 @@ import ( pref "github.com/grafana/grafana/pkg/services/preference" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/errutil" ) @@ -29,6 +28,8 @@ import ( const ( viewIndex = "index" loginErrorCookieName = "login_error" + // #nosec G101 - this is not a hardcoded secret + postLogoutRedirectParam = "post_logout_redirect_uri" ) var setIndexViewData = (*HTTPServer).setIndexViewData @@ -250,8 +251,20 @@ func (hs *HTTPServer) Logout(c *contextmodel.ReqContext) { } } + idTokenHint := "" + oidcLogout := isPostLogoutRedirectConfigured(hs.Cfg.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 { + token := hs.oauthTokenService.GetCurrentOAuthToken(c.Req.Context(), c.SignedInUser) + if oidcLogout { + if token.Valid() { + idTokenHint = token.Extra("id_token").(string) + } else { + hs.log.Warn("Token is not valid") + } + } + if err := hs.oauthTokenService.InvalidateOAuthTokens(c.Req.Context(), entry); err != nil { hs.log.Warn("failed to invalidate oauth tokens for user", "userId", c.UserID, "error", err) } @@ -264,8 +277,12 @@ func (hs *HTTPServer) Logout(c *contextmodel.ReqContext) { authn.DeleteSessionCookie(c.Resp, hs.Cfg) - if setting.SignoutRedirectUrl != "" { - c.Redirect(setting.SignoutRedirectUrl) + rdUrl := hs.Cfg.SignoutRedirectUrl + if rdUrl != "" { + if oidcLogout { + rdUrl = getPostRedirectUrl(hs.Cfg.SignoutRedirectUrl, idTokenHint) + } + c.Redirect(rdUrl) } else { hs.log.Info("Successful Logout", "User", c.Email) c.Redirect(hs.Cfg.AppSubURL + "/login") @@ -374,3 +391,38 @@ func getFirstPublicErrorMessage(err *errutil.Error) string { return errPublic.Message } + +func isPostLogoutRedirectConfigured(redirectUrl string) bool { + if redirectUrl == "" { + return false + } + + u, err := url.Parse(redirectUrl) + if err != nil { + return false + } + + q := u.Query() + _, ok := q[postLogoutRedirectParam] + return ok +} + +func getPostRedirectUrl(rdUrl string, tokenHint string) string { + if tokenHint == "" { + return rdUrl + } + if rdUrl == "" { + return rdUrl + } + + u, err := url.Parse(rdUrl) + if err != nil { + return rdUrl + } + + q := u.Query() + q.Set("id_token_hint", tokenHint) + u.RawQuery = q.Encode() + + return u.String() +} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index baff43466d9..04f7b40e2b2 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -98,7 +98,6 @@ var ( LoginHint string PasswordHint string DisableSignoutMenu bool - SignoutRedirectUrl string ExternalUserMngLinkUrl string ExternalUserMngLinkName string ExternalUserMngInfo string @@ -281,6 +280,7 @@ type Cfg struct { DisableLogin bool AdminEmail string DisableLoginForm bool + SignoutRedirectUrl string // Not documented & not supported // stand in until a more complete solution is implemented AuthConfigUIAdminAccess bool @@ -1556,7 +1556,7 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { } cfg.OAuthCookieMaxAge = auth.Key("oauth_state_cookie_max_age").MustInt(600) - SignoutRedirectUrl = valueAsString(auth, "signout_redirect_url", "") + cfg.SignoutRedirectUrl = valueAsString(auth, "signout_redirect_url", "") // Deprecated cfg.OAuthSkipOrgRoleUpdateSync = auth.Key("oauth_skip_org_role_update_sync").MustBool(false) if cfg.OAuthSkipOrgRoleUpdateSync {