Files
hanko/backend/handler/user_test.go
Matthew H. Irby e7a5c2df27 Feat: Add logout method to sdk (#566)
* Add endpoint to invalidate HTTP-Only cookie from the backend

* Add methods to the UserClient SDK for logout

* Remove session token fetch and add unit test for logout

* Update public router to use JWT middleware

* Add logout button to frontend. Route back to login page once logout is successful.

* Add a logout failur event

* Update logout logic in SDK

* Remove unneeded endpoint from main.go

* Update logoutlink reference

* Fix request path; undo change in package order

* Update common.css to incldue hanko-logout

* feat(fronend-sdk): remove cookie during cross-domain operations

* fix(frontend-sdk): No unauthorized error during logout, when the user is already logged out

* feat(backend): Create an audit log entry when the user logs off

* chore(frontend-sdk): re-generate jsdoc

* fix: Adjust logout response codes and the corresponding frontend sdk error handling

* chore(frontend-sdk): re-generate jsdoc

* feat: add logout endpoint specification to the docs

* Fix broken unit test

* Remove logout button from elements

* Add event listener on frontend to call the logout method from SDK

* Rollback changes to SecuredContent on e2e tests

* Update logout test on user

* Update quickstart/public/assets/css/common.css

Co-authored-by: bjoern-m <56024829+bjoern-m@users.noreply.github.com>

---------

Co-authored-by: Björn Müller <bjoern.mueller@hanko.io>
Co-authored-by: bjoern-m <56024829+bjoern-m@users.noreply.github.com>
2023-03-03 10:48:33 +01:00

480 lines
13 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/lib/pq"
"github.com/stretchr/testify/suite"
"github.com/teamhanko/hanko/backend/config"
"github.com/teamhanko/hanko/backend/dto"
"github.com/teamhanko/hanko/backend/persistence"
"github.com/teamhanko/hanko/backend/persistence/models"
"github.com/teamhanko/hanko/backend/test"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestUserSuite(t *testing.T) {
suite.Run(t, new(userSuite))
}
type userSuite struct {
suite.Suite
storage persistence.Storage
db *test.TestDB
}
func (s *userSuite) SetupSuite() {
if testing.Short() {
return
}
dialect := "postgres"
db, err := test.StartDB("user_test", dialect)
s.NoError(err)
storage, err := persistence.New(config.Database{
Url: db.DatabaseUrl,
})
s.NoError(err)
s.storage = storage
s.db = db
}
func (s *userSuite) SetupTest() {
if s.db != nil {
err := s.storage.MigrateUp()
s.NoError(err)
}
}
func (s *userSuite) TearDownTest() {
if s.db != nil {
err := s.storage.MigrateDown(-1)
s.NoError(err)
}
}
func (s *userSuite) TearDownSuite() {
if s.db != nil {
s.NoError(test.PurgeDB(s.db))
}
}
func (s *userSuite) TestUserHandler_Create() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
e := echo.New()
e.Validator = dto.NewCustomValidator()
body := UserCreateBody{Email: "jane.doe@example.com"}
bodyJson, err := json.Marshal(body)
s.NoError(err)
req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewReader(bodyJson))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
handler := NewUserHandler(&defaultConfig, s.storage, sessionManager{}, test.NewAuditLogger())
if s.NoError(handler.Create(c)) {
user := models.User{}
err := json.Unmarshal(rec.Body.Bytes(), &user)
s.NoError(err)
s.False(user.ID.IsNil())
count, err := s.storage.GetUserPersister().Count(uuid.Nil, "")
s.NoError(err)
s.Equal(1, count)
email, err := s.storage.GetEmailPersister().FindByAddress(body.Email)
s.NoError(err)
s.NotNil(email)
}
}
func (s *userSuite) TestUserHandler_Create_CaseInsensitive() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
e := echo.New()
e.Validator = dto.NewCustomValidator()
body := UserCreateBody{Email: "JANE.DOE@EXAMPLE.COM"}
bodyJson, err := json.Marshal(body)
s.NoError(err)
req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewReader(bodyJson))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
handler := NewUserHandler(&defaultConfig, s.storage, sessionManager{}, test.NewAuditLogger())
if s.NoError(handler.Create(c)) {
user := models.User{}
err := json.Unmarshal(rec.Body.Bytes(), &user)
s.NoError(err)
s.False(user.ID.IsNil())
count, err := s.storage.GetUserPersister().Count(uuid.Nil, "")
s.NoError(err)
s.Equal(1, count)
email, err := s.storage.GetEmailPersister().FindByAddress(strings.ToLower(body.Email))
s.NoError(err)
s.NotNil(email)
}
}
func (s *userSuite) TestUserHandler_Create_UserExists() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
err := test.LoadFixtures(s.db.DbCon, s.db.Dialect, "../test/fixtures/user")
s.Require().NoError(err)
e := echo.New()
e.Validator = dto.NewCustomValidator()
body := UserCreateBody{Email: "john.doe@example.com"}
bodyJson, err := json.Marshal(body)
s.NoError(err)
req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewReader(bodyJson))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
handler := NewUserHandler(&defaultConfig, s.storage, sessionManager{}, test.NewAuditLogger())
err = handler.Create(c)
if s.Error(err) {
httpError := dto.ToHttpError(err)
s.Equal(http.StatusConflict, httpError.Code)
}
}
func (s *userSuite) TestUserHandler_Create_UserExists_CaseInsensitive() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
err := test.LoadFixtures(s.db.DbCon, s.db.Dialect, "../test/fixtures/user")
s.Require().NoError(err)
e := echo.New()
e.Validator = dto.NewCustomValidator()
body := UserCreateBody{Email: "JOHN.DOE@EXAMPLE.COM"}
bodyJson, err := json.Marshal(body)
s.NoError(err)
req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewReader(bodyJson))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
handler := NewUserHandler(&defaultConfig, s.storage, sessionManager{}, test.NewAuditLogger())
err = handler.Create(c)
if s.Error(err) {
httpError := dto.ToHttpError(err)
s.Equal(http.StatusConflict, httpError.Code)
}
}
func (s *userSuite) TestUserHandler_Create_InvalidEmail() {
e := echo.New()
e.Validator = dto.NewCustomValidator()
req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(`{"email": 123"}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
handler := NewUserHandler(&defaultConfig, nil, nil, nil)
err := handler.Create(c)
if s.Error(err) {
httpError := dto.ToHttpError(err)
s.Equal(http.StatusBadRequest, httpError.Code)
}
}
func (s *userSuite) TestUserHandler_Create_EmailMissing() {
e := echo.New()
e.Validator = dto.NewCustomValidator()
req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(`{"bogus": 123}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
handler := NewUserHandler(&defaultConfig, nil, nil, nil)
err := handler.Create(c)
if s.Error(err) {
httpError := dto.ToHttpError(err)
s.Equal(http.StatusBadRequest, httpError.Code)
}
}
func (s *userSuite) TestUserHandler_Get() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
err := test.LoadFixtures(s.db.DbCon, s.db.Dialect, "../test/fixtures/user")
s.Require().NoError(err)
userId := "b5dd5267-b462-48be-b70d-bcd6f1bbe7a5"
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetPath("/users/:id")
c.SetParamNames("id")
c.SetParamValues(userId)
token := jwt.New()
err = token.Set(jwt.SubjectKey, userId)
s.Require().NoError(err)
c.Set("session", token)
handler := NewUserHandler(&defaultConfig, s.storage, sessionManager{}, test.NewAuditLogger())
if s.NoError(handler.Get(c)) {
s.Equal(rec.Code, http.StatusOK)
user := models.User{}
err := json.Unmarshal(rec.Body.Bytes(), &user)
s.NoError(err)
s.Equal(userId, user.ID.String())
s.Equal(len(user.WebauthnCredentials), 0)
}
}
func (s *userSuite) TestUserHandler_GetUserWithWebAuthnCredential() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
err := test.LoadFixtures(s.db.DbCon, s.db.Dialect, "../test/fixtures/user_with_webauthn_credential")
s.Require().NoError(err)
userId := "b5dd5267-b462-48be-b70d-bcd6f1bbe7a5"
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetPath("/users/:id")
c.SetParamNames("id")
c.SetParamValues(userId)
token := jwt.New()
err = token.Set(jwt.SubjectKey, userId)
s.Require().NoError(err)
c.Set("session", token)
handler := NewUserHandler(&defaultConfig, s.storage, sessionManager{}, test.NewAuditLogger())
if s.NoError(handler.Get(c)) {
s.Equal(rec.Code, http.StatusOK)
user := models.User{}
err := json.Unmarshal(rec.Body.Bytes(), &user)
s.Require().NoError(err)
s.Equal(userId, user.ID.String())
s.Equal(len(user.WebauthnCredentials), 1)
}
}
func (s *userSuite) TestUserHandler_Get_InvalidUserId() {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/users/invalidUserId", nil)
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
token := jwt.New()
err := token.Set(jwt.SubjectKey, "completelyDifferentUserId")
s.Require().NoError(err)
c.Set("session", token)
handler := NewUserHandler(&defaultConfig, nil, sessionManager{}, test.NewAuditLogger())
err = handler.Get(c)
if s.Error(err) {
httpError := dto.ToHttpError(err)
s.Equal(http.StatusForbidden, httpError.Code)
}
}
func (s *userSuite) TestUserHandler_GetUserIdByEmail_InvalidEmail() {
e := echo.New()
e.Validator = dto.NewCustomValidator()
req := httptest.NewRequest(http.MethodPost, "/user", strings.NewReader(`{"email": "123"}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
handler := NewUserHandler(&defaultConfig, nil, nil, nil)
err := handler.GetUserIdByEmail(c)
if s.Error(err) {
httpError := dto.ToHttpError(err)
s.Equal(http.StatusBadRequest, httpError.Code)
}
}
func (s *userSuite) TestUserHandler_GetUserIdByEmail_InvalidJson() {
e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/user", strings.NewReader(`"email": "123}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
handler := NewUserHandler(&defaultConfig, nil, nil, nil)
s.Error(handler.GetUserIdByEmail(c))
}
func (s *userSuite) TestUserHandler_GetUserIdByEmail_UserNotFound() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
e := echo.New()
e.Validator = dto.NewCustomValidator()
req := httptest.NewRequest(http.MethodPost, "/user", strings.NewReader(`{"email": "unknownAddress@example.com"}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
handler := NewUserHandler(&defaultConfig, s.storage, sessionManager{}, test.NewAuditLogger())
err := handler.GetUserIdByEmail(c)
if s.Error(err) {
httpError := dto.ToHttpError(err)
s.Equal(http.StatusNotFound, httpError.Code)
}
}
func (s *userSuite) TestUserHandler_GetUserIdByEmail() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
err := test.LoadFixtures(s.db.DbCon, s.db.Dialect, "../test/fixtures/user_with_webauthn_credential")
s.Require().NoError(err)
userId := "b5dd5267-b462-48be-b70d-bcd6f1bbe7a5"
e := echo.New()
e.Validator = dto.NewCustomValidator()
req := httptest.NewRequest(http.MethodPost, "/user", strings.NewReader(`{"email": "john.doe@example.com"}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
handler := NewUserHandler(&defaultConfig, s.storage, sessionManager{}, test.NewAuditLogger())
if s.NoError(handler.GetUserIdByEmail(c)) {
s.Equal(http.StatusOK, rec.Code)
response := struct {
UserId string `json:"id"`
Verified bool `json:"verified"`
}{}
err := json.Unmarshal(rec.Body.Bytes(), &response)
s.NoError(err)
s.Equal(userId, response.UserId)
s.Equal(true, response.Verified)
}
}
func (s *userSuite) TestUserHandler_GetUserIdByEmail_CaseInsensitive() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
err := test.LoadFixtures(s.db.DbCon, s.db.Dialect, "../test/fixtures/user_with_webauthn_credential")
s.Require().NoError(err)
userId := "b5dd5267-b462-48be-b70d-bcd6f1bbe7a5"
e := echo.New()
e.Validator = dto.NewCustomValidator()
req := httptest.NewRequest(http.MethodPost, "/user", strings.NewReader(`{"email": "JOHN.DOE@EXAMPLE.COM"}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
handler := NewUserHandler(&defaultConfig, s.storage, sessionManager{}, test.NewAuditLogger())
if s.NoError(handler.GetUserIdByEmail(c)) {
s.Equal(http.StatusOK, rec.Code)
response := struct {
UserId string `json:"id"`
Verified bool `json:"verified"`
}{}
err := json.Unmarshal(rec.Body.Bytes(), &response)
s.NoError(err)
s.Equal(userId, response.UserId)
s.Equal(true, response.Verified)
}
}
func (s *userSuite) TestUserHandler_Me() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
err := test.LoadFixtures(s.db.DbCon, s.db.Dialect, "../test/fixtures/user_with_webauthn_credential")
s.Require().NoError(err)
userId := "b5dd5267-b462-48be-b70d-bcd6f1bbe7a5"
e := echo.New()
e.Validator = dto.NewCustomValidator()
req := httptest.NewRequest(http.MethodGet, "/me", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
token := jwt.New()
err = token.Set(jwt.SubjectKey, userId)
s.Require().NoError(err)
c.Set("session", token)
handler := NewUserHandler(&defaultConfig, s.storage, sessionManager{}, test.NewAuditLogger())
if s.NoError(handler.Me(c)) {
s.Equal(http.StatusOK, rec.Code)
response := struct {
UserId string `json:"id"`
}{}
err = json.Unmarshal(rec.Body.Bytes(), &response)
s.NoError(err)
s.Equal(userId, response.UserId)
}
}
func (s *userSuite) TestUserHandler_Logout() {
userId, _ := uuid.NewV4()
e := echo.New()
e.Validator = dto.NewCustomValidator()
req := httptest.NewRequest(http.MethodPost, "/logout", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
token := jwt.New()
err := token.Set(jwt.SubjectKey, userId.String())
s.NoError(err)
c.Set("session", token)
handler := NewUserHandler(&defaultConfig, s.storage, sessionManager{}, test.NewAuditLogger())
if s.NoError(handler.Logout(c)) {
s.Equal(http.StatusNoContent, rec.Code)
cookie := rec.Header().Get("Set-Cookie")
s.NotEmpty(cookie)
split := strings.Split(cookie, ";")
s.Equal("Max-Age=0", strings.TrimSpace(split[1]))
}
}