What a session is
A session is one Identities::UserSession or one Identities::AdminSession, each a record in its own table.
- Rails creates it when a person signs in.
- Rails deletes it when the person signs out, or when it is older than its limit.
- Every request that needs to know who is calling ends in one question: does the
Identities::UserSessionstill exist?
The words this page uses
| Word | Meaning |
|---|---|
| Identity system | One of the two groups of people the API knows. Users are talent and employers. Each is an Identities::User. Admins are the Jod team on the Team Portal. Each is an Identities::Admin. The two systems never share a table, a cookie, or a line of authentication code |
Identities::UserSession, Identities::AdminSession | The two session models, one per identity system. A user's sign-in is an Identities::UserSession in identities_user_sessions. An admin's is an Identities::AdminSession in identities_admin_sessions. One record per sign-in. A person signed in on a laptop and on a phone has two |
| Session cookie | A cookie that holds the id of the Identities::UserSession or Identities::AdminSession. Its name is jodapp_session_id for users and teamjod_session_id for admins. The browser sends it to the API on every request |
| Signed cookie | Rails writes the cookie value together with a signature made from secret_key_base. A value changed on the way fails the signature check, so a browser cannot invent a session id. The value is readable, not secret. The signature is the protection |
| Bearer token | A random string that a mobile app receives once at login and sends in the Authorization: Bearer <token> header. Rails keeps only the token's SHA-256 digest, in token_digest on the Identities::UserSession |
| CSRF token | A second random string on the Identities::UserSession, in csrf_token. The browser holds a copy in a readable cookie and sends it back in the X-CSRF-Token header on every request that changes data |
The three callers
Rails serves three clients.
- Each one carries a different credential.
- Each one is allowed to do different things.
| Caller | What it is | What it sends | What it may do |
|---|---|---|---|
| The JodApp Web browser (client) | The person's browser, running jodapp-web | The session cookie on every request, plus X-CSRF-Token on requests that change data | Everything. It is the only caller that signs a person in or out |
| The JodApp Web server (Node) | The Node process that renders jodapp-web pages | The person's session cookie, forwarded from the page request | Reads only. It never changes data and never signs anyone in |
| The mobile app | Planned. No mobile app exists today | Authorization: Bearer <token> on every request. No cookie | Everything, with the token in place of the cookie |
Legend: The dashed box is JodApp Web. Its browser and its Node server are two separate callers of one app. Grey nodes are the callers. The orange node is the subject of this page, the authentication check inside Rails. Lavender nodes are the two tables in the Identities domain. Every arrow into Rails carries a credential. Every arrow out of Rails is one
Identities::UserSessionorIdentities::AdminSessionlookup.
What Rails sends back at login, and what the browser sends next
Picture two HTTP messages.
- The first is the response to a successful login.
- It carries two
Set-Cookieheaders and a JSON body.
- It carries two
- The second is any later request from the browser that changes data.
- It carries both cookies, added by the browser, and one header, added by the API client in the page.
Legend: Each dashed box is one HTTP message, or the browser's cookie store for
.jodapp.com. Orange boxes are written by Rails. Grey boxes are held or written by the browser. The cookie attributes are in the raw HTTP below. For an admin the names areteamjod_session_idandTEAM_CSRF_TOKEN, and the domain is.teamjod.app.
The same two messages as raw HTTP, shortened:
HTTP/1.1 200 OK
Set-Cookie: jodapp_session_id=<signed session id>; Domain=.jodapp.com; Path=/; Expires=<login + 30 days>; Secure; HttpOnly; SameSite=Lax
Set-Cookie: CSRF_TOKEN=<random string>; Domain=.jodapp.com; Path=/; Expires=<login + 30 days>; Secure; SameSite=Lax
Content-Type: application/json
{ "identities_user": { ... }, "talent_access": { ... }, "employer_access": { ... } }
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
Who writes and who reads each value:
| Value | Written by | Read by |
|---|---|---|
The jodapp_session_id cookie | Rails, once, at login | Rails, on every request. The browser only carries it. No script can read it |
The CSRF_TOKEN cookie | Rails, once, at login | The API client in the page, to fill the header. Rails never reads this cookie, even though the browser sends it |
The X-CSRF-Token header | The API client in the page, on every request that changes data | Rails, which compares it with csrf_token on the Identities::UserSession |
Why the CSRF cookie is readable and the session cookie is not.
- A cross-site attack makes the person's browser send a request to our API from another site.
- The browser attaches our cookies to that request on its own, so a cookie can never prove which page made the request.
- The proof must be something the browser does not add by itself: a header set by our own script.
- To set the header, the script must be able to read the value.
- Scripts on other sites cannot read our cookies, so the token stays secret from the attacker.
- The session cookie is the opposite case.
- It must never be readable by any script, because a stolen session id works from anywhere.
- The cookies and CSRF page covers the rest of CSRF.
The browser does not need to know any of this.
- It stores what
Set-Cookietells it to store and sends it back to the same domain. - The page's code does one thing: copy the readable cookie into the header.
jodapp-webdoes that in one place, its API client, so no page has to remember it.