From be13112d2d8a431a1aec59731809d7f99723541e Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Sun, 22 Dec 2024 14:06:08 -0500 Subject: [PATCH 01/11] add exp date to cookies --- backend/internal/v1/v1_auth/cookies.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/internal/v1/v1_auth/cookies.go b/backend/internal/v1/v1_auth/cookies.go index 92ecaa6..4003440 100644 --- a/backend/internal/v1/v1_auth/cookies.go +++ b/backend/internal/v1/v1_auth/cookies.go @@ -4,6 +4,7 @@ import ( "KonferCA/SPUR/common" "net/http" "os" + "time" "github.com/labstack/echo/v4" ) @@ -13,6 +14,7 @@ Helper function that abstract away the different cookie configuration needed based on the app environment for the refresh token cookie. */ func getRefreshTokenCookieConfig() *http.Cookie { + exp := time.Now().UTC().Add(24 * 7 * time.Hour) cookie := &http.Cookie{ Name: COOKIE_REFRESH_TOKEN, // this is a static path, that it should only be allowed in @@ -21,6 +23,9 @@ func getRefreshTokenCookieConfig() *http.Cookie { Secure: os.Getenv("APP_ENV") != common.DEVELOPMENT_ENV, SameSite: http.SameSiteStrictMode, HttpOnly: true, + Expires: exp, + // Max-Age set to 7 days in seconds + MaxAge: 7 * 24 * 60 * 60, } if os.Getenv("APP_ENV") == common.DEVELOPMENT_ENV { From 27711fbfaee7b2c80905efeb86380b99bc89aa67 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Sun, 22 Dec 2024 14:06:12 -0500 Subject: [PATCH 02/11] add cookies tests --- backend/internal/v1/v1_auth/cookies_test.go | 67 +++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 backend/internal/v1/v1_auth/cookies_test.go diff --git a/backend/internal/v1/v1_auth/cookies_test.go b/backend/internal/v1/v1_auth/cookies_test.go new file mode 100644 index 0000000..5280f2a --- /dev/null +++ b/backend/internal/v1/v1_auth/cookies_test.go @@ -0,0 +1,67 @@ +package v1_auth + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" +) + +func TestGetRefreshTokenCookieConfig(t *testing.T) { + original := os.Getenv("URL_DOMAIN") + os.Setenv("URL_DOMAIN", "localhost") + defer func() { os.Setenv("URL_DOMAIN", original) }() + + config := getRefreshTokenCookieConfig() + // 1 is added to account for the time taken to reach this point + assert.True(t, time.Now().UTC().Add(24*7*time.Hour+1).After(config.Expires)) + assert.Equal(t, COOKIE_REFRESH_TOKEN, config.Name) + assert.Equal(t, "/api/v1/auth/verify", config.Path) + assert.Equal(t, "localhost", config.Domain) + assert.True(t, config.Secure) + assert.True(t, config.HttpOnly) + assert.Equal(t, 7*24*60*60, config.MaxAge) + assert.Equal(t, http.SameSiteStrictMode, config.SameSite) +} + +func TestSetRefreshTokenCookie(t *testing.T) { + original := os.Getenv("URL_DOMAIN") + os.Setenv("URL_DOMAIN", "localhost") + defer func() { os.Setenv("URL_DOMAIN", original) }() + + e := echo.New() + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + + e.GET("/", func(c echo.Context) error { + setRefreshTokenCookie(c, "some value") + return c.NoContent(http.StatusOK) + }) + + e.ServeHTTP(rec, req) + + cookies := rec.Result().Cookies() + var tokenCookie *http.Cookie + for _, cookie := range cookies { + if cookie.Name == COOKIE_REFRESH_TOKEN { + tokenCookie = cookie + break + } + } + + assert.NotNil(t, tokenCookie) + // 1 is added to account for the time taken to reach this point + assert.True(t, time.Now().UTC().Add(24*7*time.Hour+1).After(tokenCookie.Expires)) + assert.Equal(t, COOKIE_REFRESH_TOKEN, tokenCookie.Name) + assert.Equal(t, "some value", tokenCookie.Value) + assert.Equal(t, "/api/v1/auth/verify", tokenCookie.Path) + assert.Equal(t, "localhost", tokenCookie.Domain) + assert.True(t, tokenCookie.Secure) + assert.True(t, tokenCookie.HttpOnly) + assert.Equal(t, 7*24*60*60, tokenCookie.MaxAge) + assert.Equal(t, http.SameSiteStrictMode, tokenCookie.SameSite) +} From 239e90053391e492bc95020fba8097a028c93f51 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Sun, 22 Dec 2024 14:07:08 -0500 Subject: [PATCH 03/11] set original logger after tests --- backend/internal/tests/logger_test.go | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/backend/internal/tests/logger_test.go b/backend/internal/tests/logger_test.go index 4193438..d8885e5 100644 --- a/backend/internal/tests/logger_test.go +++ b/backend/internal/tests/logger_test.go @@ -1,6 +1,7 @@ package tests import ( + customMiddleware "KonferCA/SPUR/internal/middleware" "bytes" "encoding/json" "github.com/labstack/echo/v4" @@ -8,7 +9,6 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/stretchr/testify/assert" - customMiddleware "KonferCA/SPUR/internal/middleware" "net/http" "net/http/httptest" "testing" @@ -24,11 +24,15 @@ TestLogger verifies that the logger middleware: func TestLogger(t *testing.T) { // capture log output for testing var buf bytes.Buffer + originalLogger := log.Logger + defer func() { + log.Logger = originalLogger + }() log.Logger = zerolog.New(&buf) // setup echo e := echo.New() - + // setup request ID middleware with a config that ensures ID generation e.Use(middleware.RequestIDWithConfig(middleware.RequestIDConfig{ Generator: func() string { @@ -60,7 +64,7 @@ func TestLogger(t *testing.T) { // verify each log entry var logEntry map[string]interface{} - + // check info log err := json.Unmarshal(logs[0], &logEntry) assert.NoError(t, err) @@ -68,14 +72,14 @@ func TestLogger(t *testing.T) { assert.Equal(t, "test info message", logEntry["message"]) assert.Equal(t, "test-request-id", logEntry["request_id"]) assert.Equal(t, "/test", logEntry["path"]) - + // check warning log err = json.Unmarshal(logs[1], &logEntry) assert.NoError(t, err) assert.Equal(t, "warn", logEntry["level"]) assert.Equal(t, "test warning", logEntry["message"]) assert.Equal(t, "test-request-id", logEntry["request_id"]) - + // check error log err = json.Unmarshal(logs[2], &logEntry) assert.NoError(t, err) @@ -91,18 +95,23 @@ a default logger when called without proper context */ func TestLoggerWithoutContext(t *testing.T) { var buf bytes.Buffer + originalLogger := log.Logger + defer func() { + log.Logger = originalLogger + }() log.Logger = zerolog.New(&buf) e := echo.New() c := e.NewContext(nil, nil) - + logger := customMiddleware.GetLogger(c) assert.NotNil(t, logger, "should return default logger") - + logger.Info("test message") - + var logEntry map[string]interface{} err := json.Unmarshal(buf.Bytes(), &logEntry) assert.NoError(t, err) assert.Equal(t, "test message", logEntry["message"]) -} \ No newline at end of file +} + From a42a4de94ab31f6850e4285c40c3b51e1f3d1bde Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Sun, 22 Dec 2024 14:07:25 -0500 Subject: [PATCH 04/11] add handler to verify refresh token cookie --- backend/internal/v1/v1_auth/auth.go | 57 +++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/backend/internal/v1/v1_auth/auth.go b/backend/internal/v1/v1_auth/auth.go index 30fe8b3..6ecf22f 100644 --- a/backend/internal/v1/v1_auth/auth.go +++ b/backend/internal/v1/v1_auth/auth.go @@ -155,3 +155,60 @@ func (h *Handler) handleVerifyEmail(c echo.Context) error { return c.JSON(http.StatusOK, EmailVerifiedStatusResponse{Verified: true}) } + +/* +Handle incoming requests to verify the refresh token saved in a HTTP-only cookie. +This route is used for a form to verify persistant authentication and generate +new access tokens for clients to use. + +This route will also respond with the same type of body as register/login because +it is essentially a passwordless login for the user given that the refresh token +in the cookie is valid. +*/ +func (h *Handler) handleVerifyCookie(c echo.Context) error { + cookie, err := c.Cookie(COOKIE_REFRESH_TOKEN) + if err != nil { + return v1_common.Fail(c, http.StatusUnauthorized, "Missing refresh token cookie in request", err) + } + + // verify the refresh token in the cookie + refreshToken := cookie.Value + claims, err := jwt.ParseUnverifiedClaims(refreshToken) + if err != nil { + return v1_common.Fail(c, http.StatusUnauthorized, "Cookie has invalid value.", err) + } + + ctx, cancel := context.WithTimeout(c.Request().Context(), time.Minute) + defer cancel() + + // get salt + user, err := h.server.GetQueries().GetUserByID(ctx, claims.UserID) + if err != nil { + return v1_common.Fail(c, http.StatusUnauthorized, "Cookie has invalid value.", err) + } + + claims, err = jwt.VerifyTokenWithSalt(refreshToken, user.TokenSalt) + if err != nil { + return v1_common.Fail(c, http.StatusUnauthorized, "Cookie is not valid.", err) + } + + accessToken, refreshToken, err := jwt.GenerateWithSalt(user.ID, user.Role, user.TokenSalt) + if err != nil { + return v1_common.Fail(c, http.StatusInternalServerError, "Failed to verify cookie.", err) + } + + if time.Until(claims.ExpiresAt.Time) < 3*24*time.Hour { + // refresh token is about to expired in less than 3 days + // set the new generated refresh token in the cookie + setRefreshTokenCookie(c, refreshToken) + } + + return c.JSON(http.StatusOK, map[string]any{ + "access_token": accessToken, + "user": map[string]any{ + "email": user.Email, + "email_verified": user.EmailVerified, + "role": string(user.Role), + }, + }) +} From 38e8c1f74c16ab3546c4ce7c56376c093acc3d8e Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Sun, 22 Dec 2024 14:07:37 -0500 Subject: [PATCH 05/11] register refresh token cookie verify route --- backend/internal/v1/v1_auth/routes.go | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/internal/v1/v1_auth/routes.go b/backend/internal/v1/v1_auth/routes.go index 1c1eb6e..d174277 100644 --- a/backend/internal/v1/v1_auth/routes.go +++ b/backend/internal/v1/v1_auth/routes.go @@ -20,4 +20,5 @@ func SetupAuthRoutes(e *echo.Group, s interfaces.CoreServer) { ) e.GET("/auth/verify-email", h.handleVerifyEmail) e.POST("/auth/register", h.handleRegister) + e.GET("/auth/verify", h.handleVerifyCookie) } From 2ac7442374d18b008ff552a607636fa265e0412b Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Sun, 22 Dec 2024 14:07:46 -0500 Subject: [PATCH 06/11] set original logger after test --- backend/internal/server/error_handler_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/internal/server/error_handler_test.go b/backend/internal/server/error_handler_test.go index 2d1819a..4bf35d3 100644 --- a/backend/internal/server/error_handler_test.go +++ b/backend/internal/server/error_handler_test.go @@ -171,6 +171,11 @@ func TestErrorHandler(t *testing.T) { }, } + originalLogger := log.Logger + defer func() { + log.Logger = originalLogger + }() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create a buffer to capture log output From 0dbed2afd39d153e5f4398a80d6660a4e67cade9 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Sun, 22 Dec 2024 14:08:01 -0500 Subject: [PATCH 07/11] add basic test for refresh token cookie validation --- backend/internal/tests/server_test.go | 70 +++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/backend/internal/tests/server_test.go b/backend/internal/tests/server_test.go index a01c176..d05e5ff 100644 --- a/backend/internal/tests/server_test.go +++ b/backend/internal/tests/server_test.go @@ -171,6 +171,76 @@ func TestServer(t *testing.T) { assert.NoError(t, err) }) + t.Run("/api/v1/auth/verify - 200 OK - valid cookie value", func(t *testing.T) { + // create context with timeout of 1 minute. + // tests should not hang for more than 1 minute. + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + // create request body + email := "test@mail.com" + password := "mypassword" + reqBody := map[string]string{ + "email": email, + "password": password, + } + reqBodyBytes, err := json.Marshal(reqBody) + assert.NoError(t, err) + + reader := bytes.NewReader(reqBodyBytes) + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", reader) + rec := httptest.NewRecorder() + + s.Echo.ServeHTTP(rec, req) + assert.Equal(t, http.StatusCreated, rec.Code) + + cookies := rec.Result().Cookies() + var refreshCookie *http.Cookie + for _, cookie := range cookies { + if cookie.Name == v1_auth.COOKIE_REFRESH_TOKEN { + refreshCookie = cookie + break + } + } + + assert.Equal(t, refreshCookie.Name, "refresh_token") + + // now we send a request to the actual route being tested + // to see if the verification of the cookie works + req = httptest.NewRequest(http.MethodGet, "/api/v1/auth/verify", nil) + req.AddCookie(refreshCookie) + rec = httptest.NewRecorder() + + s.Echo.ServeHTTP(rec, req) + assert.Equal(t, http.StatusOK, rec.Code) + + // read in the response body + resBodyBytes, err := io.ReadAll(rec.Body) + assert.NoError(t, err) + var resBody map[string]any + err = json.Unmarshal(resBodyBytes, &resBody) + assert.NoError(t, err) + + // the response body should include a new access token upon success + assert.NotEmpty(t, resBody["access_token"]) + + t.Log(resBody) + + // a new cookie value shouldn't be set since the refresh token hasn't expired yet + cookies = rec.Result().Cookies() + includesNewCookie := false + for _, cookie := range cookies { + if cookie.Name == v1_auth.COOKIE_REFRESH_TOKEN { + includesNewCookie = true + break + } + } + assert.False(t, includesNewCookie) + + err = removeTestUser(ctx, email, s) + assert.NoError(t, err) + }) + t.Run("/auth/verify-email - 200 OK - valid email token", func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) defer cancel() From aa9b97cab28f48f67481ca214db4363afeb41122 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Sun, 22 Dec 2024 14:22:24 -0500 Subject: [PATCH 08/11] fix tests build failure --- backend/internal/tests/logger_test.go | 9 --------- backend/internal/tests/server_test.go | 7 ++----- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/backend/internal/tests/logger_test.go b/backend/internal/tests/logger_test.go index a17472a..869a1e3 100644 --- a/backend/internal/tests/logger_test.go +++ b/backend/internal/tests/logger_test.go @@ -26,10 +26,6 @@ func TestLogger(t *testing.T) { originalLogger := log.Logger defer func() { log.Logger = originalLogger }() var buf bytes.Buffer - originalLogger := log.Logger - defer func() { - log.Logger = originalLogger - }() log.Logger = zerolog.New(&buf) // setup echo @@ -99,10 +95,6 @@ func TestLoggerWithoutContext(t *testing.T) { originalLogger := log.Logger defer func() { log.Logger = originalLogger }() var buf bytes.Buffer - originalLogger := log.Logger - defer func() { - log.Logger = originalLogger - }() log.Logger = zerolog.New(&buf) e := echo.New() @@ -118,4 +110,3 @@ func TestLoggerWithoutContext(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "test message", logEntry["message"]) } - diff --git a/backend/internal/tests/server_test.go b/backend/internal/tests/server_test.go index 0d99eb2..f95dae6 100644 --- a/backend/internal/tests/server_test.go +++ b/backend/internal/tests/server_test.go @@ -205,11 +205,6 @@ func TestServer(t *testing.T) { email := "test@mail.com" password := "mypassword" - // seed with test user - _, email, password, err := createTestUser(ctx, s) - assert.NoError(t, err) - defer removeTestUser(ctx, email, s) - reqBody := map[string]string{ "email": email, "password": password, @@ -217,8 +212,10 @@ func TestServer(t *testing.T) { reqBodyBytes, err := json.Marshal(reqBody) assert.NoError(t, err) + // get cookie by normal means reader := bytes.NewReader(reqBodyBytes) req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", reader) + req.Header.Add(echo.HeaderContentType, echo.MIMEApplicationJSON) rec := httptest.NewRecorder() s.Echo.ServeHTTP(rec, req) From 2bf37977bcfbfc0acb98035738367212054f9ccf Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Sun, 22 Dec 2024 14:55:11 -0500 Subject: [PATCH 09/11] add test helper function to fetch user token salt --- backend/internal/tests/helpers.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/backend/internal/tests/helpers.go b/backend/internal/tests/helpers.go index bd5980d..9edf117 100644 --- a/backend/internal/tests/helpers.go +++ b/backend/internal/tests/helpers.go @@ -32,6 +32,16 @@ func createTestUser(ctx context.Context, s *server.Server) (string, string, stri return userID, email, password, err } +/* +Helper function that queries the token_salt of a test user with email. +*/ +func getTestUserTokenSalt(ctx context.Context, email string, s *server.Server) ([]byte, error) { + row := s.DBPool.QueryRow(ctx, "SELECT token_salt FROM users WHERE email = $1;", email) + var salt []byte + err := row.Scan(&salt) + return salt, err +} + /* Simple wrapper with SQL to remove a user from the database. Ideally, you want to use this only for the test user created by the function createTestUser() From 4214d140d68df7fe15d13501e3e0323a0031c455 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Sun, 22 Dec 2024 14:55:27 -0500 Subject: [PATCH 10/11] update message on internal error in verify cookie route --- backend/internal/v1/v1_auth/auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/internal/v1/v1_auth/auth.go b/backend/internal/v1/v1_auth/auth.go index 69bbee0..9a70dbc 100644 --- a/backend/internal/v1/v1_auth/auth.go +++ b/backend/internal/v1/v1_auth/auth.go @@ -241,7 +241,7 @@ func (h *Handler) handleVerifyCookie(c echo.Context) error { accessToken, refreshToken, err := jwt.GenerateWithSalt(user.ID, user.Role, user.TokenSalt) if err != nil { - return v1_common.Fail(c, http.StatusInternalServerError, "Failed to verify cookie.", err) + return v1_common.Fail(c, http.StatusInternalServerError, "Oops, something went wrong.", err) } if time.Until(claims.ExpiresAt.Time) < 3*24*time.Hour { From 2b559bb0c26e419bfb24aee9cd16840a098715ae Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Sun, 22 Dec 2024 14:55:36 -0500 Subject: [PATCH 11/11] finish up verify cookie tests --- backend/internal/tests/server_test.go | 118 +++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 3 deletions(-) diff --git a/backend/internal/tests/server_test.go b/backend/internal/tests/server_test.go index f95dae6..10822f9 100644 --- a/backend/internal/tests/server_test.go +++ b/backend/internal/tests/server_test.go @@ -12,9 +12,11 @@ import ( "io" "net/http" "net/http/httptest" + "os" "testing" "time" + golangJWT "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" @@ -54,7 +56,7 @@ func TestServer(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) defer cancel() - // create test user + // create testuser userID := uuid.New() _, err = s.DBPool.Exec(ctx, ` INSERT INTO users ( @@ -230,6 +232,7 @@ func TestServer(t *testing.T) { } } + assert.NotNil(t, refreshCookie) assert.Equal(t, refreshCookie.Name, v1_auth.COOKIE_REFRESH_TOKEN) // now we send a request to the actual route being tested @@ -244,12 +247,13 @@ func TestServer(t *testing.T) { // read in the response body resBodyBytes, err := io.ReadAll(rec.Body) assert.NoError(t, err) - var resBody map[string]any + var resBody v1_auth.AuthResponse err = json.Unmarshal(resBodyBytes, &resBody) assert.NoError(t, err) // the response body should include a new access token upon success - assert.NotEmpty(t, resBody["access_token"]) + assert.NotEmpty(t, resBody.AccessToken) + assert.NotNil(t, resBody.User) // a new cookie value shouldn't be set since the refresh token hasn't expired yet cookies = rec.Result().Cookies() @@ -266,6 +270,114 @@ func TestServer(t *testing.T) { assert.NoError(t, err) }) + t.Run("/api/v1/auth/verify - 200 OK - about to expired valid cookie", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + // create new user to generate jwt + userID, email, _, err := createTestUser(ctx, s) + defer removeTestUser(ctx, email, s) + + salt, err := getTestUserTokenSalt(ctx, email, s) + assert.NoError(t, err) + + // this way we can control the exp since the inner function is not exported + // set the time to expired in two days + exp := time.Now().UTC().Add(2 * 24 * time.Hour) + claims := jwt.JWTClaims{ + UserID: userID, + Role: db.UserRoleStartupOwner, + TokenType: jwt.REFRESH_TOKEN_TYPE, + RegisteredClaims: golangJWT.RegisteredClaims{ + ExpiresAt: golangJWT.NewNumericDate(exp), + IssuedAt: golangJWT.NewNumericDate(time.Now()), + }, + } + + token := golangJWT.NewWithClaims(golangJWT.SigningMethodHS256, claims) + // combine base secret with user's salt + secret := append([]byte(os.Getenv("JWT_SECRET")), salt...) + signed, err := token.SignedString(secret) + assert.NoError(t, err) + + cookie := http.Cookie{ + Name: v1_auth.COOKIE_REFRESH_TOKEN, + Value: signed, + } + + req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/verify", nil) + req.AddCookie(&cookie) + rec := httptest.NewRecorder() + s.Echo.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + + // should return an access token + var resBody v1_auth.AuthResponse + err = json.Unmarshal(rec.Body.Bytes(), &resBody) + assert.NoError(t, err) + + assert.NotEmpty(t, resBody.AccessToken) + + // should include a new refresh token cookie + cookies := rec.Result().Cookies() + var refreshCookie *http.Cookie + for _, cookie := range cookies { + if cookie.Name == v1_auth.COOKIE_REFRESH_TOKEN { + refreshCookie = cookie + break + } + } + + assert.NotNil(t, refreshCookie) + assert.Equal(t, refreshCookie.Name, v1_auth.COOKIE_REFRESH_TOKEN) + }) + + t.Run("/api/v1/auth/verify - 401 UNAUTHORIZED - missing cookie in request", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/verify", nil) + rec := httptest.NewRecorder() + + s.Echo.ServeHTTP(rec, req) + assert.Equal(t, http.StatusUnauthorized, rec.Code) + }) + + t.Run("/api/v1/auth/verify - 401 UNAUTHORIZED - invalid cookie value", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + // create new user to generate jwt + userID, email, _, err := createTestUser(ctx, s) + defer removeTestUser(ctx, email, s) + + // the refresh token will be invalid since the salt is different + _, refreshToken, err := jwt.GenerateWithSalt(userID, db.UserRoleStartupOwner, []byte{0, 1, 2}) + assert.NoError(t, err) + + cookie := http.Cookie{ + Name: v1_auth.COOKIE_REFRESH_TOKEN, + Value: refreshToken, + } + req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/verify", nil) + req.AddCookie(&cookie) + rec := httptest.NewRecorder() + + s.Echo.ServeHTTP(rec, req) + assert.Equal(t, http.StatusUnauthorized, rec.Code) + }) + + t.Run("/api/v1/auth/verify - 401 UNAUTHORIZED - invalid cookie", func(t *testing.T) { + cookie := http.Cookie{ + Name: v1_auth.COOKIE_REFRESH_TOKEN, + Value: "invalid-refresh-token", + } + req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/verify", nil) + req.AddCookie(&cookie) + rec := httptest.NewRecorder() + + s.Echo.ServeHTTP(rec, req) + assert.Equal(t, http.StatusUnauthorized, rec.Code) + }) + t.Run("/auth/verify-email - 200 OK - valid email token", func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) defer cancel()