mirror of
https://github.com/teamhanko/hanko.git
synced 2025-10-27 22:27:23 +08:00
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>
248 lines
7.6 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|