mirror of
				https://github.com/teamhanko/hanko.git
				synced 2025-10-31 16:47:10 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			441 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			441 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package handler
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"encoding/json"
 | |
| 	"github.com/gofrs/uuid"
 | |
| 	"github.com/labstack/echo/v4"
 | |
| 	"github.com/lestrrat-go/jwx/v2/jwt"
 | |
| 	"github.com/stretchr/testify/assert"
 | |
| 	"github.com/stretchr/testify/require"
 | |
| 	"github.com/teamhanko/hanko/backend/config"
 | |
| 	"github.com/teamhanko/hanko/backend/dto"
 | |
| 	"github.com/teamhanko/hanko/backend/persistence/models"
 | |
| 	"github.com/teamhanko/hanko/backend/test"
 | |
| 	"golang.org/x/crypto/bcrypt"
 | |
| 	"net/http"
 | |
| 	"net/http/httptest"
 | |
| 	"strings"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| )
 | |
| 
 | |
| func TestPasswordHandler_Set_Create(t *testing.T) {
 | |
| 	userId, _ := uuid.FromString("ec4ef049-5b88-4321-a173-21b0eff06a04")
 | |
| 	users := []models.User{
 | |
| 		func() models.User {
 | |
| 			return models.User{
 | |
| 				ID:        userId,
 | |
| 				CreatedAt: time.Now(),
 | |
| 				UpdatedAt: time.Now(),
 | |
| 			}
 | |
| 		}(),
 | |
| 	}
 | |
| 
 | |
| 	e := echo.New()
 | |
| 	e.Validator = dto.NewCustomValidator()
 | |
| 	body := PasswordSetBody{UserID: userId.String(), Password: "verybadpassword"}
 | |
| 	bodyJson, err := json.Marshal(body)
 | |
| 	assert.NoError(t, err)
 | |
| 	req := httptest.NewRequest(http.MethodPost, "/password", bytes.NewReader(bodyJson))
 | |
| 	req.Header.Set("Content-Type", "application/json")
 | |
| 	rec := httptest.NewRecorder()
 | |
| 	c := e.NewContext(req, rec)
 | |
| 	token := jwt.New()
 | |
| 	err = token.Set(jwt.SubjectKey, userId.String())
 | |
| 	require.NoError(t, err)
 | |
| 	c.Set("session", token)
 | |
| 
 | |
| 	p := test.NewPersister(users, nil, nil, nil, nil, []models.PasswordCredential{}, nil, nil, nil, nil, nil)
 | |
| 	handler := NewPasswordHandler(p, sessionManager{}, &config.Config{}, test.NewAuditLogger())
 | |
| 
 | |
| 	if assert.NoError(t, handler.Set(c)) {
 | |
| 		assert.Equal(t, http.StatusCreated, rec.Code)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestPasswordHandler_Set_Create_PasswordTooShort(t *testing.T) {
 | |
| 	userId, _ := uuid.FromString("ec4ef049-5b88-4321-a173-21b0eff06a04")
 | |
| 	users := []models.User{
 | |
| 		func() models.User {
 | |
| 			return models.User{
 | |
| 				ID:        userId,
 | |
| 				CreatedAt: time.Now(),
 | |
| 				UpdatedAt: time.Now(),
 | |
| 			}
 | |
| 		}(),
 | |
| 	}
 | |
| 
 | |
| 	e := echo.New()
 | |
| 	e.Validator = dto.NewCustomValidator()
 | |
| 	body := PasswordSetBody{UserID: userId.String(), Password: "very"}
 | |
| 	bodyJson, err := json.Marshal(body)
 | |
| 	assert.NoError(t, err)
 | |
| 	req := httptest.NewRequest(http.MethodPost, "/password", bytes.NewReader(bodyJson))
 | |
| 	req.Header.Set("Content-Type", "application/json")
 | |
| 	rec := httptest.NewRecorder()
 | |
| 	c := e.NewContext(req, rec)
 | |
| 	token := jwt.New()
 | |
| 	err = token.Set(jwt.SubjectKey, userId.String())
 | |
| 	require.NoError(t, err)
 | |
| 	c.Set("session", token)
 | |
| 
 | |
| 	p := test.NewPersister(users, nil, nil, nil, nil, []models.PasswordCredential{}, nil, nil, nil, nil, nil)
 | |
| 	handler := NewPasswordHandler(p, sessionManager{}, &config.Config{Password: config.Password{MinPasswordLength: 8}}, test.NewAuditLogger())
 | |
| 
 | |
| 	err = handler.Set(c)
 | |
| 	if assert.Error(t, err) {
 | |
| 		httpError := dto.ToHttpError(err)
 | |
| 		assert.Equal(t, http.StatusBadRequest, httpError.Code)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestPasswordHandler_Set_Create_PasswordTooLong(t *testing.T) {
 | |
| 	userId, _ := uuid.FromString("ec4ef049-5b88-4321-a173-21b0eff06a04")
 | |
| 	users := []models.User{
 | |
| 		func() models.User {
 | |
| 			return models.User{
 | |
| 				ID:        userId,
 | |
| 				CreatedAt: time.Now(),
 | |
| 				UpdatedAt: time.Now(),
 | |
| 			}
 | |
| 		}(),
 | |
| 	}
 | |
| 
 | |
| 	e := echo.New()
 | |
| 	e.Validator = dto.NewCustomValidator()
 | |
| 	body := PasswordSetBody{UserID: userId.String(), Password: "thisIsAVeryLongPasswordThatIsUsedToTestIfAnErrorWillBeReturnedForTooLongPasswords"}
 | |
| 	bodyJson, err := json.Marshal(body)
 | |
| 	assert.NoError(t, err)
 | |
| 	req := httptest.NewRequest(http.MethodPost, "/password", bytes.NewReader(bodyJson))
 | |
| 	req.Header.Set("Content-Type", "application/json")
 | |
| 	rec := httptest.NewRecorder()
 | |
| 	c := e.NewContext(req, rec)
 | |
| 	token := jwt.New()
 | |
| 	err = token.Set(jwt.SubjectKey, userId.String())
 | |
| 	require.NoError(t, err)
 | |
| 	c.Set("session", token)
 | |
| 
 | |
| 	p := test.NewPersister(users, nil, nil, nil, nil, []models.PasswordCredential{}, nil, nil, nil, nil, nil)
 | |
| 	handler := NewPasswordHandler(p, sessionManager{}, &config.Config{Password: config.Password{MinPasswordLength: 8}}, test.NewAuditLogger())
 | |
| 
 | |
| 	err = handler.Set(c)
 | |
| 	if assert.Error(t, err) {
 | |
| 		httpError := dto.ToHttpError(err)
 | |
| 		assert.Equal(t, http.StatusBadRequest, httpError.Code)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestPasswordHandler_Set_Update(t *testing.T) {
 | |
| 	userId, _ := uuid.FromString("ec4ef049-5b88-4321-a173-21b0eff06a04")
 | |
| 	users := []models.User{
 | |
| 		func() models.User {
 | |
| 			return models.User{
 | |
| 				ID:        userId,
 | |
| 				CreatedAt: time.Now(),
 | |
| 				UpdatedAt: time.Now(),
 | |
| 			}
 | |
| 		}(),
 | |
| 	}
 | |
| 
 | |
| 	hashedPassword, err := bcrypt.GenerateFromPassword([]byte("verybadpassword"), 12)
 | |
| 	assert.NoError(t, err)
 | |
| 
 | |
| 	passwords := []models.PasswordCredential{
 | |
| 		func() models.PasswordCredential {
 | |
| 			pId, _ := uuid.NewV4()
 | |
| 			return models.PasswordCredential{
 | |
| 				ID:        pId,
 | |
| 				UserId:    userId,
 | |
| 				Password:  string(hashedPassword),
 | |
| 				CreatedAt: time.Now(),
 | |
| 				UpdatedAt: time.Now(),
 | |
| 			}
 | |
| 		}(),
 | |
| 	}
 | |
| 
 | |
| 	e := echo.New()
 | |
| 	e.Validator = dto.NewCustomValidator()
 | |
| 	body := PasswordSetBody{UserID: userId.String(), Password: "anotherbadnewpassword"}
 | |
| 	bodyJson, err := json.Marshal(body)
 | |
| 	assert.NoError(t, err)
 | |
| 	req := httptest.NewRequest(http.MethodPost, "/password", bytes.NewReader(bodyJson))
 | |
| 	req.Header.Set("Content-Type", "application/json")
 | |
| 	rec := httptest.NewRecorder()
 | |
| 	c := e.NewContext(req, rec)
 | |
| 	token := jwt.New()
 | |
| 	err = token.Set(jwt.SubjectKey, userId.String())
 | |
| 	require.NoError(t, err)
 | |
| 	c.Set("session", token)
 | |
| 
 | |
| 	p := test.NewPersister(users, nil, nil, nil, nil, passwords, nil, nil, nil, nil, nil)
 | |
| 	handler := NewPasswordHandler(p, sessionManager{}, &config.Config{}, test.NewAuditLogger())
 | |
| 
 | |
| 	if assert.NoError(t, handler.Set(c)) {
 | |
| 		assert.Equal(t, http.StatusOK, rec.Code)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestPasswordHandler_Set_UserNotFound(t *testing.T) {
 | |
| 	userId, _ := uuid.FromString("ec4ef049-5b88-4321-a173-21b0eff06a04")
 | |
| 
 | |
| 	e := echo.New()
 | |
| 	e.Validator = dto.NewCustomValidator()
 | |
| 	body := PasswordSetBody{UserID: userId.String(), Password: "anotherbadnewpassword"}
 | |
| 	bodyJson, err := json.Marshal(body)
 | |
| 	assert.NoError(t, err)
 | |
| 	req := httptest.NewRequest(http.MethodPost, "/password", bytes.NewReader(bodyJson))
 | |
| 	req.Header.Set("Content-Type", "application/json")
 | |
| 	rec := httptest.NewRecorder()
 | |
| 	c := e.NewContext(req, rec)
 | |
| 	token := jwt.New()
 | |
| 	err = token.Set(jwt.SubjectKey, userId.String())
 | |
| 	require.NoError(t, err)
 | |
| 	c.Set("session", token)
 | |
| 
 | |
| 	p := test.NewPersister([]models.User{}, nil, nil, nil, nil, []models.PasswordCredential{}, nil, nil, nil, nil, nil)
 | |
| 	handler := NewPasswordHandler(p, sessionManager{}, &config.Config{}, test.NewAuditLogger())
 | |
| 
 | |
| 	err = handler.Set(c)
 | |
| 	if assert.Error(t, err) {
 | |
| 		httpError := dto.ToHttpError(err)
 | |
| 		assert.Equal(t, http.StatusUnauthorized, httpError.Code)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestPasswordHandler_Set_TokenHasWrongSubject(t *testing.T) {
 | |
| 	userId, _ := uuid.FromString("ec4ef049-5b88-4321-a173-21b0eff06a04")
 | |
| 	users := []models.User{
 | |
| 		func() models.User {
 | |
| 			return models.User{
 | |
| 				ID:        userId,
 | |
| 				CreatedAt: time.Now(),
 | |
| 				UpdatedAt: time.Now(),
 | |
| 			}
 | |
| 		}(),
 | |
| 	}
 | |
| 
 | |
| 	hashedPassword, err := bcrypt.GenerateFromPassword([]byte("verybadpassword"), 12)
 | |
| 	assert.NoError(t, err)
 | |
| 
 | |
| 	passwords := []models.PasswordCredential{
 | |
| 		func() models.PasswordCredential {
 | |
| 			pId, _ := uuid.NewV4()
 | |
| 			return models.PasswordCredential{
 | |
| 				ID:        pId,
 | |
| 				UserId:    userId,
 | |
| 				Password:  string(hashedPassword),
 | |
| 				CreatedAt: time.Now(),
 | |
| 				UpdatedAt: time.Now(),
 | |
| 			}
 | |
| 		}(),
 | |
| 	}
 | |
| 	e := echo.New()
 | |
| 	e.Validator = dto.NewCustomValidator()
 | |
| 	body := PasswordSetBody{UserID: userId.String(), Password: "anotherbadnewpassword"}
 | |
| 	bodyJson, err := json.Marshal(body)
 | |
| 	assert.NoError(t, err)
 | |
| 	req := httptest.NewRequest(http.MethodPost, "/password", bytes.NewReader(bodyJson))
 | |
| 	req.Header.Set("Content-Type", "application/json")
 | |
| 	rec := httptest.NewRecorder()
 | |
| 	c := e.NewContext(req, rec)
 | |
| 	token := jwt.New()
 | |
| 	wrongUid, _ := uuid.NewV4()
 | |
| 	err = token.Set(jwt.SubjectKey, wrongUid.String())
 | |
| 	require.NoError(t, err)
 | |
| 	c.Set("session", token)
 | |
| 
 | |
| 	p := test.NewPersister(users, nil, nil, nil, nil, passwords, nil, nil, nil, nil, nil)
 | |
| 	handler := NewPasswordHandler(p, sessionManager{}, &config.Config{}, test.NewAuditLogger())
 | |
| 
 | |
| 	err = handler.Set(c)
 | |
| 	if assert.Error(t, err) {
 | |
| 		httpError := dto.ToHttpError(err)
 | |
| 		assert.Equal(t, http.StatusForbidden, httpError.Code)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestPasswordHandler_Set_BadRequestBody(t *testing.T) {
 | |
| 	e := echo.New()
 | |
| 	e.Validator = dto.NewCustomValidator()
 | |
| 	body := `{"user_id_wrong": "123", "password_wrong": "badpassword"}`
 | |
| 
 | |
| 	req := httptest.NewRequest(http.MethodPost, "/password", strings.NewReader(body))
 | |
| 	req.Header.Set("Content-Type", "application/json")
 | |
| 	rec := httptest.NewRecorder()
 | |
| 	c := e.NewContext(req, rec)
 | |
| 	token := jwt.New()
 | |
| 	uId, _ := uuid.NewV4()
 | |
| 	err := token.Set(jwt.SubjectKey, uId.String())
 | |
| 	require.NoError(t, err)
 | |
| 	c.Set("session", token)
 | |
| 
 | |
| 	p := test.NewPersister(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
 | |
| 	handler := NewPasswordHandler(p, sessionManager{}, &config.Config{}, test.NewAuditLogger())
 | |
| 
 | |
| 	err = handler.Set(c)
 | |
| 	if assert.Error(t, err) {
 | |
| 		httpError := dto.ToHttpError(err)
 | |
| 		assert.Equal(t, http.StatusBadRequest, httpError.Code)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestPasswordHandler_Login(t *testing.T) {
 | |
| 	userId, _ := uuid.FromString("ec4ef049-5b88-4321-a173-21b0eff06a04")
 | |
| 	users := []models.User{
 | |
| 		func() models.User {
 | |
| 			return models.User{
 | |
| 				ID:        userId,
 | |
| 				CreatedAt: time.Now(),
 | |
| 				UpdatedAt: time.Now(),
 | |
| 			}
 | |
| 		}(),
 | |
| 	}
 | |
| 
 | |
| 	hashedPassword, err := bcrypt.GenerateFromPassword([]byte("verybadpassword"), 12)
 | |
| 	assert.NoError(t, err)
 | |
| 
 | |
| 	passwords := []models.PasswordCredential{
 | |
| 		func() models.PasswordCredential {
 | |
| 			pId, _ := uuid.NewV4()
 | |
| 			return models.PasswordCredential{
 | |
| 				ID:        pId,
 | |
| 				UserId:    userId,
 | |
| 				Password:  string(hashedPassword),
 | |
| 				CreatedAt: time.Now(),
 | |
| 				UpdatedAt: time.Now(),
 | |
| 			}
 | |
| 		}(),
 | |
| 	}
 | |
| 	e := echo.New()
 | |
| 	e.Validator = dto.NewCustomValidator()
 | |
| 	body := `{"user_id": "ec4ef049-5b88-4321-a173-21b0eff06a04", "password": "verybadpassword"}`
 | |
| 
 | |
| 	req := httptest.NewRequest(http.MethodPost, "/password/login", strings.NewReader(body))
 | |
| 	req.Header.Set("Content-Type", "application/json")
 | |
| 	rec := httptest.NewRecorder()
 | |
| 	c := e.NewContext(req, rec)
 | |
| 
 | |
| 	p := test.NewPersister(users, nil, nil, nil, nil, passwords, nil, nil, nil, nil, nil)
 | |
| 	handler := NewPasswordHandler(p, sessionManager{}, &config.Config{}, test.NewAuditLogger())
 | |
| 
 | |
| 	if assert.NoError(t, handler.Login(c)) {
 | |
| 		assert.Equal(t, http.StatusOK, rec.Code)
 | |
| 		cookies := rec.Result().Cookies()
 | |
| 		if assert.NotEmpty(t, cookies) {
 | |
| 			for _, cookie := range cookies {
 | |
| 				if cookie.Name == "hanko" {
 | |
| 					assert.Equal(t, "ec4ef049-5b88-4321-a173-21b0eff06a04", cookie.Value)
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestPasswordHandler_Login_WrongPassword(t *testing.T) {
 | |
| 	userId, _ := uuid.FromString("ec4ef049-5b88-4321-a173-21b0eff06a04")
 | |
| 	users := []models.User{
 | |
| 		func() models.User {
 | |
| 			return models.User{
 | |
| 				ID:        userId,
 | |
| 				CreatedAt: time.Now(),
 | |
| 				UpdatedAt: time.Now(),
 | |
| 			}
 | |
| 		}(),
 | |
| 	}
 | |
| 
 | |
| 	hashedPassword, err := bcrypt.GenerateFromPassword([]byte("verybadpassword"), 12)
 | |
| 	assert.NoError(t, err)
 | |
| 
 | |
| 	passwords := []models.PasswordCredential{
 | |
| 		func() models.PasswordCredential {
 | |
| 			pId, _ := uuid.NewV4()
 | |
| 			return models.PasswordCredential{
 | |
| 				ID:        pId,
 | |
| 				UserId:    userId,
 | |
| 				Password:  string(hashedPassword),
 | |
| 				CreatedAt: time.Now(),
 | |
| 				UpdatedAt: time.Now(),
 | |
| 			}
 | |
| 		}(),
 | |
| 	}
 | |
| 	e := echo.New()
 | |
| 	e.Validator = dto.NewCustomValidator()
 | |
| 	body := `{"user_id": "ec4ef049-5b88-4321-a173-21b0eff06a04", "password": "wrongpassword"}`
 | |
| 
 | |
| 	req := httptest.NewRequest(http.MethodPost, "/password/login", strings.NewReader(body))
 | |
| 	req.Header.Set("Content-Type", "application/json")
 | |
| 	rec := httptest.NewRecorder()
 | |
| 	c := e.NewContext(req, rec)
 | |
| 
 | |
| 	p := test.NewPersister(users, nil, nil, nil, nil, passwords, nil, nil, nil, nil, nil)
 | |
| 	handler := NewPasswordHandler(p, sessionManager{}, &config.Config{}, test.NewAuditLogger())
 | |
| 
 | |
| 	err = handler.Login(c)
 | |
| 	if assert.Error(t, err) {
 | |
| 		httpError := dto.ToHttpError(err)
 | |
| 		assert.Equal(t, http.StatusUnauthorized, httpError.Code)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestPasswordHandler_Login_NonExistingUser(t *testing.T) {
 | |
| 	e := echo.New()
 | |
| 	e.Validator = dto.NewCustomValidator()
 | |
| 	body := `{"user_id": "ec4ef049-5b88-4321-a173-21b0eff06a04", "password": "wrongpassword"}`
 | |
| 
 | |
| 	req := httptest.NewRequest(http.MethodPost, "/password/login", strings.NewReader(body))
 | |
| 	req.Header.Set("Content-Type", "application/json")
 | |
| 	rec := httptest.NewRecorder()
 | |
| 	c := e.NewContext(req, rec)
 | |
| 
 | |
| 	p := test.NewPersister([]models.User{}, nil, nil, nil, nil, []models.PasswordCredential{}, nil, nil, nil, nil, nil)
 | |
| 	handler := NewPasswordHandler(p, sessionManager{}, &config.Config{}, test.NewAuditLogger())
 | |
| 
 | |
| 	err := handler.Login(c)
 | |
| 	if assert.Error(t, err) {
 | |
| 		httpError := dto.ToHttpError(err)
 | |
| 		assert.Equal(t, http.StatusUnauthorized, httpError.Code)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // 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 _, test := range tests {
 | |
| 		t.Run(test.name, func(t *testing.T) {
 | |
| 			hash, err := bcrypt.GenerateFromPassword([]byte(test.creationPassword), 12)
 | |
| 			if test.wantErr {
 | |
| 				assert.Error(t, err)
 | |
| 			} else {
 | |
| 				assert.NoError(t, err)
 | |
| 			}
 | |
| 
 | |
| 			err = bcrypt.CompareHashAndPassword(hash, []byte(test.loginPassword))
 | |
| 			if test.wantErr {
 | |
| 				assert.Error(t, err)
 | |
| 			} else {
 | |
| 				assert.NoError(t, err)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | 
