mirror of
https://github.com/gofiber/fiber.git
synced 2025-05-19 06:00:03 +00:00
* feat!(middleware/session): re-write session middleware with handler * test(middleware/session): refactor to IdleTimeout * fix: lint errors * test: Save session after setting or deleting raw data in CSRF middleware * Update middleware/session/middleware.go Co-authored-by: Renan Bastos <renanbastos.tec@gmail.com> * fix: mutex and globals order * feat: Re-Add read lock to session Get method * feat: Migrate New() to return middleware * chore: Refactor session middleware to improve session handling * chore: Private get on store * chore: Update session middleware to use saveSession instead of save * chore: Update session middleware to use getSession instead of get * chore: Remove unused error handler in session middleware config * chore: Update session middleware to use NewWithStore in CSRF tests * test: add test * fix: destroyed session and GHSA-98j2-3j3p-fw2v * chore: Refactor session_test.go to use newStore() instead of New() * feat: Improve session middleware test coverage and error handling This commit improves the session middleware test coverage by adding assertions for the presence of the Set-Cookie header and the token value. It also enhances error handling by checking for the expected number of parts in the Set-Cookie header. * chore: fix lint issues * chore: Fix session middleware locking issue and improve error handling * test: improve middleware test coverage and error handling * test: Add idle timeout test case to session middleware test * feat: add GetSession(id string) (*Session, error) * chore: lint * docs: Update session middleware docs * docs: Security Note to examples * docs: Add recommendation for CSRF protection in session middleware * chore: markdown lint * docs: Update session middleware docs * docs: makrdown lint * test(middleware/session): Add unit tests for session config.go * test(middleware/session): Add unit tests for store.go * test(middleware/session): Add data.go unit tests * refactor(middleware/session): session tests and add session release test - Refactor session tests to improve readability and maintainability. - Add a new test case to ensure proper session release functionality. - Update session.md * refactor: session data locking in middleware/session/data.go * refactor(middleware/session): Add unit test for session middleware store * test: fix session_test.go and store_test.go unit tests * refactor(docs): Update session.md with v3 changes to Expiration * refactor(middleware/session): Improve data pool handling and locking * chore(middleware/session): TODO for Expiration field in session config * refactor(middleware/session): Improve session data pool handling and locking * refactor(middleware/session): Improve session data pool handling and locking * test(middleware/csrf): add session middleware coverage * chroe(middleware/session): TODO for unregistered session middleware * refactor(middleware/session): Update session middleware for v3 changes * refactor(middleware/session): Update session middleware for v3 changes * refactor(middleware/session): Update session middleware idle timeout - Update the default idle timeout for session middleware from 24 hours to 30 minutes. - Add a note in the session middleware documentation about the importance of the middleware order. * docws(middleware/session): Add note about IdleTimeout requiring save using legacy approach * refactor(middleware/session): Update session middleware idle timeout Update the idle timeout for the session middleware to 30 minutes. This ensures that the session expires after a period of inactivity. The previous value was 24 hours, which is too long for most use cases. This change improves the security and efficiency of the session management. * docs(middleware/session): Update session middleware idle timeout and configuration * test(middleware/session): Fix tests for updated panics * refactor(middleware/session): Update session middleware initialization and saving * refactor(middleware/session): Remove unnecessary comment about negative IdleTimeout value * refactor(middleware/session): Update session middleware make NewStore public * refactor(middleware/session): Update session middleware Set, Get, and Delete methods Refactor the Set, Get, and Delete methods in the session middleware to use more descriptive parameter names. Instead of using "middlewareContextKey", the methods now use "key" to represent the key of the session value. This improves the readability and clarity of the code. * feat(middleware/session): AbsoluteTimeout and key any * fix(middleware/session): locking issues and lint errors * chore(middleware/session): Regenerate code in data_msgp.go * refactor(middleware/session): rename GetSessionByID to GetByID This commit also includes changes to the session_test.go and store_test.go files to add test cases for the new GetByID method. * docs(middleware/session): AbsoluteTimeout * refactor(middleware/csrf): Rename Expiration to IdleTimeout * docs(whats-new): CSRF Rename Expiration to IdleTimeout and remove SessionKey field * refactor(middleware/session): Rename expirationKeyType to absExpirationKeyType and update related functions * refactor(middleware/session): rename Test_Session_Save_Absolute to Test_Session_Save_AbsoluteTimeout * chore(middleware/session): update as per PR comments * docs(middlware/session): fix indent lint * fix(middleware/session): Address EfeCtn Comments * refactor(middleware/session): Move bytesBuffer to it's own pool * test(middleware/session): add decodeSessionData error coverage * refactor(middleware/session): Update absolute timeout handling - Update absolute timeout handling in getSession function - Set absolute expiration time in getSession function - Delete expired session in GetByID function * refactor(session/middleware): fix *Session nil ctx when using Store.GetByID * refactor(middleware/session): Remove unnecessary line in session_test.go * fix(middleware/session): *Session lifecycle issues * docs(middleware/session): Update GetByID method documentation * docs(middleware/session): Update GetByID method documentation * docs(middleware/session): markdown lint * refactor(middleware/session): Simplify error handling in DefaultErrorHandler * fix( middleware/session/config.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * add ctx releases for the test cases --------- Co-authored-by: Renan Bastos <renanbastos.tec@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Co-authored-by: René <rene@gofiber.io>
332 lines
7.3 KiB
Go
332 lines
7.3 KiB
Go
package session
|
|
|
|
import (
|
|
"encoding/gob"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v3"
|
|
"github.com/gofiber/fiber/v3/internal/storage/memory"
|
|
"github.com/gofiber/fiber/v3/log"
|
|
"github.com/gofiber/utils/v2"
|
|
)
|
|
|
|
// ErrEmptySessionID is an error that occurs when the session ID is empty.
|
|
var (
|
|
ErrEmptySessionID = errors.New("session ID cannot be empty")
|
|
ErrSessionAlreadyLoadedByMiddleware = errors.New("session already loaded by middleware")
|
|
ErrSessionIDNotFoundInStore = errors.New("session ID not found in session store")
|
|
)
|
|
|
|
// sessionIDKey is the local key type used to store and retrieve the session ID in context.
|
|
type sessionIDKey int
|
|
|
|
const (
|
|
// sessionIDContextKey is the key used to store the session ID in the context locals.
|
|
sessionIDContextKey sessionIDKey = iota
|
|
)
|
|
|
|
type Store struct {
|
|
Config
|
|
}
|
|
|
|
// New creates a new session store with the provided configuration.
|
|
//
|
|
// Parameters:
|
|
// - config: Variadic parameter to override default config.
|
|
//
|
|
// Returns:
|
|
// - *Store: The session store.
|
|
//
|
|
// Usage:
|
|
//
|
|
// store := session.New()
|
|
func NewStore(config ...Config) *Store {
|
|
// Set default config
|
|
cfg := configDefault(config...)
|
|
|
|
if cfg.Storage == nil {
|
|
cfg.Storage = memory.New()
|
|
}
|
|
|
|
store := &Store{
|
|
Config: cfg,
|
|
}
|
|
|
|
if cfg.AbsoluteTimeout > 0 {
|
|
store.RegisterType(absExpirationKey)
|
|
store.RegisterType(time.Time{})
|
|
}
|
|
|
|
return store
|
|
}
|
|
|
|
// RegisterType registers a custom type for encoding/decoding into any storage provider.
|
|
//
|
|
// Parameters:
|
|
// - i: The custom type to register.
|
|
//
|
|
// Usage:
|
|
//
|
|
// store.RegisterType(MyCustomType{})
|
|
func (*Store) RegisterType(i any) {
|
|
gob.Register(i)
|
|
}
|
|
|
|
// Get will get/create a session.
|
|
//
|
|
// This function will return an ErrSessionAlreadyLoadedByMiddleware if
|
|
// the session is already loaded by the middleware.
|
|
//
|
|
// Parameters:
|
|
// - c: The Fiber context.
|
|
//
|
|
// Returns:
|
|
// - *Session: The session object.
|
|
// - error: An error if the session retrieval fails or if the session is already loaded by the middleware.
|
|
//
|
|
// Usage:
|
|
//
|
|
// sess, err := store.Get(c)
|
|
// if err != nil {
|
|
// // handle error
|
|
// }
|
|
func (s *Store) Get(c fiber.Ctx) (*Session, error) {
|
|
// If session is already loaded in the context,
|
|
// it should not be loaded again
|
|
_, ok := c.Locals(middlewareContextKey).(*Middleware)
|
|
if ok {
|
|
return nil, ErrSessionAlreadyLoadedByMiddleware
|
|
}
|
|
|
|
return s.getSession(c)
|
|
}
|
|
|
|
// getSession retrieves a session based on the context.
|
|
//
|
|
// Parameters:
|
|
// - c: The Fiber context.
|
|
//
|
|
// Returns:
|
|
// - *Session: The session object.
|
|
// - error: An error if the session retrieval fails.
|
|
//
|
|
// Usage:
|
|
//
|
|
// sess, err := store.getSession(c)
|
|
// if err != nil {
|
|
// // handle error
|
|
// }
|
|
func (s *Store) getSession(c fiber.Ctx) (*Session, error) {
|
|
var rawData []byte
|
|
var err error
|
|
|
|
id, ok := c.Locals(sessionIDContextKey).(string)
|
|
if !ok {
|
|
id = s.getSessionID(c)
|
|
}
|
|
|
|
fresh := ok // Assume the session is fresh if the ID is found in locals
|
|
|
|
// Attempt to fetch session data if an ID is provided
|
|
if id != "" {
|
|
rawData, err = s.Storage.Get(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if rawData == nil {
|
|
// Data not found, prepare to generate a new session
|
|
id = ""
|
|
}
|
|
}
|
|
|
|
// Generate a new ID if needed
|
|
if id == "" {
|
|
fresh = true // The session is fresh if a new ID is generated
|
|
id = s.KeyGenerator()
|
|
c.Locals(sessionIDContextKey, id)
|
|
}
|
|
|
|
// Create session object
|
|
sess := acquireSession()
|
|
|
|
sess.mu.Lock()
|
|
|
|
sess.ctx = c
|
|
sess.config = s
|
|
sess.id = id
|
|
sess.fresh = fresh
|
|
|
|
// Decode session data if found
|
|
if rawData != nil {
|
|
sess.data.Lock()
|
|
err := sess.decodeSessionData(rawData)
|
|
sess.data.Unlock()
|
|
if err != nil {
|
|
sess.mu.Unlock()
|
|
sess.Release()
|
|
return nil, fmt.Errorf("failed to decode session data: %w", err)
|
|
}
|
|
}
|
|
|
|
sess.mu.Unlock()
|
|
|
|
if fresh && s.AbsoluteTimeout > 0 {
|
|
sess.setAbsExpiration(time.Now().Add(s.AbsoluteTimeout))
|
|
} else if sess.isAbsExpired() {
|
|
if err := sess.Reset(); err != nil {
|
|
return nil, fmt.Errorf("failed to reset session: %w", err)
|
|
}
|
|
sess.setAbsExpiration(time.Now().Add(s.AbsoluteTimeout))
|
|
}
|
|
|
|
return sess, nil
|
|
}
|
|
|
|
// getSessionID returns the session ID from cookies, headers, or query string.
|
|
//
|
|
// Parameters:
|
|
// - c: The Fiber context.
|
|
//
|
|
// Returns:
|
|
// - string: The session ID.
|
|
//
|
|
// Usage:
|
|
//
|
|
// id := store.getSessionID(c)
|
|
func (s *Store) getSessionID(c fiber.Ctx) string {
|
|
id := c.Cookies(s.sessionName)
|
|
if len(id) > 0 {
|
|
return utils.CopyString(id)
|
|
}
|
|
|
|
if s.source == SourceHeader {
|
|
id = string(c.Request().Header.Peek(s.sessionName))
|
|
if len(id) > 0 {
|
|
return id
|
|
}
|
|
}
|
|
|
|
if s.source == SourceURLQuery {
|
|
id = fiber.Query[string](c, s.sessionName)
|
|
if len(id) > 0 {
|
|
return utils.CopyString(id)
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// Reset deletes all sessions from the storage.
|
|
//
|
|
// Returns:
|
|
// - error: An error if the reset operation fails.
|
|
//
|
|
// Usage:
|
|
//
|
|
// err := store.Reset()
|
|
// if err != nil {
|
|
// // handle error
|
|
// }
|
|
func (s *Store) Reset() error {
|
|
return s.Storage.Reset()
|
|
}
|
|
|
|
// Delete deletes a session by its ID.
|
|
//
|
|
// Parameters:
|
|
// - id: The unique identifier of the session.
|
|
//
|
|
// Returns:
|
|
// - error: An error if the deletion fails or if the session ID is empty.
|
|
//
|
|
// Usage:
|
|
//
|
|
// err := store.Delete(id)
|
|
// if err != nil {
|
|
// // handle error
|
|
// }
|
|
func (s *Store) Delete(id string) error {
|
|
if id == "" {
|
|
return ErrEmptySessionID
|
|
}
|
|
return s.Storage.Delete(id)
|
|
}
|
|
|
|
// GetByID retrieves a session by its ID from the storage.
|
|
// If the session is not found, it returns nil and an error.
|
|
//
|
|
// Unlike session middleware methods, this function does not automatically:
|
|
//
|
|
// - Load the session into the request context.
|
|
//
|
|
// - Save the session data to the storage or update the client cookie.
|
|
//
|
|
// Important Notes:
|
|
//
|
|
// - The session object returned by GetByID does not have a context associated with it.
|
|
//
|
|
// - When using this method alongside session middleware, there is a potential for collisions,
|
|
// so be mindful of interactions between manually retrieved sessions and middleware-managed sessions.
|
|
//
|
|
// - If you modify a session returned by GetByID, you must call session.Save() to persist the changes.
|
|
//
|
|
// - When you are done with the session, you should call session.Release() to release the session back to the pool.
|
|
//
|
|
// Parameters:
|
|
// - id: The unique identifier of the session.
|
|
//
|
|
// Returns:
|
|
// - *Session: The session object if found, otherwise nil.
|
|
// - error: An error if the session retrieval fails or if the session ID is empty.
|
|
//
|
|
// Usage:
|
|
//
|
|
// sess, err := store.GetByID(id)
|
|
// if err != nil {
|
|
// // handle error
|
|
// }
|
|
func (s *Store) GetByID(id string) (*Session, error) {
|
|
if id == "" {
|
|
return nil, ErrEmptySessionID
|
|
}
|
|
|
|
rawData, err := s.Storage.Get(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if rawData == nil {
|
|
return nil, ErrSessionIDNotFoundInStore
|
|
}
|
|
|
|
sess := acquireSession()
|
|
|
|
sess.mu.Lock()
|
|
|
|
sess.config = s
|
|
sess.id = id
|
|
sess.fresh = false
|
|
|
|
sess.data.Lock()
|
|
decodeErr := sess.decodeSessionData(rawData)
|
|
sess.data.Unlock()
|
|
sess.mu.Unlock()
|
|
if decodeErr != nil {
|
|
sess.Release()
|
|
return nil, fmt.Errorf("failed to decode session data: %w", decodeErr)
|
|
}
|
|
|
|
if s.AbsoluteTimeout > 0 {
|
|
if sess.isAbsExpired() {
|
|
if err := sess.Destroy(); err != nil {
|
|
sess.Release()
|
|
log.Errorf("failed to destroy session: %v", err)
|
|
}
|
|
return nil, ErrSessionIDNotFoundInStore
|
|
}
|
|
}
|
|
|
|
return sess, nil
|
|
}
|