Dancer2-Session-Pg
view release on metacpan or search on metacpan
["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.
lib/Dancer2/Session/Pg.pm view on Meta::CPAN
L<Dancer2::Session::Pg/A configuration file>. Installing the engine by hand is
for when C<dbh> has to
be a coderef, something YAML cannot express, and there is a trap in doing it
which L<Dancer2::Session::Pg/CONNECTIONS> describes.
=head1 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:
=over 4
=item 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. B<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.
L<Dancer2::Session::Pg/SECURITY> says what that stops, where the key should
live, and when to rotate
it.
lib/Dancer2/Session/Pg.pm view on Meta::CPAN
'SHA-256 of the Dancer2 session id -- NOT the id itself, which is the session cookie and would be replayable from a dump. Arbiter for ON CONFLICT.';
COMMENT ON COLUMN web.sessions.principal_id IS
'OPTIONAL -- drop this column if principal_key is not configured. Clear copy of the value named by principal_key, so sessions can be found and revoked without decrypting. May be free text as here, or a foreign key; see "THE PRINCIPAL COLUMN" ...
COMMENT ON COLUMN web.sessions.session_data IS
'AEAD-encrypted session payload: version || cipher id || key id || iv || tag || ciphertext. Unreadable without the application key, and tamper-evident.';
COMMENT ON COLUMN web.sessions.created IS
'When the row was first written. Server clock.';
COMMENT ON COLUMN web.sessions.updated IS
'When the row was last written. Server clock.';
COMMENT ON COLUMN web.sessions.expires IS
'When the session stops being valid. Every read filters on it. NULL means no expiry, which is rarely right for a session holding credentials.';
CREATE INDEX sessions_principal_id_idx
ON web.sessions (principal_id) WHERE principal_id IS NOT NULL;
COMMENT ON INDEX web.sessions_principal_id_idx IS
'OPTIONAL. Supports destroy_for_principal and sessions_for_principal. Partial: sessions without a principal id are not indexed.';
CREATE INDEX sessions_expires_idx
ON web.sessions (expires) WHERE expires IS NOT NULL;
COMMENT ON INDEX web.sessions_expires_idx IS
'Supports reap(). Partial: rows without an expiry are never reaped.';
lib/Dancer2/Session/Pg.pm view on Meta::CPAN
if you have not set one, a blocked session write waits as long as the database
makes it wait.
=head1 SECURITY
=head2 The threat this module is built for
Somebody reads the database and does not have the application. A backup, a
replica, a support copy of a table, a dump in a ticket, a stolen disk. The
payload is encrypted with an AEAD cipher, so what they get is the shape of your
session traffic and not the credentials in it.
B<It is not built for an attacker who has the application's memory or its
configuration.> The key is in the process, so anyone who can read the process or
the file the key came from can read every session. Encryption at rest moves the
secret from the database to the key store; it does not remove it.
=head2 Why the session id is stored as a digest
Encrypting the payload would be half a job. B<The session id is itself a bearer
token> -- L<Dancer2> reads it from the cookie and this module hands back
lib/Dancer2/Session/Pg.pm view on Meta::CPAN
info the connection had gone away and was reopened. One after
a database restart is the system working; a stream of
them is something killing connections.
Each is reported once per engine rather than once per request, because anything
on the request path otherwise produces a log nobody can read -- and because a
repeated message is how an attacker poking at cookies fills your disk.
If you want per-session audit logging, do it in the application, where you have
the request, the user and a policy about retention. A session store is the wrong
layer to decide that credentials belong in a log file.
=head1 WHAT AN UNREADABLE ROW DOES
A row that cannot be decrypted -- wrong key, a cipher this engine cannot read, a
payload somebody has edited -- is treated as B<a session that does not exist>.
Dancer2 then starts a fresh one, so the user is logged out rather than shown an
error. That is deliberate: the alternative is handing the application a structure
whose integrity has not been established.
It is also reported, once per engine per distinct reason, through the engine's
lib/Dancer2/Session/Pg/Cipher.pm view on Meta::CPAN
the cipher a session was written with is a property of the row rather than of the
configuration. That is what makes a cipher replaceable: point C<alg> at a new one
and new sessions use it while existing rows stay readable until they expire.
This role is the contract such a cipher implements. Consume it with C<with>
rather than duck-typing the methods: that is how L</cipher_self_check> arrives,
and the engine refuses a cipher that does not provide it.
=head2 Only authenticated modes
A session frequently carries the credentials that prove who somebody is. A row
that has been altered in the database must B<fail to decrypt>, not deserialise
into a structure the application then trusts, so every cipher here produces a
tag and verifies it. L</cipher_self_check> tests exactly that, by flipping a bit
and insisting the result is refused.
=head2 Cipher ids are permanent
The id is one byte in every stored payload. Changing a cipher's id makes rows
written under the old one unreadable, which logs those users out.
);
T2->is( scalar $engine->_principal_of( { identity => { id => 7, token => 'SECRET' } } ),
undef, 'a reference still yields no principal' );
T2->is( scalar @logged, 1, 'and it is reported' );
T2->is( $logged[0][0], 'warning', 'at warning level' );
T2->like( $logged[0][1], qr/destroy_for_principal/msx, 'naming the consequence rather than just the symptom' );
T2->like( $logged[0][1], qr/HASH[ ]reference/msx, 'and what it got' );
T2->ok( index( $logged[0][1], 'SECRET' ) < 0,
'but NOT the value -- that is session content, and some of it is credentials' );
$engine->_principal_of( { identity => { id => $_ } } ) for 1 .. 5;
T2->is( scalar @logged, 1, 'reported once per engine, not once per request' );
}
);
# The warning that a supplied handle is in manual-commit mode, which the module
# issues because a session write joining somebody else's transaction is real in
# the process and absent from the database until they commit. It needs no
# THE CODEREF FORM, which is the one the documentation recommends for
xt/author/weave_README_md.t view on Meta::CPAN
["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.
( run in 0.806 second using v1.01-cache-2.11-cpan-3f962061493 )