Building the OpenTaxi app — Part 3: Authentication

Pieter Raubenheimer
6 min readJun 26, 2018

This series of articles, will take us through a prototyping process: designing, architecting and coding; including wireframes, deployment templates, source code (Python and JavaScript), and references to tools/documentation we used.

Our focus is not on optimising user experience, but rather on how efficiently we can build something end-to-end. We’re going to try to be innovative, so we are likely to make some mistakes along the way.

Security is arguably the most important aspect of online systems today.

I was a bit of a network snoop from a young age and even practised as a network administrator for some time. When I first started working with professional software developers I found it alarming how much they relied on implicit trust and security by obscurity. They’d argue that it gets in the way of delivery and that the cost is hard to justify.

In the cloud there’s no excuse for turning a blind eye. Experts have created the infrastructure we need to be both efficient and secure at any scale.

As we saw in Part 2, roles and permissions often make up the largest part of our CloudFormation templates. It may seem daunting at first and will take some iteration to get right, but once we nail it we can sleep like never before.

Building our User Sign-in/Authentication

We noted that AWS IoT MQTT does not include sender identification in messages delivered to client devices. In a peer-to-peer scenario, we cannot trust that the devices will be running our own, unmodified code, which means that the contents of such messages cannot be trusted. We found a function in IoT Rule SQL to inject the ‘client id’ into messages. Using this technique in combination with locking down the ‘client id’ with the iot:Connect permission, we can determine that a message comes from a particular session.

To summarise the related requirements:

  • Messages sent to a reply topic must be seen only by the intended recipient
  • A reply topic specified in a message must belong to the sender
  • A user profile specified in a message must belong to the sender

We used the STS AssumeRole API, see (1) above, to generate temporary credentials that we use when connecting with MQTT. As a development on our previous implementation, which used a custom policy, we can use the policy variable aws:userid to explicitly tie our connected session (2) to ‘reply topics’. Using an identifier for our user profile as the session name (1 & 3), messages with an injected ‘client id’ can be cross-referenced to the user profile.

Experimenting with Cognito

Cognito offers standards-based, federated identity management, allowing us to use protocols like OpenId and OAuth 2.0 to integrate identity providers like Google and Facebook. It is not a requirement right now, but worth checking out for future-proofing our solution and also potentially offloading some responsibility to a managed service.

Looking at the authentication flows documentation, we see that, given we aren’t using an external or login provider or user pool, we’d use one of the Developer Authenticated Identities flows:

Basic vs Enhanced flow

Would the aws:userid policy variable work as before? We used the AWS console to create and Identity Pool linked to our Role and wrote a script to test both flows:

With the Basic flow all was fine, but with Enhanced flow, the ‘user id’ became static. We were no longer able to link our user profile to the ‘user id’:

Digging a bit further, we found that Cognito has some policy variables of its own. We could use cognito-identity.amazonaws.com:sub in our policies and correlate that with our user profile. It is available to us in the OpenId token (JWT), and you can see for yourself by copy-pasting the a token into the nifty debugger tool at jwt.io.

Is the added complexity worth it? If we were actually going to use the OpenId token from the client side, using the full AWS SDK, it would be needed. But are we gaining anything right now? We could strip the AssumeRole permissions from our auth function role, but we’d hardly be hardening security for this function. It already handles the OpenId token and can get the same credentials without any added permissions. For now, let’s stick to what we had.

Coding our Auth API

We’ll be exposing our Authentication function through API Gateway. Our CloudFormation transform generates the API specification based on event sources defined on the Serverless functions to be deployed with our template.

We will not need any API keys, but can always generate some using the AWS Console if we want to limit the damage that can be done from client applications. If API keys are exposed in distributed apps, or a single-page web app, we can rotate as neccesary.

For every user profile submitted (we’ll call them ‘registrations’ here), we can send an SMS text message containing the secret code. We’ll save a hash of the secret code, along with the profile details to DynamoDB. The user would enter the code and the client app would submit this in exchange for temporary AWS credentials. These credentials should have a limited lifetime, so, along with the credentials we’ll send a ‘next’ auth code which the client app can use to get fresh credentials. We don’t want unused registrations hanging around and can manage expiry of registrations by means of a TTL (time-to-live) value on the DynamoDB records.

Like this:

Hopefully, we can clearly see what to needs to be done, so we’ll dive right in.

I like to create a little request/response specification in text before I start:

-> POST /registrations
{
"rider": {
"nickname": "Sam",
"phone_number": "+447776000000"
}
}
<- 201
{
"id": "abc123"
}

And for getting the credentials:

-> PUT /registrations/abc123/credentials
{
"auth_code": "888666"
}
<- 200
{
"next_auth_code": "sUPQjK6rddM58yq6F4PDdIsDb9MH7oFgMu4LWGgG",
"access_key_id": "ASIAIZA...",
"secret_access_key": "/B9yosl9...",
"session_token": "FQoDYX/////...",
"duration_seconds": 3600,
"user_id": "AROAIYNP3R63Y5J4OLFXQ:abc123"
}

So, after a few make clean build package deploy, we can submit a new registration, and… hey presto, I have a new message!

To run our scripts, we can copy-paste the details from a curl response:

client_id = "AROAIYNP3R63Y5J4OLFXQ:abc123"
reply_topic = f'ot/replies/{client_id}'
mqtt = client.get_client({
"client_id": "...",
"access_key_id": "...",
"secret_access_key": "...",
"session_token": "..."})
mqtt.connect()

And when I run it:

(.venv) ➜  open-taxi git:(master) ✗ python examples/rider.py
subscribing to ot/replies/AROAIYNP3R63Y5J4OLFXQ:abc123
published to ot/riders/broadcast
subscribing to 19 and unsubscribing from 0

It works.

Next Up

  • Client App: We still have some wireframes to code up into something that looks respectable. I’m starting to think we should do a mobile web app. What do you think?
  • Client Library: A JavaScript library that will handle the interface with our API and the AWS IoT MQTT SDK would be handy to have.

You can check out the current source code at: https://github.com/jupiter/open-taxi.

--

--