mirror of https://github.com/grafana/grafana.git
				
				
				
			AuthProxy: Can now login with auth proxy and get a login token (#20175)
* AuthProxy: Can now login with auth proxy and get a login token * added unit tests * renamed setting and updated docs * AuthProxy: minor tweak * Fixed tests and namings * spellfix * fix * remove unused setting, probably from merge conflict * fix
This commit is contained in:
		
							parent
							
								
									818aa8eefa
								
							
						
					
					
						commit
						be2bf1a297
					
				|  | @ -438,6 +438,7 @@ ldap_sync_ttl = 60 | ||||||
| sync_ttl = 60 | sync_ttl = 60 | ||||||
| whitelist = | whitelist = | ||||||
| headers = | headers = | ||||||
|  | enable_login_token = false | ||||||
| 
 | 
 | ||||||
| #################################### Auth LDAP ########################### | #################################### Auth LDAP ########################### | ||||||
| [auth.ldap] | [auth.ldap] | ||||||
|  |  | ||||||
|  | @ -399,6 +399,8 @@ | ||||||
| ;ldap_sync_ttl = 60 | ;ldap_sync_ttl = 60 | ||||||
| ;whitelist = 192.168.1.1, 192.168.2.1 | ;whitelist = 192.168.1.1, 192.168.2.1 | ||||||
| ;headers = Email:X-User-Email, Name:X-User-Name | ;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 ########################## | #################################### Basic Auth ########################## | ||||||
| [auth.basic] | [auth.basic] | ||||||
|  |  | ||||||
|  | @ -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/; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -36,6 +36,8 @@ whitelist = | ||||||
| # Optionally define more headers to sync other user attributes | # Optionally define more headers to sync other user attributes | ||||||
| # Example `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL Groups:X-WEBAUTH-GROUPS` | # Example `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL Groups:X-WEBAUTH-GROUPS` | ||||||
| headers = | headers = | ||||||
|  | # Checkout docs on this for more details on the below setting | ||||||
|  | enable_login_token = false | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ## Interacting with Grafana’s AuthProxy via curl | ## 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. | 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" >}}) | [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" >}}) | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										2
									
								
								go.mod
								
								
								
								
							|  | @ -67,7 +67,7 @@ require ( | ||||||
| 	github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect | 	github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect | ||||||
| 	github.com/yudai/pp v2.0.1+incompatible // indirect | 	github.com/yudai/pp v2.0.1+incompatible // indirect | ||||||
| 	go.uber.org/atomic v1.3.2 // 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/net v0.0.0-20190724013045-ca1201d0de80 | ||||||
| 	golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914 | 	golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914 | ||||||
| 	golang.org/x/sync v0.0.0-20190423024810-112230192c58 | 	golang.org/x/sync v0.0.0-20190423024810-112230192c58 | ||||||
|  |  | ||||||
|  | @ -57,18 +57,31 @@ func (hs *HTTPServer) LoginView(c *models.ReqContext) { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if !c.IsSignedIn { | 	if c.IsSignedIn { | ||||||
| 		c.HTML(200, ViewIndex, viewData) | 		// 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) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		c.Redirect(setting.AppSubUrl + "/") | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if redirectTo, _ := url.QueryUnescape(c.GetCookie("redirect_to")); len(redirectTo) > 0 { | 	c.HTML(200, ViewIndex, viewData) | ||||||
| 		c.SetCookie("redirect_to", "", -1, setting.AppSubUrl+"/") | } | ||||||
| 		c.Redirect(redirectTo) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	c.Redirect(setting.AppSubUrl + "/") | 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 { | func tryOAuthAutoLogin(c *models.ReqContext) bool { | ||||||
|  |  | ||||||
|  | @ -10,7 +10,9 @@ import ( | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
| 	"github.com/grafana/grafana/pkg/api/dtos" | 	"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/models" | ||||||
|  | 	"github.com/grafana/grafana/pkg/services/auth" | ||||||
| 	"github.com/grafana/grafana/pkg/setting" | 	"github.com/grafana/grafana/pkg/setting" | ||||||
| 	"github.com/grafana/grafana/pkg/util" | 	"github.com/grafana/grafana/pkg/util" | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
|  | @ -68,8 +70,6 @@ func TestLoginErrorCookieApiEndpoint(t *testing.T) { | ||||||
| 		hs.LoginView(c) | 		hs.LoginView(c) | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	setting.OAuthService = &setting.OAuther{} |  | ||||||
| 	setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo) |  | ||||||
| 	setting.LoginCookieName = "grafana_session" | 	setting.LoginCookieName = "grafana_session" | ||||||
| 	setting.SecretKey = "login_testing" | 	setting.SecretKey = "login_testing" | ||||||
| 
 | 
 | ||||||
|  | @ -136,3 +136,59 @@ func TestLoginOAuthRedirect(t *testing.T) { | ||||||
| 	assert.True(t, ok) | 	assert.True(t, ok) | ||||||
| 	assert.Equal(t, location[0], "/login/github") | 	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 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -8,12 +8,6 @@ import ( | ||||||
| 	"github.com/grafana/grafana/pkg/setting" | 	"github.com/grafana/grafana/pkg/setting" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const ( |  | ||||||
| 
 |  | ||||||
| 	// cachePrefix is a prefix for the cache key
 |  | ||||||
| 	cachePrefix = authproxy.CachePrefix |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var header = setting.AuthProxyHeaderName | var header = setting.AuthProxyHeaderName | ||||||
| 
 | 
 | ||||||
| func initContextWithAuthProxy(store *remotecache.RemoteCache, ctx *m.ReqContext, orgID int64) bool { | func initContextWithAuthProxy(store *remotecache.RemoteCache, ctx *m.ReqContext, orgID int64) bool { | ||||||
|  |  | ||||||
|  | @ -17,6 +17,7 @@ import ( | ||||||
| 	"github.com/grafana/grafana/pkg/api/dtos" | 	"github.com/grafana/grafana/pkg/api/dtos" | ||||||
| 	"github.com/grafana/grafana/pkg/bus" | 	"github.com/grafana/grafana/pkg/bus" | ||||||
| 	"github.com/grafana/grafana/pkg/infra/remotecache" | 	"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/models" | ||||||
| 	"github.com/grafana/grafana/pkg/services/auth" | 	"github.com/grafana/grafana/pkg/services/auth" | ||||||
| 	"github.com/grafana/grafana/pkg/services/login" | 	"github.com/grafana/grafana/pkg/services/login" | ||||||
|  | @ -346,7 +347,7 @@ func TestMiddlewareContext(t *testing.T) { | ||||||
| 					return nil | 					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) | 				err := sc.remoteCacheService.Set(key, int64(33), 0) | ||||||
| 				So(err, ShouldBeNil) | 				So(err, ShouldBeNil) | ||||||
| 				sc.fakeReq("GET", "/") | 				sc.fakeReq("GET", "/") | ||||||
|  |  | ||||||
|  | @ -143,13 +143,14 @@ var ( | ||||||
| 	AnonymousOrgRole string | 	AnonymousOrgRole string | ||||||
| 
 | 
 | ||||||
| 	// Auth proxy settings
 | 	// Auth proxy settings
 | ||||||
| 	AuthProxyEnabled        bool | 	AuthProxyEnabled          bool | ||||||
| 	AuthProxyHeaderName     string | 	AuthProxyHeaderName       string | ||||||
| 	AuthProxyHeaderProperty string | 	AuthProxyHeaderProperty   string | ||||||
| 	AuthProxyAutoSignUp     bool | 	AuthProxyAutoSignUp       bool | ||||||
| 	AuthProxySyncTtl        int | 	AuthProxyEnableLoginToken bool | ||||||
| 	AuthProxyWhitelist      string | 	AuthProxySyncTtl          int | ||||||
| 	AuthProxyHeaders        map[string]string | 	AuthProxyWhitelist        string | ||||||
|  | 	AuthProxyHeaders          map[string]string | ||||||
| 
 | 
 | ||||||
| 	// Basic Auth
 | 	// Basic Auth
 | ||||||
| 	BasicAuthEnabled bool | 	BasicAuthEnabled bool | ||||||
|  | @ -854,6 +855,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	AuthProxyAutoSignUp = authProxy.Key("auto_sign_up").MustBool(true) | 	AuthProxyAutoSignUp = authProxy.Key("auto_sign_up").MustBool(true) | ||||||
|  | 	AuthProxyEnableLoginToken = authProxy.Key("enable_login_token").MustBool(false) | ||||||
| 
 | 
 | ||||||
| 	ldapSyncVal := authProxy.Key("ldap_sync_ttl").MustInt() | 	ldapSyncVal := authProxy.Key("ldap_sync_ttl").MustInt() | ||||||
| 	syncVal := authProxy.Key("sync_ttl").MustInt() | 	syncVal := authProxy.Key("sync_ttl").MustInt() | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue