Good evening, Code Wolfpack! Welcome to our advanced track, where we focus on features that require a combination of cross-cutting aspects to bring to fruition. Today’s case: QR Sign-in!
Magic with Cameras
You’ve likely seen this before in other apps, but if you haven’t, how it looks from the perspective of a user feels like magic: You, as a user, logged in on your mobile phone, scan a QR code displayed on the desktop app. The desktop app shows some visual indicator of communication success, you tap a button on your phone, and boom! Logged in on your desktop, no password, no 2FA required.
In short: magic. But every magic trick has slight of hand that can be explained and understood. Pulling the rabbit out of the hat can be explained in three acts: Presenting a QR Code, supporting mixed authentication providers, and creating a channel to allow the authenticated client to send what it needs to for the unauthenticated client to log in.
Presenting QR Codes
QR Codes are a matrix barcode format which stores data with error-correction keys. Thankfully, presenting QR Codes is a very well-solved problem, and there are great libraries to do this. In our case, supporting the use of QR Codes is as simple as
and then dropping in the component:
Supporting Mixed Auth Providers
In our project, Howler, we support server federation, which is allowed to designate their own auth server instead of using the official Howler auth gateway. Because of this, we rule out any ability to use our built-in auth gateway (per separation of concerns) or the Cognito service’s auth challenge hooks, as we need to obtain auth tokens for the federated servers the user is connected to. The multi security domain model for authentication is not relevant to today’s topic, so for the sake of simplifying the example, it will be noted that auth tokens will be directly managed via localStorage.
Rather than complicating our workflows by requiring the user go through custom re-authorization flows, which can be relatively fragile, the approach we will use is simply copying the existing tokens for the user from the authorized client to the unauthorized client. But how do we get there? From the perspective of users, the benefit of Howler’s design is that federated servers do not need to rely on Howler infrastructure at all to operate, which means likewise the users who have a similar concern with Howler’s degree of access to auth credentials for those federated providers would object to simply trusting us popping open an SSL-wrapped socket and shuttling traffic through — unless we can prove that you don’t have to trust us. Enter the wonderful world of cryptographic key agreement protocols.
Creating the Channel
Let’s take the naive example first. Assume that you do trust we’re encrypting data at the very least, and the data is eventually expunged at some point, but obviously not for the duration of the exchange. It would likely look something like this: the desktop client creates the channel by providing an id, the mobile client sends the credentials to that id by scanning it as the QR code.
Here’s why that’s a mistake. An attacker can see the credentials too, by behaving like the unauthenticated client. An attacker can also substitute credentials under their own control as well, to honeypot valuable targets. This is an uncommon attack scenario for most users, but not remotely unheard of for activists and journalists.
So how do we solve the nosy neighbor reading credentials? Asymmetric cryptography? Just have the desktop client send a new public key and hang on to the private key to decrypt. The data gets encrypted by the sender, so only the desktop client can decrypt it. That should solve the nosy neighbor problem!
Except it doesn’t, it just changes the game. Compromised networks, either as a corporate or campus network with an SSL decrypting proxy or a malicious attacker controlled network offer the potential of intercepting the request, with the attacker substituting their own public key, allowing them to decrypt the auth tokens, then pass them back along using the desktop client’s public key, leaving the user none-the-wiser. And again, still no solution to the attacker sending credentials.
So how can we ensure that the mobile client receives the right public key? By making the ID itself a derivation of the public key — using a strong hashing algorithm is a great approach. Now the mobile client can confirm the public key is originating from the desktop client. But how do we know the mobile client is the one sending the right credentials? We can send a confirmation of the sender, like a user name or photo. This is about where most services stop their efforts. It looks something like this:
But here’s where they’re getting it wrong. This requires some intermediary trust in the provider of that data. Either the data itself is being implicitly trusted and displayed (the mobile client sent the user profile to show), or it is sourced from a third participant (a user info server), which doesn’t work well in our own model, but also can only be trusted if the auth tokens involved to source that data are exposed to that service, meaning this channel is no longer private to the two participants.
So we’re left with the final problem, eliminating any need for trust at all, by considering the channel to be completely hostile, and performing a type of key exchange where both sides generate elliptic curve keys, and perform a Diffie-Hellman exchange (ECDH). This exchange requires no revealing of private keys, and the mutually derived key never is exposed outside of the memory of the clients. The auth tokens can be safely passed through the channel when encrypted using the derived key as a symmetric key. The final consideration — a man in the middle exchanging keys with both sides — can be solved with a derivation of the symmetric key itself (i.e. a hash), and displaying either part or whole of the hash on both clients for the user to confirm. If they match, there is no MITM.
Want to see it all pieced together, take a look at the commit!
It’s very rudimentary, but it works, and we’ll style it, and get it rolled out before next Code Wolfpack. See you then!