user auth in Ruby - Cuba, Shield and Mongoid
Tags: ruby, mongodb, beginners
Published: 2020-05-02 01:07:07


User authentication should be secure and easy to understand. The crew at Openredis provide an authentication ecosystem that's expressive and Ruby  idiomatic. The following provides the building blocks for a basic authentication system for your Ruby apps:

### Tools
- **Cuba**: Popular microframeworks like Sinatra and Ramaze will forever have a place in Rubyland. However, Cuba and its small codebase (~400 LOC) is succinct enough and friendly to new developers. It's also quite readable and terse if you're into that.
- **Mongoid**: Data migrations are a pain. Why not work with a JSON object.. oh - Mongo does that. Mongo has an ORM called Mongoid we can leverage.
- **Shield**: A dead simple solution for user authentication (~110 LOC)

**shell** 
```bash
$ mkdir auth_example
$ cd auth_example
$ touch app.rb
$ vim app.rb
```

**app.rb**
```ruby
require "cuba"

Cuba.define do
  on get do
    on root do
      res.write "Welcome home!"
    end

    on "login" do
      res.write "Let me in!"
    end
  end
end
```

Here's what you'll need to know: `Cuba#define` takes a block. It responds to route matchers similar to a Rails controller. The name of your route is like a Rails controller route action. So "login" would be an action on whatever controller you define. 

Notice #root and #login are nested inside `get` blocks. In a second terminal, you can run `$ ruby app.rb`. Nothing will happen. 

Cuba users [Rack](http://rack.github.io/) under the hood. Rack responds to a rackup file. In the same directory, create `config.ru`:

**config.ru**
```ruby
require "./app"

run Cuba
```

To clarify, `config.ru` tells Rack where to look in order to run the app. Thus, we simply include `app.rb` and instantiate Cuba. 

Run `rackup config.ru` and point your browser to localhost:9292. Try  localhost:9292/login. See messages? Good -let's move on. 

Next, we should connect Mongo as a backend server. We're ready to add Mongoid to the project.

Before we continue, let's set our dependencies in a Gemfile:

**Gemfile**
```ruby
source "https://rubygems.org"

ruby "2.1.1"

gem "cuba",    "~> 3.2.0"
gem "mongoid", "~> 4.0.0"
```
Run `bundle install` to ensure your gem libraries get installed locally.

In regards to Mongo, I won't go into detail on how to install it. They do a better at that... I'll refer you [to Mongo's docs](http://docs.mongodb.org/manual/tutorial/install-mongodb-on-os-x/#install-mongodb-with-homebrew). 

Once you get it installed, start server with `$ mongod` and create a config file for our ORM

**config/mongoid.yml**
```ruby
development:
  sessions:
    default:
      database: auth_example
      hosts: 
        - localhost:27017
```

Pretty simple. We're connecting Mongo to the app and setting the name as `auth_example`. From here, we can leverage Mongoid to talk to our server.

**app.rb**
```ruby
require "cuba"
require "mongoid"

ENV["RACK_ENV"] ||= "development"
Mongoid.load!("#{Dir.pwd}/config/mongoid.yml")

Cuba.define do
  on get do
    on root do
.
..
...
```

`Mongoid.load` is our adapter that connects the server to the app. Let's initialize a Mongo database to interact with. Start a Mongo session in a terminal `$ mongo`.


```
> mongo
MongoDB shell version: 2.3.8
...
> show dbs
admin (empty)
local 0.078GB
> use auth_example
> show dbs
admin (empty)
local 0.078GB
auth_example (empty)
```

`use <name_of_database>` instantiates a new database if there isn't one. That's pretty much it! Now that the setup is done, all we need is the authentication logic. Shield does this well plays nicely with our setup. It assumes we have a user model so create a `user.rb` file.

**models/user.rb**
```ruby
class User
  include Shield::Model
  include Mongoid::Document

  field :email, type: String
  field :crypted_password, type: String
  
  def self.fetch(identifier)
    where(email: identifier).first
  end

  def self.[](id)
    find(id)
  end
end
```

Let's break this down:

- Including `Shield::Model` means Sheild is expecting your user to have `User#fetch` and a `crypted_password` attribute to function properly.
- User#fetch uses Mongoid's where query to find the first instance of an email.
- `User.[](id)` just finds a user instance by id. If you're unfamiliar with this syntax, the way you would pass an id as a param is `User[42]`

Updating `app.rb` will wire User to the rest of the app.

**app.rb**
```ruby
require "cuba"
require "mongoid"
require "shield"

require "./models/user"

ENV['RACK_ENV'] ||= "development"
Mongoid.load!("#{Dir.pwd}/config/mongoid.yml")

Cuba.define do
  on get do
    on root do
...
```

Our code is sufficient to start interacting with a fake user now. Open a console with "$ irb -r ./app"

**Console**
```
$ user = User.new(email: "foo@bar.com")
=> #<User _id: <LONG_SHA>, email: "foo@bar.com", crypted_password: nil>
$ user.password = "foobar123"
=> "foobar123"
$ Shield::Password.check("foobar123", user.crypted_password)
=> true 
$ nil == User.authenticate("foo@bar.com", "foobar123")
=> true
$ user.save
=> true
$ nil == User.authenticate("foo@bar.com", "foobar123")
=> false
```

Fun fact: `-r` autoloads whatever file you pass to the irb console you're opening.

Let's double check we wrote to our database. Open mongo again with `> mongo auth_example`

**Mongo**
```
MongoDB shell version: 2.6.3
connecting to: auth_example

> db.users.find({email: "foo@bar.com"})
{ "_id" : ObjectId("54b43b4d416e64b333000000"), "email" : "foo@bar.com", "crypted_password" : "900923b82a97febe274" }
```

From here, you can create a login flow for your users. I recommend using [mote](https://github.com/soveran/mote), also by Openredis, for template rendering. I'll follow up with another post on the markup we can include to really see Cuba in action.