From 8ee069a2a360a73bb50c95431ed8e86804a6b208 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 25 Feb 2026 09:33:51 +0100 Subject: [PATCH] feat: add session-based auth with refresh token rotation - Login creates a server-side session and sets an HttpOnly refresh token cookie alongside the short-lived JWT - POST /user/token/refresh exchanges the cookie for a new JWT and rotates the refresh token atomically - POST /user/logout destroys the session and clears the cookie - POST /user/token restricted to link share tokens only - Session list (GET) and delete (DELETE) routes for /user/sessions - All user sessions invalidated on password change and reset - CORS configured to allow credentials for cross-origin cookies - JWT 401 responses use structured error code 11 for client detection - Refresh token cookie name constants annotated for gosec G101 --- pkg/modules/auth/auth.go | 72 ++++++- pkg/routes/api/v1/login.go | 199 ++++++++++++++++---- pkg/routes/api/v1/user_password_reset.go | 7 +- pkg/routes/api/v1/user_update_password.go | 5 + pkg/routes/api_tokens.go | 14 +- pkg/routes/routes.go | 17 +- pkg/user/user_password_reset.go | 15 +- pkg/user/user_test.go | 8 +- pkg/webtests/api_tokens_test.go | 14 +- pkg/webtests/error_responses_test.go | 2 +- pkg/webtests/integrations.go | 2 +- pkg/webtests/task_attachment_upload_test.go | 2 +- 12 files changed, 292 insertions(+), 65 deletions(-) diff --git a/pkg/modules/auth/auth.go b/pkg/modules/auth/auth.go index 95ccfdeaa..8cf5cb2ae 100644 --- a/pkg/modules/auth/auth.go +++ b/pkg/modules/auth/auth.go @@ -19,6 +19,7 @@ package auth import ( "fmt" "net/http" + "strings" "time" "code.vikunja.io/api/pkg/config" @@ -29,6 +30,7 @@ import ( petname "github.com/dustinkirkland/golang-petname" "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" "github.com/labstack/echo/v5" "xorm.io/xorm" ) @@ -45,35 +47,85 @@ type Token struct { Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"` } +const RefreshTokenCookieName = "vikunja_refresh_token" //nolint:gosec // not a credential +const refreshTokenCookiePath = "/api/v1/user/token/refresh" //nolint:gosec // not a credential + +// SetRefreshTokenCookie sets an HttpOnly cookie containing the refresh token. +// The cookie is path-scoped to the refresh endpoint so the browser only sends +// it on refresh requests. HttpOnly prevents JavaScript access (XSS protection). +func SetRefreshTokenCookie(c *echo.Context, token string, maxAge int) { + secure := strings.HasPrefix(config.ServicePublicURL.GetString(), "https") + c.SetCookie(&http.Cookie{ + Name: RefreshTokenCookieName, + Value: token, + Path: refreshTokenCookiePath, + MaxAge: maxAge, + HttpOnly: true, + Secure: secure, + SameSite: http.SameSiteStrictMode, + }) +} + +// ClearRefreshTokenCookie removes the refresh token cookie. +func ClearRefreshTokenCookie(c *echo.Context) { + SetRefreshTokenCookie(c, "", -1) +} + // NewUserAuthTokenResponse creates a new user auth token response from a user object. func NewUserAuthTokenResponse(u *user.User, c *echo.Context, long bool) error { - t, err := NewUserJWTAuthtoken(u, long) + s := db.NewSession() + defer s.Close() + + deviceInfo := c.Request().UserAgent() + ipAddress := c.RealIP() + + session, err := models.CreateSession(s, u.ID, deviceInfo, ipAddress, long) if err != nil { + _ = s.Rollback() return err } + t, err := NewUserJWTAuthtoken(u, session.ID) + if err != nil { + _ = s.Rollback() + return err + } + + if err := s.Commit(); err != nil { + _ = s.Rollback() + return err + } + + // Set the refresh token as an HttpOnly cookie. The cookie is path-scoped + // to the refresh endpoint, so the browser only sends it there. JavaScript + // never sees the refresh token — this protects it from XSS. + cookieMaxAge := int(config.ServiceJWTTTL.GetInt64()) + if long { + cookieMaxAge = int(config.ServiceJWTTTLLong.GetInt64()) + } + SetRefreshTokenCookie(c, session.RefreshToken, cookieMaxAge) + + c.Response().Header().Set("Cache-Control", "no-store") return c.JSON(http.StatusOK, Token{Token: t}) } -// NewUserJWTAuthtoken generates and signs a new jwt token for a user. This is a global function to be able to call it from web tests. -func NewUserJWTAuthtoken(u *user.User, long bool) (token string, err error) { +// NewUserJWTAuthtoken generates and signs a new short-lived jwt token for a user. +// The token includes the session UUID as the `sid` claim. This is a global +// function to be able to call it from web tests. +func NewUserJWTAuthtoken(u *user.User, sessionID string) (token string, err error) { t := jwt.New(jwt.SigningMethodHS256) - var ttl = time.Duration(config.ServiceJWTTTL.GetInt64()) - if long { - ttl = time.Duration(config.ServiceJWTTTLLong.GetInt64()) - } + var ttl = time.Duration(config.ServiceJWTTTLShort.GetInt64()) var exp = time.Now().Add(time.Second * ttl).Unix() - // Set claims claims := t.Claims.(jwt.MapClaims) claims["type"] = AuthTypeUser claims["id"] = u.ID claims["username"] = u.Username claims["exp"] = exp - claims["long"] = long + claims["sid"] = sessionID + claims["jti"] = uuid.New().String() - // Generate encoded token and send it as response. return t.SignedString([]byte(config.ServiceJWTSecret.GetString())) } diff --git a/pkg/routes/api/v1/login.go b/pkg/routes/api/v1/login.go index eb5655eca..1fb674d88 100644 --- a/pkg/routes/api/v1/login.go +++ b/pkg/routes/api/v1/login.go @@ -18,6 +18,7 @@ package v1 import ( "net/http" + "time" "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/db" @@ -116,47 +117,43 @@ func Login(c *echo.Context) (err error) { return auth.NewUserAuthTokenResponse(user, c, u.LongToken) } -// RenewToken gives a new token to every user with a valid token -// If the token is valid is checked in the middleware. -// @Summary Renew user token -// @Description Returns a new valid jwt user token with an extended length. -// @tags user +// RenewToken renews a link share token only. User tokens must use +// POST /user/token/refresh with a refresh token instead. +// @Summary Renew link share token +// @Description Returns a new valid jwt link share token. Only works for link share tokens. +// @tags auth // @Accept json // @Produce json // @Success 200 {object} auth.Token -// @Failure 400 {object} models.Message "Only user token are available for renew." +// @Failure 400 {object} models.Message "Only link share tokens can be renewed." // @Router /user/token [post] func RenewToken(c *echo.Context) (err error) { + jwtinf := c.Get("user").(*jwt.Token) + claims := jwtinf.Claims.(jwt.MapClaims) + typ := int(claims["type"].(float64)) + + if typ == auth.AuthTypeUser { + return echo.NewHTTPError( + http.StatusBadRequest, + "User tokens cannot be renewed via this endpoint. Use POST /user/token/refresh with a refresh token.", + ) + } + + if typ != auth.AuthTypeLinkShare { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid token type.") + } s := db.NewSession() defer s.Close() - jwtinf := c.Get("user").(*jwt.Token) - claims := jwtinf.Claims.(jwt.MapClaims) - typ := int(claims["type"].(float64)) - if typ == auth.AuthTypeLinkShare { - share := &models.LinkSharing{} - share.ID = int64(claims["id"].(float64)) - err := share.ReadOne(s, share) - if err != nil { - _ = s.Rollback() - return err - } - t, err := auth.NewLinkShareJWTAuthtoken(share) - if err != nil { - _ = s.Rollback() - return err - } - return c.JSON(http.StatusOK, auth.Token{Token: t}) - } - - u, err := user2.GetUserFromClaims(claims) + share := &models.LinkSharing{} + share.ID = int64(claims["id"].(float64)) + err = share.ReadOne(s, share) if err != nil { _ = s.Rollback() return err } - - user, err := user2.GetUserWithEmail(s, &user2.User{ID: u.ID}) + t, err := auth.NewLinkShareJWTAuthtoken(share) if err != nil { _ = s.Rollback() return err @@ -167,12 +164,146 @@ func RenewToken(c *echo.Context) (err error) { return err } - var long bool - lng, has := claims["long"] - if has { - long = lng.(bool) + return c.JSON(http.StatusOK, auth.Token{Token: t}) +} + +// RefreshToken exchanges a valid refresh token (sent as an HttpOnly cookie) for +// a new short-lived JWT. The refresh token is rotated on every call. +// @Summary Refresh user token +// @Description Exchanges the refresh token cookie for a new short-lived JWT. +// @tags auth +// @Produce json +// @Success 200 {object} auth.Token +// @Failure 401 {object} models.Message "Invalid or expired refresh token." +// @Router /user/token/refresh [post] +func RefreshToken(c *echo.Context) (err error) { + cookie, err := c.Cookie(auth.RefreshTokenCookieName) + if err != nil || cookie.Value == "" { + return echo.NewHTTPError(http.StatusUnauthorized, "No refresh token provided.") + } + rawToken := cookie.Value + + s := db.NewSession() + defer s.Close() + + session, err := models.GetSessionByRefreshToken(s, rawToken) + if err != nil { + _ = s.Rollback() + if models.IsErrSessionNotFound(err) { + // Don't clear the cookie here — another tab may have already + // rotated the token, and clearing would overwrite the new cookie. + return echo.NewHTTPError(http.StatusUnauthorized, "Invalid or expired refresh token.") + } + return err } - // Create token - return auth.NewUserAuthTokenResponse(user, c, long) + // Check if the session has expired based on its type + maxAge := time.Duration(config.ServiceJWTTTL.GetInt64()) * time.Second + if session.IsLongSession { + maxAge = time.Duration(config.ServiceJWTTTLLong.GetInt64()) * time.Second + } + if time.Since(session.LastActive) > maxAge { + if _, err := s.Where("id = ?", session.ID).Delete(&models.Session{}); err != nil { + _ = s.Rollback() + return err + } + if err := s.Commit(); err != nil { + return err + } + auth.ClearRefreshTokenCookie(c) + return echo.NewHTTPError(http.StatusUnauthorized, "Session expired.") + } + + if err := models.UpdateSessionLastActive(s, session.ID); err != nil { + _ = s.Rollback() + return err + } + + newRawToken, err := models.RotateRefreshToken(s, session) + if err != nil { + _ = s.Rollback() + if models.IsErrSessionNotFound(err) { + // Don't clear the cookie — a concurrent request in another tab + // may have already rotated the token successfully. + return echo.NewHTTPError(http.StatusUnauthorized, "Refresh token already used.") + } + return err + } + + u, err := user2.GetUserWithEmail(s, &user2.User{ID: session.UserID}) + if err != nil { + _ = s.Rollback() + return err + } + + if u.Status == user2.StatusDisabled { + if _, err := s.Where("id = ?", session.ID).Delete(&models.Session{}); err != nil { + _ = s.Rollback() + return err + } + if err := s.Commit(); err != nil { + return err + } + auth.ClearRefreshTokenCookie(c) + return &user2.ErrAccountDisabled{UserID: u.ID} + } + + if err := s.Commit(); err != nil { + _ = s.Rollback() + return err + } + + t, err := auth.NewUserJWTAuthtoken(u, session.ID) + if err != nil { + return err + } + + cookieMaxAge := int(config.ServiceJWTTTL.GetInt64()) + if session.IsLongSession { + cookieMaxAge = int(config.ServiceJWTTTLLong.GetInt64()) + } + auth.SetRefreshTokenCookie(c, newRawToken, cookieMaxAge) + + c.Response().Header().Set("Cache-Control", "no-store") + return c.JSON(http.StatusOK, auth.Token{Token: t}) +} + +// Logout deletes the current session from the server. +// @Summary Logout +// @Description Destroys the current session and clears the refresh token cookie. +// @tags auth +// @Produce json +// @Success 200 {object} models.Message "Successfully logged out." +// @Router /user/logout [post] +func Logout(c *echo.Context) (err error) { + auth.ClearRefreshTokenCookie(c) + + var sid string + if raw := c.Get("user"); raw != nil { + if jwtinf, ok := raw.(*jwt.Token); ok { + if claims, ok := jwtinf.Claims.(jwt.MapClaims); ok { + sid, _ = claims["sid"].(string) + } + } + } + + if sid == "" { + return c.JSON(http.StatusOK, models.Message{Message: "Successfully logged out."}) + } + + s := db.NewSession() + defer s.Close() + + _, err = s.Where("id = ?", sid).Delete(&models.Session{}) + if err != nil { + _ = s.Rollback() + return err + } + + if err := s.Commit(); err != nil { + _ = s.Rollback() + return err + } + + return c.JSON(http.StatusOK, models.Message{Message: "Successfully logged out."}) } diff --git a/pkg/routes/api/v1/user_password_reset.go b/pkg/routes/api/v1/user_password_reset.go index f448f53f1..09a2dbc53 100644 --- a/pkg/routes/api/v1/user_password_reset.go +++ b/pkg/routes/api/v1/user_password_reset.go @@ -47,12 +47,17 @@ func UserResetPassword(c *echo.Context) error { s := db.NewSession() defer s.Close() - err := user.ResetPassword(s, &pwReset) + userID, err := user.ResetPassword(s, &pwReset) if err != nil { _ = s.Rollback() return err } + if err := models.DeleteAllUserSessions(s, userID); err != nil { + _ = s.Rollback() + return err + } + if err := s.Commit(); err != nil { _ = s.Rollback() return err diff --git a/pkg/routes/api/v1/user_update_password.go b/pkg/routes/api/v1/user_update_password.go index 8ccedc503..32a34de11 100644 --- a/pkg/routes/api/v1/user_update_password.go +++ b/pkg/routes/api/v1/user_update_password.go @@ -77,6 +77,11 @@ func UserChangePassword(c *echo.Context) error { return err } + if err := models.DeleteAllUserSessions(s, doer.ID); err != nil { + _ = s.Rollback() + return err + } + if err := s.Commit(); err != nil { _ = s.Rollback() return err diff --git a/pkg/routes/api_tokens.go b/pkg/routes/api_tokens.go index 860be063b..015b3542b 100644 --- a/pkg/routes/api_tokens.go +++ b/pkg/routes/api_tokens.go @@ -26,11 +26,17 @@ import ( "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/user" + "code.vikunja.io/api/pkg/web" echojwt "github.com/labstack/echo-jwt/v5" "github.com/labstack/echo/v5" ) +// ErrCodeInvalidToken is the error code returned when the JWT is missing, +// malformed, or expired. The frontend uses this to distinguish "token expired, +// try refreshing" from other 401s (disabled account, wrong API token, etc.). +const ErrCodeInvalidToken = 11 + func SetupTokenMiddleware() echo.MiddlewareFunc { return echojwt.WithConfig(echojwt.Config{ SigningKey: []byte(config.ServiceJWTSecret.GetString()), @@ -53,9 +59,13 @@ func SetupTokenMiddleware() echo.MiddlewareFunc { return false }, - ErrorHandler: func(_ *echo.Context, err error) error { + ErrorHandler: func(c *echo.Context, err error) error { if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, "missing, malformed, expired or otherwise invalid token provided") + return c.JSON(http.StatusUnauthorized, web.HTTPError{ + HTTPCode: http.StatusUnauthorized, + Code: ErrCodeInvalidToken, + Message: "missing, malformed, expired or otherwise invalid token provided", + }) } return nil diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index 4b35ee9a3..cca423044 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -227,7 +227,8 @@ func RegisterRoutes(e *echo.Echo) { UnsafeAllowOriginFunc: func(_ *echo.Context, origin string) (string, bool, error) { return matchCORSOrigin(origin, allowedOrigins) }, - MaxAge: config.CorsMaxAge.GetInt(), + AllowCredentials: true, + MaxAge: config.CorsMaxAge.GetInt(), Skipper: func(context *echo.Context) bool { // Since it is not possible to register this middleware just for the api group, // we just disable it when for caldav requests. @@ -254,6 +255,7 @@ var unauthenticatedAPIPaths = map[string]bool{ "/api/v1/user/password/reset": true, "/api/v1/user/confirm": true, "/api/v1/login": true, + "/api/v1/user/token/refresh": true, "/api/v1/auth/openid/:provider/callback": true, "/api/v1/test/:table": true, "/api/v1/info": true, @@ -326,6 +328,10 @@ func registerAPIRoutes(a *echo.Group) { ur.POST("/login", apiv1.Login) } + // Refresh token endpoint — unauthenticated because it uses the refresh + // token cookie instead of a JWT bearer token. + ur.POST("/user/token/refresh", apiv1.RefreshToken) + if config.AuthOpenIDEnabled.GetBool() { ur.POST("/auth/openid/:provider/callback", openid.HandleCallback) } @@ -366,6 +372,7 @@ func registerAPIRoutes(a *echo.Group) { u.POST("/password", apiv1.UserChangePassword) u.GET("s", apiv1.UserList) u.POST("/token", apiv1.RenewToken) + u.POST("/logout", apiv1.Logout) u.POST("/settings/email", apiv1.UpdateUserEmail) u.GET("/settings/avatar", apiv1.GetUserAvatarProvider) u.POST("/settings/avatar", apiv1.ChangeUserAvatarProvider) @@ -379,6 +386,14 @@ func registerAPIRoutes(a *echo.Group) { u.GET("/settings/token/caldav", apiv1.GetCaldavTokens) u.DELETE("/settings/token/caldav/:id", apiv1.DeleteCaldavToken) + sessionProvider := &handler.WebHandler{ + EmptyStruct: func() handler.CObject { + return &models.Session{} + }, + } + u.GET("/sessions", sessionProvider.ReadAllWeb) + u.DELETE("/sessions/:session", sessionProvider.DeleteWeb) + if config.ServiceEnableTotp.GetBool() { u.GET("/settings/totp", apiv1.UserTOTP) u.POST("/settings/totp/enroll", apiv1.UserTOTPEnroll) diff --git a/pkg/user/user_password_reset.go b/pkg/user/user_password_reset.go index 0774c069e..9c2e1c684 100644 --- a/pkg/user/user_password_reset.go +++ b/pkg/user/user_password_reset.go @@ -30,31 +30,34 @@ type PasswordReset struct { NewPassword string `json:"new_password"` } -// ResetPassword resets a users password -func ResetPassword(s *xorm.Session, reset *PasswordReset) (err error) { +// ResetPassword resets a users password. It returns the ID of the user whose +// password was reset so callers can perform additional cleanup (e.g. session +// invalidation). +func ResetPassword(s *xorm.Session, reset *PasswordReset) (userID int64, err error) { // Check if the password is not empty if reset.NewPassword == "" { - return ErrNoUsernamePassword{} + return 0, ErrNoUsernamePassword{} } if reset.Token == "" { - return ErrNoPasswordResetToken{} + return 0, ErrNoPasswordResetToken{} } // Check if we have a token token, err := getToken(s, reset.Token, TokenPasswordReset) if err != nil { - return err + return 0, err } if token == nil { - return ErrInvalidPasswordResetToken{Token: reset.Token} + return 0, ErrInvalidPasswordResetToken{Token: reset.Token} } user, err := GetUserByID(s, token.UserID) if err != nil { return } + userID = user.ID // Hash the password user.Password, err = HashPassword(reset.NewPassword) diff --git a/pkg/user/user_test.go b/pkg/user/user_test.go index ce106c543..2af7b9273 100644 --- a/pkg/user/user_test.go +++ b/pkg/user/user_test.go @@ -555,7 +555,7 @@ func TestUserPasswordReset(t *testing.T) { Token: "passwordresettesttoken", NewPassword: "12345", } - err := ResetPassword(s, reset) + _, err := ResetPassword(s, reset) require.NoError(t, err) }) t.Run("without password", func(t *testing.T) { @@ -566,7 +566,7 @@ func TestUserPasswordReset(t *testing.T) { reset := &PasswordReset{ Token: "passwordresettesttoken", } - err := ResetPassword(s, reset) + _, err := ResetPassword(s, reset) require.Error(t, err) assert.True(t, IsErrNoUsernamePassword(err)) }) @@ -579,7 +579,7 @@ func TestUserPasswordReset(t *testing.T) { Token: "", NewPassword: "12345", } - err := ResetPassword(s, reset) + _, err := ResetPassword(s, reset) require.Error(t, err) assert.True(t, IsErrNoPasswordResetToken(err)) }) @@ -592,7 +592,7 @@ func TestUserPasswordReset(t *testing.T) { Token: "somethingsomething", NewPassword: "12345", } - err := ResetPassword(s, reset) + _, err := ResetPassword(s, reset) require.Error(t, err) assert.True(t, IsErrInvalidPasswordResetToken(err)) }) diff --git a/pkg/webtests/api_tokens_test.go b/pkg/webtests/api_tokens_test.go index 92acc9953..2202987d5 100644 --- a/pkg/webtests/api_tokens_test.go +++ b/pkg/webtests/api_tokens_test.go @@ -63,7 +63,9 @@ func TestAPIToken(t *testing.T) { }) req.Header.Set(echo.HeaderAuthorization, "Bearer tk_loremipsumdolorsitamet") - require.Error(t, h(c)) + require.NoError(t, h(c)) + assert.Equal(t, http.StatusUnauthorized, res.Code) + assert.Contains(t, res.Body.String(), `"code":11`) }) t.Run("expired token", func(t *testing.T) { e, err := setupTestEnv() @@ -76,7 +78,9 @@ func TestAPIToken(t *testing.T) { }) req.Header.Set(echo.HeaderAuthorization, "Bearer tk_a5e6f92ddbad68f49ee2c63e52174db0235008c8") // Token 2 - require.Error(t, h(c)) + require.NoError(t, h(c)) + assert.Equal(t, http.StatusUnauthorized, res.Code) + assert.Contains(t, res.Body.String(), `"code":11`) }) t.Run("valid token, invalid scope", func(t *testing.T) { e, err := setupTestEnv() @@ -89,7 +93,9 @@ func TestAPIToken(t *testing.T) { }) req.Header.Set(echo.HeaderAuthorization, "Bearer tk_2eef46f40ebab3304919ab2e7e39993f75f29d2e") - require.Error(t, h(c)) + require.NoError(t, h(c)) + assert.Equal(t, http.StatusUnauthorized, res.Code) + assert.Contains(t, res.Body.String(), `"code":11`) }) t.Run("jwt", func(t *testing.T) { e, err := setupTestEnv() @@ -105,7 +111,7 @@ func TestAPIToken(t *testing.T) { defer s.Close() u, err := user.GetUserByID(s, 1) require.NoError(t, err) - jwt, err := auth.NewUserJWTAuthtoken(u, false) + jwt, err := auth.NewUserJWTAuthtoken(u, "test-session-id") require.NoError(t, err) req.Header.Set(echo.HeaderAuthorization, "Bearer "+jwt) diff --git a/pkg/webtests/error_responses_test.go b/pkg/webtests/error_responses_test.go index a6cb9ad46..6448c6ca6 100644 --- a/pkg/webtests/error_responses_test.go +++ b/pkg/webtests/error_responses_test.go @@ -49,7 +49,7 @@ func TestErrorResponseFormats(t *testing.T) { require.NoError(t, err) // Get auth token for testuser1 - token, err := auth.NewUserJWTAuthtoken(&testuser1, false) + token, err := auth.NewUserJWTAuthtoken(&testuser1, "test-session-id") require.NoError(t, err) t.Run("validation error returns invalid_fields in JSON body", func(t *testing.T) { diff --git a/pkg/webtests/integrations.go b/pkg/webtests/integrations.go index 67a3ad400..a733358ac 100644 --- a/pkg/webtests/integrations.go +++ b/pkg/webtests/integrations.go @@ -123,7 +123,7 @@ func newTestRequest(t *testing.T, method string, handler func(ctx *echo.Context) func addUserTokenToContext(t *testing.T, user *user.User, c *echo.Context) { // Get the token as a string - token, err := auth.NewUserJWTAuthtoken(user, false) + token, err := auth.NewUserJWTAuthtoken(user, "test-session-id") require.NoError(t, err) // We send the string token through the parsing function to get a valid jwt.Token tken, err := jwt.Parse(token, func(_ *jwt.Token) (interface{}, error) { diff --git a/pkg/webtests/task_attachment_upload_test.go b/pkg/webtests/task_attachment_upload_test.go index 179da43d0..94c9ca2ef 100644 --- a/pkg/webtests/task_attachment_upload_test.go +++ b/pkg/webtests/task_attachment_upload_test.go @@ -96,7 +96,7 @@ func TestTaskAttachmentUploadSize(t *testing.T) { req.Header.Set(echo.HeaderContentType, writer.FormDataContentType()) // Add JWT token to request header for authentication - token, err := auth.NewUserJWTAuthtoken(&testuser1, false) + token, err := auth.NewUserJWTAuthtoken(&testuser1, "test-session-id") require.NoError(t, err) req.Header.Set("Authorization", "Bearer "+token)