diff --git a/rails-session.cabal b/rails-session.cabal
--- a/rails-session.cabal
+++ b/rails-session.cabal
@@ -1,5 +1,5 @@
 name:                rails-session
-version:             0.1.3.0
+version:             0.1.4.0
 synopsis:            Decrypt Ruby on Rails sessions in Haskell
 description:         Please see README.md
 homepage:            http://github.com/iconnect/rails-session#readme
@@ -12,6 +12,8 @@
 build-type:          Simple
 cabal-version:       >=1.10
 extra-source-files:  CHANGELOG.md
+tested-with:         GHC ==9.4.8 || ==9.6.4
+data-files:          test/Rails3 test/Rails4 test/Rails7
 
 Source-repository head
   type: git
@@ -19,40 +21,48 @@
 
 library
   hs-source-dirs:      src
-  exposed-modules:     Web.Rails.Session
+  exposed-modules:     Web.Rails.Session.Types
+                     , Web.Rails4.Session
                      , Web.Rails3.Session
-  build-depends:       base              >= 4.7 && < 5
+                     , Web.Rails7.Session
+  build-depends:       aeson <= 3.0
+                     , base              >= 4.7 && < 5
                      , base-compat       >= 0.8.2
+                     , base16-bytestring >= 1.0.1.0
                      , base64-bytestring >= 1.0.0.1
+                     , bytestring
                      , bytestring        >= 0.10.6.0
-                     , cryptonite        >= 0.6
+                     , containers
+                     , crypton           >= 1.0.0
                      , http-types        >= 0.8.6
+                     , memory
                      , pbkdf             >= 1.1.1.1
                      , ruby-marshal      >= 0.1.1
                      , string-conv       >= 0.1
+                     , text              < 3
                      , vector            >= 0.10.12.3
-                     , bytestring
-                     , base16-bytestring >= 1.0.1.0
-                     , containers
   if impl(ghc < 8.0)
      build-depends: semigroups
-  default-language:    Haskell2010
+  default-language:    GHC2021
 
 test-suite specs
   ghc-options:         -Wall
   type:                exitcode-stdio-1.0
   hs-source-dirs:      test
   main-is:             Spec.hs
-  build-depends:       base
+  build-depends:       aeson
+                     , aeson-qq
+                     , base
                      , bytestring
                      , filepath
                      , hspec
-                     , semigroups
                      , rails-session
                      , ruby-marshal
+                     , semigroups
                      , tasty
                      , tasty-hspec
+                     , text
                      , transformers
                      , vector
   ghc-options:         -threaded -rtsopts -with-rtsopts=-N
-  default-language:    Haskell2010
+  default-language:    GHC2021
diff --git a/src/Web/Rails/Session.hs b/src/Web/Rails/Session.hs
deleted file mode 100644
--- a/src/Web/Rails/Session.hs
+++ /dev/null
@@ -1,210 +0,0 @@
-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE PackageImports #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-
-module Web.Rails.Session (
-  -- * Decoding
-    decode
-  , decodeEither
-  -- * Decrypting
-  , decrypt
-  -- * Utilities
-  , csrfToken
-  , sessionId
-  , lookupString
-  , lookupFixnum
-  -- * Lifting weaker types into stronger types
-  , Cookie
-  , mkCookie
-  , Salt
-  , mkSalt
-  , SecretKeyBase
-  , mkSecretKeyBase
-  , DecryptedData
-  , unwrapDecryptedData
-  ) where
-
-import              Control.Applicative ((<$>))
-import "cryptonite" Crypto.Cipher.AES (AES256)
-import "cryptonite" Crypto.Cipher.Types (cbcDecrypt, cipherInit, makeIV)
-import "cryptonite" Crypto.Error (CryptoFailable(CryptoFailed, CryptoPassed))
-import              Crypto.PBKDF.ByteString (sha1PBKDF2)
-import              Data.ByteString (ByteString)
-import qualified    Data.ByteString as BS
-import qualified    Data.ByteString.Base64 as B64
-import              Data.Either (Either(..), either)
-import              Data.Function.Compat ((&))
-import              Data.Maybe (Maybe(..), fromMaybe)
-import              Data.Monoid ((<>))
-import              Data.Ruby.Marshal (RubyObject(..), RubyStringEncoding(..))
-import qualified    Data.Ruby.Marshal as Ruby
-import              Data.String.Conv (toS)
-import qualified    Data.Vector as Vec
-import              Network.HTTP.Types (urlDecode)
-import              Prelude (Bool(..), Eq, Int, Ord, Show, String, ($!), (.)
-                            , (==), const, error, fst, show, snd)
-
--- TYPES
-
--- | Wrapper around data after it has been decrypted.
-newtype DecryptedData =
-  DecryptedData ByteString
-  deriving (Show, Ord, Eq)
-
--- | Wrapper around data before it has been decrypted.
-newtype EncryptedData =
-  EncryptedData ByteString
-  deriving (Show, Ord, Eq)
-
--- | Wrapper around initialisation vector.
-newtype InitVector =
-  InitVector ByteString
-  deriving (Show, Ord, Eq)
-
--- | Wrapper around raw cookie.
-newtype Cookie =
-  Cookie ByteString
-  deriving (Show, Ord, Eq)
-
--- | Wrapper around salt.
-newtype Salt =
-  Salt ByteString
-  deriving (Show, Ord, Eq)
-
--- | Wrapper around secret.
-newtype SecretKey =
-  SecretKey ByteString
-  deriving (Show, Ord, Eq)
-
--- | Wrapper around secret key base.
-newtype SecretKeyBase =
-  SecretKeyBase ByteString
-  deriving (Show, Ord, Eq)
-
--- SMART CONSTRUCTORS
-
--- | Lift a cookie into a richer type.
-mkCookie :: ByteString -> Cookie
-mkCookie = Cookie
-
--- | Lift salt into a richer type.
-mkSalt :: ByteString -> Salt
-mkSalt = Salt
-
--- | Lifts secret into a richer type.
-mkSecretKeyBase :: ByteString -> SecretKeyBase
-mkSecretKeyBase = SecretKeyBase
-
--- SMART DESTRUCTORS
-
-unwrapDecryptedData :: DecryptedData -> ByteString
-unwrapDecryptedData (DecryptedData deData) =
-  deData
-
--- EXPORTS
-
--- | Decode a cookie encrypted by Rails.
-decode :: Maybe Salt
-       -> SecretKeyBase
-       -> Cookie
-       -> Maybe RubyObject
-decode mbSalt secretKeyBase cookie =
-  either (const Nothing) Just (decodeEither mbSalt secretKeyBase cookie)
-
--- | Decode a cookie encrypted by Rails and retain some error information on failure.
-decodeEither :: Maybe Salt
-             -> SecretKeyBase
-             -> Cookie
-             -> Either String RubyObject
-decodeEither mbSalt secretKeyBase cookie = do
-  case decrypt mbSalt secretKeyBase cookie of
-    Left errorMessage ->
-      Left errorMessage
-    Right (DecryptedData deData) ->
-      Ruby.decodeEither deData
-
--- | Decrypts a cookie encrypted by Rails. Use this if you are using a
--- serialisation format other than Ruby's Marshal format.
-decrypt :: Maybe Salt
-        -> SecretKeyBase
-        -> Cookie
-        -> Either String DecryptedData
-decrypt mbSalt secretKeyBase cookie =
-  let salt = fromMaybe defaultSalt mbSalt
-      (SecretKey secret) = generateSecret salt secretKeyBase
-      (EncryptedData encData, InitVector initVec) = prepare cookie
-  in case makeIV initVec of
-       Nothing ->
-         Left $! "Failed to build init. vector for: " <> show initVec
-       Just initVec' -> do
-         let key = BS.take 32 secret
-         case (cipherInit key :: CryptoFailable AES256) of
-           CryptoFailed errorMessage ->
-             Left (show errorMessage)
-           CryptoPassed cipher ->
-             Right . DecryptedData $! cbcDecrypt cipher initVec' encData
-  where
-    defaultSalt :: Salt
-    defaultSalt = Salt "encrypted cookie"
-
--- UTIL
-
--- | Helper function for looking up the csrf token in a cooie.
-csrfToken :: RubyObject -> Maybe ByteString
-csrfToken = lookupString "_csrf_token" US_ASCII
-
--- | Helper function for looking up the session id in a cookie.
-sessionId :: RubyObject -> Maybe ByteString
-sessionId = lookupString "session_id" UTF_8
-
--- | Lookup integer for a given key.
-lookupFixnum :: ByteString -> RubyStringEncoding -> RubyObject -> Maybe Int
-lookupFixnum key enc rubyObject =
-  case lookup (RIVar (RString key, enc)) rubyObject of
-    Just (RFixnum val) ->
-      Just val
-    _ ->
-      Nothing
-
--- | Lookup string for a given key and throw away encoding information.
-lookupString :: ByteString
-             -> RubyStringEncoding
-             -> RubyObject
-             -> Maybe ByteString
-lookupString key enc rubyObject =
-  case lookup (RIVar (RString key, enc)) rubyObject of
-    Just (RIVar (RString val, _)) ->
-      Just val
-    _ ->
-      Nothing
-
--- PRIVATE
-
--- | Generate secret key using same cryptographic routines as Rails.
-generateSecret :: Salt -> SecretKeyBase -> SecretKey
-generateSecret (Salt salt) (SecretKeyBase secret) =
-  SecretKey $! sha1PBKDF2 secret salt 1000 64
-
--- | Prepare a cookie for decryption.
-prepare :: Cookie -> (EncryptedData, InitVector)
-prepare (Cookie cookie) =
-  urlDecode True cookie
-  & (fst . split)
-  & base64decode
-  & split
-  & (\(x, y) -> (EncryptedData (base64decode x), InitVector (base64decode y)))
-  where
-    base64decode :: ByteString -> ByteString
-    base64decode = B64.decodeLenient
-
-    separator :: ByteString
-    separator = "--"
-
-    split :: ByteString -> (ByteString, ByteString)
-    split = BS.breakSubstring separator
-
--- | Lookup value for a given key.
-lookup :: RubyObject -> RubyObject -> Maybe RubyObject
-lookup key (RHash vec) = snd <$> Vec.find (\element -> fst element == key) vec
-lookup _ _ = Nothing
diff --git a/src/Web/Rails/Session/Types.hs b/src/Web/Rails/Session/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Rails/Session/Types.hs
@@ -0,0 +1,62 @@
+module Web.Rails.Session.Types where
+
+import Data.ByteString (ByteString)
+
+-- TYPES
+
+newtype Secret = Secret ByteString
+
+-- | Wrapper around data after it has been decrypted.
+newtype DecryptedData =
+  DecryptedData ByteString
+  deriving (Show, Ord, Eq)
+
+-- | Wrapper around data before it has been decrypted.
+newtype EncryptedData =
+  EncryptedData ByteString
+  deriving (Show, Ord, Eq)
+
+-- | Wrapper around initialisation vector.
+newtype InitVector =
+  InitVector ByteString
+  deriving (Show, Ord, Eq)
+
+-- | Wrapper around raw cookie.
+newtype Cookie =
+  Cookie ByteString
+  deriving (Show, Ord, Eq)
+
+-- | Wrapper around salt.
+newtype Salt =
+  Salt ByteString
+  deriving (Show, Ord, Eq)
+
+-- | Wrapper around secret.
+newtype SecretKey =
+  SecretKey ByteString
+  deriving (Show, Ord, Eq)
+
+-- | Wrapper around secret key base.
+newtype SecretKeyBase =
+  SecretKeyBase ByteString
+  deriving (Show, Ord, Eq)
+
+-- SMART CONSTRUCTORS
+
+-- | Lift a cookie into a richer type.
+mkCookie :: ByteString -> Cookie
+mkCookie = Cookie
+
+-- | Lift salt into a richer type.
+mkSalt :: ByteString -> Salt
+mkSalt = Salt
+
+-- | Lifts secret into a richer type.
+mkSecretKeyBase :: ByteString -> SecretKeyBase
+mkSecretKeyBase = SecretKeyBase
+
+-- SMART DESTRUCTORS
+
+unwrapDecryptedData :: DecryptedData -> ByteString
+unwrapDecryptedData (DecryptedData deData) =
+  deData
diff --git a/src/Web/Rails3/Session.hs b/src/Web/Rails3/Session.hs
--- a/src/Web/Rails3/Session.hs
+++ b/src/Web/Rails3/Session.hs
@@ -13,8 +13,6 @@
   , lookupUserIds
     -- * Throw-away data-types
     -- $datatypes
-  , Secret(..)
-  , Cookie(..)
   )
 where
 
@@ -28,6 +26,7 @@
 import           Data.Ruby.Marshal            as Marshal hiding (decode, decodeEither)
 import qualified Data.Ruby.Marshal            as Marshal (decodeEither)
 import           Data.Ruby.Marshal.RubyObject
+import           Web.Rails.Session.Types
 import Network.HTTP.Types (urlDecode)
 import Data.List.NonEmpty as NE
 import Data.List as DL
@@ -66,9 +65,6 @@
 -- These data-types exist only as a way to semantically differentiate between
 -- various ByteString arguments when they are passed to functions. This is required
 -- only because Haskell doesn't have proper keywords-arguments.
-
-newtype Secret = Secret ByteString
-newtype Cookie = Cookie ByteString
 
 maybeToEither :: a -> Maybe b -> Either a b
 maybeToEither _ (Just b) = Right b
diff --git a/src/Web/Rails4/Session.hs b/src/Web/Rails4/Session.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Rails4/Session.hs
@@ -0,0 +1,143 @@
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Web.Rails4.Session (
+  -- * Decoding
+    decode
+  , decodeEither
+  -- * Decrypting
+  , decrypt
+  -- * Utilities
+  , csrfToken
+  , sessionId
+  , lookupString
+  , lookupFixnum
+  ) where
+
+import              Control.Applicative ((<$>))
+import              Crypto.PBKDF.ByteString (sha1PBKDF2)
+import              Data.ByteString (ByteString)
+import              Data.Either (Either(..), either)
+import              Data.Function.Compat ((&))
+import              Data.Maybe (Maybe(..), fromMaybe)
+import              Data.Monoid ((<>))
+import              Data.Ruby.Marshal (RubyObject(..), RubyStringEncoding(..))
+import              Data.String.Conv (toS)
+import              Network.HTTP.Types (urlDecode)
+import              Prelude (Bool(..), Eq, Int, Ord, Show, String, ($!), (.) , (==), const, error, fst, show, snd)
+import              Web.Rails.Session.Types
+import              Crypto.Cipher.AES (AES256)
+import              Crypto.Cipher.Types (cbcDecrypt, cipherInit, makeIV)
+import              Crypto.Error (CryptoFailable(CryptoFailed, CryptoPassed))
+import qualified    Data.ByteString as BS
+import qualified    Data.ByteString.Base64 as B64
+import qualified    Data.Ruby.Marshal as Ruby
+import qualified    Data.Vector as Vec
+
+-- EXPORTS
+
+-- | Decode a cookie encrypted by Rails.
+decode :: Maybe Salt
+       -> SecretKeyBase
+       -> Cookie
+       -> Maybe RubyObject
+decode mbSalt secretKeyBase cookie =
+  either (const Nothing) Just (decodeEither mbSalt secretKeyBase cookie)
+
+-- | Decode a cookie encrypted by Rails and retain some error information on failure.
+decodeEither :: Maybe Salt
+             -> SecretKeyBase
+             -> Cookie
+             -> Either String RubyObject
+decodeEither mbSalt secretKeyBase cookie = do
+  case decrypt mbSalt secretKeyBase cookie of
+    Left errorMessage ->
+      Left errorMessage
+    Right (DecryptedData deData) ->
+      Ruby.decodeEither deData
+
+-- | Decrypts a cookie encrypted by Rails. Use this if you are using a
+-- serialisation format other than Ruby's Marshal format.
+decrypt :: Maybe Salt
+        -> SecretKeyBase
+        -> Cookie
+        -> Either String DecryptedData
+decrypt mbSalt secretKeyBase cookie =
+  let salt = fromMaybe defaultSalt mbSalt
+      (SecretKey secret) = generateSecret salt secretKeyBase
+      (EncryptedData encData, InitVector initVec) = prepare cookie
+  in case makeIV initVec of
+       Nothing ->
+         Left $! "Failed to build init. vector for: " <> show initVec
+       Just initVec' -> do
+         let key = BS.take 32 secret
+         case (cipherInit key :: CryptoFailable AES256) of
+           CryptoFailed errorMessage ->
+             Left (show errorMessage)
+           CryptoPassed cipher ->
+             Right . DecryptedData $! cbcDecrypt cipher initVec' encData
+  where
+    defaultSalt :: Salt
+    defaultSalt = Salt "encrypted cookie"
+
+-- UTIL
+
+-- | Helper function for looking up the csrf token in a cooie.
+csrfToken :: RubyObject -> Maybe ByteString
+csrfToken = lookupString "_csrf_token" US_ASCII
+
+-- | Helper function for looking up the session id in a cookie.
+sessionId :: RubyObject -> Maybe ByteString
+sessionId = lookupString "session_id" UTF_8
+
+-- | Lookup integer for a given key.
+lookupFixnum :: ByteString -> RubyStringEncoding -> RubyObject -> Maybe Int
+lookupFixnum key enc rubyObject =
+  case lookup (RIVar (RString key, enc)) rubyObject of
+    Just (RFixnum val) ->
+      Just val
+    _ ->
+      Nothing
+
+-- | Lookup string for a given key and throw away encoding information.
+lookupString :: ByteString
+             -> RubyStringEncoding
+             -> RubyObject
+             -> Maybe ByteString
+lookupString key enc rubyObject =
+  case lookup (RIVar (RString key, enc)) rubyObject of
+    Just (RIVar (RString val, _)) ->
+      Just val
+    _ ->
+      Nothing
+
+-- PRIVATE
+
+-- | Generate secret key using same cryptographic routines as Rails.
+generateSecret :: Salt -> SecretKeyBase -> SecretKey
+generateSecret (Salt salt) (SecretKeyBase secret) =
+  SecretKey $! sha1PBKDF2 secret salt 1000 64
+
+-- | Prepare a cookie for decryption.
+prepare :: Cookie -> (EncryptedData, InitVector)
+prepare (Cookie cookie) =
+  urlDecode True cookie
+  & (fst . split)
+  & base64decode
+  & split
+  & (\(x, y) -> (EncryptedData (base64decode x), InitVector (base64decode y)))
+  where
+    base64decode :: ByteString -> ByteString
+    base64decode = B64.decodeLenient
+
+    separator :: ByteString
+    separator = "--"
+
+    split :: ByteString -> (ByteString, ByteString)
+    split = BS.breakSubstring separator
+
+-- | Lookup value for a given key.
+lookup :: RubyObject -> RubyObject -> Maybe RubyObject
+lookup key (RHash vec) = snd <$> Vec.find (\element -> fst element == key) vec
+lookup _ _ = Nothing
diff --git a/src/Web/Rails7/Session.hs b/src/Web/Rails7/Session.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Rails7/Session.hs
@@ -0,0 +1,149 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE LambdaCase #-}
+
+module Web.Rails7.Session (
+  -- * Decoding
+    decode
+  , decodeEither
+  , DecodingError(..)
+  -- * Decrypting
+  , decrypt
+  ) where
+
+import Control.Applicative ((<$>))
+import Control.Monad
+import Crypto.PBKDF.ByteString (sha1PBKDF2, sha256PBKDF2)
+import Data.Aeson qualified as JSON
+import Data.Bifunctor
+import Data.ByteArray qualified as BA
+import Data.ByteString (ByteString)
+import Data.ByteString qualified as BS
+import Data.ByteString.Base64 qualified as B64
+import Data.ByteString.Char8 qualified as C8
+import Data.ByteString.Lazy qualified as BL
+import Data.Either (Either(..), either)
+import Data.Function.Compat ((&))
+import Data.Maybe (Maybe(..), fromMaybe)
+import Data.Monoid ((<>))
+import Data.Ruby.Marshal (RubyObject(..), RubyStringEncoding(..))
+import Data.String.Conv (toS)
+import Data.Vector qualified as Vec
+import Network.HTTP.Types (urlDecode)
+import Prelude (Bool(..), Eq, Int, Ord, Show, String, ($!), (.) , (==), const, error, fst, show, snd)
+import Prelude hiding (lookup)
+import Web.Rails.Session.Types
+import Crypto.Cipher.AES (AES256)
+import Crypto.Cipher.AESGCMSIV qualified as AESGCM
+import Crypto.Cipher.Types (cbcDecrypt, cipherInit, makeIV, aeadInit, AEADMode (..), aeadSimpleDecrypt, AuthTag(..))
+import Crypto.Error (CryptoFailable(CryptoFailed, CryptoPassed))
+
+data DecodingError
+  = InvalidCookieFormat
+  | InvalidAuthTagSize Int
+  | InvalidIVSize Int
+  | InvalidJSON String
+  | InvalidBase64 String
+  | InvalidCryptoStep String
+  | MakeIVFailed String
+  | DecryptionIsEmpty
+  deriving (Show, Eq)
+
+-- EXPORTS
+
+-- | Decode a cookie encrypted by Rails.
+decode :: Maybe Salt
+       -> SecretKeyBase
+       -> Cookie
+       -> Maybe JSON.Value
+decode mbSalt secretKeyBase cookie =
+  either (const Nothing) Just (decodeEither mbSalt secretKeyBase cookie)
+
+-- | Decode a cookie encrypted by Rails and retain some error information on failure.
+decodeEither :: Maybe Salt
+             -> SecretKeyBase
+             -> Cookie
+             -> Either DecodingError JSON.Value
+decodeEither mbSalt secretKeyBase cookie = do
+  case decrypt mbSalt secretKeyBase cookie of
+    Left errorMessage ->
+      Left errorMessage
+    Right (DecryptedData deData) ->
+      first InvalidJSON $ JSON.eitherDecode (BL.fromStrict deData)
+
+-- | Decrypts a cookie encrypted by Rails. It returns the encrypted
+-- data as a 'ByteString' blob, which is your responsibility to deserialise.
+decrypt :: Maybe Salt
+        -> SecretKeyBase
+        -> Cookie
+        -> Either DecodingError DecryptedData
+decrypt mbSalt secretKeyBase cookie = do
+  (EncryptedData encData, InitVector ivVec, autTag) <- prepare cookie
+  nonce <- doCryptoStep $ AESGCM.nonce ivVec
+  cipher <- doCryptoStep (cipherInit cipherKey :: CryptoFailable AES256)
+  aad <- doCryptoStep (aeadInit AEAD_GCM cipher ivVec)
+  case aeadSimpleDecrypt aad (mempty :: ByteString) encData autTag of
+    Nothing -> Left DecryptionIsEmpty
+    Just dt -> Right $ DecryptedData dt
+
+  where
+
+    cipherKey :: ByteString
+    (SecretKey cipherKey) = generateSecret salt secretKeyBase
+
+    salt :: Salt
+    salt = fromMaybe defaultSalt mbSalt
+
+    defaultSalt :: Salt
+    defaultSalt = Salt "authenticated encrypted cookie"
+
+doCryptoStep :: CryptoFailable a -> Either DecodingError a
+doCryptoStep = \case
+  CryptoFailed errorMessage ->
+    Left (InvalidCryptoStep $ show errorMessage)
+  CryptoPassed a -> Right a
+
+-- PRIVATE
+
+-- | Generate secret key using same cryptographic routines as Rails.
+generateSecret :: Salt -> SecretKeyBase -> SecretKey
+generateSecret (Salt salt) (SecretKeyBase secret) =
+  SecretKey $! sha256PBKDF2 secret salt 1000 32
+
+-- | Prepare a cookie for decryption.
+-- /NOTE/: Unlike Rails4, Rails7 cookies contains a final auth tag at the end.
+prepare :: Cookie -> Either DecodingError (EncryptedData, InitVector, AuthTag)
+prepare (Cookie cookie) =
+  case tokenise "--" cookie of
+    [encDataB64, ivVectorB64, autTagB64]
+     -> do
+       encData  <- base64decode encDataB64
+       ivVector <- enforceIVLen     =<< base64decode ivVectorB64
+       autTag   <- enforceAutTagLen =<< base64decode autTagB64
+       Right (EncryptedData encData, InitVector ivVector, AuthTag $ BA.convert autTag)
+    _ -> Left InvalidCookieFormat
+  where
+    base64decode :: ByteString -> Either DecodingError ByteString
+    base64decode = Right . B64.decodeLenient . C8.filter (/= '\n')
+
+    enforceAutTagLen :: ByteString -> Either DecodingError ByteString
+    enforceAutTagLen b
+      | BS.length b == 16
+      = Right b
+      | otherwise
+      = Left $ InvalidAuthTagSize (BS.length b)
+
+    enforceIVLen :: ByteString -> Either DecodingError ByteString
+    enforceIVLen b
+      | BS.length b == 12
+      = Right b
+      | otherwise
+      = Left $ InvalidIVSize (BS.length b)
+
+
+separator :: ByteString
+separator = "--"
+
+tokenise :: ByteString -> ByteString -> [ByteString]
+tokenise x y = h : if BS.null t then [] else tokenise x (BS.drop (BS.length x) t)
+    where (h,t) = BS.breakSubstring x y
diff --git a/test/Rails3 b/test/Rails3
new file mode 100644
--- /dev/null
+++ b/test/Rails3
@@ -0,0 +1,1 @@
+BAh7CEkiD3Nlc3Npb25faWQGOgZFVEkiJTNlMGQ2NzJkMmQ1OTRkMDJjOGYwMTUzODhhZTM4MGE4BjsAVEkiGXdhcmRlbi51c2VyLnVzZXIua2V5BjsAVFsHWwZpcEkiIiQyYSQxMCRoRHU3d0huZU53NEEuNldnNjFSNHZPBjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMWZLdWkyTVpWM3U1amhHQklndnZyT2k3bG8wbUQvSmdlM2UwTE9BUzE5Vmc9BjsARg==--7af4bed089e97eea2af563abe937e05263d2db4f
diff --git a/test/Rails4 b/test/Rails4
new file mode 100644
--- /dev/null
+++ b/test/Rails4
@@ -0,0 +1,1 @@
+T3NpYnRsWGJsZ25qL0R4MFlZdE9wZVBrZXc3VnFHMHhYMFgvemlVUjlTekZKbERXbVd2Mkwra0xxTmNOYnZaWktBQWo3T25RV3VPRkR6RU9BQ3dub2FSWExlZmZ1RUxVWG9vZjRTbWphRzBFSW5nMVJOSklPTVRPRGxKN0tvdGxhZzVlZktLTmhxc0V1a1FCWHpwNVJGTjdON0JCbjZQWHM0R1M1SWIzeUkzalhVNWdVbnd2Z2E5RlBMSXcvR0tNSHVOZ0NiK1RBTzVxcCtMK3hJa1daYW13allNb1NyN3pOUTFBQ0tRcFNEdUVJelVMZE8rVXhwM3RhcHFONWRwby9kRStNNkRUVXNQTDNKcjF0ZWpGTUE9PS0tb3NGeEJiZ1dLeFFKcFV0WXgyUytnZz09--e996601dbf39ff5ffbf6e2f23f6ddf78c5179baa
diff --git a/test/Rails7 b/test/Rails7
new file mode 100644
--- /dev/null
+++ b/test/Rails7
@@ -0,0 +1,1 @@
+NFvd2rISL046kEAqcX9r4G8PEt+gvSPcPMcRy72gMShbYuMZ/bktE1BtZb8APJOMYC9IgPPSLxMPrZaFPTt7LipAmJ3wKFs+QDsmz/hysPg4lDwfzWAzeeca+uQZoaNIMaoSwlm47xtLVCu9GoDjuhafEK+9kJQjOPLNzYVQDJ9U5HkEl1k5zXlU5hrvYlvd0eNbJZyO9FTtKOq+ffNzd4dAfis+l/xMy9HJQ3sl5SOnV4yr17HeWJ2FPlECtkyH/2hTPggsjm5VvJwwxC4PzF5oSmLLV6RFCdXlUNfHqxL2rwp6G7kHf2TnjfNz2fU99/SLPjEgLr8bxNFQJ9EjnlTp5Ars0ICNRdzcUTecsVieZnjL0CXqHLfk5+JmoFTr1FSwCBVYKetfqu8yiGxEsTOoxqnfvhHNnDXrCeotazaJIUi/DBeO4Z0poalxGcx/MVxgfoQecTN3a0dA+9oQHvvIc/NBL0p5b6tO4oxs8q4J3babUHAsPLeiZpFfqxk02vgLSZKV/AO4ylbRWnaLDXEIL1T/KIWdqgMMcNumriXqragavvtvIsPGOubDpCHo/1YnOCIQra2k9xI=--wkpytrsNTB4nszC0--LQ3bgugxWSkf0ZXAjYxh8g==
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -1,32 +1,41 @@
 {-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
 {-# LANGUAGE QuasiQuotes #-}
+{-# HLINT ignore "Use <&>" #-}
 
 import           Data.ByteString (ByteString)
-import qualified Data.ByteString as BS
 import           Data.Either (isRight)
-import           Data.Monoid ((<>))
+import           Data.Functor ((<&>))
 import           Data.Ruby.Marshal hiding (decodeEither)
 import           Data.Vector (fromList)
-import           System.IO.Unsafe (unsafePerformIO)
+import           Test.Hspec (describe, it, shouldBe, shouldSatisfy, Spec)
 import           Test.Tasty (defaultMain, testGroup)
 import           Test.Tasty.Hspec (testSpec)
-import           Test.Hspec (describe, it, shouldBe, shouldSatisfy, Spec)
-import           Web.Rails.Session
-import qualified Web.Rails3.Session as R3
+import           Web.Rails.Session.Types
+import           Data.Aeson.QQ
+import qualified Data.ByteString as BS
+import qualified Data.Aeson      as JSON
 import qualified Data.List.NonEmpty as NE
+import qualified Web.Rails3.Session as R3
+import qualified Web.Rails4.Session as R4
+import qualified Web.Rails7.Session as R7
 
 -- SPECS
 
 main :: IO ()
 main = do
-  rails4 <- testSpec "Web.Rails.Session: Rails4" $ specsFor Rails4
-  rails3 <- testSpec "Web.Rails3.Session: Rails4" specsForRails3
-  defaultMain (testGroup "All the Specs" [rails4, rails3])
+  r3Cookie <- readCookie Rails3
+  r4Cookie <- readCookie Rails4
+  r7Cookie <- readCookie Rails7
 
-specsForRails3 :: Spec
-specsForRails3 = do
-  let cookie = R3.Cookie $ unsafePerformIO $ BS.readFile "test/Rails3"
-      secret_ = R3.Secret "test secret token for testing cookies"
+  rails4 <- testSpec "Web.Rails.Session: Rails4 (Marshal)" $ specsFor r4Cookie
+  rails3 <- testSpec "Web.Rails3.Session: Rails4 (Marshal)" $ specsForRails3 r3Cookie
+  rails7 <- testSpec "Web.Rails7.Session: Rails7 (JSON)" $ specsForRails7 r7Cookie
+  defaultMain (testGroup "All the Specs" [rails7, rails4, rails3])
+
+specsForRails3 :: Cookie -> Spec
+specsForRails3 cookie = do
+  let secret_ = Secret "test secret token for testing cookies"
       sessionIdVal_ = "3e0d672d2d594d02c8f015388ae380a8"
       csrfTokenVal_ = "fKui2MZV3u5jhGBIgvvrOi7lo0mD/Jge3e0LOAS19Vg="
       userIdVal_ = 107 :: Int
@@ -43,105 +52,108 @@
       let result = R3.decodeEither secret_ cookie
       result `shouldSatisfy` isRight
 
-    it "should be a fully-formed Ruby object" $ do
-      case R3.decodeEither secret_ cookie of
-        Left e -> error $ "decodeEither failed:" ++ (show e)
-        Right result -> do
-          result `shouldBe` cookieContents_
+    it "should be a fully-formed Ruby object" $ case R3.decodeEither secret_ cookie of
+      Left e -> error $ "decodeEither failed:" ++ show e
+      Right result -> do
+        result `shouldBe` cookieContents_
 
-    it "should look up the '_csrf_token'" $ do
-      case R3.decodeEither secret_ cookie of
-        Left e -> error $ "decodeEither failed:" ++ (show e)
-        Right result ->
-          csrfToken result `shouldBe` Just csrfTokenVal_
+    it "should look up the '_csrf_token'" $ case R3.decodeEither secret_ cookie of
+      Left e -> error $ "decodeEither failed:" ++ show e
+      Right result ->
+        R4.csrfToken result `shouldBe` Just csrfTokenVal_
 
-    it "should look up the 'session_id'" $ do
-      case R3.decodeEither secret_ cookie of
-        Left e -> error $ "decodeEither failed:" ++ (show e)
-        Right result ->
-          sessionId result `shouldBe` Just sessionIdVal_
+    it "should look up the 'session_id'" $ case R3.decodeEither secret_ cookie of
+      Left e -> error $ "decodeEither failed:" ++ show e
+      Right result ->
+        R4.sessionId result `shouldBe` Just sessionIdVal_
 
-    it "should look up the warden user_id(s)" $ do
-      case R3.decodeEither secret_ cookie of
-        Left e -> error $ "decodeEither failed:" ++ (show e)
-        Right result ->
-          R3.lookupUserIds result `shouldBe` Just (userIdVal_ NE.:| [] :: NE.NonEmpty Int)
+    it "should look up the warden user_id(s)" $ case R3.decodeEither secret_ cookie of
+      Left e -> error $ "decodeEither failed:" ++ show e
+      Right result ->
+        R3.lookupUserIds result `shouldBe` Just (userIdVal_ NE.:| [] :: NE.NonEmpty Int)
 
-specsFor :: Rails -> Spec
-specsFor rails = do
-  let cookie = unsafeReadCookie rails
+specsFor :: Cookie -> Spec
+specsFor cookie = do
 
   describe "decode" $ do
     it "should be a Right(..)" $ do
-      let result = decodeEither Nothing secret cookie
+      let result = R4.decodeEither Nothing secret cookie
       result `shouldSatisfy` isRight
 
-    it "should be a fully-formed Ruby object" $ do
-      case decodeEither Nothing secret cookie of
-        Left _ -> error "decode failed"
-        Right result -> do
-          result `shouldBe` rubySession
-  describe "decrypt" $ do
-    it "should be a Right(..)" $ do
-      let result = decrypt Nothing secret cookie
-      result `shouldSatisfy` isRight
+    it "should be a fully-formed Ruby object" $ case R4.decodeEither Nothing secret cookie of
+      Left _ -> error "decode failed"
+      Right result -> do
+        result `shouldBe` rubySession
+  describe "decrypt" $ it "should be a Right(..)" $ do
+    let result = R4.decrypt Nothing secret cookie
+    result `shouldSatisfy` isRight
 
-  describe "csrfToken" $ do
-    it "should look up the '_csrf_token'" $ do
-      case decodeEither Nothing secret cookie of
-        Left _ -> error "lookup failed"
-        Right result ->
-          csrfToken result `shouldBe` Just csrfTokenVal
+  describe "csrfToken" $ it "should look up the '_csrf_token'" $ do
+    case R4.decodeEither Nothing secret cookie of
+      Left _ -> error "lookup failed"
+      Right result ->
+        R4.csrfToken result `shouldBe` Just csrfTokenVal
 
-  describe "sessionId" $ do
-    it "should look up the 'session_id'" $ do
-      case decodeEither Nothing secret cookie of
-        Left _ -> error "lookup failed"
-        Right result ->
-          sessionId result `shouldBe` Just sessionIdVal
+  describe "sessionId" $ it "should look up the 'session_id'" $ do
+    case R4.decodeEither Nothing secret cookie of
+      Left _ -> error "lookup failed"
+      Right result ->
+        R4.sessionId result `shouldBe` Just sessionIdVal
 
   describe "lookupString" $ do
-    it "should look up the '_csrf_token'" $ do
-      case decodeEither Nothing secret cookie of
-        Left _ -> error "lookup failed"
-        Right result ->
-          lookupString "_csrf_token" US_ASCII result `shouldBe`
-          Just csrfTokenVal
+    it "should look up the '_csrf_token'" $ case R4.decodeEither Nothing secret cookie of
+      Left _ -> error "lookup failed"
+      Right result ->
+        R4.lookupString "_csrf_token" US_ASCII result `shouldBe`
+        Just csrfTokenVal
 
-    it "should look up the 'session_id'" $ do
-      case decodeEither Nothing secret cookie of
-        Left _ -> error "lookup failed"
-        Right result ->
-          lookupString "session_id" UTF_8 result `shouldBe`
-          Just sessionIdVal
+    it "should look up the 'session_id'" $ case R4.decodeEither Nothing secret cookie of
+      Left _ -> error "lookup failed"
+      Right result ->
+        R4.lookupString "session_id" UTF_8 result `shouldBe`
+        Just sessionIdVal
 
-    it "should look up the 'token'" $ do
-      case decodeEither Nothing secret cookie of
-        Left _ -> error "lookup failed"
-        Right result ->
-          lookupString "token" US_ASCII result `shouldBe`
-          Just authToken
+    it "should look up the 'token'" $ case R4.decodeEither Nothing secret cookie of
+      Left _ -> error "lookup failed"
+      Right result ->
+        R4.lookupString "token" US_ASCII result `shouldBe`
+        Just authToken
 
+specsForRails7 :: Cookie -> Spec
+specsForRails7 cookie = do
+  describe "decode" $ do
+    it "should be a Right(..)" $ do
+      let result = R7.decodeEither Nothing secret cookie
+      result `shouldSatisfy` isRight
+
+    it "should be a fully-formed JSON (Rails) object" $ do
+      case R7.decodeEither Nothing secret cookie of
+        Left x -> fail (show x)
+        Right result -> result `shouldBe` rubyJSONObject
+
 -- EXAMPLES
 
-example :: FilePath -> IO (Maybe ByteString)
-example path = do
+_example :: FilePath -> IO (Maybe ByteString)
+_example path = do
   rawCookie <- BS.readFile path
   let appSecret = mkSecretKeyBase "development_secret_token"
   let cookie = mkCookie rawCookie
-  case decodeEither Nothing appSecret cookie of
+  case R4.decodeEither Nothing appSecret cookie of
     Left _ ->
       pure Nothing
     Right rubyObject ->
-      pure $ sessionId rubyObject
+      pure $ R4.sessionId rubyObject
 
 -- UTIL
 
-data Rails = Rails4 | Rails3 deriving (Show)
+data Rails =
+    Rails4
+  | Rails3
+  | Rails7
+  deriving (Show)
 
-unsafeReadCookie :: Rails -> Cookie
-unsafeReadCookie rails = unsafePerformIO $
-  BS.readFile ("test/" <> (show rails)) >>= pure . mkCookie
+readCookie :: Rails -> IO Cookie
+readCookie rails = BS.readFile ("test/" <> show rails) <&> mkCookie
 
 -- CONFIG
 
@@ -171,3 +183,6 @@
        , ( RIVar (RString "token", US_ASCII)
          , RIVar (RString authToken, UTF_8))
        ])
+
+rubyJSONObject :: JSON.Value
+rubyJSONObject = [aesonQQ|{"_rails":{"message":"eyJzZXNzaW9uX2lkIjoiMjY1ZWY5NmVjOTIxMmM3ZGJhOWRjZTM3OGRmNDBjYjIiLCJsb2NhbGUiOiJlbl9HQiIsImJyYW5kaW5nIjoiaXJpc19jb25uZWN0IiwiY3VycmVudF9vcmdhbml6YXRpb25faWQiOjEsIl9jc3JmX3Rva2VuIjoiMTlGMkVlSmRrdUM4TzJxM2tTX3gtVnV1cWd5cWc0N2tXVVFFYW9Nbm5UNCIsInRva2VuIjoidzNSM2Q1ekJHdFFDdkw4eEZtUDlsY0g5TkVQWFprMzhTaUtBMURpWGEwUT0iLCJjdXJyZW50X3VzZXJfaWQiOjEsIm91dHN0YW5kaW5nX2V1bGFzIjpbXX0=","exp":null,"pur":"cookie._athena_new_session"}}|]
