View Source Charon.SessionStore.RedisStore (Charon v3.2.0)

A persistent session store based on Redis, which implements behaviour Charon.SessionStore. In addition to the required callbacks, this store also provides get_all/3 and delete_all/3 (for a user) functions.

Redis requirements

This module needs a Redis >= 7.0.0 instance and needs permissions to create Redis functions. The optimistic-locking functionality of the store was not designed with a Redis cluster in mind and will behave unpredictably when used with a distributed Redis deployment. Using a failover-replica should be fine, however. The Redis functions are registered with a name that includes the hashed Charon version, to make sure that the called function matches the expected code, and multiple Charon deployments can share a Redis instance.

Config

Additional config is required for this module (see Charon.SessionStore.RedisStore.Config):

Charon.Config.from_enum(
  ...,
  optional_modules: %{
    Charon.SessionStore.RedisStore => %{
      key_prefix: "charon_",
      get_signing_key: &RedisStore.default_signing_key/1
    }
  }
)

The following options are supported:

  • :key_prefix (optional). A string prefix for the Redis keys that are sessions.
  • :get_signing_key (optional). A getter/1 that returns the key that is used to sign and verify serialized session binaries.

Initialize store

RedisStore uses a connection pool and has to register Redis functions on startup. In order to initialize it, you must add RedisStore to your application's supervision tree.

# in application.ex
def start(_, ) do
  redix_opts = [host: "localhost", port: 6379, password: "supersecret", database: 0]

  children = [
    ...
    {Charon.SessionStore.RedisStore, pool_size: 15, redix_opts: redix_opts},
    ...
  ]

  opts = [strategy: :one_for_one, name: MyApp.Supervisor]
  Supervisor.start_link(children, opts)
end

Options

  • :name (default module name) name of the supervisor
  • :pool_size (default 10) total number of (local) normal workers
  • :pool_max_overflow (default 5) max number of (local) extra workers in case of high load
  • :redix_opts passed to Redix.start_link/1

The pool and its config options are local to the Elixir/OTP node, so if you use multiple nodes, the total connection count to Redis maxes out at (pool_size + pool_max_overflow) * number_of_nodes.

Session signing

In order to offer some defense-in-depth against a compromised Redis server, the serialized sessions are signed using a HMAC. The key is derived from the Charon base secret, but can be overridden in the config. This is not usually necessary, except when you're changing the base secret and still want to access your sessions. It is debatable, of course, in case your Redis server is compromised, if your application server holding the signing key can still be considered secure. However, given that there are no real drawbacks to using signed session binaries, since the performance cost is negligible, no extra config is needed, it is easy to implement, and defense-in-depth is a good guiding principle, RedisStore implements this feature anyway.

Implementation details

RedisStore stores sessions until their associated refresh token(s) expire. That means that :refresh_expires_at is used to determine if a session is "alive", not :expires_at. This makes sense, because a session without a valid refresh token is effectively useless, and it means we can support "infinite lifetime" sessions while still pruning sessions that aren't used. This is one reason to set :refresh_ttl to a limited value, no more than six months is recommended.

Last but not least, Redis 7 functions are used to implement some features, specifically optimistic locking, and to ensure all callbacks use only a single round-trip to the Redis instance.

Summary

Functions

Returns a specification to start this module under a supervisor.

Get the default session signing key that is used if config option :get_signing_key is not set explicitly.

Start the RedisStore supervisor, which registeres all required Redis functions and initiates the connection pool.

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

default_signing_key(config)

@spec default_signing_key(Charon.Config.t()) :: binary()

Get the default session signing key that is used if config option :get_signing_key is not set explicitly.

start_link(opts \\ [])

@spec start_link(keyword()) :: :ignore | {:error, any()} | {:ok, pid()}

Start the RedisStore supervisor, which registeres all required Redis functions and initiates the connection pool.

Options

  • :name (default module name) name of the supervisor
  • :pool_size (default 10) total number of (local) normal workers
  • :pool_max_overflow (default 5) max number of (local) extra workers in case of high load
  • :redix_opts passed to Redix.start_link/1

The pool and its config options are local to the Elixir/OTP node, so if you use multiple nodes, the total connection count to Redis maxes out at (pool_size + pool_max_overflow) * number_of_nodes.