Skip to main content

What JodApp Web knows about a session

JodApp Web never holds a session. It holds the cookie that names one.

  • A session is one Identities::UserSession or one Identities::AdminSession, a record in the Rails database.
  • JodApp Web runs in two places, the browser and a Node server.
    • Each one sends the cookie to Rails and acts on the answer.
    • Neither one judges a session on its own.
  • This page covers three things.
    • The two runtimes, and what each one may do.
    • The two paths a call can take to Rails.
    • The three rules every later page relies on.

The words this page uses​

WordMeaning
JodApp WebThe React Router application in jodapp-web. It runs in framework mode with ssr: true, so every page renders once on our server and then runs in the browser. It serves jodapp.com for talent and employers, and teamjod.app for the Jod team
Identity systemOne of the two groups of people Rails knows. Users are talent and employers, each an Identities::User. Admins are the Jod team, each an Identities::Admin. JodApp Web keeps the two apart in two root routes and never shares identity code between them
RuntimeOne of the two places JodApp Web's code runs: the browser, or the Node server. The same route file holds code for both, and React Router decides which export runs where
The browserThe person's own browser, running JodApp Web after the first page has loaded. The clientLoader and clientAction exports and every event handler run here
NodeOur server process that runs JodApp Web. It answers a document request with HTML and a .data request with route data. The middleware and loader exports run here
Document requestThe browser asks Node for a whole page. A new tab, a refresh, a typed URL and a link from an email are all document requests
.data requestThe browser asks Node for route data only, on a click inside the app. Node runs the matched middleware and loader exports and answers with data, not HTML
Session cookiejodapp_session_id for users, teamjod_session_id for admins. It holds the signed id of the session. No script can read it, because Rails set it HttpOnly
CSRF cookieCSRF_TOKEN for users, TEAM_CSRF_TOKEN for admins. Page scripts can read it. The API client copies it into the X-CSRF-Token header on every request that changes data
Current-session JSONWhat GET /identities/user_sessions/current returns for a user: identities_user, talent_access and employer_access. The admin endpoint returns one key, admin. It is the only description of the signed-in person that JodApp Web ever has
API clientThe one ky instance per identity system in app/api/ky-client.js. Every call to Rails from either runtime goes through it

The two runtimes​

A runtime is one of the two places JodApp Web's code runs. The same application runs in both, and each one is allowed to do different things.

RuntimeRuns whereWhat it sends to RailsWhat it may do
The browserThe person's deviceBoth cookies, added by the browser itself. X-CSRF-Token on every request that changes data, added by the API clientEverything. It signs the person in and out, and it makes every write
NodeOur server, inside our networkThe person's Cookie header, forwarded exactly as the browser sent itReads only. It calls GET /identities/user_sessions/current, and the read endpoints a server loader needs. It never signs anyone in or out, never changes data, and never passes a Set-Cookie header back to the browser

Why the two runtimes are allowed different things:

  • Rails checks every request on its own, so the browser may call Rails directly.
    • Sending every call through Node would add one more process to every click and gain nothing.
  • Node writes nothing because no form in JodApp Web needs to work without JavaScript.
    • A server action exists for that case only, and no route exports one.
  • Node forwards no Set-Cookie because Rails never sets a cookie on a read.
    • The two cookies are written once, at login, by a request the browser makes itself.
  • JodApp Web D3 — Node reads, the browser writes, Rails decides records the rule and the measurements behind it.

Two paths to Rails​

Every call JodApp Web makes to Rails takes one of two paths.

  • Through Node.
    • A document request or a .data request reaches Node first.
    • Node's middleware and loader exports call Rails with the person's cookie.
  • Direct from the browser.
    • A clientLoader, a clientAction or an event handler calls Rails itself.
    • The browser adds the cookies. The API client adds the CSRF header on writes.

The picture below shows one of each. Mei is a talent on her laptop.

Legend: The orange box is JodApp Web, this page's subject. Its browser and its Node server are two runtimes of one application. Rails sits outside it. Every arrow into Rails carries the session cookie. Only the browser's arrow carries X-CSRF-Token, because only the browser writes.

The same three requests as raw HTTP, shortened:

GET /talent/profile HTTP/1.1
Host: jodapp.com
Cookie: jodapp_session_id=<signed session id>; CSRF_TOKEN=<random string>
GET /identities/user_sessions/current HTTP/1.1
Host: api.internal.jodapp.com
Cookie: jodapp_session_id=<signed session id>; CSRF_TOKEN=<random string>
PATCH /identities/users/current HTTP/1.1
Host: api.jodapp.com
Cookie: jodapp_session_id=<signed session id>; CSRF_TOKEN=<random string>
X-CSRF-Token: <random string>
Content-Type: application/json

Two details in those messages:

  • Node calls each API on a private host that only our network can reach.
    • Users: api.internal.jodapp.com, set by the server-only variable API_INTERNAL_JODAPP_URL.
    • Admins: api.internal.teamjod.app, set by the server-only variable API_INTERNAL_TEAMJOD_URL.
    • Both names are records in a private Route 53 zone attached to the production VPC. The browser never sees them and uses the public hosts.
  • Node forwards the whole Cookie header, so the CSRF cookie travels too.
    • Rails ignores that cookie. Only the X-CSRF-Token header is compared, and Node never sends one.

The browser keeps the two cookies in one place and sends them to two hosts.

  • Rails set both cookies with Domain=.jodapp.com.
    • That domain covers jodapp.com, where Node answers, and api.jodapp.com, where Rails answers.
  • So the same cookie jar feeds a page request to Node and a write to Rails.
    • This is the only reason Node ever sees the session cookie.
  • For an admin, the names are teamjod_session_id and TEAM_CSRF_TOKEN, and the domain is .teamjod.app.

Legend: Each dashed box is one HTTP message, or the browser's cookie store for .jodapp.com. Orange boxes are held or sent by JodApp Web, this page's subject. Grey boxes were written by Rails. The browser builds one Cookie header from the jar and attaches it to both requests. The API client adds X-CSRF-Token to the write only. What a session is, on the Rails side draws the same picture with Rails as the subject.

Three rules every page relies on​

Every later page in this set assumes these three rules. Each one has a reason on the Rails side.

RuleWhat JodApp Web doesWhy it is safe
No cookie means no callA request to Node with no session cookie is treated as signed out. Node never calls Rails for itRails finds a session only through the cookie, so a browser with no cookie cannot have one. Most visitors, and every search engine, are signed out. This saves a Rails call on every public page
Only 401 means signed outA 401 from any endpoint means the session is gone, and the person is signed out. 403, 5xx and a network failure are errors. They change nothing about the sessionRails gives 401 one meaning and gives that meaning to no other status. A catch that treats every failure as signed out would turn an API outage into a mass logout
Neither runtime judges a sessionJodApp Web never reads a cookie's age, never refreshes anything, never retries a refused request, and never navigates or clears state because one call was refused. It sends the cookie and acts on the answerRails reads the session record on every request and enforces the limits there. Any copy JodApp Web holds can be stale, and a stale copy grants nothing

The four Rails endpoints hold the full error table behind rule 2.

A cookie can outlive its session. Rule 1 does not cover that case. Rule 2 does.

Identity systemThe cookie expiresThe session ends when unused for
Users30 days after login7 days
Admins7 days after login1 day
  • So Mei can hold a cookie that names a session Rails has already deleted.
    • Node forwards it. Rails answers 401. JodApp Web treats her as signed out.
  • A missing cookie has three possible causes, and all three mean the same thing: nobody is signed in here.
    • There was never a login on this browser.
    • The person logged out, and Rails deleted both cookies.
    • The cookie reached its own expiry date.
  • The session models, on the Rails side sets both limits.

What JodApp Web never reads​

JodApp Web knows two things about the signed-in person, and reads nothing else.

  • The session cookie exists.
  • The current-session JSON says who they are.
It never readsBecause
The value inside the session cookieIt is a signed id for Rails. JodApp Web only checks that the cookie is present
The meaning of the CSRF cookieThe API client copies it into a header. Nothing in JodApp Web compares it with anything
Any fact about the person from a cookieName, email and access states come from the current-session JSON, fetched from Rails. A cookie is never a source of facts
The cookie's expiry, or the session'sRails enforces both limits. JodApp Web learns the result as a 200 or a 401

Reading the session shows how Node fetches the current-session JSON once per request and hands it to every reader.