Dancer2-Session-Pg
view release on metacpan or search on metacpan
[![DeepWiki][deepWikiBadge]](https://deepwiki.com/mikkoi/dancer2-session-pg)
[![GH Actions: Linux Build][ciLinux]](https://github.com/mikkoi/dancer2-session-pg/actions/workflows/linux.yml)
[![GH Actions: Windows Build][ciWindows]](https://github.com/mikkoi/dancer2-session-pg/actions/workflows/windows.yml)
[![GitHub repo size][repoSizeBadge]](https://github.com/mikkoi/dancer2-session-pg/archive/refs/heads/main.zip)
[![GitHub pull requests][githubPullRequestsBadge]](https://github.com/mikkoi/dancer2-session-pg/pulls)
# Dancer2-Session-Pg
PostgreSQL session backend for Dancer2
# VERSION
version 0.001
# STATUS
Package Dancer2::Session::Pg is under development so changes in the API
are possible, though not likely.
# SYNOPSIS
use Dancer2::Session::Pg ();
my $engine = Dancer2::Session::Pg->new(
dsn => 'dbi:Pg:dbname=app;host=db',
dbuser => 'app_web',
dbpass => $ENV{'APP_DB_PASSWORD'},
dbtable => 'sessions', # required
dbschema => 'web', # optional; else search_path
session_duration => 900,
# One or more SLOTS, each pairing a key with the cipher that uses it.
# Exactly one is active: that is the one sessions are written with, and
# the rest stay to be read. See SECURITY for where the key comes from.
encryption_keys => {
0 => {
key => $ENV{'SESSION_KEY_0'},
alg => 'AES-256-GCM',
active => 1,
},
},
# Optional; see THE PRINCIPAL COLUMN for whether you want it at all.
principal_key => 'principal',
principal_column => 'account_id',
);
Most applications configure this from `config.yml` rather than in Perl -- see
["A configuration file" in Dancer2::Session::Pg](https://metacpan.org/pod/Dancer2%3A%3ASession%3A%3APg#A-configuration-file). Installing the engine by hand is
for when `dbh` has to
be a coderef, something YAML cannot express, and there is a trap in doing it
which ["CONNECTIONS" in Dancer2::Session::Pg](https://metacpan.org/pod/Dancer2%3A%3ASession%3A%3APg#CONNECTIONS) describes.
# DESCRIPTION
Stores Dancer2 sessions in PostgreSQL, and uses PostgreSQL's own features to
make that storage safer than a serialised blob in a table.
A web session is not ordinary data. It frequently carries the credentials that
prove who somebody is -- with OpenID Connect, an access token and a refresh
token -- so the store is worth more than the account it belongs to. Three
properties follow from that, and each is provided by the database rather than by
convention:
- Authenticated encryption at rest
The payload is encrypted with an AEAD cipher, so a dump, a backup or a support
copy of the table does not hand over the contents of a session, and a row that
has been altered fails to decrypt instead of deserialising into a structure the
application would then trust.
Nor does it hand over a way in. **The session id is stored as a SHA-256 digest,
not verbatim**, because the id is the session cookie: a table full of raw ids
would be a table full of working credentials, usable against the live
application by anyone who read a backup, no key required. What a dump contains
is digests, which open nothing.
The id is also authenticated with the payload, so a sealed payload opens only
under the session it was written for and cannot be moved from one row to
another.
["SECURITY" in Dancer2::Session::Pg](https://metacpan.org/pod/Dancer2%3A%3ASession%3A%3APg#SECURITY) says what that stops, where the key should
live, and when to rotate
it.
Which cipher is a property of the key it is used with, and both are
**replaceable**: every payload records the key and the cipher that sealed it, so
a cipher found wanting next year is three deployments rather than a forced
logout. See ["THE STORED PAYLOAD" in Dancer2::Session::Pg](https://metacpan.org/pod/Dancer2%3A%3ASession%3A%3APg#THE-STORED-PAYLOAD),
["Rotating the key" in Dancer2::Session::Pg](https://metacpan.org/pod/Dancer2%3A%3ASession%3A%3APg#Rotating-the-key) and
[Dancer2::Session::Pg::Cipher](https://metacpan.org/pod/Dancer2%3A%3ASession%3A%3APg%3A%3ACipher).
- Expiry decided by the server's clock
`expires` is a `timestamptz` and every read filters on it. Application clocks
drift; the database's clock is the one every process shares, so all of them
agree about whether a session is still alive.
The expiry is set when the row is created and **is not moved by later writes**.
`session_duration` is therefore an absolute cap measured from creation, which is
what [Dancer2::Core::Role::SessionFactory](https://metacpan.org/pod/Dancer2%3A%3ACore%3A%3ARole%3A%3ASessionFactory) describes: a limit on session
validity, regardless of the cookie. An idle timeout is a different thing and is
the cookie's job -- see `cookie_duration`, which slides.
This matters more than it sounds. A cap that every request pushes further away is
never reached by a session in continuous use, and a session in continuous use is
what somebody holding stolen cookies has.
- Atomic writes
Sessions are written with `INSERT ... ON CONFLICT DO UPDATE`, which is atomic.
Any number of workers may write one session id concurrently without producing a
duplicate row, a unique violation or a deadlock, and without moving the expiry
cap.
That is a guarantee about database integrity, not about every write succeeding:
concurrent writers to one row serialise on its lock, and a waiter that exceeds
`statement_timeout` is cancelled on purpose rather than holding a worker. See
["A blocked write fails rather than waiting" in Dancer2::Session::Pg](https://metacpan.org/pod/Dancer2%3A%3ASession%3A%3APg#A-blocked-write-fails-rather-than-waiting).
It does **not** mean two workers cannot lose each other's changes. The payload is
one encrypted blob, so a write replaces all of it and the last writer wins. See
["CONCURRENCY" in Dancer2::Session::Pg](https://metacpan.org/pod/Dancer2%3A%3ASession%3A%3APg#CONCURRENCY), which says exactly what is and is not
promised, and is backed by
a test rather than by this paragraph.
On top of that, an **optional** clear column beside the encrypted payload makes it
possible to find and end every session belonging to one account without
decrypting anything -- see ["destroy\_for\_principal" in Dancer2::Session::Pg](https://metacpan.org/pod/Dancer2%3A%3ASession%3A%3APg#destroy_for_principal).
Suspending an account has
little effect while the suspended user's cookie still works. That column is off
by default and need not exist; ["THE PRINCIPAL COLUMN" in Dancer2::Session::Pg](https://metacpan.org/pod/Dancer2%3A%3ASession%3A%3APg#THE-PRINCIPAL-COLUMN) is
about whether you want
it.
( run in 0.859 second using v1.01-cache-2.11-cpan-3f962061493 )