refactor: extract shared RefreshSession helper
The cookie-based /user/token/refresh handler had session refresh logic (lookup, expiry check, token rotation, user fetch, JWT generation) that will be reused by the OAuth token endpoint. Extract it into auth.RefreshSession() and rewrite RefreshToken to use it.
This commit is contained in:
parent
39e16653aa
commit
7a258f67c7
|
|
@ -216,3 +216,95 @@ func CreateUserWithRandomUsername(s *xorm.Session, uu *user.User) (u *user.User,
|
|||
err = models.CreateNewProjectForUser(s, u)
|
||||
return
|
||||
}
|
||||
|
||||
// RefreshResult holds the result of a successful session refresh.
|
||||
type RefreshResult struct {
|
||||
AccessToken string
|
||||
NewRefreshToken string
|
||||
ExpiresIn int64
|
||||
IsLongSession bool
|
||||
SessionID string
|
||||
}
|
||||
|
||||
// RefreshSession looks up a session by its raw refresh token, validates it,
|
||||
// rotates the refresh token, fetches the user, and generates a new JWT.
|
||||
// It handles its own DB session (open/commit/rollback).
|
||||
//
|
||||
// On user status errors (disabled/locked), the session is deleted before
|
||||
// returning the error so the caller can handle cleanup (e.g. clearing cookies).
|
||||
func RefreshSession(rawRefreshToken string) (*RefreshResult, error) {
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
session, err := models.GetSessionByRefreshToken(s, rawRefreshToken)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
if models.IsErrSessionNotFound(err) {
|
||||
return nil, echo.NewHTTPError(http.StatusUnauthorized, "Invalid or expired refresh token.")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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 nil, err
|
||||
}
|
||||
if err := s.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, echo.NewHTTPError(http.StatusUnauthorized, "Session expired.")
|
||||
}
|
||||
|
||||
if err := models.UpdateSessionLastActive(s, session.ID); err != nil {
|
||||
_ = s.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newRawToken, err := models.RotateRefreshToken(s, session)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
if models.IsErrSessionNotFound(err) {
|
||||
return nil, echo.NewHTTPError(http.StatusUnauthorized, "Refresh token already used.")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u, err := user.GetUserByID(s, session.UserID)
|
||||
if err != nil {
|
||||
if user.IsErrUserStatusError(err) {
|
||||
if _, delErr := s.Where("id = ?", session.ID).Delete(&models.Session{}); delErr != nil {
|
||||
_ = s.Rollback()
|
||||
return nil, delErr
|
||||
}
|
||||
if commitErr := s.Commit(); commitErr != nil {
|
||||
return nil, commitErr
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
_ = s.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessToken, err := NewUserJWTAuthtoken(u, session.ID)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &RefreshResult{
|
||||
AccessToken: accessToken,
|
||||
NewRefreshToken: newRawToken,
|
||||
ExpiresIn: config.ServiceJWTTTLShort.GetInt64(),
|
||||
IsLongSession: session.IsLongSession,
|
||||
SessionID: session.ID,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ package v1
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
|
@ -189,90 +188,23 @@ func RefreshToken(c *echo.Context) (err error) {
|
|||
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)
|
||||
result, err := auth.RefreshSession(cookie.Value)
|
||||
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.")
|
||||
if user2.IsErrUserStatusError(err) {
|
||||
auth.ClearRefreshTokenCookie(c)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 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 user2.IsErrUserStatusError(err) {
|
||||
if _, delErr := s.Where("id = ?", session.ID).Delete(&models.Session{}); delErr != nil {
|
||||
_ = s.Rollback()
|
||||
return delErr
|
||||
}
|
||||
if commitErr := s.Commit(); commitErr != nil {
|
||||
return commitErr
|
||||
}
|
||||
auth.ClearRefreshTokenCookie(c)
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
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 {
|
||||
if result.IsLongSession {
|
||||
cookieMaxAge = int(config.ServiceJWTTTLLong.GetInt64())
|
||||
}
|
||||
auth.SetRefreshTokenCookie(c, newRawToken, cookieMaxAge)
|
||||
auth.SetRefreshTokenCookie(c, result.NewRefreshToken, cookieMaxAge)
|
||||
|
||||
c.Response().Header().Set("Cache-Control", "no-store")
|
||||
return c.JSON(http.StatusOK, auth.Token{Token: t})
|
||||
return c.JSON(http.StatusOK, auth.Token{Token: result.AccessToken})
|
||||
}
|
||||
|
||||
// Logout deletes the current session from the server.
|
||||
|
|
|
|||
Loading…
Reference in New Issue