Sessions
Returning fans want one place with all their tickets, and they'd rather not type their email at every checkout. Right now, anyone who knows an order's address can see it, too. Time for Box Office accounts.
A fan signs in by clicking a link the box office emails them. Margaret doesn't decide how people prove who they are. It runs the sessions: once you know who someone is, you start a session, and every route can ask for the current customer.
Sessions need a token issuer
A session is backed by signed tokens, so the box office declares itself a token issuer, under the URL its public server will run at:
use issues_tokens;
;box_office is a tag: a name the other auth attributes use to refer to this issuer. Then declare the sessions it issues:
use issues_sessions;
use SessionCookies;
;A session is two cookies:
__Host-margaret-sessionholds a random secret, valid for three days. The database keeps only its digest.__Host-margaret-session-accessholds a signed token, valid for 15 minutes, so most requests never touch the database. When it expires, Margaret checks the secret and signs a new one.
Margaret signs those tokens with keys it creates, rotates every hour, and keeps in your database, so every instance of the box office shares them. The schema command now also includes the framework's own tables for signing keys and sessions, in a separate margaret schema. Recreate your database to get them.
Sign-in links
A sign-in link carries a random token. The box office stores only the token's digest, and when it expires:
use DateTime;
use Utc;
use model;
Margaret ships the pieces for secrets: random_token creates one, and TokenDigest hashes it. A link can be used once. Redeeming it deletes it, but only while it hasn't expired, so two clicks on the same link can't both succeed:
pub async
pub async Issuing a link also sweeps away the expired ones. The sign-in form posts the email address, and the route sends the link through the outbox:
pub async The answer is the same whether the address belongs to a customer or not, so nobody can find out who has an account.
Starting a session
Email security scanners open links to check them. If opening the link signed the fan in, a scanner would burn it. So GET /sign-in/{token} only shows a button, and the button posts to POST /sign-in/{token}, which redeems the link, finds or creates the customer, and starts the session:
pub async IssuedSessions comes from crate::margaret::sessions, generated from #[issues_sessions]. start takes the customer's id and the time they proved who they are, and returns the cookies to set. precede puts them on the redirect.
$ curl -i -X POST https://tickets.example.com/sign-in/s5xxE049NafXZxdYDi131UCo8uPlB04Vjbo2RfNa3y8
HTTP/1.1 303 See Other
set-cookie: __Host-margaret-session=...; HttpOnly; SameSite=Lax; Secure; Path=/; Max-Age=259200
set-cookie: __Host-margaret-session-access=...; HttpOnly; SameSite=Lax; Secure; Path=/; Max-Age=900
location: https://tickets.example.com/my-ticketsClick the link a second time, and you get the 410.
Session cookies are Secure, so browsers only send them over HTTPS. Run the public server behind a TLS-terminating proxy, even on the office network.
Recognizing the customer
Write a singleton with #[infers_authenticated_user], naming the user type. Its #[infer_from_request] method asks for the session with #[session], and decides who's there:
There are three possible answers: Authenticated with the customer, Anonymous for guests, and Interrupted to stop the request with a response of your choice. Each user type has exactly one provider, and asking for a user type without one is a build error. Session subjects are UUIDs, like the customers' primary keys.
Asking for the customer
Ask for the customer with #[authenticated_user]. The checkout no longer needs an email address, and guests get a 401:
pub async Order pages are private now. A customer only sees their own orders:
When guests are welcome
Ask for an Option instead. The new My tickets page sends guests to the sign-in form, and shows customers their latest orders, newest first:
pub async pub async The concert page asks for an Option too: customers see the seat picker, and guests see a "Sign in to buy tickets" link.
Signing out
Margaret handles signing out. Declare a route, and say where to land afterwards:
use responds_to_http;
use serves_session_endpoint;
use RouteMethod;
use SessionEndpoint;
use crateListConcerts;
;There's no #[singleton] and no method: the framework serves this route. It forgets the session, clears both cookies, and redirects to the line-up. My tickets has a "Sign out" button that posts to routes.public.sign_out.url().