mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-03-11 17:48:44 -05:00
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
This commit is contained in:
@@ -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()))
|
||||
}
|
||||
|
||||
|
||||
@@ -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."})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user