Sinatra is a great choice for building out simple JSON APIs. Combined with Ember.js, it is now my preferred stack to build out web apps on, as opposed to using Ruby on Rails. However, there aren’t as many existing, ‘standard’ libraries for Sinatra, like Rails' devise. So, how shall we secure a Sinatra API?
Note that I’m not a security expert and this is probably not best practice. Please treat what I’m doing here as a learning experiment.
First, a word on security: if every request between the application and the outside world is going to be routed over a secure connection using TLS (formerly SSL), then we can simplify our authentication design significantly. For many APIs using TLS, just using the bog-standard HTTP Basic Authentication is totally acceptable. Note that HTTP Basic Auth just sends a “username” and “password” in the HTTP headers without any encryption, on every request.
So, let’s assume that we’re going to use TLS when we deploy the application. We could just use HTTP Basic Auth - this is well documented. But, there are certain oddities of Basic Auth, including the way that browsers often interact with it. Plus, if we use basic auth, the user has to send their password to the server with every request, and we have to do a password comparison on every request. This isn’t fun, especially if we’re using a computationally expensive password hashing algorithm, as we should.
Instead, we will implement a basic variation of token based authentication.
Here’s the procedure:
- User sends their username and password to a
- The server generates and stores a very large random token, and returns it to the user
- The user sends their token with future requests to the server
There are several major advantages here. One, the user doesn’t repeatedly send their password - if a token gets stolen or packet sniffed, it is a concern, but we can just expire the token. We can also change our token scheme easily, issuing multiple tokens to the same user, revoking all of a user’s tokens, issuing different tokens with different rights, and so on, as our applications' requirements change. If we want, we can issue different tokens with different expiry times - say, a 120 day token for a user who runs our app on a kiosk, and a 15 minute token when the user logs in from a new IP. Finally, we don’t have to do any password hashing or user lookup by email or username when we receive a token, which can increase speeds.
First, a basic
user model - I’ll not mention how the user is persisted, but assume that it is. To secure the password, we can use the bcrypt-ruby gem.
1 2 3 4 5 6 7 8 9 10 11 12
The ruby BCrypt gem is great, because it lets us do password comparison between regular strings and the encoded hash automagically. So, our login check can be as simple as:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
before do block is just some magic to parse a JSON request body - you can replace it as you desire if you’d rather accept non-JSON requests, auth details in headers, or something else. If the user is logged in, we want to generate a token, so let’s do that:
1 2 3 4 5 6 7 8 9
SecureRandom.urlsafe_base64 method just generates a nice, long, random, url-safe string. There is a similar
base64 method that doesn’t guarantee url-safety; for web applications, there is no real disadvantage to making it url-safe. Whether or not you make a
generate_token! method that does the saving itself or defer persistence to the controller is up to you.
Now, we can finish out our controller method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Now we need to be able to protect API routes and restrict them to logged in users. All we need to do is check the token that the user sends us to see if it is valid. Here’s an example:
1 2 3 4 5 6 7 8 9 10 11 12 13
If the user has a valid token, we’ll be able to pull up their record, set the user record as appropriate, and do something. If not, halt and send them a 403 Unauthorized response - by calling
halt, Sinatra will make sure nothing more gets executed from that route handler.
From here, you can add features as desired - accept the token in a HTTP header instead of in the request body (good for when clients might do pre-flight HEAD requests), expire the tokens automatically based on their age, create a
delete action that removes tokens, allow users to have multiple tokens, associate certain access rights with a token, and so on, all within the same basic framework. Just make sure you put your web service behind TLS!