multisession

package module
v0.0.0-...-eeabb0f Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 15, 2026 License: MIT Imports: 13 Imported by: 0

README

GoModularAuth MultiSession Plugin

Go Reference

gomodularauth-multi-session provides advanced management of multiple concurrent Core Sessions for GoModularAuth users while strictly preserving Core session semantics.


Architecture & Design Principles

┌────────────────────────────────────────────────────────┐
│                      Application                       │
└────────────────────────────────────────────────────────┘
                            │
                            ▼
┌────────────────────────────────────────────────────────┐
│                   GoModularAuth Core                   │
│   (SessionStore, auth.Auth, Transaction, Events/Hooks) │
└────────────────────────────────────────────────────────┘
                            │
                            ▼
┌────────────────────────────────────────────────────────┐
│             gomodularauth-multi-session                │
│   - MultiSession Capability Interface                  │
│   - Enforces Max Concurrent Sessions via Hooks         │
│   - Device & Session Metadata Management               │
│   - Store-First Atomic Revocations                     │
│   - Immediate Cache Invalidation                       │
└────────────────────────────────────────────────────────┘
  • Reuses Core Session Entities: Never creates an alternative session model. Operates directly on session.Session from github.com/BladiCreator/go-modular-auth/session.
  • Core SessionStore as Source of Truth: All session persistence and revocations use the Core store.SessionStore.
  • Store-First Revocation: Revocations update the persistent store first before invalidating cache or emitting events.
  • Immediate Cache Purging: Invalidation removes cached session tokens immediately upon successful store commit.
  • Concurrency Safe: Handles simultaneous revocation requests and concurrency limit enforcement safely.
  • Zero Framework Bloat: No HTTP or third-party web framework dependencies.

Installation

go get github.com/BladiCreator/go-modular-auth/plugins/multisession

Quick Start

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/BladiCreator/go-modular-auth/auth"
	memcache "github.com/BladiCreator/go-modular-auth/cache/memory"
	adaptermem "github.com/BladiCreator/go-modular-auth/adapter/memory"
	multisession "github.com/BladiCreator/go-modular-auth/plugins/multisession"
	"github.com/BladiCreator/go-modular-auth/plugins/multisession/plugins"
)

func main() {
	ctx := context.Background()

	// 1. Initialize GoModularAuth with MultiSession plugin
	app, err := auth.New(auth.Config{
		Adapter: adaptermem.New(),
		Cache: memcache.New(),
		Plugins: []auth.PluginModule{
			multisession.New(
				multisession.WithMaxConcurrentSessions(5, multisession.OverflowRevokeOldest),
			),
		},
	})
	if err != nil {
		log.Fatalf("failed to initialize auth: %v", err)
	}

	// 2. Retrieve the typed MultiSession capability
	sessionMgr, err := auth.Get[plugins.MultiSession](app)
	if err != nil {
		log.Fatalf("failed to resolve capability: %v", err)
	}

	// 3. User signs in, producing sessions
	up, err := app.SignUp(ctx, auth.SignUpInput{
		Email:    "alice@example.com",
		Password: "SuperSecurePassword123!",
		Session: auth.SessionOptions{
			IPAddress: "192.168.1.100",
			UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0.0.0",
		},
	})
	if err != nil {
		log.Fatalf("signup failed: %v", err)
	}

	// 4. List user's active sessions with rich device metadata
	sessions, err := sessionMgr.ListDetailedSessions(ctx, up.User.ID, up.Session.ID)
	if err != nil {
		log.Fatalf("list sessions failed: %v", err)
	}

	for _, s := range sessions {
		fmt.Printf("Session ID: %s | Device: %s (%s) | Current: %t\n",
			s.Session.ID, s.Metadata.Device.Name, s.Metadata.Device.OS, s.IsCurrent)
	}

	// 5. Revoke other sessions (e.g. "Log out of all other devices")
	if err := sessionMgr.RevokeOtherSessions(ctx, up.User.ID, up.Session.ID); err != nil {
		log.Fatalf("failed to revoke other sessions: %v", err)
	}
}

Configuration

Configure the plugin using functional options in multisession.New(...):

mod := multisession.New(
    // Limit to 3 concurrent active sessions.
    // When exceeded, automatically evict the oldest session (FIFO).
    multisession.WithMaxConcurrentSessions(3, multisession.OverflowRevokeOldest),

    // Or reject new session creation when limit is reached:
    // multisession.WithMaxConcurrentSessions(3, multisession.OverflowReject),

    // Custom metadata persistence store (defaults to thread-safe in-memory store)
    multisession.WithMetadataStore(customStore),

    // Metadata cache TTL (default: 10 minutes)
    multisession.WithMetadataCacheTTL(15 * time.Minute),
)

Public API

Capability Interface

Applications access the capability via auth.Get[plugins.MultiSession](app) or auth.Get[multisession.MultiSession](app):

type MultiSession interface {
    // ListSessions returns all active, valid sessions for a user from Core SessionStore.
    ListSessions(ctx context.Context, userID string) ([]*session.Session, error)

    // ListDetailedSessions returns all active sessions for a user, enriched with device
    // and custom metadata, and flags the current session if currentSessionID matches.
    ListDetailedSessions(ctx context.Context, userID string, currentSessionID string) ([]*SessionDetail, error)

    // GetSession returns an active session by its ID. Returns ErrSessionNotFound if not found or expired.
    GetSession(ctx context.Context, sessionID string) (*session.Session, error)

    // GetDetailedSession returns an active session enriched with its metadata.
    GetDetailedSession(ctx context.Context, sessionID string) (*SessionDetail, error)

    // RevokeSession revokes a specific session by ID, updating the Store first,
    // invalidating relevant Cache entries, and publishing revocation events.
    RevokeSession(ctx context.Context, sessionID string) error

    // RevokeOtherSessions revokes all active sessions for userID except currentSessionID.
    RevokeOtherSessions(ctx context.Context, userID, currentSessionID string) error

    // RevokeAllSessions revokes every active session for userID.
    RevokeAllSessions(ctx context.Context, userID string) error

    // SetMetadata persists or updates device and custom metadata for a session.
    SetMetadata(ctx context.Context, sessionID string, meta MetadataInput) error

    // GetMetadata retrieves metadata associated with a session.
    GetMetadata(ctx context.Context, sessionID string) (*Metadata, error)
}
Domain Errors

Sentinel errors allow explicit error handling:

  • multisession.ErrSessionNotFound: Session does not exist or has expired.
  • multisession.ErrSessionAlreadyRevoked: Session is already revoked.
  • multisession.ErrInvalidInput: Required parameter (e.g. userID, sessionID) is empty.
  • multisession.ErrUnauthorized: Target session does not belong to the user.
  • multisession.ErrMaxConcurrentSessionsReached: Session limit reached under OverflowReject policy.
  • multisession.ErrMetadataNotFound: No metadata recorded for session.

Device & Session Metadata

When sessions are created, User-Agent strings and client IP addresses are automatically parsed into typed DeviceMetadata:

type DeviceMetadata struct {
    Name      string `json:"name,omitempty"`      // e.g. "Chrome on macOS"
    Type      string `json:"type,omitempty"`      // e.g. "desktop", "mobile", "tablet", "bot"
    OS        string `json:"os,omitempty"`        // e.g. "macOS", "iOS", "Windows", "Linux", "Android"
    Browser   string `json:"browser,omitempty"`   // e.g. "Chrome", "Safari", "Firefox", "Edge"
    IPAddress string `json:"ip_address,omitempty"`
}

Applications can also associate custom key-values (e.g. 2FA verification status, office location) via SetMetadata:

err := sessionMgr.SetMetadata(ctx, sessionID, multisession.MetadataInput{
    Device: multisession.DeviceMetadata{
        Name: "HQ Office Desktop",
    },
    Custom: map[string]string{
        "trusted_device": "true",
        "office":         "Zurich",
    },
})

Persistence & SQL Migrations

Session records reside in the Core sessions table. Optional metadata records are stored in session_metadata:

CREATE TABLE IF NOT EXISTS session_metadata (
    session_id      VARCHAR(64) PRIMARY KEY,
    user_id         VARCHAR(64) NOT NULL,
    device_name     VARCHAR(255),
    device_type     VARCHAR(64),
    device_os       VARCHAR(64),
    device_browser  VARCHAR(64),
    device_ip       VARCHAR(64),
    custom_metadata TEXT,
    created_at      TIMESTAMP WITH TIME ZONE NOT NULL,
    updated_at      TIMESTAMP WITH TIME ZONE NOT NULL,
    CONSTRAINT fk_session_metadata_session
        FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_session_metadata_user_id ON session_metadata(user_id);

An in-memory metadata store (store/memory) is enabled by default.


Cache Invalidation Strategy

The plugin maintains strict cache-aside semantics:

  1. Store is Source of Truth: All operations commit to the database before modifying cache.
  2. Immediate Invalidation:
    • RevokeSession: Stores revocation, then deletes gomodularauth:session:<tokenHash> and gomodularauth:multisession:meta:<sessionID>.
    • RevokeOtherSessions / RevokeAllSessions: Atomically revokes sessions inside Store transaction, then invalidates all corresponding cache entries post-commit.
  3. Resilience: Cache misses degrade gracefully to the persistent Store.

Observational Events

The plugin publishes non-sensitive events to the GoModularAuth event dispatcher:

  • multisession.session_revoked: A single session was revoked.
  • multisession.sessions_revoked: Bulk sessions were revoked (e.g. revoke others or revoke all).
  • multisession.session_evicted: A session was evicted due to concurrent session limits.
  • multisession.metadata_updated: Metadata was saved or updated.

Payloads never include secrets or token hashes.


Security Model

  • Authorization & Ownership Checks: RevokeOtherSessions verifies that the currentSessionID belongs to the requesting userID before revoking other sessions, preventing cross-tenant privilege escalation.
  • No Secret Exposure: Plaintext tokens are never stored, logged, or exposed. Tokens hashes are never included in event payloads.
  • Concurrent Revocation Safety: Calling RevokeSession concurrently on the same session executes idempotently without race conditions.
  • Atomic Concurrency Limiting: BeforeSessionCreate hook executes inside the Core Store transaction, ensuring session counts cannot be exceeded through parallel sign-ins.

Testing

Run tests across the entire module:

go test -v ./...
go vet ./...

License

MIT License. See LICENSE for details.

Documentation

Index

Constants

View Source
const (
	EventSessionRevoked  auth.EventName = "multisession.session_revoked"
	EventSessionsRevoked auth.EventName = "multisession.sessions_revoked"
	EventSessionEvicted  auth.EventName = "multisession.session_evicted"
	EventMetadataUpdated auth.EventName = "multisession.metadata_updated"
)

Event names emitted by the multisession module.

View Source
const (
	ModelSessionMetadata = "session_metadata"
)
View Source
const PluginID = "multisession"

PluginID identifies this plugin in the GoModularAuth runtime.

Variables

View Source
var (
	// ErrSessionNotFound is returned when the target session does not exist.
	ErrSessionNotFound = errors.New("multisession: session not found")

	// ErrSessionAlreadyRevoked is returned when operating on an already revoked session.
	ErrSessionAlreadyRevoked = errors.New("multisession: session already revoked")

	// ErrInvalidInput is returned when required parameters (e.g., userID, sessionID) are empty or malformed.
	ErrInvalidInput = errors.New("multisession: invalid input")

	// ErrUnauthorized is returned when a session does not belong to the specified user.
	ErrUnauthorized = errors.New("multisession: unauthorized session access")

	// ErrMaxConcurrentSessionsReached is returned when a new session cannot be created
	// because the user reached the configured maximum allowed concurrent sessions and
	// the overflow policy is set to OverflowReject.
	ErrMaxConcurrentSessionsReached = errors.New("multisession: maximum concurrent sessions reached")

	// ErrMetadataNotFound is returned when no metadata is stored for the requested session.
	ErrMetadataNotFound = errors.New("multisession: session metadata not found")
)

Functions

func RegisterSchema

func RegisterSchema(s *schema.Schema) error

RegisterSchema registers the session_metadata model with the unified schema.

Types

type Capability

type Capability = MultiSession

Capability is an alias for MultiSession to support standard capability discovery conventions.

type Config

type Config struct {
	// MaxConcurrentSessions sets the maximum concurrent active sessions per user.
	// 0 or negative value disables concurrency limiting.
	MaxConcurrentSessions int

	// OverflowPolicy controls what happens when the session concurrency limit is exceeded.
	OverflowPolicy OverflowPolicy

	// MetadataCacheTTL controls how long session metadata is cached.
	MetadataCacheTTL time.Duration
}

Config holds configuration parameters for the multisession plugin.

type DeviceMetadata

type DeviceMetadata struct {
	Name      string `json:"name,omitempty"`
	Type      string `json:"type,omitempty"`
	OS        string `json:"os,omitempty"`
	Browser   string `json:"browser,omitempty"`
	IPAddress string `json:"ip_address,omitempty"`
}

DeviceMetadata contains optional device identification information.

func ParseUserAgent

func ParseUserAgent(ua, ip string) DeviceMetadata

ParseUserAgent extracts basic device metadata from a User-Agent string and IP address using pure Go string matching without external dependencies.

type DeviceRecord

type DeviceRecord struct {
	Name      string `json:"name,omitempty"`
	Type      string `json:"type,omitempty"`
	OS        string `json:"os,omitempty"`
	Browser   string `json:"browser,omitempty"`
	IPAddress string `json:"ip_address,omitempty"`
}

DeviceRecord contains persistent device identification details.

type Metadata

type Metadata struct {
	SessionID string            `json:"session_id"`
	UserID    string            `json:"user_id"`
	Device    DeviceMetadata    `json:"device"`
	Custom    map[string]string `json:"custom,omitempty"`
	CreatedAt time.Time         `json:"created_at"`
	UpdatedAt time.Time         `json:"updated_at"`
}

Metadata represents optional session and device metadata associated with a Core Session.

type MetadataInput

type MetadataInput struct {
	Device DeviceMetadata    `json:"device"`
	Custom map[string]string `json:"custom,omitempty"`
}

MetadataInput defines parameters for setting or updating session metadata.

type MetadataRecord

type MetadataRecord struct {
	SessionID string            `json:"session_id"`
	UserID    string            `json:"user_id"`
	Device    DeviceRecord      `json:"device"`
	Custom    map[string]string `json:"custom,omitempty"`
	CreatedAt time.Time         `json:"created_at"`
	UpdatedAt time.Time         `json:"updated_at"`
}

MetadataRecord is the persistent representation of session metadata.

type MetadataUpdatedPayload

type MetadataUpdatedPayload struct {
	SessionID string    `json:"session_id"`
	UserID    string    `json:"user_id"`
	UpdatedAt time.Time `json:"updated_at"`
}

MetadataUpdatedPayload represents observational data when session metadata is set or changed.

type Module

type Module struct {
	// contains filtered or unexported fields
}

Module implements auth.PluginModule using plugin-sdk.

func New

func New(opts ...Option) *Module

New creates an installable multisession PluginModule with optional configurations.

func (*Module) ID

func (m *Module) ID() string

ID returns the module's unique identifier.

func (*Module) Init

func (m *Module) Init(h *auth.Host) error

Init configures infrastructure, registers hooks, and provides the typed Capability into GoModularAuth via plugin-sdk.

func (*Module) RegisterSchema

func (m *Module) RegisterSchema(s *schema.Schema) error

RegisterSchema registers the plugin's models into the schema.

type MultiSession

type MultiSession interface {
	// ListSessions returns all active, valid sessions for a user from Core SessionStore.
	ListSessions(ctx context.Context, userID string) ([]*session.Session, error)

	// ListDetailedSessions returns all active sessions for a user, enriched with device
	// and custom metadata, and flags the current session if currentSessionID matches.
	ListDetailedSessions(ctx context.Context, userID string, currentSessionID string) ([]*SessionDetail, error)

	// GetSession returns an active session by its ID. Returns ErrSessionNotFound if not found or expired.
	GetSession(ctx context.Context, sessionID string) (*session.Session, error)

	// GetDetailedSession returns an active session enriched with its metadata.
	GetDetailedSession(ctx context.Context, sessionID string) (*SessionDetail, error)

	// RevokeSession revokes a specific session by ID, updating the Store first,
	// invalidating relevant Cache entries, and publishing revocation events.
	RevokeSession(ctx context.Context, sessionID string) error

	// RevokeOtherSessions revokes all active sessions for userID except currentSessionID.
	RevokeOtherSessions(ctx context.Context, userID, currentSessionID string) error

	// RevokeAllSessions revokes every active session for userID.
	RevokeAllSessions(ctx context.Context, userID string) error

	// SetMetadata persists or updates device and custom metadata for a session.
	SetMetadata(ctx context.Context, sessionID string, meta MetadataInput) error

	// GetMetadata retrieves metadata associated with a session.
	GetMetadata(ctx context.Context, sessionID string) (*Metadata, error)
}

MultiSession is the strongly typed capability provided by the multisession module.

type Option

type Option func(*Config)

Option configures the multisession plugin.

func WithMaxConcurrentSessions

func WithMaxConcurrentSessions(limit int, policy ...OverflowPolicy) Option

WithMaxConcurrentSessions configures the concurrent session limit and optional overflow policy.

func WithMetadataCacheTTL

func WithMetadataCacheTTL(ttl time.Duration) Option

WithMetadataCacheTTL sets the cache duration for session metadata.

func WithOverflowPolicy

func WithOverflowPolicy(policy OverflowPolicy) Option

WithOverflowPolicy sets the strategy when MaxConcurrentSessions is exceeded.

type OverflowPolicy

type OverflowPolicy string

OverflowPolicy defines the action taken when a user exceeds MaxConcurrentSessions.

const (
	// OverflowRevokeOldest automatically revokes the user's oldest active session(s)
	// to make room for the newly requested session (FIFO eviction).
	OverflowRevokeOldest OverflowPolicy = "revoke_oldest"

	// OverflowReject rejects the new session creation request by returning
	// ErrMaxConcurrentSessionsReached.
	OverflowReject OverflowPolicy = "reject"
)

type SessionDetail

type SessionDetail struct {
	Session   *session.Session `json:"session"`
	Metadata  *Metadata        `json:"metadata,omitempty"`
	IsCurrent bool             `json:"is_current"`
}

SessionDetail represents a Core session enriched with optional metadata and current status flag. Crucially, it reuses the Core session.Session entity and does not replace it.

type SessionEvictedPayload

type SessionEvictedPayload struct {
	SessionID string    `json:"session_id"`
	UserID    string    `json:"user_id"`
	EvictedAt time.Time `json:"evicted_at"`
	Reason    string    `json:"reason"`
}

SessionEvictedPayload represents observational data when a session is evicted due to concurrency limits.

type SessionRevokedPayload

type SessionRevokedPayload struct {
	SessionID string    `json:"session_id"`
	UserID    string    `json:"user_id"`
	RevokedAt time.Time `json:"revoked_at"`
	Reason    string    `json:"reason,omitempty"`
}

SessionRevokedPayload represents observational data when a single session is revoked.

type SessionsRevokedPayload

type SessionsRevokedPayload struct {
	UserID          string    `json:"user_id"`
	RevokedCount    int       `json:"revoked_count"`
	ExceptSessionID string    `json:"except_session_id,omitempty"`
	RevokedAt       time.Time `json:"revoked_at"`
	Reason          string    `json:"reason,omitempty"`
}

SessionsRevokedPayload represents observational data when bulk sessions are revoked.

Directories

Path Synopsis
Package plugins exposes the capability aliases for GoModularAuth plugins.
Package plugins exposes the capability aliases for GoModularAuth plugins.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL