NAME

Punk::Session - signed cookie sessions

SYNOPSIS

package MyApp;
use Punk;

session
    secret   => secret('session_key'),   # from the secrets system
    expires  => '7d',
    samesite => 'Lax';

post '/login' => sub {
    my ($c) = @_;
    $c->session->{user_id} = $user->id;   # signed into the cookie
    $c->redirect('/');
};

get '/me' => sub {
    my ($c) = @_;
    my $id = $c->session->{user_id} or return $c->redirect('/login');
    $c->json({ id => $id });
};

post '/logout' => sub { my ($c) = @_; $c->session_expire; $c->redirect('/') };

DESCRIPTION

A session is a hashref carried in a cookie: the handler reads and writes $c->session, and at the end of the request Punk serializes it (JSON), signs it with HMAC-SHA256 and the configured secret, and sets the cookie - only when it actually changed. The client can read the contents but cannot forge them, so do not put secrets in a session; a tampered cookie is rejected and the session comes up empty.

"Cannot forge them" is exactly as true as the secret is, which is why there is no default for it and no way to start without one. See "THE KEYWORD".

There is no server-side store in this cut: the whole session lives in the cookie. A session that serializes over ~4KB croaks, pointing at where a server-side store would go.

THE KEYWORD

session secret => secret('key'), expires => '7d', secure => 1;

Enables sessions for the application. Options:

  • secret - the signing key. Required, and required to be non-empty: session croaks at boot without one rather than starting, because the alternative is signing with a zero-length HMAC key, which anybody who knows the cookie format can reproduce offline. Source it from the secrets system so it never sits in the code - and because secret itself fails closed, an unset environment variable or a missing config path becomes a startup error rather than a forgeable cookie.

  • cookie - the cookie name (default punk.sid).

  • expires - a lifetime like '7d' / '12h' / '30m' / seconds; omitted means a session cookie (gone when the browser closes).

    This is a real lifetime, not just a Max-Age: the expiry is stamped inside the signed payload and checked when the cookie is read, so a cookie past it is refused whatever the client chose to keep. A cookie with no expires is still bounded, at 30 days - "until the browser closes" is a promise the client makes, not a limit on how long the value stays good. The stamp refreshes every time the session is written, so an active session does not expire out from under someone.

  • path (default /), domain, secure, httponly (default on), samesite (default Lax) - the cookie attributes.

It also reads from config/punk.yml under a session: block.

CONTEXT METHODS

session

The session hashref. Read and write it; it is written back to the cookie at the end of the request if it changed.

session_expire

Log out: empty the session and delete the cookie.

Be clear about what that can and cannot do. It tells the browser in front of you to drop the value; it cannot reach a copy someone already took, because there is no server-side record of the session to mark dead - the cookie is the session. A stolen cookie therefore stays good until its stamped expiry runs out, which is the ceiling expires sets and the reason it is enforced server-side rather than left to Max-Age. If you need logout to revoke immediately, the session has to have server-side state to revoke: keep a per-user token or a session id in your own table and check it in a guard.

flash

flash_keep

One-request messages riding the session - see "FLASH".

FLASH

post '/save' => sub {
    my ($c) = @_;
    $c->flash(notice => 'Saved.');       # for the NEXT request
    $c->redirect('/list');
};

get '/list' => sub {
    my ($c) = @_;
    $c->render('list', { flash => $c->flash });
};

A flash message lives exactly one request: set it, redirect, and the redirected-to page reads it once. It rides the session under one reserved key (punk.flash), so it shares the session's machinery - the signing, the change-detected write-back, and the ~4KB cap - and needs the session keyword configured.

$c->flash(key => $value, ...) sets messages for the next request; $c->flash('key') reads one of this request's inbound messages; a bare $c->flash returns the whole inbound hashref (possibly empty) - which is also how a template gets it, passed explicitly like any other render data. $c->flash_keep re-arms the inbound messages for one more request - the redirect-through-a-redirect case.

The rotation is the session's own change detection: the first flash call of a request moves the inbound hash out of the session, so the consuming response rewrites the cookie without it, while a request that never touches flash leaves it riding untouched. Setting and reading in the same request do not meet: reads see the previous request's messages, writes feed the next one's.

SEE ALSO

Punk, "cookie" in Punk::Context, "secret" in Punk.

AUTHOR

LNATION <email@lnation.org>

LICENSE AND COPYRIGHT

This software is Copyright (c) 2026 by LNATION <email@lnation.org>.

This is free software, licensed under:

The Artistic License 2.0 (GPL Compatible)