mirror of
https://github.com/portainer/portainer.git
synced 2025-07-19 05:19:39 +02:00
feat(auth): save jwt in cookie [EE-5864] (#10527)
This commit is contained in:
parent
ecce501cf3
commit
436da01bce
51 changed files with 679 additions and 312 deletions
|
@ -10,7 +10,7 @@ import (
|
|||
"github.com/portainer/portainer/api/apikey"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/portainer/portainer/api/jwt"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -21,21 +21,24 @@ var testHandler200 = http.HandlerFunc(func(w http.ResponseWriter, r *http.Reques
|
|||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
func tokenLookupSucceed(dataStore dataservices.DataStore, jwtService dataservices.JWTService) tokenLookup {
|
||||
return func(r *http.Request) *portainer.TokenData {
|
||||
func tokenLookupSucceed(dataStore dataservices.DataStore, jwtService portainer.JWTService) tokenLookup {
|
||||
return func(r *http.Request) (*portainer.TokenData, error) {
|
||||
uid := portainer.UserID(1)
|
||||
dataStore.User().Create(&portainer.User{ID: uid})
|
||||
jwtService.GenerateToken(&portainer.TokenData{ID: uid})
|
||||
return &portainer.TokenData{ID: 1}
|
||||
return &portainer.TokenData{ID: 1}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func tokenLookupFail(r *http.Request) *portainer.TokenData {
|
||||
return nil
|
||||
func tokenLookupFail(r *http.Request) (*portainer.TokenData, error) {
|
||||
return nil, ErrInvalidKey
|
||||
}
|
||||
|
||||
func tokenLookupEmpty(r *http.Request) (*portainer.TokenData, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func Test_mwAuthenticateFirst(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
_, store := datastore.MustNewTestStore(t, true, true)
|
||||
|
||||
|
@ -79,17 +82,28 @@ func Test_mwAuthenticateFirst(t *testing.T) {
|
|||
wantStatusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "mwAuthenticateFirst succeeds if last middleware successfully handles request",
|
||||
name: "mwAuthenticateFirst fails if first middleware fails",
|
||||
verificationMiddlwares: []tokenLookup{
|
||||
tokenLookupFail,
|
||||
tokenLookupSucceed(store, jwtService),
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
wantStatusCode: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
name: "mwAuthenticateFirst fails if first middleware has no token, but second middleware fails",
|
||||
verificationMiddlwares: []tokenLookup{
|
||||
tokenLookupEmpty,
|
||||
tokenLookupFail,
|
||||
|
||||
tokenLookupSucceed(store, jwtService),
|
||||
},
|
||||
wantStatusCode: http.StatusUnauthorized,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
|
@ -101,9 +115,46 @@ func Test_mwAuthenticateFirst(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_extractBearerToken(t *testing.T) {
|
||||
func Test_extractKeyFromCookie(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
token string
|
||||
succeeds bool
|
||||
}{
|
||||
{
|
||||
name: "missing cookie",
|
||||
token: "",
|
||||
succeeds: false,
|
||||
},
|
||||
|
||||
{
|
||||
name: "valid cookie",
|
||||
token: "abc",
|
||||
succeeds: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tt {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
if test.token != "" {
|
||||
testhelpers.AddTestSecurityCookie(req, test.token)
|
||||
}
|
||||
|
||||
apiKey, err := extractKeyFromCookie(req)
|
||||
is.Equal(test.token, apiKey)
|
||||
if !test.succeeds {
|
||||
is.Error(err, "Should return error")
|
||||
is.ErrorIs(err, http.ErrNoCookie)
|
||||
} else {
|
||||
is.NoError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_extractBearerToken(t *testing.T) {
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
requestHeader string
|
||||
|
@ -142,16 +193,14 @@ func Test_extractBearerToken(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, test := range tt {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set(test.requestHeader, test.requestHeaderValue)
|
||||
apiKey, err := extractBearerToken(req)
|
||||
is.Equal(test.wantToken, apiKey)
|
||||
if !test.succeeds {
|
||||
is.Error(err, "Should return error")
|
||||
is.ErrorIs(err, httperrors.ErrUnauthorized)
|
||||
} else {
|
||||
is.NoError(err)
|
||||
}
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set(test.requestHeader, test.requestHeaderValue)
|
||||
apiKey, ok := extractBearerToken(req)
|
||||
is.Equal(test.wantToken, apiKey)
|
||||
is.Equal(test.succeeds, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -274,16 +323,17 @@ func Test_apiKeyLookup(t *testing.T) {
|
|||
|
||||
t.Run("missing x-api-key header fails api-key lookup", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
// req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", jwt))
|
||||
token := bouncer.apiKeyLookup(req)
|
||||
// testhelpers.AddTestSecurityCookie(req, jwt)
|
||||
token, _ := bouncer.apiKeyLookup(req)
|
||||
is.Nil(token)
|
||||
})
|
||||
|
||||
t.Run("invalid x-api-key header fails api-key lookup", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Add("x-api-key", "random-failing-api-key")
|
||||
token := bouncer.apiKeyLookup(req)
|
||||
token, err := bouncer.apiKeyLookup(req)
|
||||
is.Nil(token)
|
||||
is.Error(err)
|
||||
})
|
||||
|
||||
t.Run("valid x-api-key header succeeds api-key lookup", func(t *testing.T) {
|
||||
|
@ -293,7 +343,7 @@ func Test_apiKeyLookup(t *testing.T) {
|
|||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
|
||||
token := bouncer.apiKeyLookup(req)
|
||||
token, err := bouncer.apiKeyLookup(req)
|
||||
|
||||
expectedToken := &portainer.TokenData{ID: user.ID, Username: user.Username, Role: portainer.StandardUserRole}
|
||||
is.Equal(expectedToken, token)
|
||||
|
@ -307,7 +357,7 @@ func Test_apiKeyLookup(t *testing.T) {
|
|||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
|
||||
token := bouncer.apiKeyLookup(req)
|
||||
token, err := bouncer.apiKeyLookup(req)
|
||||
|
||||
expectedToken := &portainer.TokenData{ID: user.ID, Username: user.Username, Role: portainer.StandardUserRole}
|
||||
is.Equal(expectedToken, token)
|
||||
|
@ -321,7 +371,7 @@ func Test_apiKeyLookup(t *testing.T) {
|
|||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
|
||||
token := bouncer.apiKeyLookup(req)
|
||||
token, err := bouncer.apiKeyLookup(req)
|
||||
|
||||
expectedToken := &portainer.TokenData{ID: user.ID, Username: user.Username, Role: portainer.StandardUserRole}
|
||||
is.Equal(expectedToken, token)
|
||||
|
@ -332,3 +382,68 @@ func Test_apiKeyLookup(t *testing.T) {
|
|||
is.True(apiKeyUpdated.LastUsed > apiKey.LastUsed)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_ShouldSkipCSRFCheck(t *testing.T) {
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
cookieValue string
|
||||
apiKey string
|
||||
authHeader string
|
||||
expectedResult bool
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "Should return false when cookie is present",
|
||||
cookieValue: "test-cookie",
|
||||
},
|
||||
{
|
||||
name: "Should return true when cookie is not present",
|
||||
cookieValue: "",
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "Should return true when api key is present",
|
||||
cookieValue: "",
|
||||
apiKey: "test-api-key",
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "Should return true when auth header is present",
|
||||
cookieValue: "",
|
||||
authHeader: "test-auth-header",
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "Should return false and error when both api key and auth header are present",
|
||||
cookieValue: "",
|
||||
apiKey: "test-api-key",
|
||||
authHeader: "test-auth-header",
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tt {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
if test.cookieValue != "" {
|
||||
req.AddCookie(&http.Cookie{Name: portainer.AuthCookieKey, Value: test.cookieValue})
|
||||
}
|
||||
if test.apiKey != "" {
|
||||
req.Header.Set(apiKeyHeader, test.apiKey)
|
||||
}
|
||||
if test.authHeader != "" {
|
||||
req.Header.Set(jwtTokenHeader, test.authHeader)
|
||||
}
|
||||
|
||||
result, err := ShouldSkipCSRFCheck(req)
|
||||
is.Equal(test.expectedResult, result)
|
||||
if test.expectedError {
|
||||
is.Error(err)
|
||||
} else {
|
||||
is.NoError(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue