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::UserSessionor oneIdentities::AdminSession, a record in the Rails database.- What a session is, on the Rails side defines the record, the cookies and the limits.
- 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
| Word | Meaning |
|---|---|
| JodApp Web | The 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 system | One 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 |
| Runtime | One 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 browser | The person's own browser, running JodApp Web after the first page has loaded. The clientLoader and clientAction exports and every event handler run here |
| Node | Our 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 request | The 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 request | The 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 cookie | jodapp_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 cookie | CSRF_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 JSON | What 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 client | The 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.
| Runtime | Runs where | What it sends to Rails | What it may do |
|---|---|---|---|
| The browser | The person's device | Both cookies, added by the browser itself. X-CSRF-Token on every request that changes data, added by the API client | Everything. It signs the person in and out, and it makes every write |
| Node | Our server, inside our network | The person's Cookie header, forwarded exactly as the browser sent it | Reads 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
actionexists for that case only, and no route exports one.
- A server
- Node forwards no
Set-Cookiebecause 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
.datarequest reaches Node first. - Node's
middlewareandloaderexports call Rails with the person's cookie.
- A document request or a
- Direct from the browser.
- A
clientLoader, aclientActionor an event handler calls Rails itself. - The browser adds the cookies. The API client adds the CSRF header on writes.
- A
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 variableAPI_INTERNAL_JODAPP_URL. - Admins:
api.internal.teamjod.app, set by the server-only variableAPI_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.
- Users:
- Node forwards the whole
Cookieheader, so the CSRF cookie travels too.- Rails ignores that cookie. Only the
X-CSRF-Tokenheader is compared, and Node never sends one.
- Rails ignores that cookie. Only the
One cookie jar, two destinations
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, andapi.jodapp.com, where Rails answers.
- That domain covers
- 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_idandTEAM_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 oneCookieheader from the jar and attaches it to both requests. The API client addsX-CSRF-Tokento 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.
| Rule | What JodApp Web does | Why it is safe |
|---|---|---|
| No cookie means no call | A request to Node with no session cookie is treated as signed out. Node never calls Rails for it | Rails 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 out | A 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 session | Rails 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 session | JodApp 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 answer | Rails 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 system | The cookie expires | The session ends when unused for |
|---|---|---|
| Users | 30 days after login | 7 days |
| Admins | 7 days after login | 1 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.
- Node forwards it. Rails answers
- 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 reads | Because |
|---|---|
| The value inside the session cookie | It is a signed id for Rails. JodApp Web only checks that the cookie is present |
| The meaning of the CSRF cookie | The API client copies it into a header. Nothing in JodApp Web compares it with anything |
| Any fact about the person from a cookie | Name, 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's | Rails 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.