Mike Slinn
Mike Slinn

Authentication & Authorization With Sinatra / Warden

Published 2022-12-05. Last modified 2023-01-27.
Time to read: 3 minutes.

This page is part of the posts collection, categorized under Ruby, Sinatra, e-commerce, nginx.

Authentication can be defined as any process that presents a challenge to the user, followed by a successful presentation of credentials.

Authorization can be defined as granting access to a resource to a process that runs on behalf of an authenticated user.

A&A is a short form for Authentication and Authorization.

I want to add bespoke A&A to an nginx web server. For an online training website, students should have access only to the collection of pages corresponding to the courses that they signed up for, plus any public pages.

Nginx can act as a proxy server, whereby it interacts with another web service. The output of the proxied service can be incorporated into the nginx server's output, or it can be used to control the content displayed to the user.

Software Layers

This post builds upon the previous blog post, Sinatra Request Explorer, and demonstrates bespoke A&A, implemented via a small web application. The webapp is written in Ruby, and complies with the Rack specification. I will discuss how this A&A implementation can be used in an nginx webapp in a future blog post.

Both Ruby Sinatra and Ruby on Rails are Rack-compliant. Software is best built in layers, after all. Warden, used by Sinatra and Rails, is a Rack-compliant generalized authentication framework.

Revitalizing the Sinatra Warden Example

Web applications built from Ruby Sinatra can use Warden to provide A&A on a route-by-route basis. I found a dusty old GitHub project, sinatra-warden-example, originally written by Steve Klise, that did a good job of demonstrating how to use Warden for multi-user authentication. This demo webapp featured a sqlite database for storing user credentials.

Sinatra-warden-example had not been kept up to date, so I forked and updated it. Steve has archived the repository, so it is now read-only, thus I was unable to submit a pull request with my changes.

Steve had written a good README for the webapp, with useful links that explain the details. I updated the README as well. Go ahead and read it to learn about the webapp – no point rewriting it here.

Run the Webapp

shell – Fetch project
$ git clone git@github.com:mslinn/sinatra-warden-example.git
Cloning into 'sinatra-warden-example'...
remote: Enumerating objects: 147, done.
remote: Counting objects: 100% (18/18), done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 147 (delta 5), reused 12 (delta 4), pack-reused 129
Receiving objects: 100% (147/147), 36.24 KiB | 12.08 MiB/s, done.
Resolving deltas: 100% (71/71), done. 

$ cd sinatra-warden-example/

sinatra-warden-example defines the following routes in app/app.rb:

  • get '/'
  • get '/auth/login'
  • post '/auth/login'
  • get '/auth/logout'
  • post '/auth/unauthenticated'
  • get '/protected'
shell – Download dependencies
$ bundle install
Fetching gem metadata from https://rubygems.org/........
Using bundler 2.3.18
Using multi_json 1.15.0
Using public_suffix 5.0.0
Fetching json 1.8.6
Using ruby2_keywords 0.0.5
Using rack 2.2.4
Fetching uuidtools 2.2.0
Using tilt 2.0.11
Fetching stringex 1.5.1
Fetching bcrypt 3.1.18
Fetching json_pure 1.8.6
Using webrick 1.7.0
Fetching fastercsv 1.5.5
Using addressable 2.8.1
Using mustermann 3.0.0
Using rack-protection 3.0.4
Fetching shotgun 0.9.2
Fetching warden 1.2.9
Installing shotgun 0.9.2
Installing warden 1.2.9
Installing uuidtools 2.2.0
Installing bcrypt 3.1.18 with native extensions
Installing stringex 1.5.1
Installing fastercsv 1.5.5
Installing json_pure 1.8.6
Installing json 1.8.6 with native extensions
Fetching data_objects 0.10.17
Fetching dm-core 1.2.1
Using sinatra 3.0.4
Fetching sinatra-flash 0.3.0
Installing data_objects 0.10.17
Installing sinatra-flash 0.3.0
Fetching do_sqlite3 0.10.17
Installing dm-core 1.2.1
Fetching dm-migrations 1.2.0
Fetching dm-transactions 1.2.0
Fetching dm-do-adapter 1.2.0
Fetching dm-validations 1.2.0
Fetching dm-timestamps 1.2.0
Installing do_sqlite3 0.10.17 with native extensions
Installing dm-transactions 1.2.0
Installing dm-timestamps 1.2.0
Installing dm-migrations 1.2.0
Installing dm-do-adapter 1.2.0
Installing dm-validations 1.2.0
Fetching bcrypt-ruby 3.1.5
Installing bcrypt-ruby 3.1.5
Fetching dm-serializer 1.2.2
Fetching dm-types 1.2.2
Installing dm-types 1.2.2
Installing dm-serializer 1.2.2
Fetching dm-sqlite-adapter 1.2.0
Installing dm-sqlite-adapter 1.2.0
Bundle complete! 14 Gemfile dependencies, 32 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
Post-install message from bcrypt-ruby:

#######################################################

The bcrypt-ruby gem has changed its name to just bcrypt.  Instead of
installing `bcrypt-ruby`, you should install `bcrypt`.  Please update your
dependencies accordingly.

####################################################### 

Run the sinatra-warden-example webapp like this:

shell – Run webapp
$ bundle exec rackup
[2023-01-27 12:27:25] INFO  WEBrick 1.7.0
[2023-01-27 12:27:25] INFO  ruby 3.1.0 (2021-12-25) [x86_64-linux]
[2023-01-27 12:27:25] INFO  WEBrick::HTTPServer#start: pid=1232025 port=9292 

Webapps launched by rackup run at localhost:9292. The webapp looks like this:

Each request made to the webapp echos to the console. Ctrl-C terminates the webapp.

HTTP log on console
127.0.0.1 - - [06/Dec/2022:22:51:52 -0500] "GET / HTTP/1.1" 200 376 0.0411
127.0.0.1 - - [06/Dec/2022:22:51:52 -0500] "GET /favicon.ico HTTP/1.1" 404 518 0.0029
127.0.0.1 - - [06/Dec/2022:22:52:02 -0500] "GET /auth/login HTTP/1.1" 200 596 0.0022
127.0.0.1 - - [06/Dec/2022:22:52:04 -0500] "POST /protected HTTP/1.1" 303 - 0.0056
127.0.0.1 - - [06/Dec/2022:22:52:04 -0500] "GET /auth/login HTTP/1.1" 200 663 0.0028
^C[2022-12-06 22:52:09] INFO  going to shutdown ...
[2022-12-06 22:52:10] INFO  WEBrick::HTTPServer#start done. 

Log in with userid admin and password admin. The webapp redirects to /protected.

When you log out, the webapp redirects back to /.

Invalid URLs, such as http://localhost:9292/asdf, display this:

Alternative Invocation

The webapp can also be run this way:

$ ruby app/app.rb
[2023-01-27 16:30:22] INFO  WEBrick 1.7.0
[2023-01-27 16:30:22] INFO  ruby 3.1.0 (2021-12-25) [x86_64-linux]
== Sinatra (v3.0.3) has taken the stage on 4567 for development with backup from WEBrick
[2023-01-27 16:30:22] INFO  WEBrick::HTTPServer#start: pid=1270346 port=4567
^C== Sinatra has ended his set (crowd applauds)
[2023-01-27 16:30:24] INFO  going to shutdown ...
[2023-01-27 16:30:24] INFO  WEBrick::HTTPServer#start done. 

For debugging, the above can be used in a Visual Studio Code launch configuration:

.vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "args": [],
      "cwd": "${workspaceRoot}",
      "name": "Run SinatraWardenExample",
      "program": "${workspaceRoot}/app/app.rb",
      "request": "launch",
      "type": "Ruby",
    }
  ]
}

Rack::Request::Env

It is helpful to see the contents of the Rack::Request::Env module in a debugger, which is available as request.env in app code and ERBs:

Sinatra Session Secrets

Martin Fowler wrote about generating session secrets securely. He suggested using a bash script like this:

shell – Generate Session Secret Using bash
$ export SESSION_SECRET="#$( head -c64 /dev/urandom | base64 )"

A Sinatra committer wrote up session secret recommendations). He recommended using the sysrandom gem.

Here is a way to generate the session secret in an environment variable using the SysRandom gem:

shell – Generate Session Secret Using SecureRandom
$ export SESSION_SECRET="$(
  ruby -e "require 'sysrandom/securerandom'; p SecureRandom.hex(64)" )"

$ echo $SESSION_SECRET
"5f55076c016fc465d2dfee5d9a8273e12fc1de51891fc1aefe190d3cb7468e1f4eff30215ea81aac9dd962a52be23a297a3adb32c0cd534cfab94d1815d0b910"

Next Steps

I will transform this webapp into something generally useful, and I will explain how that works in a future blog post.