This will be the second part of the code examples, and the third in the series about Drupal and the Internet of Things.
If you haven't read the other parts of it, you are going to be all fine. But if you want to, the front page has a complete list
In this part we will look at simplifying the request flow for the client, while still keeping a certain level of security for our endpoint. There are several ways of doing this, but today we will look at a suggestion on how to implement API keys per user.
Let me just start first by saying there are several additional steps you could (and ideally should) implement to get a more secure platform, but I will touch on these towards the end of the article.
First, let's look at a video I posted in the first blog post. It shows an example of posting the temperature to a Drupal site.
Architecture
Let's look at the scenario we will be implementing
- A user registers an account on our Drupal 8 site
- The user is presented with a path where they can POST their temperatures (for example example.com/user/5/user_temp_post)
- The temperature is saved for that user when that request is made
See any potential problems? I sure hope so. Let's move on.
Registering the temperature in a room and sending to Drupal
As last time, I will not go into details on the implementation of any micro controller or hardware specific details in the blog post. But the code is available on github. I will quickly go through the technical steps and technologies used here:
- I use a Raspberry pi 2, but the code should work on any model Raspberry pi
- I use a waterproof dsb18b20 sensor, but any dsb18b20 should work. I have a waterproof one because I use it to monitor my beer brewing :)
- The sensor checks the temperature at a certain interval (per default, 1 minute)
- The temperature data is sent to the Drupal site and a node is created for each new registration
- To authenticate the requests, the requests are sent with a
x-user-temp
header including the API key
This scenario is a bit different from the very real time example in the video above, but it is both more flexible (in terms of having a history of temperatures) and real-life (since temperatures seldom have such real-time changes as the one above).
Receiving temperatures in Drupal
The obvious problem with the situation described above, is the authentication and security of the transferred data. Not only do we not want people to be able to just POST data to our site with no authentication, we are also dealing with temperatures per user. So what is to stop a person to just POST a temperature on behalf of another user? Last post dealt with using the same user session as your web browser, but today we are going to look at using API keys.
If you have ever integrated a third party service to Drupal (or used a module that integrates a third party service) you are probably familiar with the concept of API keys. API keys are used to specify that even though a "regular" request is made, a secret token is used to prove that the request originates from a certain user. This makes it easy to use together with internet connected devices, as you would not need to obtain (and possibly maintain) a session cookie to authenticate as your user.
Implementation details
So for this example, I went ahead and implemented a "lo-fi" version of this as a module for Drupal 8. You can check out the code at github if you are eager to get all the details. Also, I deployed the code on Pantheon so you can actually go there and register an account and POST temperatures if you want!
The first step is to actually generate API keys to users that wants one. My implementation just generates one for users when they visit their "user temperatures" tab for the first time.
Side note: The API key in the picture is not the real one for my user.
Next step is to make sure that we use a custom access callback for the path we have defined as the endpoint for temperatures. In my case, I went with making the endpoint per user, so the path is /user/{uid}/user_temp_post
. In Drupal 7 you would accomplish this custom access check by simply specifying something like this in your hook_menu:
'access callback' => 'my_module_access_callback',
In Drupal 8, however, we are using a my_module.routing.yml file for routes we are defining. So we also need to specify in this file what the criteria for allowing access should be. For a very good example of this, I found the user.module to be very helpful. My route for the temperature POST ended up like this:
user_temp.post_temperatures:
path: '/user/{user}/user_temp_post'
defaults:
_controller: '\Drupal\user_temp\Controller\UserTempController::post'
_title: 'Post user temperatures'
requirements:
_access_user_temp_post: 'TRUE'
In this case '_access_user_temp_post' is what will be the criteria of allowing access. You can see this in the user_temp.services.yml file of the module. From there you can also see that Drupal\user_temp\Access\PostTempAccessCheck is the class responsible for checking access to the route. In this class we must make sure to return a Drupal\Core\Access\AccessResult
to indicate if the user is allowed access or not.
Some potential questions about the approach
From there on in, the code for the POST controller should provide you with the answers you need. And if the code is not enough, you can try to read the tests of the client part or the Drupal part. I will proceed with making assumptions about theoretical questions to the implementation:
How is this different from using the session cookie?
It is different in 2 aspects. The API key will not expire for reasons beyond your control. Or more precisely, the device's control. You can also reset the API key manually if you would want it to expire. The other big difference is that if your API key should be compromised, your account is not compromised in any way (as would be the case if a valid session cookie were to be compromised). Beyond that, please observe that in one area this is not different from using a session cookie: The requests should be made over https, especially if you are using a wifi connection.
How can I further strengthen the security of this model?
One "easy" way to do this is to not expose the API key as part of the request. I was originally planning to implement this, but realised this might make my original point a little less clear. What I would do as a another "lo-fi" hardening would be to make the x-user-temp
header just include a hash of the temperature sent and the user API key. This way, if someone were sniffing the requests, they would just see that the x-user-temp
header would change all the time, and so it would take a considerable effort to actually forge the requests (compared to just observing the key in the header).
Why are you using nodes? Isn't that very much overhead for this?
This is a fair point. It might be a bit overkill for something so simple. But there are two bonus parts about using nodes:
- We can use views to display our data.
- We can ship the views, content types and fields as configuration with our module.
This last part is especially powerful in Drupal 8, and incredibly easy to accomplish. For the files required for this particular implementation, you can reference the config/install directory of the module.
But since you are posting nodes, why aren't you using the REST module?
I admit it, I have no good reason for this beyond that I wanted to make this post be about implementing API keys for authentication. Also, here is a spoiler alert: Code examples part 3 will actually be using the REST module for creating nodes.
What if I want to monitor both my living room and my wine cellar? This is only one endpoint per user!
I am sorry for not implementing that in my proof of concept code, but I am sure you can think of a creative solution to the problem. Also, luckily for you, the code is open source so you are free to make any changes required to monitor your wine cellar. "Pull requests welcome" as they say.
As always, if you have any question or criticism (preferably beyond the points made above) I would love to hear thoughts on this subject in the comments. To finish it all off, I made an effort to find a temperature related gif. Not sure the effort shows in the end result.