Neat authentication for Hanami
by Paweł Świątkowski
02 May 2017
In new frameworks like Hanami, which recently hit 1.0, there is an ongoing problem with libraries for performing simple tasks. One canonical example is user authentication. For now we have Tachiban, which is work in progress, and a number of articles about rolling your own authentication system or use OAuth.
None of them, however, addresses a problem I personally have with many existing authentication solutions (Devise included). When choosing between password-based authentication and OAuth one, the lib always let’s use one of them or favours one heavily. Examples?
- Rodauth does not integrate with OAuth at all
- Devise if generally password-based. Its integration with OAuth is obviously secondary and built upon existing solutions (with some hacking).
- Clearance had some intention of introducing OAuth (see here), but it was abandoned years ago with suggestion to use separate library for handling OAuth.
Can’t we have both?
This question is easy to answer. We can. The answer is omniauth-identity, which basically turns your application database into Yet Another OAuth Provider, letting you have consistent OAuth experience, no matter which method you use.
As awesome as it sounds, a word of warning has to be given: this only handles signing in itself. No registration, no confirmation emails, no account locking – you have to do it all by yourself. But my personal opinion is that you probably be better off doing it manually anyway. Generic solutions like Device are great to bootstrap your project, but they become quite a pain later, when you want to customize it.
Now, can we have ominauth-identity in Hanami? Unfortunately no, as it relies heavily on active record pattern. Therefore I have written Hanami provider for OmniAuth and I’m going to show you how to get it to work in your Hanami app.
The solution is heavily based on Monterail’s tutorial (which is currently out-of-date).
Database
We start with database. What I want is to have a User
entity separate from it’s credentials, so that I don’t need to create passwords etc. every time I create a user in tests. I do it by creating two repositories:
In this example, credentials
table stores information about how the authentication data should be mapped to user. It is kind of interleaved and you might want to separate password-based credentials from OAuth-based ones, if you are SRP fanatic, but here we are going to have just one table for both.
Setup OAuth
Let’s start with setting up authentication with Github OAuth. Refer to Monterail’s tutorial for further explanations. Add this to Gemfile
:
Then to application.rb
:
And adjust your .env.development
:
web/config/routes.rb
:
Now we should create session#create
controller.
and in UserRepository
We are more or less done here. If a user gets redirected to /auth/github
endpoint, their should be asked by Github to authorize your app and logged in. Appropriate records for credentials
and users
tables should be created. Let’s move to another step then.
Add identity provider
We are going to start with Gemfile
again:
In this example I use scrypt
, but feel completely free to use whatever you like. You will see that control over hashing algorithm stays always in your application and the gem does not assume anything about it (unlike Tachiban).
Now, edit application.rb
section:
omniauth-hanami
assumes the repository defines find_by_credentials
method. You can change it by passing appropriate options to the provider configuration, but let’s define it according to convention.
Here is a part about hashing algorithm of choice I mentioned. As you see, checking with SHA-512
or bcrypt
would be equally easy. It’s all up to you.
Also, note a convention here. When using email + password combination to sign in, provider
column in credentials
is set to self
. Of course, you might use anything you want. Just make sure to set it correctly while registering a user. In case of my application registration looks like this:
Finishing up
To have it working in full, we need a few details. For example, a sign in form:
It contains a hack I don’t really like – sending a POST
request to OAuth’s callback URL. However, it is perhaps the easiest way to maintain security of a login. If you have a better idea, let me know. Anyway, to make it work you need to add this to routes.rb
:
Want to see a complete example?
There is a working example here, just copy .env.example
to .env.development
and fill it with your Github credentials.