2019-04-28 07:30:35 +02:00
package scs
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"sort"
"sync"
"time"
)
// Status represents the state of the session data during a request cycle.
type Status int
const (
// Unmodified indicates that the session data hasn't been changed in the
// current request cycle.
Unmodified Status = iota
// Modified indicates that the session data has been changed in the current
// request cycle.
Modified
// Destroyed indicates that the session data has been destroyed in the
// current request cycle.
Destroyed
)
type sessionData struct {
2019-09-03 18:34:13 +02:00
deadline time . Time
2019-04-28 07:30:35 +02:00
status Status
token string
2019-09-03 18:34:13 +02:00
values map [ string ] interface { }
2019-04-28 07:30:35 +02:00
mu sync . Mutex
}
func newSessionData ( lifetime time . Duration ) * sessionData {
return & sessionData {
2019-09-03 18:34:13 +02:00
deadline : time . Now ( ) . Add ( lifetime ) . UTC ( ) ,
2019-04-28 07:30:35 +02:00
status : Unmodified ,
2019-09-03 18:34:13 +02:00
values : make ( map [ string ] interface { } ) ,
2019-04-28 07:30:35 +02:00
}
}
// Load retrieves the session data for the given token from the session store,
// and returns a new context.Context containing the session data. If no matching
// token is found then this will create a new session.
//
// Most applications will use the LoadAndSave() middleware and will not need to
// use this method.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) Load ( ctx context . Context , token string ) ( context . Context , error ) {
2019-04-28 07:30:35 +02:00
if _ , ok := ctx . Value ( s . contextKey ) . ( * sessionData ) ; ok {
return ctx , nil
}
if token == "" {
return s . addSessionDataToContext ( ctx , newSessionData ( s . Lifetime ) ) , nil
}
b , found , err := s . Store . Find ( token )
if err != nil {
return nil , err
} else if ! found {
return s . addSessionDataToContext ( ctx , newSessionData ( s . Lifetime ) ) , nil
}
sd := & sessionData {
status : Unmodified ,
token : token ,
}
2019-09-03 18:34:13 +02:00
sd . deadline , sd . values , err = s . Codec . Decode ( b )
2019-04-28 07:30:35 +02:00
if err != nil {
return nil , err
}
// Mark the session data as modified if an idle timeout is being used. This
// will force the session data to be re-committed to the session store with
// a new expiry time.
if s . IdleTimeout > 0 {
sd . status = Modified
}
return s . addSessionDataToContext ( ctx , sd ) , nil
}
2019-10-01 20:30:00 +02:00
// LoadNew returns a new context.Context containing a new empty instance of
// session data.
//
// Most applications will use the LoadAndSave() middleware and will not need to
// use this method.
func ( s * SessionManager ) LoadNew ( ctx context . Context ) ( context . Context , error ) {
return s . addSessionDataToContext ( ctx , newSessionData ( s . Lifetime ) ) , nil
}
2019-04-28 07:30:35 +02:00
// Commit saves the session data to the session store and returns the session
// token and expiry time.
//
// Most applications will use the LoadAndSave() middleware and will not need to
// use this method.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) Commit ( ctx context . Context ) ( string , time . Time , error ) {
2019-04-28 07:30:35 +02:00
sd := s . getSessionDataFromContext ( ctx )
sd . mu . Lock ( )
defer sd . mu . Unlock ( )
if sd . token == "" {
var err error
sd . token , err = generateToken ( )
if err != nil {
return "" , time . Time { } , err
}
}
2019-09-03 18:34:13 +02:00
b , err := s . Codec . Encode ( sd . deadline , sd . values )
2019-04-28 07:30:35 +02:00
if err != nil {
return "" , time . Time { } , err
}
2019-09-03 18:34:13 +02:00
expiry := sd . deadline
2019-04-28 07:30:35 +02:00
if s . IdleTimeout > 0 {
ie := time . Now ( ) . Add ( s . IdleTimeout )
if ie . Before ( expiry ) {
expiry = ie
}
}
err = s . Store . Commit ( sd . token , b , expiry )
if err != nil {
return "" , time . Time { } , err
}
return sd . token , expiry , nil
}
// Destroy deletes the session data from the session store and sets the session
2019-05-04 19:17:39 +02:00
// status to Destroyed. Any further operations in the same request cycle will
2019-04-28 07:30:35 +02:00
// result in a new session being created.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) Destroy ( ctx context . Context ) error {
2019-04-28 07:30:35 +02:00
sd := s . getSessionDataFromContext ( ctx )
sd . mu . Lock ( )
defer sd . mu . Unlock ( )
err := s . Store . Delete ( sd . token )
if err != nil {
return err
}
sd . status = Destroyed
// Reset everything else to defaults.
sd . token = ""
2019-09-03 18:34:13 +02:00
sd . deadline = time . Now ( ) . Add ( s . Lifetime ) . UTC ( )
for key := range sd . values {
delete ( sd . values , key )
2019-04-28 07:30:35 +02:00
}
return nil
}
// Put adds a key and corresponding value to the session data. Any existing
// value for the key will be replaced. The session data status will be set to
// Modified.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) Put ( ctx context . Context , key string , val interface { } ) {
2019-04-28 07:30:35 +02:00
sd := s . getSessionDataFromContext ( ctx )
sd . mu . Lock ( )
2019-09-03 18:34:13 +02:00
sd . values [ key ] = val
2019-04-28 07:30:35 +02:00
sd . status = Modified
sd . mu . Unlock ( )
}
// Get returns the value for a given key from the session data. The return
// value has the type interface{} so will usually need to be type asserted
// before you can use it. For example:
//
// foo, ok := session.Get(r, "foo").(string)
// if !ok {
// return errors.New("type assertion to string failed")
// }
//
// Also see the GetString(), GetInt(), GetBytes() and other helper methods which
// wrap the type conversion for common types.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) Get ( ctx context . Context , key string ) interface { } {
2019-04-28 07:30:35 +02:00
sd := s . getSessionDataFromContext ( ctx )
sd . mu . Lock ( )
defer sd . mu . Unlock ( )
2019-09-03 18:34:13 +02:00
return sd . values [ key ]
2019-04-28 07:30:35 +02:00
}
// Pop acts like a one-time Get. It returns the value for a given key from the
// session data and deletes the key and value from the session data. The
// session data status will be set to Modified. The return value has the type
// interface{} so will usually need to be type asserted before you can use it.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) Pop ( ctx context . Context , key string ) interface { } {
2019-04-28 07:30:35 +02:00
sd := s . getSessionDataFromContext ( ctx )
sd . mu . Lock ( )
defer sd . mu . Unlock ( )
2019-09-03 18:34:13 +02:00
val , exists := sd . values [ key ]
2019-04-28 07:30:35 +02:00
if ! exists {
return nil
}
2019-09-03 18:34:13 +02:00
delete ( sd . values , key )
2019-04-28 07:30:35 +02:00
sd . status = Modified
return val
}
// Remove deletes the given key and corresponding value from the session data.
// The session data status will be set to Modified. If the key is not present
// this operation is a no-op.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) Remove ( ctx context . Context , key string ) {
2019-04-28 07:30:35 +02:00
sd := s . getSessionDataFromContext ( ctx )
sd . mu . Lock ( )
defer sd . mu . Unlock ( )
2019-09-03 18:34:13 +02:00
_ , exists := sd . values [ key ]
2019-04-28 07:30:35 +02:00
if ! exists {
return
}
2019-09-03 18:34:13 +02:00
delete ( sd . values , key )
2019-04-28 07:30:35 +02:00
sd . status = Modified
}
2019-05-27 14:34:27 +02:00
// Clear removes all data for the current session. The session token and
// lifetime are unaffected. If there is no data in the current session this is
// a no-op.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) Clear ( ctx context . Context ) error {
2019-05-27 14:34:27 +02:00
sd := s . getSessionDataFromContext ( ctx )
sd . mu . Lock ( )
defer sd . mu . Unlock ( )
2019-09-03 18:34:13 +02:00
if len ( sd . values ) == 0 {
2019-05-27 14:34:27 +02:00
return nil
}
2019-09-03 18:34:13 +02:00
for key := range sd . values {
delete ( sd . values , key )
2019-05-27 14:34:27 +02:00
}
sd . status = Modified
return nil
}
2019-04-28 07:30:35 +02:00
// Exists returns true if the given key is present in the session data.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) Exists ( ctx context . Context , key string ) bool {
2019-04-28 07:30:35 +02:00
sd := s . getSessionDataFromContext ( ctx )
sd . mu . Lock ( )
2019-09-03 18:34:13 +02:00
_ , exists := sd . values [ key ]
2019-04-28 07:30:35 +02:00
sd . mu . Unlock ( )
return exists
}
// Keys returns a slice of all key names present in the session data, sorted
// alphabetically. If the data contains no data then an empty slice will be
// returned.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) Keys ( ctx context . Context ) [ ] string {
2019-04-28 07:30:35 +02:00
sd := s . getSessionDataFromContext ( ctx )
sd . mu . Lock ( )
2019-09-03 18:34:13 +02:00
keys := make ( [ ] string , len ( sd . values ) )
2019-04-28 07:30:35 +02:00
i := 0
2019-09-03 18:34:13 +02:00
for key := range sd . values {
2019-04-28 07:30:35 +02:00
keys [ i ] = key
i ++
}
sd . mu . Unlock ( )
sort . Strings ( keys )
return keys
}
// RenewToken updates the session data to have a new session token while
// retaining the current session data. The session lifetime is also reset and
// the session data status will be set to Modified.
//
// The old session token and accompanying data are deleted from the session store.
//
// To mitigate the risk of session fixation attacks, it's important that you call
// RenewToken before making any changes to privilege levels (e.g. login and
// logout operations). See https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Session_Management_Cheat_Sheet.md#renew-the-session-id-after-any-privilege-level-change
// for additional information.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) RenewToken ( ctx context . Context ) error {
2019-04-28 07:30:35 +02:00
sd := s . getSessionDataFromContext ( ctx )
sd . mu . Lock ( )
defer sd . mu . Unlock ( )
err := s . Store . Delete ( sd . token )
if err != nil {
return err
}
newToken , err := generateToken ( )
if err != nil {
return err
}
sd . token = newToken
2019-09-03 18:34:13 +02:00
sd . deadline = time . Now ( ) . Add ( s . Lifetime ) . UTC ( )
2019-04-28 07:30:35 +02:00
sd . status = Modified
return nil
}
// Status returns the current status of the session data.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) Status ( ctx context . Context ) Status {
2019-04-28 07:30:35 +02:00
sd := s . getSessionDataFromContext ( ctx )
sd . mu . Lock ( )
defer sd . mu . Unlock ( )
return sd . status
}
// GetString returns the string value for a given key from the session data.
// The zero value for a string ("") is returned if the key does not exist or the
// value could not be type asserted to a string.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) GetString ( ctx context . Context , key string ) string {
2019-04-28 07:30:35 +02:00
val := s . Get ( ctx , key )
str , ok := val . ( string )
if ! ok {
return ""
}
return str
}
// GetBool returns the bool value for a given key from the session data. The
// zero value for a bool (false) is returned if the key does not exist or the
// value could not be type asserted to a bool.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) GetBool ( ctx context . Context , key string ) bool {
2019-04-28 07:30:35 +02:00
val := s . Get ( ctx , key )
b , ok := val . ( bool )
if ! ok {
return false
}
return b
}
// GetInt returns the int value for a given key from the session data. The
// zero value for an int (0) is returned if the key does not exist or the
// value could not be type asserted to an int.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) GetInt ( ctx context . Context , key string ) int {
2019-04-28 07:30:35 +02:00
val := s . Get ( ctx , key )
i , ok := val . ( int )
if ! ok {
return 0
}
return i
}
// GetFloat returns the float64 value for a given key from the session data. The
// zero value for an float64 (0) is returned if the key does not exist or the
// value could not be type asserted to a float64.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) GetFloat ( ctx context . Context , key string ) float64 {
2019-04-28 07:30:35 +02:00
val := s . Get ( ctx , key )
f , ok := val . ( float64 )
if ! ok {
return 0
}
return f
}
// GetBytes returns the byte slice ([]byte) value for a given key from the session
// data. The zero value for a slice (nil) is returned if the key does not exist
// or could not be type asserted to []byte.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) GetBytes ( ctx context . Context , key string ) [ ] byte {
2019-04-28 07:30:35 +02:00
val := s . Get ( ctx , key )
b , ok := val . ( [ ] byte )
if ! ok {
return nil
}
return b
}
// GetTime returns the time.Time value for a given key from the session data. The
// zero value for a time.Time object is returned if the key does not exist or the
// value could not be type asserted to a time.Time. This can be tested with the
// time.IsZero() method.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) GetTime ( ctx context . Context , key string ) time . Time {
2019-04-28 07:30:35 +02:00
val := s . Get ( ctx , key )
t , ok := val . ( time . Time )
if ! ok {
return time . Time { }
}
return t
}
// PopString returns the string value for a given key and then deletes it from the
// session data. The session data status will be set to Modified. The zero
// value for a string ("") is returned if the key does not exist or the value
// could not be type asserted to a string.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) PopString ( ctx context . Context , key string ) string {
2019-04-28 07:30:35 +02:00
val := s . Pop ( ctx , key )
str , ok := val . ( string )
if ! ok {
return ""
}
return str
}
// PopBool returns the bool value for a given key and then deletes it from the
// session data. The session data status will be set to Modified. The zero
// value for a bool (false) is returned if the key does not exist or the value
// could not be type asserted to a bool.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) PopBool ( ctx context . Context , key string ) bool {
2019-04-28 07:30:35 +02:00
val := s . Pop ( ctx , key )
b , ok := val . ( bool )
if ! ok {
return false
}
return b
}
// PopInt returns the int value for a given key and then deletes it from the
// session data. The session data status will be set to Modified. The zero
// value for an int (0) is returned if the key does not exist or the value could
// not be type asserted to an int.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) PopInt ( ctx context . Context , key string ) int {
2019-04-28 07:30:35 +02:00
val := s . Pop ( ctx , key )
i , ok := val . ( int )
if ! ok {
return 0
}
return i
}
// PopFloat returns the float64 value for a given key and then deletes it from the
// session data. The session data status will be set to Modified. The zero
// value for an float64 (0) is returned if the key does not exist or the value
// could not be type asserted to a float64.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) PopFloat ( ctx context . Context , key string ) float64 {
2019-04-28 07:30:35 +02:00
val := s . Pop ( ctx , key )
f , ok := val . ( float64 )
if ! ok {
return 0
}
return f
}
// PopBytes returns the byte slice ([]byte) value for a given key and then
// deletes it from the from the session data. The session data status will be
// set to Modified. The zero value for a slice (nil) is returned if the key does
// not exist or could not be type asserted to []byte.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) PopBytes ( ctx context . Context , key string ) [ ] byte {
2019-04-28 07:30:35 +02:00
val := s . Pop ( ctx , key )
b , ok := val . ( [ ] byte )
if ! ok {
return nil
}
return b
}
// PopTime returns the time.Time value for a given key and then deletes it from
// the session data. The session data status will be set to Modified. The zero
// value for a time.Time object is returned if the key does not exist or the
// value could not be type asserted to a time.Time.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) PopTime ( ctx context . Context , key string ) time . Time {
2019-04-28 07:30:35 +02:00
val := s . Pop ( ctx , key )
t , ok := val . ( time . Time )
if ! ok {
return time . Time { }
}
return t
}
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) addSessionDataToContext ( ctx context . Context , sd * sessionData ) context . Context {
2019-04-28 07:30:35 +02:00
return context . WithValue ( ctx , s . contextKey , sd )
}
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) getSessionDataFromContext ( ctx context . Context ) * sessionData {
2019-04-28 07:30:35 +02:00
c , ok := ctx . Value ( s . contextKey ) . ( * sessionData )
if ! ok {
panic ( "scs: no session data in context" )
}
return c
}
func generateToken ( ) ( string , error ) {
b := make ( [ ] byte , 32 )
_ , err := rand . Read ( b )
if err != nil {
return "" , err
}
return base64 . RawURLEncoding . EncodeToString ( b ) , nil
}
type contextKey string
var contextKeyID int
func generateContextKey ( ) contextKey {
contextKeyID = contextKeyID + 1
return contextKey ( fmt . Sprintf ( "session.%d" , contextKeyID ) )
}