No seriously, "session" != "state"06 Sep 2018
In my last article, I showed a RESTful authentication flow… yet, whenever I go to find new things people are saying about REST APIs, I invariably run into the concept that a session is equal to application state. This concept is misleading, confusing, and harmful. It’s just wrong.
The problem with JWTs
I recently came across the article RESTful API Design Tips from Experience and its companion article Force Expiring of JWTs with Refresh Tokens. The latter seems to have come about as a result of feedback the author received on Reddit.
The relevant discussion is largely about JWTs, which are an attempt to create “truly stateless” authentication and/or authorization. The argument goes, essentially, that if you have to include some kind of session identifier with a request, that constitutes statefulness because the application (i.e. the API) must check the status of that provided token.
JWTs attempt to address that problem by including all relevant “claims” (e.g. user id) in a JSON object cryptographically signed by the issuer (in this case, the API); when the client returns the token to the API, the API can then verify the signature and be sure that the claims inside are valid.
The problem comes from the fact that as a result, JWTs are unable to be revoked. This leads to a messy situation for both authentication and (if you rely on permission claims in the JWT) authorization.
Back to “statefulness”?
There are a number of different solutions to the irrevocable-JWT problem, but they all share a theme: you keep track of tokens in the application. Whether that is tracking all issued tokens or just a blacklist of revoked tokens, it basically comes down to the same thing – the JWT becomes like any other session token.
The solution proposed by the author of the above articles takes it one step further, by having a “refresh token id” claim in the JWT, which should be validated by the application. Under this scheme, tokens are revoked by revoking the long-lived refresh token that was used to generate them. But… that’s just the same thing, deferred an additional level.
Basically, if you want to use JWTs, but without the revocability problem, you end up with what amounts to a normal arbitrary session token plus significant overhead.
Wait… were we ever even “stateless”?
Let’s circle back on JWTs even without the revocability nonsense, though. The
sub claim in the token is for the “subject” – for our purposes, the user for whom the token was issued. So when I send this token to the API… then what?
…You mean I have to look up that user anyway? How is that “stateless” under the definition we’ve been working with? Hint: it’s not. That is true for pretty much any authentication scheme you can devise – it’s just deferred to some other level (for example, a plain session token is a stand-in for the username/password; similarly, the JWTs in the scheme above are a stand-in for the “refresh token”, which is itself a stand-in for the username/password).
RESTfulAPI.net’s page on Statelessness differentiates between resource state and application state, giving the following definitions:
Application state is server-side data which servers store to identify incoming client requests, their previous interaction details, and current context information.
Resource state is the current state of a resource on a server at any point of time – and it has nothing to do with the interaction between client and server. It is what you get as a response from the server as API response. You refer to it as resource representation.
That is a pretty good explanation of the distinction, in my opinion. However, just before that… (emphasis mine)
For becoming stateless, do not store even authentication/authorization details of client. Provide credentials with the request. Each request MUST stand alone and should not be affected by the previous conversation happened from the same client in past.
Ugh… and you were doing so well. Even though the article goes on to explain quite clearly the difference between application state (prohibited in REST) and resource state (absolutely necessary for REST), it gets this wrong. The idea that each request “should not be affected by the previous conversation” is simply not true – or at the very least is too broad a claim. For example, take the following exchange:
404 Not Found
Wait… what happened? Obviously, the second request changed the state of (in this case, by deleting) the resource (not session); i.e. subsequent requests were affected by the “previous conversation”. Yes, yes, I know that certainly isn’t what the author meant. Here’s the thing, though: session identifiers are just another resource.
Then what even is application state?
Application state is just what the name suggests, as described above – the state of the user’s interaction with the application, for example what tab they have open, where in a process they are, etc. What is really a no-no, though, is hard to put into words – so instead I will use a very contrived example.
Let’s consider some kind of pagination system, whether it be pages in an article or book, a list of users, or whatever. A good, RESTful way to get the second page in a paginated list would be something like…
GET /users?p=2 GET /users?p=3 GET /users?p=4
A bad, stateful way to do it would be like this:
GET /users/next_page GET /users/next_page GET /users/next_page
The only way that can function is to store the last page retrieved for the requestor in some variable on the server – a store for data like is what is traditionally referred to as a “session”, with the data stored in it being “session data”. That’s bad for a lot of reasons, but one of them is that it’s unpredictable, so there is no consistent definition of a resource.
GET /users/next_page could refer to any page of users, and there is no way to know beforehand.
That kind of session data is the application state that REST is worried about – it’s the kind of mixing of concerns that leads to drastic increases of complexity in an application, with everything that entails. Unsurprisingly, the best way to do that is by storing that data in the client itself.
So how is a session token not application state?
As always, the best way to explain this is likely by example. Take the two resources
/users/2/posts… the beginning and end are the same, but the two URIs above refer to two different lists. That makes sense – it’s right in the URI, and relatively straightforward. That said, although Fielding gives URIs as an example of “resource identifiers”, REST has no requirement that resource identifiers be URIs. With that in mind, the following request is perfectly RESTful:
GET /user/posts X-User-Id: 1
The only requirement is that the request include everything necessary to identify the resource and fulfill the request in a stateless way. So does the following request look any different to you now?
GET /users Authorization: bearer 758e3940a50685dc33436f9268628b52
What about if I wrote it like this…?
Now it looks like a resource – because that’s what a session identifier is – and resources are what REST is all about.
Now, normally I wouldn’t recommend including resource identifier information in the headers as it is non-standard and would very likely be confusing – that is unless there is a compelling reason to do so. With sessions, there is such a reason: you don’t want what amounts to authentication/authorization credentials in a URL because those are often logged to files or other insecure locations. It’s the same reason (among others) that in the past, you stored session ids in a cookie, rather than the URL. I won’t say headers and cookies can’t be logged to an insecure location (because of course they can), but I will say that it is extremely unlikely for them to be without very good cause.
That’s all I have, really. I hope that I’ve demonstrated that resources which you call “sessions” are not inherently stateful – state is all about the logic of your application. We need to stop spreading the misinformation about sessions because it is confusing and decreases understanding in what REST is, how it should work, and most importantly, why.