Files
hanko/backend/handler/token_test.go
2025-09-25 19:15:20 +02:00

224 lines
6.6 KiB
Go

package handler
import (
"bytes"
"encoding/json"
"github.com/gofrs/uuid"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/suite"
"github.com/teamhanko/hanko/backend/v2/config"
"github.com/teamhanko/hanko/backend/v2/persistence/models"
"github.com/teamhanko/hanko/backend/v2/test"
"net/http"
"net/http/httptest"
"testing"
)
func TestTokenSuite(t *testing.T) {
t.Parallel()
suite.Run(t, new(tokenSuite))
}
type tokenSuite struct {
test.Suite
}
func (s *tokenSuite) TestToken_Validate_TokenInCookie() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
err := s.LoadFixtures("../test/fixtures/token")
// must create and insert a valid token manually instead of using fixtures, because token
// validation is time sensitive (expiration is checked relative to current time)
uId := uuid.FromStringOrNil("b5dd5267-b462-48be-b70d-bcd6f1bbe7a5")
token, err := models.NewToken(uId)
s.NoError(err)
err = s.Storage.GetTokenPersister().Create(*token)
s.NoError(err)
body := TokenValidationBody{Value: token.Value}
bodyJson, err := json.Marshal(body)
s.NoError(err)
req := httptest.NewRequest(http.MethodPost, "/token", bytes.NewReader(bodyJson))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
cfg := s.setupConfig()
cfg.Session.EnableAuthTokenHeader = false
e := NewPublicRouter(cfg, s.Storage, nil, nil)
e.ServeHTTP(rec, req)
s.Equal(rec.Code, http.StatusOK)
t, err := s.Storage.GetTokenPersister().GetByValue(token.Value)
s.NoError(err)
s.Nil(t)
s.Empty(rec.Header().Get("X-Auth-Token"))
cookies := rec.Result().Cookies()
rec.Result().Cookies()
s.NotEmpty(cookies)
for _, cookie := range cookies {
if cookie.Name == "hanko" {
s.Regexp(".*\\..*\\..*", cookie.Value)
}
}
logs, err := s.Storage.GetAuditLogPersister().List(0, 0, nil, nil, []string{"token_exchange_succeeded"}, "b5dd5267-b462-48be-b70d-bcd6f1bbe7a5", "", "", "")
s.Len(logs, 1)
}
func (s *tokenSuite) TestToken_Validate_TokenInHeader() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
err := s.LoadFixtures("../test/fixtures/token")
// must create and insert a valid token manually instead of using fixtures, because token
// validation is time sensitive (expiration is checked relative to current time)
uId := uuid.FromStringOrNil("b5dd5267-b462-48be-b70d-bcd6f1bbe7a5")
token, err := models.NewToken(uId)
s.NoError(err)
err = s.Storage.GetTokenPersister().Create(*token)
s.NoError(err)
body := TokenValidationBody{Value: token.Value}
bodyJson, err := json.Marshal(body)
s.NoError(err)
req := httptest.NewRequest(http.MethodPost, "/token", bytes.NewReader(bodyJson))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
cfg := s.setupConfig()
e := NewPublicRouter(cfg, s.Storage, nil, nil)
e.ServeHTTP(rec, req)
s.Equal(rec.Code, http.StatusOK)
t, err := s.Storage.GetTokenPersister().GetByValue(token.Value)
s.NoError(err)
s.Nil(t)
s.Empty(rec.Result().Cookies())
responseToken := rec.Header().Get("X-Auth-Token")
s.NotEmpty(responseToken)
s.Regexp(".*\\..*\\..*", responseToken)
logs, err := s.Storage.GetAuditLogPersister().List(0, 0, nil, nil, []string{"token_exchange_succeeded"}, "b5dd5267-b462-48be-b70d-bcd6f1bbe7a5", "", "", "")
s.Len(logs, 1)
}
func (s *tokenSuite) TestToken_Validate_ExpiredToken() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
err := s.LoadFixtures("../test/fixtures/token")
expiredTokenValue := "Trkauhl3q7XVxw5JcDH80lTe1KxzydIw0OcizH7umWk="
body := TokenValidationBody{Value: expiredTokenValue}
bodyJson, err := json.Marshal(body)
s.NoError(err)
req := httptest.NewRequest(http.MethodPost, "/token", bytes.NewReader(bodyJson))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
e := NewPublicRouter(s.setupConfig(), s.Storage, nil, nil)
e.ServeHTTP(rec, req)
s.Equal(rec.Code, http.StatusUnprocessableEntity)
var errorResponse echo.HTTPError
marshalErr := json.Unmarshal(rec.Body.Bytes(), &errorResponse)
s.NoError(marshalErr)
s.Contains(errorResponse.Message, "expired")
logs, lerr := s.Storage.GetAuditLogPersister().List(0, 0, nil, nil, []string{"token_exchange_failed"}, "", "", "", "")
s.NoError(lerr)
s.Len(logs, 1)
}
func (s *tokenSuite) TestToken_Validate_MissingTokenFromRequest() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
req := httptest.NewRequest(http.MethodPost, "/token", nil)
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
e := NewPublicRouter(s.setupConfig(), s.Storage, nil, nil)
e.ServeHTTP(rec, req)
s.Equal(rec.Code, http.StatusBadRequest)
var errorResponse echo.HTTPError
marshalErr := json.Unmarshal(rec.Body.Bytes(), &errorResponse)
s.NoError(marshalErr)
s.Contains("value is a required field", errorResponse.Message)
logs, lerr := s.Storage.GetAuditLogPersister().List(0, 0, nil, nil, []string{"token_exchange_failed"}, "", "", "", "")
s.NoError(lerr)
s.Len(logs, 1)
}
func (s *tokenSuite) TestToken_Validate_InvalidJson() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
req := httptest.NewRequest(http.MethodPost, "/token", bytes.NewReader([]byte("invalid")))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
e := NewPublicRouter(s.setupConfig(), s.Storage, nil, nil)
e.ServeHTTP(rec, req)
s.Equal(rec.Code, http.StatusBadRequest)
logs, lerr := s.Storage.GetAuditLogPersister().List(0, 0, nil, nil, []string{"token_exchange_failed"}, "", "", "", "")
s.NoError(lerr)
s.Len(logs, 1)
}
func (s *tokenSuite) TestToken_Validate_TokenNotFound() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
uId := uuid.FromStringOrNil("b5dd5267-b462-48be-b70d-bcd6f1bbe7a5")
token, err := models.NewToken(uId)
s.NoError(err)
body := TokenValidationBody{Value: token.Value}
bodyJson, err := json.Marshal(body)
s.NoError(err)
req := httptest.NewRequest(http.MethodPost, "/token", bytes.NewReader(bodyJson))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
e := NewPublicRouter(s.setupConfig(), s.Storage, nil, nil)
e.ServeHTTP(rec, req)
s.Equal(rec.Code, http.StatusNotFound)
var errorResponse echo.HTTPError
marshalErr := json.Unmarshal(rec.Body.Bytes(), &errorResponse)
s.NoError(marshalErr)
s.Contains("token not found", errorResponse.Message)
logs, lerr := s.Storage.GetAuditLogPersister().List(0, 0, nil, nil, []string{"token_exchange_failed"}, "", "", "", "")
s.NoError(lerr)
s.Len(logs, 1)
}
func (s *tokenSuite) setupConfig() *config.Config {
cfg := test.DefaultConfig
cfg.Session.EnableAuthTokenHeader = true
cfg.AuditLog.Storage.Enabled = true
return &cfg
}