Files
hanko/backend/handler/passcode_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

340 lines
10 KiB
Go

package handler
import (
"bytes"
"encoding/json"
"fmt"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/suite"
"github.com/teamhanko/hanko/backend/config"
"github.com/teamhanko/hanko/backend/crypto/jwk"
"github.com/teamhanko/hanko/backend/dto"
"github.com/teamhanko/hanko/backend/persistence/models"
"github.com/teamhanko/hanko/backend/session"
"github.com/teamhanko/hanko/backend/test"
"golang.org/x/crypto/bcrypt"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestPasscodeSuite(t *testing.T) {
s := new(passcodeSuite)
s.WithEmailServer = true
suite.Run(t, s)
}
type passcodeSuite struct {
test.Suite
}
func (s *passcodeSuite) TestPasscodeHandler_Init() {
if testing.Short() {
s.T().Skip("skipping test in short mode")
}
err := s.LoadFixtures("../test/fixtures/passcode")
s.Require().NoError(err)
cfg := func() *config.Config {
cfg := &test.DefaultConfig
cfg.EmailDelivery.SMTP.Host = s.EmailServer.SmtpHost
cfg.EmailDelivery.SMTP.Port = s.EmailServer.SmtpPort
return cfg
}
e := NewPublicRouter(cfg(), s.Storage, nil, nil)
emailId := "51b7c175-ceb6-45ba-aae6-0092221c1b84"
unknownEmailId := "83618f24-2db8-4ea2-b370-ac8335f782d8"
tests := []struct {
name string
body dto.PasscodeInitRequest
expectedStatusCode int
expectedEmailAddress string
}{
{
name: "with userID and emailID",
body: dto.PasscodeInitRequest{
UserId: "b5dd5267-b462-48be-b70d-bcd6f1bbe7a5",
EmailId: &emailId,
},
expectedStatusCode: http.StatusOK,
expectedEmailAddress: "john.doe@example.com",
},
{
name: "only with userID",
body: dto.PasscodeInitRequest{
UserId: "b5dd5267-b462-48be-b70d-bcd6f1bbe7a5",
},
expectedStatusCode: http.StatusOK,
expectedEmailAddress: "john.doe@example.com",
},
{
name: "with unknown userID",
body: dto.PasscodeInitRequest{
UserId: "83618f24-2db8-4ea2-b370-ac8335f782d8",
},
expectedStatusCode: http.StatusBadRequest,
},
{
name: "with unknown emailID",
body: dto.PasscodeInitRequest{
UserId: "b5dd5267-b462-48be-b70d-bcd6f1bbe7a5",
EmailId: &unknownEmailId,
},
expectedStatusCode: http.StatusBadRequest,
},
}
for _, currentTest := range tests {
s.Run(currentTest.name, func() {
bodyJson, err := json.Marshal(currentTest.body)
s.Require().NoError(err)
req := httptest.NewRequest(http.MethodPost, "/passcode/login/initialize", bytes.NewReader(bodyJson))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if s.Equal(currentTest.expectedStatusCode, rec.Code) && currentTest.expectedStatusCode >= 200 && currentTest.expectedStatusCode <= 299 {
emails, err := s.EmailServer.GetEmails()
s.Require().NoError(err)
messages := emails.MailItems
s.Require().Greater(len(messages), 0)
emailAddress := messages[len(messages)-1].ToAddresses[0]
s.Equal(currentTest.expectedEmailAddress, emailAddress)
}
})
}
}
func (s *passcodeSuite) TestPasscodeHandler_Finish() {
if testing.Short() {
s.T().Skip("skipping test in short mode")
}
err := s.LoadFixtures("../test/fixtures/passcode")
s.Require().NoError(err)
now := time.Now().UTC()
hashedPasscode, err := bcrypt.GenerateFromPassword([]byte("123456"), 12)
userId := uuid.FromStringOrNil("b5dd5267-b462-48be-b70d-bcd6f1bbe7a5")
emailId := uuid.FromStringOrNil("51b7c175-ceb6-45ba-aae6-0092221c1b84")
passcode := models.Passcode{
ID: uuid.FromStringOrNil("a2383922-dea3-46c8-be17-85b267c0d135"),
UserId: &userId,
EmailID: &emailId,
Ttl: 300,
Code: string(hashedPasscode),
TryCount: 0,
CreatedAt: now,
UpdatedAt: now,
}
passcodeWithExpiredTimeout := models.Passcode{
ID: uuid.FromStringOrNil("a2383922-dea3-46c8-be17-85b267c0d135"),
UserId: &userId,
EmailID: &emailId,
Ttl: 300,
Code: string(hashedPasscode),
TryCount: 0,
CreatedAt: now.Add(-500 * time.Second),
UpdatedAt: now,
}
emailIdNotAssigned := uuid.FromStringOrNil("7c4473b8-ddcc-480b-b01f-df89e99f74c9")
passcodeForNonAssignedEmail := models.Passcode{
ID: uuid.FromStringOrNil("494129d5-76de-4fae-b07d-f2a521e1804d"),
UserId: &userId,
EmailID: &emailIdNotAssigned,
Ttl: 300,
Code: string(hashedPasscode),
TryCount: 0,
CreatedAt: now,
UpdatedAt: now,
}
cfg := func() *config.Config {
return &test.DefaultConfig
}
tests := []struct {
name string
passcodeId string
retryCount int
passcode models.Passcode
code string
expectedStatusCode int
cfg func() *config.Config
userId string
sendSessionTokenInCookie bool
sendSessionTokenInAuthHeader bool
}{
{
name: "finish successful",
passcodeId: "a2383922-dea3-46c8-be17-85b267c0d135",
passcode: passcode,
code: "123456",
expectedStatusCode: http.StatusOK,
cfg: cfg,
},
{
name: "finish successful with token in header",
passcodeId: "a2383922-dea3-46c8-be17-85b267c0d135",
passcode: passcode,
code: "123456",
expectedStatusCode: http.StatusOK,
cfg: func() *config.Config {
c := test.DefaultConfig
c.Session.EnableAuthTokenHeader = true
return &c
},
},
{
name: "with wrong code",
passcodeId: "a2383922-dea3-46c8-be17-85b267c0d135",
passcode: passcode,
code: "654321",
expectedStatusCode: http.StatusUnauthorized,
cfg: cfg,
},
{
name: "with wrong code 3 times",
passcodeId: "a2383922-dea3-46c8-be17-85b267c0d135",
retryCount: 2,
passcode: passcode,
code: "654321",
expectedStatusCode: http.StatusGone,
cfg: cfg,
},
{
name: "with wrong passcode ID",
passcodeId: "297cfc1b-98cc-4ae1-bc83-bcafc7f0e876",
passcode: passcode,
code: "123456",
expectedStatusCode: http.StatusUnauthorized,
cfg: cfg,
},
{
name: "after passcode expired",
passcodeId: "a2383922-dea3-46c8-be17-85b267c0d135",
passcode: passcodeWithExpiredTimeout,
code: "123456",
expectedStatusCode: http.StatusRequestTimeout,
cfg: cfg,
},
{
name: "create email with session in cookie",
passcodeId: "494129d5-76de-4fae-b07d-f2a521e1804d",
passcode: passcodeForNonAssignedEmail,
code: "123456",
expectedStatusCode: http.StatusOK,
cfg: cfg,
userId: "b5dd5267-b462-48be-b70d-bcd6f1bbe7a5",
sendSessionTokenInCookie: true,
},
{
name: "do not create email with wrong session in cookie",
passcodeId: "494129d5-76de-4fae-b07d-f2a521e1804d",
passcode: passcodeForNonAssignedEmail,
code: "123456",
expectedStatusCode: http.StatusForbidden,
cfg: cfg,
userId: "851842a9-db50-49b5-aa00-1c447c31d819",
sendSessionTokenInCookie: true,
},
{
name: "create email with session in Authorization header",
passcodeId: "494129d5-76de-4fae-b07d-f2a521e1804d",
passcode: passcodeForNonAssignedEmail,
code: "123456",
expectedStatusCode: http.StatusOK,
cfg: cfg,
userId: "b5dd5267-b462-48be-b70d-bcd6f1bbe7a5",
sendSessionTokenInAuthHeader: true,
},
{
name: "do not create email with wrong session in Authorization header",
passcodeId: "494129d5-76de-4fae-b07d-f2a521e1804d",
passcode: passcodeForNonAssignedEmail,
code: "123456",
expectedStatusCode: http.StatusForbidden,
cfg: cfg,
userId: "851842a9-db50-49b5-aa00-1c447c31d819",
sendSessionTokenInAuthHeader: true,
},
}
for _, currentTest := range tests {
s.Run(currentTest.name, func() {
s.SetupTest()
err := s.LoadFixtures("../test/fixtures/passcode")
s.Require().NoError(err)
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)
e := NewPublicRouter(currentTest.cfg(), s.Storage, nil, nil)
// Setup passcode
err = s.Storage.GetPasscodePersister().Create(currentTest.passcode)
s.Require().NoError(err)
body := dto.PasscodeFinishRequest{
Id: currentTest.passcodeId,
Code: currentTest.code,
}
bodyJson, err := json.Marshal(body)
s.Require().NoError(err)
responseCode := 0
var response *http.Response
var headers http.Header
for i := 0; i <= currentTest.retryCount; i++ {
req := httptest.NewRequest(http.MethodPost, "/passcode/login/finalize", bytes.NewReader(bodyJson))
req.Header.Set("Content-Type", "application/json")
if currentTest.sendSessionTokenInAuthHeader {
sessionToken, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(currentTest.userId), nil)
s.Require().NoError(err)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", sessionToken))
}
if currentTest.sendSessionTokenInCookie {
sessionToken, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(currentTest.userId), nil)
s.Require().NoError(err)
sessionCookie, err := sessionManager.GenerateCookie(sessionToken)
s.Require().NoError(err)
req.AddCookie(sessionCookie)
}
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
responseCode = rec.Code
response = rec.Result()
headers = rec.Header()
}
s.Equal(currentTest.expectedStatusCode, responseCode)
if currentTest.cfg().Session.EnableAuthTokenHeader {
s.Empty(response.Cookies())
token := headers.Get("X-Auth-Token")
s.NotEmpty(token)
s.Regexp(".*\\..*\\..*", token)
}
// remove passcode
_ = s.Storage.GetPasscodePersister().Delete(currentTest.passcode)
s.TearDownTest()
})
}
}