My n00bish server side adventures with session cookies and OAuth2

In the world of functional web services, everybody seems to get to “beam me up scotty” and then peace out. This is enough for a lot of stuff, and so far I’ve got by on simply requiring separate authentication on every endpoint, since I’ve really only written SPA style web apps so far. I came to a point where I needed more however, as, although I worked things out, using OAuth2 was a big pain, and some things, such as access controlled files, would either need a lot of url junk or a better way.

People who are more embedded in the web world than I am yet likely have their favorite ways of getting this provided more effortlessly. I’m by no means an expert; I’m writing here mainly because I rarely see these assumptions documented or explicit context provided. My goal here is to explain why the status quo exists from my naive perspective and give my take on how to support cookie based sessions in a functional, microservice oriented web backend. There are likely lots of other ways to do it, possibly better.

The realization that I needed a session cookie with a service side idea of logged-in-state came when adding an OAuth2 service on the backend. You generally don’t get to add parameters to the redirect url that receives the OAuth2 token, so it kind of dawned on me that most people were doing this kind of authentication with a persistent session delivered by cookie. In fact there’s no other really clean way to do it except using a short lived cookie just to connect the foreign login screen and the redirect url. There are other benefits as well; the backend can have what amounts to user or session as ambient properties that are guaranteed available when certain things are invoked.

A little reading suggested using HttpOnly for the session cookie, which I had to think about to ultimately see the wisdom of. The point is that since the cookie value isn’t available to javascript code, it can’t be expropriated even if the page winds up containing malicious code from another source, such as an RSS feed display, a faulty web control that can paste HTML or a vulnerable postMessage handler brought in by any of the approximately 100000000 modules imported by the average web page. This also means that the SPA won’t be able to use (for example, a JWT formatted) cookie to know that the server considers it logged in unless it checks or is told at pageload time.

I’ve known conceptually how session authentication should work, but it was admittedly magical to me, never having interacted with a web framework that would have hidden this behind the scenes, and even with that, the framework user seems encouraged to just forget that session management even exists. While all of the above linked web services have attachable middleware that provides sessions, I’ve divided the backend into smaller services to enforce some separation after starting down and quickly exiting a route to a very highly coupled system in favor of more explicit but less coupled interactions.

So I wanted to understand sessions and provide session as a service via the backend router so I could ultimately just wrap backend endpoints like

driveList_ cfg |> withUser |> wrapRunPromise |> errorWrap

Which nicely states the user’s intention, captures and reports errors, provides a json body and stringifies a json result, and more importantly, does “withUser” as middleware that requires authentication. This backend has more than one layer, so a live session may need to be passed on as well.

So here’s what I came up with:

  • Every request to the router creates a session ID using a good random source unless it received a cookie header with a valid JWT encoded cookie containing a session id. The router only serves static data on its own; otherwise it provides routes for other frontline microservices.
  • A backend service can have no requirement, require a session with an optional user or require a logged-in user. The latter two are guaranteed upon entry by middleware.
  • To provide an easy path for auth information, the object the backend service receives also contains wrappers for http requests that allow it to easily make a backend service call with typed encoding and decoding wrappers and that passes on the session.

It looks something like this:

[ browser ]
^ |
| | . o O ( Cookie: session=eyJ0... ? )
| |
[| router ]
^ |
| | . o O /json object summarizing the user\ DB.read(session.id)
| | \request and the current session / DB.read?(user)
| |
`------< Set-Cookie header set, regardless of result
^ |
| v
[| user_accessible_service http user ]
| |
| | . o O /StorageBackend.request http.get "/api/v1/stored"\
| | \ http passes on the session or user we have /
| |
`------< Return JSON
|
v
[ real_backend_service http user ]

This kind of architecture has good properties; it can be layered, provides proper checking at every level and yet allows backend services to freely access other services that require the same level or lower of authorization.