Setup a basic authentication in Rails with HTTP Authentication
Tags: rails, ruby, http, tutorial
Published: 2023-05-22 15:33:10

## Introduction

When speaking of authentication, `devise` is usually the go-to gem developers choose in the Rails world. However, do you feel that `devise` is sometimes a bit overkilling? 

For example,  you're building a personal hobby website that you use to upload your cat's photos. It doesn't need to let users sign up, sign in, reset passwords, etc. Why do you need to use `devise` to create a `users` table to store usernames and passwords and introduce many related controllers and views? You just want a simple way to have some restricted areas. 

Fortunately, Rails has a built-in method called `http_basic_authenticate_with` to perform a basic authentication by utilizing the **HTTP Authentication** framework.

If you're interested in how to use `devise`, I wrote an article to set up a basic environment for `devise` for you to play around
{% embed https://dev.to/kevinluo201/how-to-setup-very-basic-devise-in-rails-7-55ia %}

## An example

In this example, we're going to make an application that has 2 pages, **index** and **info**. The index page will be public and anyone can access it. On the other hand, the info page will be a private page for some reason and you'll need to enter credentials to view that page.

First, let us create a new rails project:
```bash
$ rails new http_basic_auth
$ rails generate controller pages index info
```
`rails generate controller pages index info` is a shortcut to make the controller and its actions. It will generate `PagesController` with 2 actions: `:index` and `:info` and the corresponding routes will be added into `config/routes.rb`

Second, we add `http_basic_authenticate_with` with credentials to the controller so the code should look like below.

```ruby
class PagesController < ApplicationController
  http_basic_authenticate_with name: "kevin", password: "12345", only: :info
  
  def index
  end

  def info
  end
end
```

That means we want to use 

* username: 'kevin'
* password: '12345'

as the login credential. We add `only: :info` because we only want it to be required for the `:info` action.

### Result

We can view the **index** page when enter `http://localhost:3000/pages/index`

![Index page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9m60he0478b45fcs15ze.png)

However, when we want to visit `http://localhost:3000/pages/info` a prompt window will pop up and ask us to enter the username and the password.

![login prompt](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dw9edq9zw488ugyqkrbc.png)

If you refuse to enter them, it will show `HTTP Basic: Access denied.`

![HTTP Basic: Access denied.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2tbmy2kpefron5rstkfk.png)

Only when you enter the correct login credential, you can enter the **info** page.

![Info page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ltv0dq6xfs87o9mzz29r.png)

That's it! Now the visitors must use '**kevin**' and '**12345**' to enter the page.

## How does it work?
So, how does it work? Is it safe? 

In fact, it's an authentication feature [supported by most browsers](https://caniuse.com/mdn-http_headers_authorization_basic). It's called HTTP Authentication and it's defined in [RFC7235](https://datatracker.ietf.org/doc/html/rfc7235). The workflow is like this:

1. The client (browser) makes a request to a restricted URL
2. The server return with `401 Unauthorized` with header `WWW-Authenticate: Basic`
3. When the browser gets this specific response, it will pop up a prompt window to let the user enter the login credential
4. After entering the credential, the browser will send the same request again but with a header `Authorization: Basic a_base64_string`. `a_base64_string` here will be the credential encoded as base64.
5. Browser will cache the credential and add it in the header for the later requests


![http authenticate](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k25g5ki7j3u11w48pdw5.png)
(the diagram is from https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication)

In our case, after we enter `http://localhost:3000/pages/info` in the browser, you'll find that it responds with status `401` and with `WWW-Authenticate: Basic realm="Application"`. The `realm` is an optional description that we can ignore now. The browser will pop up a prompt to ask you to enter login information.

![prompt window for login](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bo40j5rbmre2ecohl10r.png)

After you enter the correct username and password, your browser will make the same request but with the header `Authorization: Basic a2V2aW46MTIzNDU=`. This time, you can see the info page.

![Authorization header](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wk1ujclit1wyeph9jd4g.png)

`a2V2aW46MTIzNDU=` is the credential we just entered. It's not encrypted but just encoded in base64:
```ruby
require 'base64'

Base64.decode64('a2V2aW46MTIzNDU=')
# => "kevin:12345"
```
I guess you may be thinking: that's too insecure! Well, you're right. Therefore, you must use **HTTPS** when using this authentication strategy; otherwise, the credentials can be easily stolen.

## Good practice

If you feel unsafe when exposing the credential in the `PagesController`, we can manage them in **Rail's credentials**. Go to the terminal and type
```bash
EDITOR=vim rails credentials:edit
```
then add login credentials in the file:
```
login:
  username: kevin
  password: 12345
```
then replace the code of `http_basic_authenticate_with` in the `PagesController`:
```ruby
http_basic_authenticate_with(
    name: Rails.application.credentials.dig(:login, :username),
    password: Rails.application.credentials.dig(:login, :password),
    only: :info
  )
```

## Pros and cons
### Pros
* Easy to implement
* You don't need to change anything in the database
* You don't even need to implement a sign-in page

### Cons
* **Every request** will carry the username and password you enter. It increases the risk of information leaks.
* You can only use one pair of username and passwords for one controller.
  * Yes, you can extend this mechanism to make it use different login credentials stored in the database, but in that case, maybe using `devise` would be a better choice
* You cannot customize the login prompt of browsers or the workflow of the authentication.

## Conclusion
HTTP Authentication is very easy to implement in Rails. However, It has reasons for it becoming less and less popular. It's not flexible and increases the risk of leaking credentials. If your application is suitable for this method, congratulation, you can try that and save some time. 

## References
https://api.rubyonrails.org/v7.0.4.2/classes/ActionController/HttpAuthentication/Basic.html
https://datatracker.ietf.org/doc/html/rfc7235
https://caniuse.com/mdn-http_headers_authorization_basic