// package sessionmdl provides APIs to Add, Validate and Delete user sessions. These APIs must be used along with JWT Auth. // If you want to use this functionality, a jwt token must contain `userId` and `sessionId` fields. // A user can have multiple active sessions for different use cases. To check if user has an active session for particular usecase use `CheckForSessionAvailability()`. // And to check user session on each request use `ValidateSessionFromToken()`. // // An in memory cache is used to store sessions. It automatically falls back to redis cache if -gridmode=1 is set. // // The expiraion of session must be same as of token expiration. package sessionmdl import ( "errors" "corelab.mkcl.org/MKCLOS/coredevelopmentplatform/corepkgv2/cachemdl" ) type Session struct { SessionFor string SessionId string } // store is used to store sessions in memory, falls back to redis cache on grid mode. var store cachemdl.Cacher var ( ErrUserNotFound = errors.New("user not found") ErrSessionNotFound = errors.New("session not found") ErrInvalidSessionInstance = errors.New("got invalid session instance id") ErrSessionValidationFailed = errors.New("session validation failed") ) // Init initializes sessions with provided cache. Subsequent calls will not have any effect after first initialization. func Init(cache cachemdl.Cacher) { if store != nil { return } store = cache } // Set stores the sessions for provided userId. Session is appended to the list. It does not check if the same session exists or not. func Set(userId string, s ...Session) { i, ok := store.Get(userId) if !ok || i == nil { set(userId, s) return } sessions, ok := i.([]Session) if !ok { set(userId, s) return } set(userId, append(sessions, s...)) } func set(key string, val interface{}) { // Set the user sessions with no expiry as each session can have different expiry depending on the JWT token expiry. store.SetNoExpiration(key, val) } // Get returns all the available sessions for the user. This may contain expired but not deleted sessions. func Get(userId string) ([]Session, error) { var ( s []Session i interface{} ok bool ) i, ok = store.Get(userId) if !ok { return s, ErrUserNotFound } s, _ = i.([]Session) // if !ok { // return s, errors.New("failed to retrieve previous sessions") // } return s, nil } // Delete removes all the sessions associated with the user. func Delete(userId string) { store.Delete(userId) } // DeleteSession removes a particular session for user, if present. func DeleteSession(userId, sessionFor string) { sessions, err := Get(userId) if err != nil { return } for i := 0; i < len(sessions); i++ { if sessions[i].SessionFor == sessionFor { sessions[i] = sessions[len(sessions)-1] sessions = sessions[:len(sessions)-1] } } if len(sessions) == 0 { store.Delete(userId) return } set(userId, sessions) } // ValidateSessionFromToken checks for session id in claims against available sessions. // Validate only if a nonempty `sessionId` is present. The claims must contain `userId` field if session is present. func ValidateSessionFromToken(claims map[string]interface{}) error { // check for sessionId field, if not present then it is ignored at the time of token generation. // This means user doesn't want to validate session. i, ok := claims["sessionId"] if !ok || i == nil { return nil } sessionId, _ := i.(string) if len(sessionId) == 0 { return errors.New("\"sessionId\" field is empty") } i, ok = claims["userId"] if !ok { return errors.New("\"userId\" field not found in token") } userId, _ := i.(string) if len(userId) == 0 { return errors.New("\"userId\" field is empty") } sessions, err := Get(userId) if err != nil { return err } for i := range sessions { if sessions[i].SessionId == sessionId { return nil } } return ErrSessionNotFound } // CheckForSessionAvailability checks if the user has active session for provided `sessionFor`. Returns true if session is available. func CheckForSessionAvailability(userId, sessionFor string) bool { sessions, _ := Get(userId) for i := range sessions { if sessions[i].SessionFor == sessionFor { return true } } return false }