Files
hanko/backend/handler/password_test.go
bjoern-m 601ffaae92 Introduce Flowpilot - integration (#1532)
This pull request introduces the new Flowpilot system along with several new features and various improvements. The key enhancements include configurable authorization, registration, and profile flows, as well as the ability to enable and disable user identifiers (e.g., email addresses and usernames) and login methods.

---------

Co-authored-by: Frederic Jahn <frederic.jahn@hanko.io>
Co-authored-by: Lennart Fleischmann <lennart.fleischmann@hanko.io>
Co-authored-by: lfleischmann <67686424+lfleischmann@users.noreply.github.com>
Co-authored-by: merlindru <hello@merlindru.com>
2024-08-06 16:07:29 +02:00

248 lines
7.6 KiB
Go

package handler
import (
"fmt"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/teamhanko/hanko/backend/config"
"github.com/teamhanko/hanko/backend/crypto/jwk"
"github.com/teamhanko/hanko/backend/session"
"github.com/teamhanko/hanko/backend/test"
"golang.org/x/crypto/bcrypt"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestPasswordSuite(t *testing.T) {
t.Parallel()
suite.Run(t, new(passwordSuite))
}
type passwordSuite struct {
test.Suite
}
func (s *passwordSuite) TestPasswordHandler_Set_Create() {
if testing.Short() {
s.T().Skip("skipping in short mode")
}
userWithNoPassword := uuid.FromStringOrNil("b5dd5267-b462-48be-b70d-bcd6f1bbe7a5")
userWithPassword := uuid.FromStringOrNil("38bf5a00-d7ea-40a5-a5de-48722c148925")
unknownUser := uuid.FromStringOrNil("6a565180-2366-45b1-8785-39f7902c7f2e")
cfg := &test.DefaultConfig
cfg.Password.Enabled = true
cfg.Password.MinLength = 8
tests := []struct {
name string
body string
userId uuid.UUID
expectedCode int
}{
{
name: "should create a password successful",
body: fmt.Sprintf(`{"user_id": "%s", "password": "verybadpassword"}`, userWithNoPassword),
userId: userWithNoPassword,
expectedCode: http.StatusCreated,
},
{
name: "should update a password successful",
body: fmt.Sprintf(`{"user_id": "%s", "password": "verybadpassword"}`, userWithPassword),
userId: userWithPassword,
expectedCode: http.StatusOK,
},
{
name: "should not create a password that is too short",
body: fmt.Sprintf(`{"user_id": "%s", "password": "very"}`, userWithNoPassword),
userId: userWithNoPassword,
expectedCode: http.StatusBadRequest,
},
{
name: "should not create a password that is too long",
body: fmt.Sprintf(`{"user_id": "%s", "password": "thisIsAVeryLongPasswordThatIsUsedToTestIfAnErrorWillBeReturnedForTooLongPasswords"}`, userWithNoPassword),
userId: userWithNoPassword,
expectedCode: http.StatusBadRequest,
},
{
name: "should not create a password for an unknown user",
body: fmt.Sprintf(`{"user_id": "%s", "password": "verybadpassword"}`, unknownUser),
userId: unknownUser,
expectedCode: http.StatusUnauthorized,
},
{
name: "should not create a password for a different user",
body: fmt.Sprintf(`{"user_id": "%s", "password": "verybadpassword"}`, userWithNoPassword),
userId: userWithPassword,
expectedCode: http.StatusForbidden,
},
}
for _, currentTest := range tests {
s.Run(currentTest.name, func() {
s.SetupTest()
defer s.TearDownTest()
err := s.LoadFixtures("../test/fixtures/password")
s.Require().NoError(err)
sessionManager := s.GetDefaultSessionManager()
token, err := sessionManager.GenerateJWT(currentTest.userId, nil)
s.Require().NoError(err)
cookie, err := sessionManager.GenerateCookie(token)
s.Require().NoError(err)
req := httptest.NewRequest(http.MethodPut, "/password", strings.NewReader(currentTest.body))
req.Header.Set("Content-Type", "application/json")
req.AddCookie(cookie)
rec := httptest.NewRecorder()
e := NewPublicRouter(cfg, s.Storage, nil, nil)
e.ServeHTTP(rec, req)
s.Equal(currentTest.expectedCode, rec.Code)
})
}
}
func (s *passwordSuite) TestPasswordHandler_Login() {
if testing.Short() {
s.T().Skip("skipping in short mode")
}
err := s.LoadFixtures("../test/fixtures/password")
s.Require().NoError(err)
userWithPassword := uuid.FromStringOrNil("38bf5a00-d7ea-40a5-a5de-48722c148925")
unknownUser := uuid.FromStringOrNil("6a565180-2366-45b1-8785-39f7902c7f2e")
cfg := func() *config.Config {
cfg := test.DefaultConfig
cfg.Password.Enabled = true
return &cfg
}
tests := []struct {
name string
body string
expectedCode int
cfg func() *config.Config
shouldContainCookie bool
}{
{
name: "should login successful",
body: fmt.Sprintf(`{"user_id": "%s", "password": "SuperSecure"}`, userWithPassword),
cfg: cfg,
expectedCode: http.StatusOK,
shouldContainCookie: true,
},
{
name: "should login successful with token in header",
body: fmt.Sprintf(`{"user_id": "%s", "password": "SuperSecure"}`, userWithPassword),
cfg: func() *config.Config {
cfg := test.DefaultConfig
cfg.Password.Enabled = true
cfg.Session.EnableAuthTokenHeader = true
return &cfg
},
expectedCode: http.StatusOK,
},
{
name: "should not login with wrong password",
body: fmt.Sprintf(`{"user_id": "%s", "password": "verybadpassword"}`, userWithPassword),
cfg: cfg,
expectedCode: http.StatusUnauthorized,
},
{
name: "should not login with non existing user",
body: fmt.Sprintf(`{"user_id": "%s", "password": "verybadpassword"}`, unknownUser),
cfg: cfg,
expectedCode: http.StatusUnauthorized,
},
}
for _, currentTest := range tests {
s.Run(currentTest.name, func() {
req := httptest.NewRequest(http.MethodPost, "/password/login", strings.NewReader(currentTest.body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
e := NewPublicRouter(currentTest.cfg(), s.Storage, nil, nil)
e.ServeHTTP(rec, req)
if s.Equal(currentTest.expectedCode, rec.Code) {
if currentTest.shouldContainCookie {
s.Empty(rec.Header().Get("X-Auth-Token"))
cookies := rec.Result().Cookies()
if s.NotEmpty(cookies) {
for _, cookie := range cookies {
if cookie.Name == "hanko" {
s.Regexp(".*\\..*\\..*", cookie.Value) // check if cookie contains a jwt
}
}
}
} else if currentTest.cfg().Session.EnableAuthTokenHeader {
s.Empty(rec.Result().Cookies())
token := rec.Header().Get("X-Auth-Token")
s.NotEmpty(token)
s.Regexp(".*\\..*\\..*", token)
}
}
})
}
}
func (s *passwordSuite) GetDefaultSessionManager() session.Manager {
jwkManager, err := jwk.NewDefaultManager(test.DefaultConfig.Secrets.Keys, s.Storage.GetJwkPersister())
s.Require().NoError(err)
sessionManager, err := session.NewManager(jwkManager, test.DefaultConfig)
s.Require().NoError(err)
return sessionManager
}
// TestMaxPasswordLength bcrypt since version 0.5.0 only accepts passwords at least 72 bytes long. This test documents this behaviour.
func TestMaxPasswordLength(t *testing.T) {
tests := []struct {
name string
creationPassword string
loginPassword string
wantErr bool
}{
{
name: "login password 72 bytes long",
creationPassword: "012345678901234567890123456789012345678901234567890123456789012345678901",
loginPassword: "012345678901234567890123456789012345678901234567890123456789012345678901",
wantErr: false,
},
{
name: "login password over 72 bytes long",
creationPassword: "0123456789012345678901234567890123456789012345678901234567890123456789012",
loginPassword: "0123456789012345678901234567890123456789012345678901234567890123456789012",
wantErr: true,
},
}
for _, passwordTest := range tests {
t.Run(passwordTest.name, func(t *testing.T) {
hash, err := bcrypt.GenerateFromPassword([]byte(passwordTest.creationPassword), 12)
if passwordTest.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
err = bcrypt.CompareHashAndPassword(hash, []byte(passwordTest.loginPassword))
if passwordTest.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}