I get so depressed with authentication complexity. Seems to me there are two ways to avoid it: first, write a service that doesn't care about user identity (it is a fun exercise to think of such a thing!), and second, secure messages, not connections or sessions.
Securing messages with public/private key encryption, I believe, is the best possible general approach. Thinking in messages yields the programmer great benefits, not just inside code (it is the cornerstone of the OOP paradigm, after all), but outside of code (message passing is also the cornerstone of distributed programming). Even if you are building a webapp it helps to think in terms of something more general. HTTP becomes just another channel over which messages move. Your app can (and probably will) become sensitive to other channels: email, SMS, webhooks, etc. If you embrace message-level security then you can ignore the channel and deal with the message. Channels may change, but your application code doesn't need to.
For an SPA, the key problem (no pun intended) is that a users private key is on the users browser, in the simplest implementation, accessible by everything on the page. If you're not end-to-end encrypted and don't use 3rd party resources at all (its possible!) then the naive solution is fine. The most robust solution is to use a browser extension to isolate the private key. The site requests encryption services from the extension.
Another fun and interesting problem is the multi-device user. Do we allow copying the private key, and if not, how do we associate private keys together? I think this is a fun problem from lots of angles, particularly the prospect of your own devices inviting each other to share an identity.