Neat authentication for Hanami
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).
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.
Let’s start with setting up authentication with Github OAuth. Refer to Monterail’s tutorial for further explanations. Add this to
And adjust your
Now we should create
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
users tables should be created. Let’s move to another step then.
Add identity provider
We are going to start with
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).
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
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:
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
Want to see a complete example?
There is a working example here, just copy
.env.development and fill it with your Github credentials.