From e634e00d69f85a5babee6488cc4776a0d354971d Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Thu, 19 Dec 2024 19:59:27 -0500 Subject: [PATCH 01/23] add JWT_SECRET in setupEnv function for tests --- backend/internal/tests/setup.go | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/internal/tests/setup.go b/backend/internal/tests/setup.go index 397a2f67..2e08f6d0 100644 --- a/backend/internal/tests/setup.go +++ b/backend/internal/tests/setup.go @@ -20,4 +20,5 @@ func setupEnv() { os.Setenv("DB_PASSWORD", "postgres") os.Setenv("DB_NAME", "postgres") os.Setenv("DB_SSLMODE", "disable") + os.Setenv("JWT_SECRET", "jwt") } From 6199c1c7546bc51f62470325c3c119f807ed167e Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Thu, 19 Dec 2024 20:04:36 -0500 Subject: [PATCH 02/23] add cors middleware --- backend/internal/server/middleware.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/internal/server/middleware.go b/backend/internal/server/middleware.go index 9973d9b9..dde7609e 100644 --- a/backend/internal/server/middleware.go +++ b/backend/internal/server/middleware.go @@ -10,4 +10,6 @@ Setup all the global middlewares used in the server. func (s *Server) setupMiddlewares() { s.Echo.Use(middleware.RequestID()) s.Echo.Use(middleware.LoggerMiddleware()) -} \ No newline at end of file + s.Echo.Use(middleware.CORS()) +} + From 98c5773bb256449e25449ce85d280ea8ade89e48 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Fri, 20 Dec 2024 02:31:48 -0500 Subject: [PATCH 03/23] add queries for new users and check user existence --- backend/.sqlc/queries/users.sql | 9 +++++++ backend/db/users.sql.go | 45 +++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/backend/.sqlc/queries/users.sql b/backend/.sqlc/queries/users.sql index 548f39ec..731740e3 100644 --- a/backend/.sqlc/queries/users.sql +++ b/backend/.sqlc/queries/users.sql @@ -5,3 +5,12 @@ WHERE id = $1; -- name: GetUserEmailVerifiedStatusByEmail :one SELECT email_verified FROM users WHERE email = $1; + +-- name: UserExistsByEmail :one +SELECT EXISTS(SELECT 1 FROM users WHERE email = $1); + +-- name: NewUser :one +INSERT INTO users +(email, password, role) +VALUES +($1, $2, $3) RETURNING id, email, email_verified, role, token_salt; diff --git a/backend/db/users.sql.go b/backend/db/users.sql.go index 29578600..063e285f 100644 --- a/backend/db/users.sql.go +++ b/backend/db/users.sql.go @@ -46,3 +46,48 @@ func (q *Queries) GetUserEmailVerifiedStatusByEmail(ctx context.Context, email s err := row.Scan(&email_verified) return email_verified, err } + +const newUser = `-- name: NewUser :one +INSERT INTO users +(email, password, role) +VALUES +($1, $2, $3) RETURNING id, email, email_verified, role, token_salt +` + +type NewUserParams struct { + Email string + Password string + Role UserRole +} + +type NewUserRow struct { + ID string + Email string + EmailVerified bool + Role UserRole + TokenSalt []byte +} + +func (q *Queries) NewUser(ctx context.Context, arg NewUserParams) (NewUserRow, error) { + row := q.db.QueryRow(ctx, newUser, arg.Email, arg.Password, arg.Role) + var i NewUserRow + err := row.Scan( + &i.ID, + &i.Email, + &i.EmailVerified, + &i.Role, + &i.TokenSalt, + ) + return i, err +} + +const userExistsByEmail = `-- name: UserExistsByEmail :one +SELECT EXISTS(SELECT 1 FROM users WHERE email = $1) +` + +func (q *Queries) UserExistsByEmail(ctx context.Context, email string) (bool, error) { + row := q.db.QueryRow(ctx, userExistsByEmail, email) + var exists bool + err := row.Scan(&exists) + return exists, err +} From a9c64b3fdd533a6b09c51a9e0af4e51001f9c729 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Fri, 20 Dec 2024 02:32:04 -0500 Subject: [PATCH 04/23] add cookies helper functions --- backend/internal/v1/v1_auth/cookies.go | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 backend/internal/v1/v1_auth/cookies.go diff --git a/backend/internal/v1/v1_auth/cookies.go b/backend/internal/v1/v1_auth/cookies.go new file mode 100644 index 00000000..92ecaa6d --- /dev/null +++ b/backend/internal/v1/v1_auth/cookies.go @@ -0,0 +1,40 @@ +package v1_auth + +import ( + "KonferCA/SPUR/common" + "net/http" + "os" + + "github.com/labstack/echo/v4" +) + +/* +Helper function that abstract away the different cookie configuration needed +based on the app environment for the refresh token cookie. +*/ +func getRefreshTokenCookieConfig() *http.Cookie { + cookie := &http.Cookie{ + Name: COOKIE_REFRESH_TOKEN, + // this is a static path, that it should only be allowed in + Path: "/api/v1/auth/verify", + Domain: os.Getenv("URL_DOMAIN"), + Secure: os.Getenv("APP_ENV") != common.DEVELOPMENT_ENV, + SameSite: http.SameSiteStrictMode, + HttpOnly: true, + } + + if os.Getenv("APP_ENV") == common.DEVELOPMENT_ENV { + cookie.SameSite = http.SameSiteLaxMode + } + + return cookie +} + +/* +Sets the refresh token cookie in the given context with the value. +*/ +func setRefreshTokenCookie(c echo.Context, value string) { + cookie := getRefreshTokenCookieConfig() + cookie.Value = value + c.SetCookie(cookie) +} From 1db36e408e69d48439adfd54c5710c5e8530e742 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Fri, 20 Dec 2024 02:32:11 -0500 Subject: [PATCH 05/23] setup register route --- backend/internal/v1/v1_auth/auth.go | 76 +++++++++++++++++++++++++++ backend/internal/v1/v1_auth/routes.go | 1 + backend/internal/v1/v1_auth/types.go | 8 +++ 3 files changed, 85 insertions(+) diff --git a/backend/internal/v1/v1_auth/auth.go b/backend/internal/v1/v1_auth/auth.go index aef97f0c..2423d12e 100644 --- a/backend/internal/v1/v1_auth/auth.go +++ b/backend/internal/v1/v1_auth/auth.go @@ -2,11 +2,22 @@ package v1_auth import ( "KonferCA/SPUR/db" + "KonferCA/SPUR/internal/jwt" "KonferCA/SPUR/internal/v1/v1_common" + "context" + "encoding/json" "errors" + "io" "net/http" + "time" "github.com/labstack/echo/v4" + "golang.org/x/crypto/bcrypt" +) + +const ( + // Name of the cookie that holds the refresh token. + COOKIE_REFRESH_TOKEN string = "refresh_token" ) /* @@ -20,3 +31,68 @@ func (h *Handler) handleEmailVerificationStatus(c echo.Context) error { return c.JSON(http.StatusOK, EmailVerifiedStatusResponse{Verified: user.EmailVerified}) } + +/* +Route handles incoming requests to register/create a new account. +- Allow if the email is valid +- Allow if no other user has the same email already +- Responds with the access token and basic user information upon success +- HTTP-only cookie is also set with the refresh token value +*/ +func (h *Handler) handleRegister(c echo.Context) error { + reqBodyBytes, err := io.ReadAll(c.Request().Body) + if err != nil { + return v1_common.Fail(c, http.StatusInternalServerError, "", err) + } + var reqBody RegisterRequest + err = json.Unmarshal(reqBodyBytes, &reqBody) + if err != nil { + return v1_common.Fail(c, http.StatusInternalServerError, "", err) + } + + ctx, cancel := context.WithTimeout(c.Request().Context(), time.Minute) + defer cancel() + + q := h.server.GetQueries() + + exists, err := q.UserExistsByEmail(ctx, reqBody.Email) + if err != nil { + return v1_common.Fail(c, http.StatusInternalServerError, "", err) + } + + if exists { + return v1_common.Fail(c, http.StatusBadRequest, "Email has already been occupied.", nil) + } + + passwordHash, err := bcrypt.GenerateFromPassword([]byte(reqBody.Password), bcrypt.DefaultCost) + if err != nil { + return v1_common.Fail(c, http.StatusInternalServerError, "", err) + } + + newUser, err := q.NewUser(ctx, db.NewUserParams{ + Email: reqBody.Email, + Password: string(passwordHash), + Role: db.UserRoleStartupOwner, + }) + if err != nil { + return v1_common.Fail(c, http.StatusInternalServerError, "", err) + } + + // generate new access and refresh tokens + accessToken, refreshToken, err := jwt.GenerateWithSalt(newUser.ID, newUser.Role, newUser.TokenSalt) + if err != nil { + return v1_common.Success(c, http.StatusOK, "Registration complete but failed to sign in. Please sign in manually.") + } + + // set the refresh token cookie + setRefreshTokenCookie(c, refreshToken) + + return c.JSON(http.StatusCreated, map[string]any{ + "access_token": accessToken, + "user": map[string]any{ + "email": newUser.Email, + "email_verified": newUser.EmailVerified, + "role": newUser.Role, + }, + }) +} diff --git a/backend/internal/v1/v1_auth/routes.go b/backend/internal/v1/v1_auth/routes.go index 40bdde95..2880687a 100644 --- a/backend/internal/v1/v1_auth/routes.go +++ b/backend/internal/v1/v1_auth/routes.go @@ -18,4 +18,5 @@ func SetupAuthRoutes(e *echo.Group, s interfaces.CoreServer) { h.handleEmailVerificationStatus, middleware.Auth(s.GetDB(), db.UserRoleStartupOwner, db.UserRoleAdmin, db.UserRoleStartupOwner), ) + e.POST("/auth/register", h.handleRegister) } diff --git a/backend/internal/v1/v1_auth/types.go b/backend/internal/v1/v1_auth/types.go index 7a2770ad..94d95f08 100644 --- a/backend/internal/v1/v1_auth/types.go +++ b/backend/internal/v1/v1_auth/types.go @@ -15,3 +15,11 @@ Response body for route /auth/ami-verified type EmailVerifiedStatusResponse struct { Verified bool `json:"verified"` } + +/* +Request body for route /auth/register +*/ +type RegisterRequest struct { + Email string `json:"email" validate:"required,email"` + Password string `json:"password" validate:"required,min=8"` +} From e19a0f31b53f2e25fe4c36ce9883e77ee6f538bb Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Fri, 20 Dec 2024 02:32:17 -0500 Subject: [PATCH 06/23] add register route test --- backend/internal/tests/server_test.go | 71 +++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/backend/internal/tests/server_test.go b/backend/internal/tests/server_test.go index a4824422..6b963b10 100644 --- a/backend/internal/tests/server_test.go +++ b/backend/internal/tests/server_test.go @@ -5,6 +5,7 @@ import ( "KonferCA/SPUR/internal/jwt" "KonferCA/SPUR/internal/server" "KonferCA/SPUR/internal/v1/v1_auth" + "bytes" "context" "encoding/json" "fmt" @@ -96,5 +97,75 @@ func TestServer(t *testing.T) { t.Fatalf("failed to clean up test user: %v", err) } }) + + t.Run("/api/v1/auth/register - 201 CREATED - successful registration", func(t *testing.T) { + url := "/api/v1/auth/register" + + // 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, url, reader) + rec := httptest.NewRecorder() + + s.Echo.ServeHTTP(rec, req) + assert.Equal(t, http.StatusCreated, 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) + + // make sure that the response body has all the expected fields + // it should have the an access token + assert.NotEmpty(t, resBody["access_token"]) + assert.NotEmpty(t, resBody["user"]) + + // get the token salt of newly created user + row := s.DBPool.QueryRow(ctx, "SELECT token_salt FROM users WHERE email = $1;", email) + var salt []byte + err = row.Scan(&salt) + assert.NoError(t, err) + + // make sure it generated a valid access token by verifying it + claims, err := jwt.VerifyTokenWithSalt(resBody["access_token"].(string), salt) + assert.NoError(t, err) + assert.Equal(t, claims.TokenType, jwt.ACCESS_TOKEN_TYPE) + + // make sure that the headers include the Set-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, "Refresh token cookie should be set.") + if refreshCookie != nil { + assert.True(t, refreshCookie.HttpOnly, "Cookie should be HTTP-only") + assert.Equal(t, "/api/v1/auth/verify", refreshCookie.Path, "Cookie path should be /api") + assert.NotEmpty(t, refreshCookie.Value, "Cookie should have refresh token string as value") + // make sure the cookie value holds a valid refresh token + claims, err := jwt.VerifyTokenWithSalt(refreshCookie.Value, salt) + assert.NoError(t, err) + assert.Equal(t, claims.TokenType, jwt.REFRESH_TOKEN_TYPE) + } + }) }) } From 37061ce3005c9340fd1af7ed602eaec326cd36ab Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Sat, 21 Dec 2024 12:47:23 -0500 Subject: [PATCH 07/23] add remove test user after registration test --- backend/internal/tests/server_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/internal/tests/server_test.go b/backend/internal/tests/server_test.go index 92511598..a01c176a 100644 --- a/backend/internal/tests/server_test.go +++ b/backend/internal/tests/server_test.go @@ -166,6 +166,9 @@ func TestServer(t *testing.T) { assert.NoError(t, err) assert.Equal(t, claims.TokenType, jwt.REFRESH_TOKEN_TYPE) } + + err = removeTestUser(ctx, email, s) + assert.NoError(t, err) }) t.Run("/auth/verify-email - 200 OK - valid email token", func(t *testing.T) { From f9ee00a94afcad0d0cc12a167eb824769311fb80 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Sat, 21 Dec 2024 18:36:20 -0500 Subject: [PATCH 08/23] update the login/register request/response to use the same struct --- backend/internal/v1/v1_auth/auth.go | 6 +++--- backend/internal/v1/v1_auth/types.go | 12 ++---------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/backend/internal/v1/v1_auth/auth.go b/backend/internal/v1/v1_auth/auth.go index 979a7de2..57c2a020 100644 --- a/backend/internal/v1/v1_auth/auth.go +++ b/backend/internal/v1/v1_auth/auth.go @@ -51,7 +51,7 @@ func (h *Handler) handleRegister(c echo.Context) error { if err != nil { return v1_common.Fail(c, http.StatusInternalServerError, "", err) } - var reqBody RegisterRequest + var reqBody AuthRequest err = json.Unmarshal(reqBodyBytes, &reqBody) if err != nil { return v1_common.Fail(c, http.StatusInternalServerError, "", err) @@ -112,7 +112,7 @@ func (h *Handler) handleRegister(c echo.Context) error { * 4. Returns access token and user info */ func (h *Handler) handleLogin(c echo.Context) error { - var req LoginRequest + var req AuthRequest if err := c.Bind(&req); err != nil { return v1_common.Fail(c, http.StatusBadRequest, "Invalid request format", err) } @@ -147,7 +147,7 @@ func (h *Handler) handleLogin(c echo.Context) error { cookie.SameSite = http.SameSiteStrictMode c.SetCookie(cookie) - return c.JSON(http.StatusOK, LoginResponse{ + return c.JSON(http.StatusOK, AuthResponse{ AccessToken: accessToken, User: UserResponse{ Email: user.Email, diff --git a/backend/internal/v1/v1_auth/types.go b/backend/internal/v1/v1_auth/types.go index 6d378074..ad205a31 100644 --- a/backend/internal/v1/v1_auth/types.go +++ b/backend/internal/v1/v1_auth/types.go @@ -19,24 +19,16 @@ type EmailVerifiedStatusResponse struct { Verified bool `json:"verified"` } -/* -Request body for route /auth/register -*/ -type RegisterRequest struct { - Email string `json:"email" validate:"required,email"` - Password string `json:"password" validate:"required,min=8"` -} - /* * Request/response types for authentication endpoints. * These define the API contract for auth-related operations. */ -type LoginRequest struct { +type AuthRequest struct { Email string `json:"email" validate:"required,email"` // user's email Password string `json:"password" validate:"required,min=8"` // user's password } -type LoginResponse struct { +type AuthResponse struct { AccessToken string `json:"access_token"` // jwt access token User UserResponse `json:"user"` // user info } From ea27c779d252b34e99f700605c31aa7b72092908 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Sat, 21 Dec 2024 18:37:33 -0500 Subject: [PATCH 09/23] update register route response to use the AuthResponse struct --- backend/internal/v1/v1_auth/auth.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/internal/v1/v1_auth/auth.go b/backend/internal/v1/v1_auth/auth.go index 57c2a020..38a437cf 100644 --- a/backend/internal/v1/v1_auth/auth.go +++ b/backend/internal/v1/v1_auth/auth.go @@ -94,12 +94,12 @@ func (h *Handler) handleRegister(c echo.Context) error { // set the refresh token cookie setRefreshTokenCookie(c, refreshToken) - return c.JSON(http.StatusCreated, map[string]any{ - "access_token": accessToken, - "user": map[string]any{ - "email": newUser.Email, - "email_verified": newUser.EmailVerified, - "role": newUser.Role, + return c.JSON(http.StatusCreated, AuthResponse{ + AccessToken: accessToken, + User: UserResponse{ + Email: newUser.Email, + EmailVerified: newUser.EmailVerified, + Role: newUser.Role, }, }) } From f81c464df12a7bd44b5ac414e7cfa5d2b4b570f4 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Sat, 21 Dec 2024 18:38:37 -0500 Subject: [PATCH 10/23] update the login handler tests to use new structs --- backend/internal/tests/auth_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/internal/tests/auth_test.go b/backend/internal/tests/auth_test.go index 79568698..5405c4a3 100644 --- a/backend/internal/tests/auth_test.go +++ b/backend/internal/tests/auth_test.go @@ -25,7 +25,7 @@ import ( func TestAuthEndpoints(t *testing.T) { // Setup test environment setupEnv() - + // Additional required env vars os.Setenv("JWT_SECRET", "test-secret-key") @@ -45,7 +45,7 @@ func TestAuthEndpoints(t *testing.T) { ID: userID.String(), Email: "test@example.com", Password: string(hashedPassword), - Role: db.UserRoleStartupOwner, + Role: db.UserRoleStartupOwner, EmailVerified: true, CreatedAt: time.Now().Unix(), UpdatedAt: time.Now().Unix(), @@ -65,13 +65,13 @@ func TestAuthEndpoints(t *testing.T) { t.Run("Login Endpoint", func(t *testing.T) { tests := []struct { name string - payload v1_auth.LoginRequest + payload v1_auth.AuthRequest expectedStatus int checkResponse bool }{ { name: "Valid Login", - payload: v1_auth.LoginRequest{ + payload: v1_auth.AuthRequest{ Email: "test@example.com", Password: "testpassword123", }, @@ -80,7 +80,7 @@ func TestAuthEndpoints(t *testing.T) { }, { name: "Invalid Password", - payload: v1_auth.LoginRequest{ + payload: v1_auth.AuthRequest{ Email: "test@example.com", Password: "wrongpassword", }, @@ -89,7 +89,7 @@ func TestAuthEndpoints(t *testing.T) { }, { name: "Invalid Email", - payload: v1_auth.LoginRequest{ + payload: v1_auth.AuthRequest{ Email: "nonexistent@example.com", Password: "testpassword123", }, @@ -98,7 +98,7 @@ func TestAuthEndpoints(t *testing.T) { }, { name: "Invalid Email Format", - payload: v1_auth.LoginRequest{ + payload: v1_auth.AuthRequest{ Email: "invalid-email", Password: "testpassword123", }, @@ -121,7 +121,7 @@ func TestAuthEndpoints(t *testing.T) { assert.Equal(t, tc.expectedStatus, rec.Code) if tc.checkResponse { - var response v1_auth.LoginResponse + var response v1_auth.AuthResponse err := json.Unmarshal(rec.Body.Bytes(), &response) assert.NoError(t, err) assert.NotEmpty(t, response.AccessToken) From b1e5fba5d83a0731f8a5f3f18877bdd2819a6816 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Sat, 21 Dec 2024 18:39:57 -0500 Subject: [PATCH 11/23] abstract away the cookie setting in login handler unify the usage of the same function so that the cookie is always the same config --- backend/internal/v1/v1_auth/auth.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/backend/internal/v1/v1_auth/auth.go b/backend/internal/v1/v1_auth/auth.go index 38a437cf..864ed0c9 100644 --- a/backend/internal/v1/v1_auth/auth.go +++ b/backend/internal/v1/v1_auth/auth.go @@ -137,15 +137,7 @@ func (h *Handler) handleLogin(c echo.Context) error { return v1_common.Fail(c, http.StatusInternalServerError, "Failed to generate tokens", err) } - cookie := new(http.Cookie) - cookie.Name = "token" - cookie.Value = refreshToken - cookie.Path = "/api/v1/auth/verify" - cookie.Expires = time.Now().Add(24 * 7 * time.Hour) - cookie.HttpOnly = true - cookie.Secure = true - cookie.SameSite = http.SameSiteStrictMode - c.SetCookie(cookie) + setRefreshTokenCookie(c, refreshToken) return c.JSON(http.StatusOK, AuthResponse{ AccessToken: accessToken, From 4d467ce09f17d91379c4495592625b18d576af09 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Sat, 21 Dec 2024 18:42:05 -0500 Subject: [PATCH 12/23] use the request validator to validate auth request body in register route --- backend/internal/v1/v1_auth/auth.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/backend/internal/v1/v1_auth/auth.go b/backend/internal/v1/v1_auth/auth.go index 864ed0c9..869af527 100644 --- a/backend/internal/v1/v1_auth/auth.go +++ b/backend/internal/v1/v1_auth/auth.go @@ -6,9 +6,7 @@ import ( "KonferCA/SPUR/internal/middleware" "KonferCA/SPUR/internal/v1/v1_common" "context" - "encoding/json" "errors" - "io" "net/http" "time" @@ -47,14 +45,12 @@ Route handles incoming requests to register/create a new account. - HTTP-only cookie is also set with the refresh token value */ func (h *Handler) handleRegister(c echo.Context) error { - reqBodyBytes, err := io.ReadAll(c.Request().Body) - if err != nil { - return v1_common.Fail(c, http.StatusInternalServerError, "", err) - } var reqBody AuthRequest - err = json.Unmarshal(reqBodyBytes, &reqBody) - if err != nil { - return v1_common.Fail(c, http.StatusInternalServerError, "", err) + if err := c.Bind(&reqBody); err != nil { + return v1_common.Fail(c, http.StatusBadRequest, "Invalid request body", err) + } + if err := c.Validate(&reqBody); err != nil { + return v1_common.Fail(c, http.StatusBadRequest, "Invalid request body", err) } ctx, cancel := context.WithTimeout(c.Request().Context(), time.Minute) From 2ba02fd3da570bc1f77fb76aa0c8b6e9b56bfdb4 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Sun, 22 Dec 2024 12:29:09 -0500 Subject: [PATCH 13/23] pass as reference --- 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 869af527..089025e2 100644 --- a/backend/internal/v1/v1_auth/auth.go +++ b/backend/internal/v1/v1_auth/auth.go @@ -114,7 +114,7 @@ func (h *Handler) handleLogin(c echo.Context) error { } // validate request - if err := c.Validate(req); err != nil { + if err := c.Validate(&req); err != nil { return v1_common.Fail(c, http.StatusBadRequest, "Validation failed", err) } From b8468f826139f806034b3cc95ec64b4fe41fb0e1 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Sun, 22 Dec 2024 12:29:40 -0500 Subject: [PATCH 14/23] set the original logger back after substitution --- backend/internal/server/error_handler_test.go | 2 ++ backend/internal/tests/logger_test.go | 23 +++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/backend/internal/server/error_handler_test.go b/backend/internal/server/error_handler_test.go index 2d1819a1..cece5ad1 100644 --- a/backend/internal/server/error_handler_test.go +++ b/backend/internal/server/error_handler_test.go @@ -171,6 +171,8 @@ 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 diff --git a/backend/internal/tests/logger_test.go b/backend/internal/tests/logger_test.go index 41934383..2590304b 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" @@ -23,12 +23,14 @@ TestLogger verifies that the logger middleware: */ func TestLogger(t *testing.T) { // capture log output for testing + originalLogger := log.Logger + defer func() { log.Logger = originalLogger }() var buf bytes.Buffer 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 +62,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 +70,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) @@ -90,19 +92,22 @@ TestLoggerWithoutContext verifies that GetLogger returns a default logger when called without proper context */ func TestLoggerWithoutContext(t *testing.T) { + originalLogger := log.Logger + defer func() { log.Logger = originalLogger }() var buf bytes.Buffer 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 e3487f7919a1cf95df31bffdb779f9226b65b110 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Sun, 22 Dec 2024 12:29:47 -0500 Subject: [PATCH 15/23] check token with constant --- backend/internal/tests/auth_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/internal/tests/auth_test.go b/backend/internal/tests/auth_test.go index 5405c4a3..778646d4 100644 --- a/backend/internal/tests/auth_test.go +++ b/backend/internal/tests/auth_test.go @@ -133,7 +133,7 @@ func TestAuthEndpoints(t *testing.T) { cookies := rec.Result().Cookies() var foundRefreshToken bool for _, cookie := range cookies { - if cookie.Name == "token" { + if cookie.Name == v1_auth.COOKIE_REFRESH_TOKEN { foundRefreshToken = true assert.True(t, cookie.HttpOnly) assert.True(t, cookie.Secure) From fb1944cb57a1518627833947924f8b2fa18eefe2 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Sun, 22 Dec 2024 12:30:02 -0500 Subject: [PATCH 16/23] update register tests --- backend/internal/tests/server_test.go | 44 ++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/backend/internal/tests/server_test.go b/backend/internal/tests/server_test.go index a01c176a..29f9f4d1 100644 --- a/backend/internal/tests/server_test.go +++ b/backend/internal/tests/server_test.go @@ -16,6 +16,7 @@ import ( "time" "github.com/google/uuid" + "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) @@ -118,22 +119,23 @@ func TestServer(t *testing.T) { reader := bytes.NewReader(reqBodyBytes) req := httptest.NewRequest(http.MethodPost, url, reader) + req.Header.Add(echo.HeaderContentType, echo.MIMEApplicationJSON) rec := httptest.NewRecorder() s.Echo.ServeHTTP(rec, req) assert.Equal(t, http.StatusCreated, 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) + var resBody v1_auth.AuthResponse + err = json.Unmarshal(rec.Body.Bytes(), &resBody) assert.NoError(t, err) + t.Log(resBody) + // make sure that the response body has all the expected fields // it should have the an access token - assert.NotEmpty(t, resBody["access_token"]) - assert.NotEmpty(t, resBody["user"]) + assert.NotEmpty(t, resBody.AccessToken) + assert.NotEmpty(t, resBody.User) // get the token salt of newly created user row := s.DBPool.QueryRow(ctx, "SELECT token_salt FROM users WHERE email = $1;", email) @@ -142,7 +144,7 @@ func TestServer(t *testing.T) { assert.NoError(t, err) // make sure it generated a valid access token by verifying it - claims, err := jwt.VerifyTokenWithSalt(resBody["access_token"].(string), salt) + claims, err := jwt.VerifyTokenWithSalt(resBody.AccessToken, salt) assert.NoError(t, err) assert.Equal(t, claims.TokenType, jwt.ACCESS_TOKEN_TYPE) @@ -171,6 +173,34 @@ func TestServer(t *testing.T) { assert.NoError(t, err) }) + t.Run("/api/v1/auth/register - 400 Bad Request - existing user", func(t *testing.T) { + url := "/api/v1/auth/register" + + // 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() + + // 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, + } + reqBodyBytes, err := json.Marshal(reqBody) + assert.NoError(t, err) + + reader := bytes.NewReader(reqBodyBytes) + req := httptest.NewRequest(http.MethodPost, url, reader) + rec := httptest.NewRecorder() + + s.Echo.ServeHTTP(rec, req) + assert.Equal(t, http.StatusBadRequest, 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() From c76e959f1c8d070734f1df1d389311ed52d158ef Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Sun, 22 Dec 2024 12:32:32 -0500 Subject: [PATCH 17/23] add test for invalid body register route --- backend/internal/tests/server_test.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/backend/internal/tests/server_test.go b/backend/internal/tests/server_test.go index 29f9f4d1..ff4afbcb 100644 --- a/backend/internal/tests/server_test.go +++ b/backend/internal/tests/server_test.go @@ -201,6 +201,28 @@ func TestServer(t *testing.T) { assert.Equal(t, http.StatusBadRequest, rec.Code) }) + t.Run("/api/v1/auth/register - 400 Bad Request - invalid body", func(t *testing.T) { + url := "/api/v1/auth/register" + + // create request body + email := "test" + password := "short" + 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, url, reader) + req.Header.Add(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + + s.Echo.ServeHTTP(rec, req) + assert.Equal(t, http.StatusBadRequest, 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() From 5d6e62a500df2dff1c2707ae529e61f834312107 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Mon, 23 Dec 2024 02:25:37 -0500 Subject: [PATCH 18/23] send verification email upon successful registration --- backend/.sqlc/queries/email_tokens.sql | 10 +++++ backend/db/email_tokens.sql.go | 37 +++++++++++++++++ backend/db/users.sql.go | 21 +++++----- backend/internal/v1/v1_auth/auth.go | 55 ++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 10 deletions(-) diff --git a/backend/.sqlc/queries/email_tokens.sql b/backend/.sqlc/queries/email_tokens.sql index c6610b5c..43ec4120 100644 --- a/backend/.sqlc/queries/email_tokens.sql +++ b/backend/.sqlc/queries/email_tokens.sql @@ -5,3 +5,13 @@ WHERE id = $1; -- name: RemoveVerifyEmailTokenByID :exec DELETE FROM verify_email_tokens WHERE id = $1; + +-- name: NewVerifyEmailToken :one +INSERT INTO verify_email_tokens (user_id, expires_at) +VALUES ($1, $2) RETURNING id; + +-- name: ExistsVerifyEmailTokenByUserID :one +SELECT EXISTS(SELECT 1 FROM verify_email_tokens WHERE user_id = $1); + +-- name: RemoveVerifyEmailTokenByUserID :exec +DELETE FROM verify_email_tokens WHERE user_id = $1; diff --git a/backend/db/email_tokens.sql.go b/backend/db/email_tokens.sql.go index 479f8793..5b8c61d3 100644 --- a/backend/db/email_tokens.sql.go +++ b/backend/db/email_tokens.sql.go @@ -9,6 +9,17 @@ import ( "context" ) +const existsVerifyEmailTokenByUserID = `-- name: ExistsVerifyEmailTokenByUserID :one +SELECT EXISTS(SELECT 1 FROM verify_email_tokens WHERE user_id = $1) +` + +func (q *Queries) ExistsVerifyEmailTokenByUserID(ctx context.Context, userID string) (bool, error) { + row := q.db.QueryRow(ctx, existsVerifyEmailTokenByUserID, userID) + var exists bool + err := row.Scan(&exists) + return exists, err +} + const getVerifyEmailTokenByID = `-- name: GetVerifyEmailTokenByID :one SELECT id, user_id, created_at, expires_at FROM verify_email_tokens @@ -27,6 +38,23 @@ func (q *Queries) GetVerifyEmailTokenByID(ctx context.Context, id string) (Verif return i, err } +const newVerifyEmailToken = `-- name: NewVerifyEmailToken :one +INSERT INTO verify_email_tokens (user_id, expires_at) +VALUES ($1, $2) RETURNING id +` + +type NewVerifyEmailTokenParams struct { + UserID string + ExpiresAt int64 +} + +func (q *Queries) NewVerifyEmailToken(ctx context.Context, arg NewVerifyEmailTokenParams) (string, error) { + row := q.db.QueryRow(ctx, newVerifyEmailToken, arg.UserID, arg.ExpiresAt) + var id string + err := row.Scan(&id) + return id, err +} + const removeVerifyEmailTokenByID = `-- name: RemoveVerifyEmailTokenByID :exec DELETE FROM verify_email_tokens WHERE id = $1 ` @@ -35,3 +63,12 @@ func (q *Queries) RemoveVerifyEmailTokenByID(ctx context.Context, id string) err _, err := q.db.Exec(ctx, removeVerifyEmailTokenByID, id) return err } + +const removeVerifyEmailTokenByUserID = `-- name: RemoveVerifyEmailTokenByUserID :exec +DELETE FROM verify_email_tokens WHERE user_id = $1 +` + +func (q *Queries) RemoveVerifyEmailTokenByUserID(ctx context.Context, userID string) error { + _, err := q.db.Exec(ctx, removeVerifyEmailTokenByUserID, userID) + return err +} diff --git a/backend/db/users.sql.go b/backend/db/users.sql.go index cc0aab5c..64623113 100644 --- a/backend/db/users.sql.go +++ b/backend/db/users.sql.go @@ -101,16 +101,6 @@ func (q *Queries) NewUser(ctx context.Context, arg NewUserParams) (NewUserRow, e return i, err } -const userExistsByEmail = `-- name: UserExistsByEmail :one -SELECT EXISTS(SELECT 1 FROM users WHERE email = $1) -` - -func (q *Queries) UserExistsByEmail(ctx context.Context, email string) (bool, error) { - row := q.db.QueryRow(ctx, userExistsByEmail, email) - var exists bool - err := row.Scan(&exists) - return exists, err -} const updateUserEmailVerifiedStatus = `-- name: UpdateUserEmailVerifiedStatus :exec UPDATE users SET email_verified = $1 WHERE id = $2 ` @@ -124,3 +114,14 @@ func (q *Queries) UpdateUserEmailVerifiedStatus(ctx context.Context, arg UpdateU _, err := q.db.Exec(ctx, updateUserEmailVerifiedStatus, arg.EmailVerified, arg.ID) return err } + +const userExistsByEmail = `-- name: UserExistsByEmail :one +SELECT EXISTS(SELECT 1 FROM users WHERE email = $1) +` + +func (q *Queries) UserExistsByEmail(ctx context.Context, email string) (bool, error) { + row := q.db.QueryRow(ctx, userExistsByEmail, email) + var exists bool + err := row.Scan(&exists) + return exists, err +} diff --git a/backend/internal/v1/v1_auth/auth.go b/backend/internal/v1/v1_auth/auth.go index 089025e2..06056135 100644 --- a/backend/internal/v1/v1_auth/auth.go +++ b/backend/internal/v1/v1_auth/auth.go @@ -4,6 +4,7 @@ import ( "KonferCA/SPUR/db" "KonferCA/SPUR/internal/jwt" "KonferCA/SPUR/internal/middleware" + "KonferCA/SPUR/internal/service" "KonferCA/SPUR/internal/v1/v1_common" "context" "errors" @@ -11,6 +12,7 @@ import ( "time" "github.com/labstack/echo/v4" + "github.com/rs/zerolog/log" "golang.org/x/crypto/bcrypt" ) @@ -25,6 +27,54 @@ func verifyPassword(password, hash string) bool { return err == nil } +/* +Helper function that sends a new verification email. It can be use to send new verification emails +or for resending the verification email to the same recipient again. + +Keep in mind that the recipient must be the same user. +*/ +func sendEmailVerification(userID, email string, queries *db.Queries) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + exists, err := queries.ExistsVerifyEmailTokenByUserID(ctx, userID) + if err != nil { + log.Error().Err(err).Str("user_id", userID).Str("email", email).Msg("Failed to send verification email.") + return + } + + if exists { + // remove existing one + err := queries.RemoveVerifyEmailTokenByUserID(ctx, userID) + if err != nil { + log.Error().Err(err).Str("user_id", userID).Str("email", email).Msg("Failed to send verification email.") + return + } + } + + exp := time.Now().Add(time.Minute * 30).UTC() + tokenID, err := queries.NewVerifyEmailToken(ctx, db.NewVerifyEmailTokenParams{ + UserID: userID, + ExpiresAt: exp.Unix(), + }) + if err != nil { + log.Error().Err(err).Str("user_id", userID).Str("email", email).Msg("Failed to send verification email.") + return + } + + emailToken, err := jwt.GenerateVerifyEmailToken(email, tokenID, exp) + if err != nil { + log.Error().Err(err).Str("user_id", userID).Str("email", email).Msg("Failed to send verification email.") + return + } + + err = service.SendVerficationEmail(ctx, email, emailToken) + if err != nil { + log.Error().Err(err).Str("user_id", userID).Str("email", email).Msg("Failed to send verification email.") + return + } +} + /* Simple route handler that just returns whether the email has been verified or not in JSON body. */ @@ -81,6 +131,11 @@ func (h *Handler) handleRegister(c echo.Context) error { return v1_common.Fail(c, http.StatusInternalServerError, "", err) } + // send verification email using goroutine to not block + // at this point, the user has successfully been created + // so sending the verification email now is safe. + go sendEmailVerification(newUser.ID, newUser.Email, h.server.GetQueries()) + // generate new access and refresh tokens accessToken, refreshToken, err := jwt.GenerateWithSalt(newUser.ID, newUser.Role, newUser.TokenSalt) if err != nil { From d1b3ace20a3ec996ace143679a4372c574a15cf6 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Mon, 23 Dec 2024 15:28:10 -0500 Subject: [PATCH 19/23] remove reset logger --- backend/internal/server/error_handler_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/internal/server/error_handler_test.go b/backend/internal/server/error_handler_test.go index acd612a2..65b449a5 100644 --- a/backend/internal/server/error_handler_test.go +++ b/backend/internal/server/error_handler_test.go @@ -209,8 +209,6 @@ 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) { var buf bytes.Buffer From 3cb8d245c8f975b5572d78bb26001e4c4cdc58c6 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Mon, 23 Dec 2024 15:33:06 -0500 Subject: [PATCH 20/23] fail with status created on failure to generate tokens --- 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 06056135..4a00eccb 100644 --- a/backend/internal/v1/v1_auth/auth.go +++ b/backend/internal/v1/v1_auth/auth.go @@ -139,7 +139,7 @@ func (h *Handler) handleRegister(c echo.Context) error { // generate new access and refresh tokens accessToken, refreshToken, err := jwt.GenerateWithSalt(newUser.ID, newUser.Role, newUser.TokenSalt) if err != nil { - return v1_common.Success(c, http.StatusOK, "Registration complete but failed to sign in. Please sign in manually.") + return v1_common.Fail(c, http.StatusCreated, "Registration complete but failed to sign in. Please sign in manually.", err) } // set the refresh token cookie From c605bb280c268d65a5f389936ba2699d87aa2c6b Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Mon, 23 Dec 2024 15:36:21 -0500 Subject: [PATCH 21/23] log new user created --- backend/internal/v1/v1_auth/auth.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/internal/v1/v1_auth/auth.go b/backend/internal/v1/v1_auth/auth.go index 4a00eccb..f7a02389 100644 --- a/backend/internal/v1/v1_auth/auth.go +++ b/backend/internal/v1/v1_auth/auth.go @@ -8,6 +8,7 @@ import ( "KonferCA/SPUR/internal/v1/v1_common" "context" "errors" + "fmt" "net/http" "time" @@ -95,6 +96,8 @@ Route handles incoming requests to register/create a new account. - HTTP-only cookie is also set with the refresh token value */ func (h *Handler) handleRegister(c echo.Context) error { + logger := middleware.GetLogger(c) + var reqBody AuthRequest if err := c.Bind(&reqBody); err != nil { return v1_common.Fail(c, http.StatusBadRequest, "Invalid request body", err) @@ -145,6 +148,8 @@ func (h *Handler) handleRegister(c echo.Context) error { // set the refresh token cookie setRefreshTokenCookie(c, refreshToken) + logger.Info(fmt.Sprintf("New user created with email: %s", newUser.Email)) + return c.JSON(http.StatusCreated, AuthResponse{ AccessToken: accessToken, User: UserResponse{ From 2a69219ec124562f37f0fe7cccf0e3872c79f4aa Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Mon, 23 Dec 2024 15:43:13 -0500 Subject: [PATCH 22/23] add auth rate limiter --- backend/internal/v1/v1_auth/routes.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/backend/internal/v1/v1_auth/routes.go b/backend/internal/v1/v1_auth/routes.go index 44b5150e..8af2cea0 100644 --- a/backend/internal/v1/v1_auth/routes.go +++ b/backend/internal/v1/v1_auth/routes.go @@ -4,6 +4,7 @@ import ( "KonferCA/SPUR/db" "KonferCA/SPUR/internal/interfaces" "KonferCA/SPUR/internal/middleware" + "time" "github.com/labstack/echo/v4" ) @@ -14,12 +15,21 @@ Sets up the V1 auth routes. func SetupAuthRoutes(e *echo.Group, s interfaces.CoreServer) { h := Handler{server: s} + + // 5 request per minute, get block for 15 minutes, and ban up to 1 hour after four blocks. + authLimiter := middleware.NewRateLimiter(&middleware.RateLimiterConfig{ + Requests: 5, + Window: time.Minute, + BlockPeriod: time.Minute * 15, + MaxBlocks: 4, + }) + e.POST("/auth/login", h.handleLogin) e.GET( "/auth/ami-verified", h.handleEmailVerificationStatus, middleware.Auth(s.GetDB(), db.UserRoleStartupOwner, db.UserRoleAdmin), ) - e.GET("/auth/verify-email", h.handleVerifyEmail) - e.POST("/auth/register", h.handleRegister) + e.GET("/auth/verify-email", h.handleVerifyEmail, authLimiter.RateLimit()) + e.POST("/auth/register", h.handleRegister, authLimiter.RateLimit()) } From 76b9657bdb86c6aae7613e3d6edd3f1ebb683a7e Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Mon, 23 Dec 2024 15:50:19 -0500 Subject: [PATCH 23/23] increase rate limit in test --- backend/internal/v1/v1_auth/routes.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/backend/internal/v1/v1_auth/routes.go b/backend/internal/v1/v1_auth/routes.go index 8af2cea0..ebb0d4c7 100644 --- a/backend/internal/v1/v1_auth/routes.go +++ b/backend/internal/v1/v1_auth/routes.go @@ -1,9 +1,11 @@ package v1_auth import ( + "KonferCA/SPUR/common" "KonferCA/SPUR/db" "KonferCA/SPUR/internal/interfaces" "KonferCA/SPUR/internal/middleware" + "os" "time" "github.com/labstack/echo/v4" @@ -17,8 +19,13 @@ func SetupAuthRoutes(e *echo.Group, s interfaces.CoreServer) { h := Handler{server: s} // 5 request per minute, get block for 15 minutes, and ban up to 1 hour after four blocks. + maxRequests := 5 + if os.Getenv("APP_ENV") == common.TEST_ENV { + maxRequests = 5000 + } + authLimiter := middleware.NewRateLimiter(&middleware.RateLimiterConfig{ - Requests: 5, + Requests: maxRequests, Window: time.Minute, BlockPeriod: time.Minute * 15, MaxBlocks: 4,