Bennett Barouch, Advisor / Mentor / Coach
AutoJax: Secure and simple authentication, session management, and ajax, for non-enterprise budgets
A particular recurring problem is faced by indie developers and small software shops. Surprisingly, the same problem is faced by IT, Software Engineering, and Software QA departments, even in large companies.
Secure, robust, feature-rich, well-documented, authentication and session management tools are broadly available, but not in-budget for your projects. This is the kind of infrastructure you don't want to have to think about, and frankly don't have the security expertise to do well on your own, but indies and small shops have to do it again and again, and even big company departments have to do it repeatedly for in-house tools they build and support.
If this is you, you don't need globally federated single sign on. You can't possibly justify the cost of Okta or any other Identity-as-a-Service providers. Even if you could, you still would have to deal with the code-bloating, error-prone annoyance of checking every ajax call for retries, session expiration, and other possible conditions that are not the problem you are trying to solve.
Every such shop and department has several apps that never went near an enterprise identity provider. The internal admin panel. The customer portal that started as a weekend prototype. The thing the ops team uses that somehow became business-critical.
They all have auth. They all use ajax. They all have to ensure a CORS policy and a CSP policy, and use of HTTPS even on the first visit, and on every visit, and deal with rate limiting, and session management, and … you wrote most of it yourself, probably a while ago. And if you are honest, you are not completely sure it is right, robust, and as secure as it should be when your app is managing real customer data or providing access to mission-critical in-house systems.
Here's the ideal: easy to use, super secure, no manual error case handling, so you can focus on the value your app is meant to provide instead of the infrastructure burden it imposes. Do you know what the word "infrastructure" means? It means stuff we absolutely need but don't want to have to think about.
Here is the good news: There is a short list of principles that separate "quietly harboring disaster" from "excellent", and they are contained in this article and in a reference implementation I will link to at the end.
-
Don't put your auth tokens in cookies, auth headers, or url parameters.
- You've read the articles. You know these are all exposed in ways you don't control. But the articles don't always make the actual issue clear. It's obviously not that TLS fails. TLS is fine. The problem is that every one of those options gets your auth token recorded in plaintext, in a place where you have no control.
- Every url parameter lands in the webserver access log by default, on every single request, and in browser history, and in the Referer header you hand to the next site you call.
- Headers and cookies feel safer, but they are not, and here is the part nobody says out loud: the access log, the reverse proxy, the load balancer, the firewall, the performance monitoring agent, whatever debug logging a teammate switched on during an incident and forgot to turn back off. Your log store is possibly the least-guarded system you own. A token sitting in a log is a token that can be lifted and replayed. And as you probably know, a large share of security breaches are inside jobs. Headers and cookies get swept into all of these servers' logs, in plaintext.
- Surprising alternative: Run 100% of your ajax calls through POST, and include your auth token in the POST request's body. It is not that those edge servers can't see the request body. It is that, by default and by long habit, headers and cookies get written to their logs, and the request body doesn't. It's not a guarantee, but in the real world of existing practices, it's very likely a security boost.
- "100% POST? But I like GET." You'll GET over it when you see the benefits of this approach.
- "But attaching an auth token to every ajax call by hand is exactly the nuisance overhead you correctly said we want to avoid, and might even forget on some calls." That's why a solid implementation does it for you automatically, every time, the same as if you used a cookie, but without a cookie's settings to get wrong and without the token likely to be recorded somewhere readable. And, by making every ajax call work exactly the same way, we get all of the system's benefits with less code, fewer opportunities for errors (in the system itself and in your use of it), and less for developers to learn.
-
Use a single dispatcher for every ajax call.
- "But I like the clarity and separation of having one endpoint for each purpose." Yes, but you don't like that every time some junior programmer adds an endpoint they have to replicate or at least properly invoke all the CORS, rate limiting, and other stuff you get for free on every call if it's done right once, and every call uses it automatically.
- You still separate the actual service being called into its own file(s). You just enter the file from this port instead of directly by url. And the POST remains equally self-documenting: a single line such as "service: 'foo'" is no less clear than going to url /foo.
- Fringe benefit: This also abstracts the true location so if you move your services from the likes of /foo to something like /services/foo, one config value change moves every ajax call with zero code changes.
-
Build rotation in from day one, with an overlap window.
- Here is the question nobody building their own secure software asks until it is too late: "How do I change the encryption key or HMAC signing secret?" Did you have to back up on that one? – "You mean tokens should be cryptographically secure?" Yes.
- Most homegrown systems have no answer. Either they don't secure their auth tokens at all (yikes!), or the secret chosen at launch is the secret forever, which means a leaked secret is a catastrophe instead of a two-minute config file change.
- When you rotate, keep accepting the old secret for a short grace period while signing new tokens with the new one. Are you doing this? Existing sessions stay valid, re-sign themselves with the new secrets on whatever is their next ajax call, and the old secret ages out. All from a simple config file change. The same idea covers encryption keys for sensitive data in your database: During the window, decrypt with the old key, re-encrypt with the new key, and while that's happening, access tries the new key and if it fails, uses the old key.
- Done this way, rotation stops being scary or just never done, and becomes simple. Build it in at the beginning, even if you never expect to rotate. Retrofitting it onto a live system later with real sessions and real encrypted data is a genuinely painful and error-prone, possibly system-disabling, data-losing task.
-
Run with the least possible state on the server.
-
"But that means putting state in the token, which everyone says is bad."
It's bad for three reasons.
- Most people are not encrypting their tokens, so putting state in them risks exposing sensitive data. Answer: Use encryption. But that's the very thing non-security experts get wrong all the time. Yes, but a good implementation uses OpenSSL or a similarly vetted library for the actual encryption, and merely ensures it always happens automatically.
- Most people are not signing their tokens, so they cannot ensure tokens have not been altered. Answer: Sign the tokens, also automatically, using HMAC-SHA256 from a vetted crypto library.
- Pushing a lot of state back and forth over the wire is not efficient. Answer: All the state we are talking about is about 300 bytes when base 64 encoded. Your network can handle that, and some of that would be present in any scheme that transmits any type of auth token, so even 300 bytes overstates it.
-
"What's the big deal about keeping a session table on the server?"
- The approach under discussion here means one indexed read per request with no JOIN that you had to do anyway to get account status, so it's effectively free, and with no session table to grow, replicate, clean up, or accidentally mismanage through either coding errors or manual administration.
-
"But without a session table, how can you revoke a session already in progress?"
- To begin with, your tokens should be built with a short lifetime, but it remains that there are times when revocation must be no later than the next login attempt or ajax call. Presumably, you already have an account status field on each user record that you can instantly switch to "suspended" or some other suitable value. In the same query, you can also set a timestamp on the record past which no existing session will be honored. No added run time cost, and just as automated as everything else in this approach.
-
"But that means putting state in the token, which everyone says is bad."
It's bad for three reasons.
-
Encrypt all identifying or otherwise sensitive information at rest.
- "But I need an email address or other human-readable id to do lookups, at least for login." No you don't. Let's say you are using an email address and a human-readable unique id. Encrypt it so it's safe, and decrypt it in the rare cases when you need it. Aside from that, force it to all lower case and hash it. Do your lookups based on the hash.
- While we are touching on the database, route all db queries through a single utility that ensures use of parameterized statements that guarantee there are no sql injections.
-
Store db credentials, encryption keys, HMAC signing secrets and any other privileged information outside of
document root, and not in any repository.
- The usual advice is to put these things into environment variables. This is hard to do correctly on many shared hosts, and in most cases, even on a private host, really just means you've put those values in some other file somewhere, so who are we kidding? Put them in one place that's easy to keep current and coherent, and protect that one place appropriately.
- This is an easy move that closes the whole category of "we accidentally published our secrets".
You might read the list above as a tax on getting things done. It is closer to the opposite.
A design that is secure, predictable, and handles all the infrastructure logic for you is an accelerator, an annoyance remover, and a quality improver that pays dividends in fewer bugs as well as fewer security vulnerabilities. Additionally, having one such system that works for every project means learning to use it once, and never having to build or learn something else the next time. It's easy because it is both simple and predictable. All the messy, not-easy, tricky bits have been encapsulated so you don't have to think about them – like good infrastructure is supposed to be.
One client-side entry point for every request, so token handling, retries, and refresh live in one place instead of being copy-pasted into every fetch call, or collected into a bespoke library with its own issues, and that some developer someday forgets to invoke. Silent, debounced token refresh, so a session extends itself in the background without a round trip on every click. Idle-timeout handled as real UX: a warning dialog before you log someone out, and wake-and-visibility detection so a laptop that slept overnight does not pretend the session is still fresh.
Users do not notice when auth, security, and session management are thoughtfully done. They really notice when they are not.
None of this is exotic. It is a short list of principles, each defensible in a brief sentence, that hand-rolled auth tends to skip because it's not your job to think about these things in any detail. Your job is to create great apps that serve your user base. Your secret infrastructure job that no one gives a damn about until it goes badly is to get this stuff right, the first time, and every time. The cost of getting them right at the start is a few hours of learning how to hook into it. The cost of retrofitting them after an incident is measured in a different currency.
If you build web apps for other people, or internal tools, you are already in this business whether you meant to be or not. You may as well be good at the part that is supposed to disappear by being so well done you can take it for granted, like we always want infrastructure to be.
I built numerous iterations of this functionality in the course of my work as a consultant. Here is a link you can use to go from the opinions in this article to the practicalities of implementation, whether out of intellectual interest, or to use the library directly, or to use it as a reference for work you do on your own.
The back end is in PHP and MySQL because that combination remains the most popular backend for the small to medium sized businesses for which I have most often consulted. If you want to port it to Java or Node or whatever, or to a different database, it should be pretty straightforward.
As built, it is a self-contained secure session-auth library that makes every principle above concrete: a single entry point that provides all the vetting needed to discard illegitimate access and facilitate safety and predictability for legit access to any number of ajax services, encryption key and signing secret rotation with overlap windows, nearly-stateless and effectively free session management with revocation, encrypted-at-rest identifying data, secrets outside of docroot, and minimal session UX to match. You can find it at https://github.com/bjbarouch/autojax.