diff --git a/conf/defaults.ini b/conf/defaults.ini index 47b847b2a27..da19fd82bf8 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -438,6 +438,7 @@ ldap_sync_ttl = 60 sync_ttl = 60 whitelist = headers = +enable_login_token = false #################################### Auth LDAP ########################### [auth.ldap] diff --git a/conf/sample.ini b/conf/sample.ini index 22453fd5774..8a6b0efeb47 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -399,6 +399,8 @@ ;ldap_sync_ttl = 60 ;whitelist = 192.168.1.1, 192.168.2.1 ;headers = Email:X-User-Email, Name:X-User-Name +# Read the auth proxy docs for details on what the setting below enables +;enable_login_token = false #################################### Basic Auth ########################## [auth.basic] diff --git a/devenv/docker/blocks/nginx_proxy/nginx_login_only.conf b/devenv/docker/blocks/nginx_proxy/nginx_login_only.conf new file mode 100644 index 00000000000..6044efd88e3 --- /dev/null +++ b/devenv/docker/blocks/nginx_proxy/nginx_login_only.conf @@ -0,0 +1,42 @@ +events { worker_connections 1024; } + +http { + sendfile on; + + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + + server { + listen 10080; + + location /grafana/ { + ################################################################ + # Enable these settings to test with basic auth and an auth proxy header + # the htpasswd file contains an admin user with password admin and + # user1: grafana and user2: grafana + ################################################################ + + + ################################################################ + # To use the auth proxy header, set the following in custom.ini: + # [auth.proxy] + # enabled = true + # header_name = X-WEBAUTH-USER + # header_property = username + ################################################################ + + location /grafana/login { + auth_basic "Restricted Content"; + auth_basic_user_file /etc/nginx/htpasswd; + proxy_set_header X-WEBAUTH-USER $remote_user; + proxy_pass http://localhost:3000/login; + } + + proxy_set_header Authorization ""; + proxy_pass http://localhost:3000/; + } + } +} diff --git a/docs/sources/auth/auth-proxy.md b/docs/sources/auth/auth-proxy.md index 2dae5c5f1f2..18c785a4de0 100644 --- a/docs/sources/auth/auth-proxy.md +++ b/docs/sources/auth/auth-proxy.md @@ -36,6 +36,8 @@ whitelist = # Optionally define more headers to sync other user attributes # Example `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL Groups:X-WEBAUTH-GROUPS` headers = +# Checkout docs on this for more details on the below setting +enable_login_token = false ``` ## Interacting with Grafana’s AuthProxy via curl @@ -294,3 +296,13 @@ curl -H "X-WEBAUTH-USER: leonard" -H "X-WEBAUTH-GROUPS: lokiteamOnExternalSystem With this, the user `leonard` will be automatically placed into the Loki team as part of Grafana authentication. [Learn more about Team Sync]({{< relref "auth/team-sync.md" >}}) + + +## Login token and session cookie + +With `enable_login_token` set to `true` Grafana will, after successful auth proxy header validation, assign the user +a login token and cookie. You only have to configure your auth proxy to provide headers for the /login route. +Requests via other routes will be authenticated using the cookie. + +Use settings `login_maximum_inactive_lifetime_days` and `login_maximum_lifetime_days` under `[auth]` to control session +lifetime. [Read more about login tokens]({{< relref "auth/overview/#login-and-short-lived-tokens" >}}) diff --git a/go.mod b/go.mod index 9a027d2cef4..72665e21f27 100644 --- a/go.mod +++ b/go.mod @@ -67,7 +67,7 @@ require ( github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect github.com/yudai/pp v2.0.1+incompatible // indirect go.uber.org/atomic v1.3.2 // indirect - golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 + golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914 golang.org/x/sync v0.0.0-20190423024810-112230192c58 diff --git a/pkg/api/login.go b/pkg/api/login.go index 01a59df18f4..fd11d9c428a 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -57,18 +57,31 @@ func (hs *HTTPServer) LoginView(c *models.ReqContext) { return } - if !c.IsSignedIn { - c.HTML(200, ViewIndex, viewData) - return - } + if c.IsSignedIn { + // Assign login token to auth proxy users if enable_login_token = true + if setting.AuthProxyEnabled && setting.AuthProxyEnableLoginToken { + hs.loginAuthProxyUser(c) + } - if redirectTo, _ := url.QueryUnescape(c.GetCookie("redirect_to")); len(redirectTo) > 0 { - c.SetCookie("redirect_to", "", -1, setting.AppSubUrl+"/") - c.Redirect(redirectTo) + if redirectTo, _ := url.QueryUnescape(c.GetCookie("redirect_to")); len(redirectTo) > 0 { + c.SetCookie("redirect_to", "", -1, setting.AppSubUrl+"/") + c.Redirect(redirectTo) + return + } + + c.Redirect(setting.AppSubUrl + "/") return } - c.Redirect(setting.AppSubUrl + "/") + c.HTML(200, ViewIndex, viewData) +} + +func (hs *HTTPServer) loginAuthProxyUser(c *models.ReqContext) { + hs.loginUserWithUser(&models.User{ + Id: c.SignedInUser.UserId, + Email: c.SignedInUser.Email, + Login: c.SignedInUser.Login, + }, c) } func tryOAuthAutoLogin(c *models.ReqContext) bool { diff --git a/pkg/api/login_test.go b/pkg/api/login_test.go index adfe97f7bc5..48a5d1a0f74 100644 --- a/pkg/api/login_test.go +++ b/pkg/api/login_test.go @@ -10,7 +10,9 @@ import ( "testing" "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" "github.com/stretchr/testify/assert" @@ -68,8 +70,6 @@ func TestLoginErrorCookieApiEndpoint(t *testing.T) { hs.LoginView(c) }) - setting.OAuthService = &setting.OAuther{} - setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo) setting.LoginCookieName = "grafana_session" setting.SecretKey = "login_testing" @@ -136,3 +136,59 @@ func TestLoginOAuthRedirect(t *testing.T) { assert.True(t, ok) assert.Equal(t, location[0], "/login/github") } + +func TestAuthProxyLoginEnableLoginTokenDisabled(t *testing.T) { + sc := setupAuthProxyLoginTest(false) + + assert.Equal(t, sc.resp.Code, 302) + location, ok := sc.resp.Header()["Location"] + assert.True(t, ok) + assert.Equal(t, location[0], "/") + + _, ok = sc.resp.Header()["Set-Cookie"] + assert.False(t, ok, "Set-Cookie does not exist") +} + +func TestAuthProxyLoginWithEnableLoginToken(t *testing.T) { + sc := setupAuthProxyLoginTest(true) + + assert.Equal(t, sc.resp.Code, 302) + location, ok := sc.resp.Header()["Location"] + assert.True(t, ok) + assert.Equal(t, location[0], "/") + + setCookie, ok := sc.resp.Header()["Set-Cookie"] + assert.True(t, ok, "Set-Cookie exists") + assert.Equal(t, "grafana_session=; Path=/; Max-Age=0; HttpOnly", setCookie[0]) +} + +func setupAuthProxyLoginTest(enableLoginToken bool) *scenarioContext { + mockSetIndexViewData() + defer resetSetIndexViewData() + + sc := setupScenarioContext("/login") + hs := &HTTPServer{ + Cfg: setting.NewCfg(), + License: models.OSSLicensingService{}, + AuthTokenService: auth.NewFakeUserAuthTokenService(), + log: log.New("hello"), + } + + sc.defaultHandler = Wrap(func(c *models.ReqContext) { + c.IsSignedIn = true + c.SignedInUser = &models.SignedInUser{ + UserId: 10, + } + hs.LoginView(c) + }) + + setting.OAuthService = &setting.OAuther{} + setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo) + setting.AuthProxyEnabled = true + setting.AuthProxyEnableLoginToken = enableLoginToken + + sc.m.Get(sc.url, sc.defaultHandler) + sc.fakeReqNoAssertions("GET", sc.url).exec() + + return sc +} diff --git a/pkg/middleware/auth_proxy.go b/pkg/middleware/auth_proxy.go index dcb256e165e..75a72de49a6 100644 --- a/pkg/middleware/auth_proxy.go +++ b/pkg/middleware/auth_proxy.go @@ -8,12 +8,6 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -const ( - - // cachePrefix is a prefix for the cache key - cachePrefix = authproxy.CachePrefix -) - var header = setting.AuthProxyHeaderName func initContextWithAuthProxy(store *remotecache.RemoteCache, ctx *m.ReqContext, orgID int64) bool { diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 95488d67495..897ddfa36d8 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/remotecache" + authproxy "github.com/grafana/grafana/pkg/middleware/auth_proxy" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/login" @@ -346,7 +347,7 @@ func TestMiddlewareContext(t *testing.T) { return nil }) - key := fmt.Sprintf(cachePrefix, base32.StdEncoding.EncodeToString([]byte(name+"-"+group))) + key := fmt.Sprintf(authproxy.CachePrefix, base32.StdEncoding.EncodeToString([]byte(name+"-"+group))) err := sc.remoteCacheService.Set(key, int64(33), 0) So(err, ShouldBeNil) sc.fakeReq("GET", "/") diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 54bd4dc6d87..6d532471ce4 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -143,13 +143,14 @@ var ( AnonymousOrgRole string // Auth proxy settings - AuthProxyEnabled bool - AuthProxyHeaderName string - AuthProxyHeaderProperty string - AuthProxyAutoSignUp bool - AuthProxySyncTtl int - AuthProxyWhitelist string - AuthProxyHeaders map[string]string + AuthProxyEnabled bool + AuthProxyHeaderName string + AuthProxyHeaderProperty string + AuthProxyAutoSignUp bool + AuthProxyEnableLoginToken bool + AuthProxySyncTtl int + AuthProxyWhitelist string + AuthProxyHeaders map[string]string // Basic Auth BasicAuthEnabled bool @@ -854,6 +855,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { return err } AuthProxyAutoSignUp = authProxy.Key("auto_sign_up").MustBool(true) + AuthProxyEnableLoginToken = authProxy.Key("enable_login_token").MustBool(false) ldapSyncVal := authProxy.Key("ldap_sync_ttl").MustInt() syncVal := authProxy.Key("sync_ttl").MustInt()