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
Next, we add the gem 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