Dancer2-Session-Pg

 view release on metacpan or  search on metacpan

README.md  view on Meta::CPAN

[![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 )