DEV Community

William Kennedy
William Kennedy

Posted on • Originally published at williamkennedy.ninja on

Turbo Native Authentication Part 1 - Rails Backend

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, 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.

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 
Enter fullscreen mode Exit fullscreen mode

Next, we add the gem authentication-zero to our Gemfile

Authentication Zero

bundle add authentication-zero 
Enter fullscreen mode Exit fullscreen mode

I then ran the following:

rails g authentication --pwned --passwordless --omniauthable && rake db:migrate 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 } 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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] 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

Then we update our routes.rb file.

 namespace :turbo do namespace :ios do resource :path_configuration, only: [:show] end end 
Enter fullscreen mode Exit fullscreen mode

Top comments (0)