github.com/greenpau/go-authcrunch@v1.1.4/pkg/authn/cache/session.go (about)

     1  // Copyright 2022 Paul Greenberg greenpau@outlook.com
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package cache
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"sync"
    21  	"time"
    22  
    23  	"github.com/greenpau/go-authcrunch/pkg/user"
    24  )
    25  
    26  const defaultSessionCleanupInternal int = 60
    27  const minSessionCleanupInternal int = 0
    28  
    29  // SessionCacheEntry is an entry in SessionCache.
    30  type SessionCacheEntry struct {
    31  	sessionID string
    32  	createdAt time.Time
    33  	user      *user.User
    34  }
    35  
    36  // SessionCache contains cached tokens
    37  type SessionCache struct {
    38  	mu sync.RWMutex
    39  	// The interval (in seconds) at which cache maintenance task are being triggered.
    40  	// The default is 5 minutes (300 seconds)
    41  	cleanupInternal int
    42  	// The maximum number of seconds the cached entry is available to a user.
    43  	maxEntryLifetime int64
    44  	// If set to true, then the cache is being managed.
    45  	managed bool
    46  	// exit channel
    47  	exit    chan bool
    48  	Entries map[string]*SessionCacheEntry `json:"entries,omitempty" xml:"entries,omitempty" yaml:"entries,omitempty"`
    49  }
    50  
    51  // NewSessionCache returns SessionCache instance.
    52  func NewSessionCache() *SessionCache {
    53  	c := &SessionCache{
    54  		cleanupInternal: defaultSessionCleanupInternal,
    55  		Entries:         make(map[string]*SessionCacheEntry),
    56  		exit:            make(chan bool),
    57  	}
    58  	return c
    59  }
    60  
    61  // SetCleanupInterval sets cache management interval.
    62  func (c *SessionCache) SetCleanupInterval(i int) error {
    63  	if i < 1 {
    64  		return fmt.Errorf("session cache cleanup interval must be equal to or greater than %d", minSessionCleanupInternal)
    65  	}
    66  	c.cleanupInternal = i
    67  	return nil
    68  
    69  }
    70  
    71  func manageSessionCache(c *SessionCache) {
    72  	c.managed = true
    73  	intervals := time.NewTicker(time.Second * time.Duration(c.cleanupInternal))
    74  	for range intervals.C {
    75  		if c == nil {
    76  			continue
    77  		}
    78  		c.mu.Lock()
    79  		select {
    80  		case <-c.exit:
    81  			c.managed = false
    82  			break
    83  		default:
    84  			break
    85  		}
    86  		if !c.managed {
    87  			c.mu.Unlock()
    88  			break
    89  		}
    90  		if c.Entries == nil {
    91  			c.mu.Unlock()
    92  			continue
    93  		}
    94  		deleteList := []string{}
    95  		for sessionID, entry := range c.Entries {
    96  			if err := entry.Valid(); err != nil {
    97  				deleteList = append(deleteList, sessionID)
    98  				continue
    99  			}
   100  		}
   101  		if len(deleteList) > 0 {
   102  			for _, sessionID := range deleteList {
   103  				delete(c.Entries, sessionID)
   104  			}
   105  		}
   106  		c.mu.Unlock()
   107  	}
   108  }
   109  
   110  // Run starts management of SessionCache instance.
   111  func (c *SessionCache) Run() {
   112  	if c.managed {
   113  		return
   114  	}
   115  	go manageSessionCache(c)
   116  }
   117  
   118  // Stop stops management of SessionCache instance.
   119  func (c *SessionCache) Stop() {
   120  	c.mu.Lock()
   121  	defer c.mu.Unlock()
   122  	c.managed = false
   123  }
   124  
   125  // GetCleanupInterval returns cleanup interval.
   126  func (c *SessionCache) GetCleanupInterval() int {
   127  	return c.cleanupInternal
   128  }
   129  
   130  // Add adds user to the cache.
   131  func (c *SessionCache) Add(sessionID string, u *user.User) error {
   132  	c.mu.Lock()
   133  	defer c.mu.Unlock()
   134  	if c.Entries == nil {
   135  		return errors.New("session cache is not available")
   136  	}
   137  	c.Entries[sessionID] = &SessionCacheEntry{
   138  		sessionID: sessionID,
   139  		createdAt: time.Now().UTC(),
   140  		user:      u,
   141  	}
   142  	return nil
   143  }
   144  
   145  // Delete removes cached user entry.
   146  func (c *SessionCache) Delete(sessionID string) error {
   147  	c.mu.Lock()
   148  	defer c.mu.Unlock()
   149  	if c.Entries == nil {
   150  		return errors.New("session cache is not available")
   151  	}
   152  	_, exists := c.Entries[sessionID]
   153  	if !exists {
   154  		return errors.New("cached session id not found")
   155  	}
   156  	delete(c.Entries, sessionID)
   157  	return nil
   158  }
   159  
   160  // Get returns cached user entry.
   161  func (c *SessionCache) Get(sessionID string) (*user.User, error) {
   162  	if err := parseCacheID(sessionID); err != nil {
   163  		return nil, err
   164  	}
   165  	c.mu.RLock()
   166  	defer c.mu.RUnlock()
   167  	if entry, exists := c.Entries[sessionID]; exists {
   168  		if err := entry.Valid(); err != nil {
   169  			delete(c.Entries, sessionID)
   170  			return nil, fmt.Errorf("cached session id error: %s", err)
   171  		}
   172  		if entry.user == nil {
   173  			delete(c.Entries, sessionID)
   174  			return nil, fmt.Errorf("cached session id %s has nil user", sessionID)
   175  		}
   176  		return entry.user, nil
   177  	}
   178  	return nil, errors.New("cached session id not found")
   179  }
   180  
   181  // Valid checks whether SessionCacheEntry is not expired.
   182  func (e *SessionCacheEntry) Valid() error {
   183  	if err := e.user.Claims.Valid(); err != nil {
   184  		return err
   185  	}
   186  	return nil
   187  }
   188  
   189  // parseCacheID checks the id associated with the cached entry for format
   190  // requirements.
   191  func parseCacheID(s string) error {
   192  	if len(s) > 96 || len(s) < 32 {
   193  		return errors.New("cached id length is outside of 32-96 character range")
   194  	}
   195  	for _, c := range s {
   196  		if (c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && (c < '0' || c > '9') && (c != '-') {
   197  			return errors.New("cached id contains invalid characters")
   198  		}
   199  	}
   200  	return nil
   201  }