Files
grafana/pkg/api/login_test.go
Marcus Efraimsson a1579283a6 Add disabled option for cookie samesite attribute (#21472)
Breaking change: If disabled the cookie samesite cookie attribute
will not be set, but if none the attribute will be set and is a
breaking change compared to before where none did not render the
attribute. This was due to a known issue in Safari.

Co-Authored-By: Arve Knudsen <arve.knudsen@gmail.com>
Co-Authored-By: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

Fixes #19847
2020-01-14 17:41:54 +01:00

434 lines
11 KiB
Go

package api
import (
"encoding/hex"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/login"
"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"
)
func mockSetIndexViewData() {
setIndexViewData = func(*HTTPServer, *models.ReqContext) (*dtos.IndexViewData, error) {
data := &dtos.IndexViewData{
User: &dtos.CurrentUser{},
Settings: map[string]interface{}{},
NavTree: []*dtos.NavLink{},
}
return data, nil
}
}
func resetSetIndexViewData() {
setIndexViewData = (*HTTPServer).setIndexViewData
}
func mockViewIndex() {
getViewIndex = func() string {
return "index-template"
}
}
func resetViewIndex() {
getViewIndex = func() string {
return ViewIndex
}
}
func getBody(resp *httptest.ResponseRecorder) (string, error) {
responseData, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(responseData), nil
}
type FakeLogger struct {
log.Logger
}
func (stub *FakeLogger) Info(testMessage string, ctx ...interface{}) {
}
type redirectCase struct {
desc string
url string
status int
err error
appURL string
appSubURL string
}
func TestLoginErrorCookieApiEndpoint(t *testing.T) {
mockSetIndexViewData()
defer resetSetIndexViewData()
mockViewIndex()
defer resetViewIndex()
sc := setupScenarioContext("/login")
hs := &HTTPServer{
Cfg: setting.NewCfg(),
License: models.OSSLicensingService{},
}
sc.defaultHandler = Wrap(func(w http.ResponseWriter, c *models.ReqContext) {
hs.LoginView(c)
})
setting.LoginCookieName = "grafana_session"
setting.SecretKey = "login_testing"
setting.OAuthService = &setting.OAuther{}
setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo)
setting.OAuthService.OAuthInfos["github"] = &setting.OAuthInfo{
ClientId: "fake",
ClientSecret: "fakefake",
Enabled: true,
AllowSignup: true,
Name: "github",
}
setting.OAuthAutoLogin = true
oauthError := errors.New("User not a member of one of the required organizations")
encryptedError, _ := util.Encrypt([]byte(oauthError.Error()), setting.SecretKey)
cookie := http.Cookie{
Name: LoginErrorCookieName,
MaxAge: 60,
Value: hex.EncodeToString(encryptedError),
HttpOnly: true,
Path: setting.AppSubUrl + "/",
Secure: hs.Cfg.CookieSecure,
SameSite: hs.Cfg.CookieSameSiteMode,
}
sc.m.Get(sc.url, sc.defaultHandler)
sc.fakeReqNoAssertionsWithCookie("GET", sc.url, cookie).exec()
assert.Equal(t, sc.resp.Code, 200)
responseString, err := getBody(sc.resp)
assert.NoError(t, err)
assert.True(t, strings.Contains(responseString, oauthError.Error()))
}
func TestLoginViewRedirect(t *testing.T) {
mockSetIndexViewData()
defer resetSetIndexViewData()
mockViewIndex()
defer resetViewIndex()
sc := setupScenarioContext("/login")
hs := &HTTPServer{
Cfg: setting.NewCfg(),
License: models.OSSLicensingService{},
}
hs.Cfg.CookieSecure = true
sc.defaultHandler = Wrap(func(w http.ResponseWriter, 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)
redirectCases := []redirectCase{
{
desc: "grafana relative url without subpath",
url: "/profile",
appURL: "http://localhost:3000",
status: 302,
},
{
desc: "grafana relative url with subpath",
url: "/grafana/profile",
appURL: "http://localhost:3000",
appSubURL: "grafana",
status: 302,
},
{
desc: "relative url with missing subpath",
url: "/profile",
appURL: "http://localhost:3000",
appSubURL: "grafana",
status: 200,
err: login.ErrInvalidRedirectTo,
},
{
desc: "grafana absolute url",
url: "http://localhost:3000/profile",
appURL: "http://localhost:3000",
status: 200,
err: login.ErrAbsoluteRedirectTo,
},
{
desc: "non grafana absolute url",
url: "http://example.com",
appURL: "http://localhost:3000",
status: 200,
err: login.ErrAbsoluteRedirectTo,
},
{
desc: "invalid url",
url: ":foo",
appURL: "http://localhost:3000",
status: 200,
err: login.ErrInvalidRedirectTo,
},
}
for _, c := range redirectCases {
hs.Cfg.AppUrl = c.appURL
hs.Cfg.AppSubUrl = c.appSubURL
t.Run(c.desc, func(t *testing.T) {
cookie := http.Cookie{
Name: "redirect_to",
MaxAge: 60,
Value: c.url,
HttpOnly: true,
Path: hs.Cfg.AppSubUrl + "/",
Secure: hs.Cfg.CookieSecure,
SameSite: hs.Cfg.CookieSameSiteMode,
}
sc.m.Get(sc.url, sc.defaultHandler)
sc.fakeReqNoAssertionsWithCookie("GET", sc.url, cookie).exec()
assert.Equal(t, c.status, sc.resp.Code)
if c.status == 302 {
location, ok := sc.resp.Header()["Location"]
assert.True(t, ok)
assert.Equal(t, location[0], c.url)
setCookie, ok := sc.resp.Header()["Set-Cookie"]
assert.True(t, ok, "Set-Cookie exists")
assert.Greater(t, len(setCookie), 0)
var redirectToCookieFound bool
expCookieValue := fmt.Sprintf("redirect_to=%v; Path=%v; Max-Age=60; HttpOnly; Secure", c.url, hs.Cfg.AppSubUrl+"/")
for _, cookieValue := range setCookie {
if cookieValue == expCookieValue {
redirectToCookieFound = true
break
}
}
assert.True(t, redirectToCookieFound)
}
responseString, err := getBody(sc.resp)
assert.NoError(t, err)
if c.err != nil {
assert.True(t, strings.Contains(responseString, c.err.Error()))
}
})
}
}
func TestLoginPostRedirect(t *testing.T) {
mockSetIndexViewData()
defer resetSetIndexViewData()
mockViewIndex()
defer resetViewIndex()
sc := setupScenarioContext("/login")
hs := &HTTPServer{
log: &FakeLogger{},
Cfg: setting.NewCfg(),
License: models.OSSLicensingService{},
AuthTokenService: auth.NewFakeUserAuthTokenService(),
}
hs.Cfg.CookieSecure = true
sc.defaultHandler = Wrap(func(w http.ResponseWriter, c *models.ReqContext) Response {
cmd := dtos.LoginCommand{
User: "admin",
Password: "admin",
}
return hs.LoginPost(c, cmd)
})
bus.AddHandler("grafana-auth", func(query *models.LoginUserQuery) error {
query.User = &models.User{
Id: 42,
Email: "",
}
return nil
})
redirectCases := []redirectCase{
{
desc: "grafana relative url without subpath",
url: "/profile",
appURL: "https://localhost:3000",
},
{
desc: "grafana relative url with subpath",
url: "/grafana/profile",
appURL: "https://localhost:3000",
appSubURL: "grafana",
},
{
desc: "relative url with missing subpath",
url: "/profile",
appURL: "https://localhost:3000",
appSubURL: "grafana",
err: login.ErrInvalidRedirectTo,
},
{
desc: "grafana absolute url",
url: "http://localhost:3000/profile",
appURL: "http://localhost:3000",
err: login.ErrAbsoluteRedirectTo,
},
{
desc: "non grafana absolute url",
url: "http://example.com",
appURL: "https://localhost:3000",
err: login.ErrAbsoluteRedirectTo,
},
}
for _, c := range redirectCases {
hs.Cfg.AppUrl = c.appURL
hs.Cfg.AppSubUrl = c.appSubURL
t.Run(c.desc, func(t *testing.T) {
cookie := http.Cookie{
Name: "redirect_to",
MaxAge: 60,
Value: c.url,
HttpOnly: true,
Path: hs.Cfg.AppSubUrl + "/",
Secure: hs.Cfg.CookieSecure,
SameSite: hs.Cfg.CookieSameSiteMode,
}
sc.m.Post(sc.url, sc.defaultHandler)
sc.fakeReqNoAssertionsWithCookie("POST", sc.url, cookie).exec()
assert.Equal(t, sc.resp.Code, 200)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
assert.NoError(t, err)
redirectURL := respJSON.Get("redirectUrl").MustString()
if c.err != nil {
assert.Equal(t, "", redirectURL)
} else {
assert.Equal(t, c.url, redirectURL)
}
// assert redirect_to cookie is deleted
setCookie, ok := sc.resp.Header()["Set-Cookie"]
assert.True(t, ok, "Set-Cookie exists")
assert.Greater(t, len(setCookie), 0)
var redirectToCookieFound bool
expCookieValue := fmt.Sprintf("redirect_to=; Path=%v; Max-Age=0; HttpOnly; Secure", hs.Cfg.AppSubUrl+"/")
for _, cookieValue := range setCookie {
if cookieValue == expCookieValue {
redirectToCookieFound = true
break
}
}
assert.True(t, redirectToCookieFound)
})
}
}
func TestLoginOAuthRedirect(t *testing.T) {
mockSetIndexViewData()
defer resetSetIndexViewData()
sc := setupScenarioContext("/login")
hs := &HTTPServer{
Cfg: setting.NewCfg(),
License: models.OSSLicensingService{},
}
sc.defaultHandler = Wrap(func(c *models.ReqContext) {
hs.LoginView(c)
})
setting.OAuthService = &setting.OAuther{}
setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo)
setting.OAuthService.OAuthInfos["github"] = &setting.OAuthInfo{
ClientId: "fake",
ClientSecret: "fakefake",
Enabled: true,
AllowSignup: true,
Name: "github",
}
setting.OAuthAutoLogin = true
sc.m.Get(sc.url, sc.defaultHandler)
sc.fakeReqNoAssertions("GET", sc.url).exec()
assert.Equal(t, sc.resp.Code, 307)
location, ok := sc.resp.Header()["Location"]
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
}