Turbo Native Authentication Part 1 - Rails Backend
Tags: android, turbo, ios, rails
Published: 2023-08-21 10:32:00

---
title: Turbo Native Authentication Part 1 - Rails Backend
published: true
date: 2023-07-02 00:00:00 UTC
tags: android, turbo, ios, rails
canonical_url: https://williamkennedy.ninja/turbo/2023/07/02/turbo-native-auth-rails/
---

In this Turbo Native series, we’re going to build native auth. Part 1 will cover the server side. Part 2 will cover Turbo iOS, and then we’ll end with Android.

Authentication is complicated. Not because it’s hard but because it’s scary.

The Rails app is just an example of getting up and running, but you may need to adjust for your security needs if you wish to run it on production.

The source code can be found [here](https://github.com/williamkennedy/turbo_auth_rails), and it exists as a template so you can clone it into your app. With some tweaks, the app builds off the authentication-zero gem, so we have API authentication and ordinary sign-in.

## Initial Confusion

One of the parts I found confusing about authentication was this line in the [docs](https://github.com/hotwired/turbo-ios/blob/main/Docs/Authentication.md).

> For HEY, we have a slightly different approach. We get the cookies back along with our initial OAuth request and set those cookies directly to the web view and global cookie stores

This is not something I encountered before when using SPAs. When you are building a Rails backend with a SPA, I have only had experience with token-based authentication or cookie-based authentication. I have never done both.

With further ado, let’s dive in.

## Rails Scaffold

First, we start with the traditional Rails scaffold.

```
rails new turbo_auth_rails --javascript=esbuild --css=tailwind --database=postgresql

```

Next, we add the gem [authentication-zero](https://github.com/lazaronixon/authentication-zero) to our Gemfile

## Authentication Zero

```
bundle add authentication-zero

```

I then ran the following:

```
rails g authentication --pwned --passwordless --omniauthable && rake db:migrate

```

For the next part, I wanted to implement token based authentication.

```
class Api::V1::SessionsController < Api::ApplicationController
  skip_before_action :authenticate, only: :create

  before_action :set_session, only: %i[show destroy]

  def index
    render json: Current.user.sessions.order(created_at: :desc)
  end

  def show
    render json: @session
  end

  def create
    user = User.find_by(email: params[:email])

    if user && user.authenticate(params[:password])
      @session = user.sessions.create!
      response.set_header "X-Session-Token", @session.signed_id
      cookies.signed.permanent[:session_token] = { value: @session.id, httponly: true }

      render json: @session, status: :created
    else
      render json: { error: "That email or password is incorrect" }, status: :unauthorized
    end
  end

  def destroy
    @session.destroy
  end

  private
  def set_session
    @session = Current.user.sessions.find(params[:id])
  end
end

```

The controller exists in `app/controllers/api/v1/sessions_controller.rb`. The corresponding routes look like this.

```
namespace :api do
  namespace :v1 do
    resources :sessions, only: [:index, :create, :show, :destroy] 
  end
end

```

There are no views since everything renders JSON.

We set the response header in the controller and assign the cookies with the session id.

```
 response.set_header "X-Session-Token", @session.signed_id
 cookies.signed.permanent[:session_token] = { value: @session.id, httponly: true }

```

I then modified the API application controller generated by authentication-zero to the following:

```
class Api::ApplicationController < ActionController::API
  include ActionController::HttpAuthentication::Token::ControllerMethods
  include ActionController::Cookies

  before_action :set_current_request_details
  before_action :authenticate

  private
  def authenticate
    if session_record = authenticate_with_http_token { |token, _| Session.find_signed(token) }
      Current.session = session_record
    else
      request_http_token_authentication
    end
  end

  def set_current_request_details
    Current.user_agent = request.user_agent
    Current.ip_address = request.ip
  end
end

```

I also modified the main application controller:

```
class ApplicationController < ActionController::Base
  before_action :set_current_request_details
  before_action :authenticate

  def authenticate
    if authenticate_with_token || authenticate_with_cookies
      # Great! You're in
    elsif !performed?
      request_api_authentication || request_cookie_authentication
    end
  end

  def authenticate_with_token
    if session = authenticate_with_http_token { |token, _| Session.find_signed(token) }
      Current.session = session
    end
  end

  def authenticate_with_cookies
    if session = Session.find_by_id(cookies.signed[:session_token])
      Current.session = session
    end
  end

  def user_signed_in?
    authenticate_with_cookies || authenticate_with_token
  end

  helper_method :user_signed_in?
  

  def request_api_authentication
    request_http_token_authentication if request.format.json?
  end

  def request_cookie_authentication
    session[:return_path] = request.fullpath
    render 'sessions/new', status: :unauthorized, notice: "You need to sign in"
  end

  def set_current_request_details
    Current.user_agent = request.user_agent
    Current.ip_address = request.ip
  end
end

```

This checks if we either have a token or a cookie. We also return a 401 status code if a user is not logged in or needs to be authenticated.

## Posts Scaffold

Let’s now build a scaffold.

```
rails g scaffold Post title:string content:rich_text && rails db:migrate

```

Not all styling is in the repo if you wish to copy and paste. You will need to copy the `layout/application.html.erb` file, the `posts` folder, and the `sessions/new` folder.

Finally, in our posts controller, we added the following:

```
class PostsController < ApplicationController
  before_action :authenticate, except: [:index, :show]

```

This will trigger a 401 status whenever a user tries to log in. This will be used to start a login on our Turbo iOS app in the next blog post.

Finally, let’s add a path\_configuration route.

```
class Turbo::Ios::PathConfigurationsController < ApplicationController
  skip_before_action :authenticate

  def show
    render json: {
      rules: [
        {
          patterns: ["/new$", "/edit$"],
          properties: {
            presentation: "modal"
          }
        },
        {
          patterns: ["/sign_in$"],
          properties: {
            presentation: 'authentication'
          }
        }
      ]
    }
  end
end

```

Then we update our `routes.rb` file.

```
  namespace :turbo do
    namespace :ios do
      resource :path_configuration, only: [:show]
    end
  end

```