<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://sruthyjmallya.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://sruthyjmallya.com/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-09-25T14:24:20+00:00</updated><id>https://sruthyjmallya.com/feed.xml</id><title type="html">Sruthy J Mallya</title><subtitle>Software developer focused on Ruby on Rails, React, and TypeScript — with a strong bent for developer experience, code quality, and technical writing.</subtitle><author><name>Sruthy J Mallya</name></author><entry><title type="html">Adding magic link authentication to Inertia Rails + React application</title><link href="https://sruthyjmallya.com/ruby-on-rails/devise/authentication/reactjs/how-to/2026/06/29/magic-link-authentication.html" rel="alternate" type="text/html" title="Adding magic link authentication to Inertia Rails + React application" /><published>2026-06-29T04:30:00+00:00</published><updated>2026-06-29T04:30:00+00:00</updated><id>https://sruthyjmallya.com/ruby-on-rails/devise/authentication/reactjs/how-to/2026/06/29/magic-link-authentication</id><content type="html" xml:base="https://sruthyjmallya.com/ruby-on-rails/devise/authentication/reactjs/how-to/2026/06/29/magic-link-authentication.html"><![CDATA[<p>In the modern era, where each users have as many number of websites and application that they use on day to day basis, remembering passwords for them can become a headache. Magic link solves this issue.</p>

<p>Magic link allows users to enter their email and receive a one time valid link to login to their account.</p>

<p>Magic links are best suited for applications where users sign in occasionally, have reliable access to their email, and benefit from a simple, password-less login experience.</p>

<p>In this blog we will walk you through the step by step process to add magic link authentication to your Inertia Rails + React app. If you prefer to explore the finished code while following along, the complete example lives in the <a href="https://github.com/sruthy-j-mallya/magic_link_example">magic_link_example</a> repository.</p>

<details>
  <summary>What is Inertia?</summary>

  <p><a href="https://inertiajs.com/docs/v3/getting-started/index">Inertia</a> is a set of frontend and backend adapters that let you build fully client side rendered, single page application as a monolith without requiring you to build API. Inertia currently have three official client-side adapters (React, Vue, and Svelte) and four server-side adapters (Laravel, Rails, Phoenix, and Django).</p>
</details>

<p>Lets get started 🏁</p>

<h2 id="setting-up-an-inertia-rails--react-app">Setting up an Inertia Rails + React app</h2>

<p>Create a new rails app:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails new magic_link_example
<span class="nb">cd </span>magic_link_example
</code></pre></div></div>

<p>Install the Inertia server-side adapter gem:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle add inertia_rails
</code></pre></div></div>

<p>This will install and add <code class="language-plaintext highlighter-rouge">inertial_rails</code> to Gemfile.</p>

<p>The <code class="language-plaintext highlighter-rouge">inertia_rails</code> gem comes with a generator to setup Inertia in a Rails application. Execute the following command in the terminal:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bin/rails generate inertia:install
</code></pre></div></div>

<p>This command will:</p>
<ul>
  <li>Check for Vite Rails and install it if not present</li>
  <li>Ask if you want to use TypeScript</li>
  <li>Ask you to choose your preferred frontend framework (React, Vue, Svelte)</li>
  <li>Ask if you want to install Tailwind CSS</li>
  <li>Install necessary dependencies</li>
  <li>Set up the application to work with Inertia</li>
  <li>Copy example Inertia controller and views (can be skipped with the <code class="language-plaintext highlighter-rouge">--skip-example</code> option)</li>
</ul>

<p><img src="/assets/images/inertia_rails_generate_command.png" alt="Inertia Rails generate command" /></p>

<p>Now run <code class="language-plaintext highlighter-rouge">bin/dev</code> and head to http://localhost:3100/, you show see the Inertial Rails application up and running:</p>

<p><img src="/assets/images/inertia_app_example.png" alt="Inertia Rails example app" /></p>

<h2 id="review-the-inertia-components-generated">Review the Inertia components generated</h2>

<p>Let’s look at the routes.</p>

<p>At the root path, <code class="language-plaintext highlighter-rouge">index</code> action of <code class="language-plaintext highlighter-rouge">InertiaExamplesController</code> is mounted:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span>

<span class="c1"># remaining routes</span>
<span class="n">root</span> <span class="s1">'inertia_example#index'</span>
<span class="n">get</span> <span class="s1">'inertia-example'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'inertia_example#index'</span>
<span class="c1"># remaining roues</span>

<span class="k">end</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">InertiaExampleController</code> inherits the <code class="language-plaintext highlighter-rouge">InertiaController</code> and return the versions of the Rails, Ruby, Rack and Inertia:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># frozen_string_literal: true</span>

<span class="k">class</span> <span class="nc">InertiaExampleController</span> <span class="o">&lt;</span> <span class="no">InertiaController</span>
  <span class="k">def</span> <span class="nf">index</span>
    <span class="n">render</span> <span class="ss">inertia: </span><span class="p">{</span>
      <span class="ss">rails_version: </span><span class="no">Rails</span><span class="p">.</span><span class="nf">version</span><span class="p">,</span>
      <span class="ss">ruby_version: </span><span class="no">RUBY_DESCRIPTION</span><span class="p">,</span>
      <span class="ss">rack_version: </span><span class="no">Rack</span><span class="p">.</span><span class="nf">release</span><span class="p">,</span>
      <span class="ss">inertia_rails_version: </span><span class="no">InertiaRails</span><span class="o">::</span><span class="no">VERSION</span><span class="p">,</span>
    <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>At this moment, <code class="language-plaintext highlighter-rouge">InertiaController</code> doesn’t do anything:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># frozen_string_literal: true</span>

<span class="k">class</span> <span class="nc">InertiaController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="c1"># Share data with all Inertia responses</span>
  <span class="c1"># see https://inertia-rails.dev/guide/shared-data</span>
  <span class="c1">#   inertia_share user: -&gt; { Current.user&amp;.as_json(only: [:id, :name, :email]) }</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Usually, we will keep the data that all the React component needs in this controller using <code class="language-plaintext highlighter-rouge">inertia_share</code> method and inherits the <code class="language-plaintext highlighter-rouge">InertiaController</code> from all the Rails controllers.</p>

<p>The root path hits the <code class="language-plaintext highlighter-rouge">index</code> action of <code class="language-plaintext highlighter-rouge">InertiaExampleController</code> and the application versions are send to the inertia. Inertia shares the data between server and the client as <a href="https://inertia-rails.dev/guide/the-protocol#the-page-object">page object</a> with the component name and props. Here, the component name will be <code class="language-plaintext highlighter-rouge">inertia_example/index</code>. These components should be kept in <code class="language-plaintext highlighter-rouge">javascript/pages</code> directory. The index component component renders a <code class="language-plaintext highlighter-rouge">InertiaExample</code> component which displays the versions passed from the <code class="language-plaintext highlighter-rouge">InertiaExampleController</code> and react version from frontend.</p>

<p>Now, let’s update this to have just a home controller that shows a welcome message to the user. We haven’t implemented authentication yet, we will display a placeholder email.</p>

<p>Let’s add a home controller that sends an email prop to the frontend:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">HomeController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">welcome</span>
    <span class="n">render</span> <span class="ss">inertia: </span><span class="p">{</span> <span class="ss">email: </span><span class="s2">"placeholder@example.com"</span> <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>We will then mount this to the root path:</p>

<p><img src="/assets/images/replacing_example_with_home_routes.png" alt="Replacing example routes with home routes" /></p>

<p>Now, we can safely remove the inertia example related controllers and components.</p>

<p>Let’s add the <code class="language-plaintext highlighter-rouge">welcome</code> component as well:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// javascript/pages/home/welcome.tsx</span>

<span class="kd">const</span> <span class="nx">Home</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">email</span> <span class="p">}</span> <span class="p">:</span> <span class="p">{</span> <span class="nl">email</span> <span class="p">:</span> <span class="nx">string</span><span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">return </span><span class="p">(</span>
    <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">flex min-h-screen items-center justify-center bg-linear-to-br from-slate-50 to-slate-200 px-4 py-12</span><span class="dl">"</span><span class="o">&gt;</span>
      <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">w-full max-w-md rounded-2xl bg-white p-8 text-center shadow-xl sm:p-12</span><span class="dl">"</span><span class="o">&gt;</span>
        <span class="o">&lt;</span><span class="nx">h1</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl</span><span class="dl">"</span><span class="o">&gt;</span>
          <span class="nx">Welcome</span>
        <span class="o">&lt;</span><span class="sr">/h1</span><span class="err">&gt;
</span>        <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">mt-3 wrap-break-word text-base text-slate-500 sm:text-lg</span><span class="dl">"</span><span class="o">&gt;</span>
          <span class="p">{</span><span class="nx">email</span><span class="p">}</span>
        <span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt;
</span>      <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt;
</span>    <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt;
</span>  <span class="p">)</span>
<span class="p">}</span>

<span class="k">export</span> <span class="k">default</span> <span class="nx">Home</span><span class="p">;</span>
</code></pre></div></div>

<p>You should see the home component.</p>

<p><img src="/assets/images/added_home_page.png" alt="Added home page" /></p>

<p>Now, we are all set to add the authentication to our application.</p>

<p>We will show a sign in page to enter the email, create a use if a user doesn’t exist with the email. We will then send email with login link to both new and existing users. We will show the current home page only if the user is logged in.</p>

<p>We will use <code class="language-plaintext highlighter-rouge">devise-passwordles</code> gem for adding authentication to our Rails application.</p>

<h2 id="setting-up-devise-gem">Setting up devise gem</h2>

<p>Add devise gem to our application:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle add devise
</code></pre></div></div>

<p>Run the generator:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails generate devise:install
</code></pre></div></div>

<p>The above command will generate <code class="language-plaintext highlighter-rouge">config/initializers/devise.rb</code> file with necessary configurations and <code class="language-plaintext highlighter-rouge">config/locales/devise.en.yml</code> with the translation strings. It will also show a set of post install instructions that needs to be manually taken care of:</p>

<p><img src="/assets/images/devise_post_installation_instrucxtions.png" alt="Devise post installation instructions" /></p>

<p>Let’s go through each of them and ensure they are handled as needed in our case:</p>

<ol>
  <li>Setting the default url options for action mailer: This should already be set in our <code class="language-plaintext highlighter-rouge">development.rb</code> file during rails app creation process. Since our application is mounted on 3100 port we will update it in <code class="language-plaintext highlighter-rouge">development.rb</code>:
    <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">config</span><span class="p">.</span><span class="nf">action_mailer</span><span class="p">.</span><span class="nf">default_url_options</span> <span class="o">=</span> <span class="p">{</span> <span class="ss">host: </span><span class="s2">"localhost"</span><span class="p">,</span> <span class="ss">port: </span><span class="mi">3100</span> <span class="p">}</span>
</code></pre></div>    </div>
  </li>
  <li>Defining root url: We have already defined a root url in our application. Our root url points to the home controller welcome action.</li>
  <li>Holder for flash messages: We will add two <code class="language-plaintext highlighter-rouge">p</code> tags to out <code class="language-plaintext highlighter-rouge">application.html.erb</code> to see the flash messages sent by devise:
    <div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"notice"</span><span class="nt">&gt;</span><span class="cp">&lt;%=</span> <span class="n">notice</span> <span class="cp">%&gt;</span><span class="nt">&lt;/p&gt;</span>
<span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"alert"</span><span class="nt">&gt;</span><span class="cp">&lt;%=</span> <span class="n">alert</span> <span class="cp">%&gt;</span><span class="nt">&lt;/p&gt;</span>
</code></pre></div>    </div>
  </li>
  <li>We will not be copying the devise views, as we will be using React components for UI.</li>
</ol>

<p>Now, that we have setup <code class="language-plaintext highlighter-rouge">devise</code> gem, we will move forward with setting up <code class="language-plaintext highlighter-rouge">devise-passwordless</code> gem.</p>

<h2 id="setting-up-devise-passwordless">Setting up devise-passwordless</h2>

<p>Install and add <code class="language-plaintext highlighter-rouge">devise-passwordless</code> to <code class="language-plaintext highlighter-rouge">Gemfile</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle add devise-passwordless
</code></pre></div></div>

<p>Run the install generator:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails g devise:passwordless:install
</code></pre></div></div>

<p>The above statement will set the devise mailer and passwordless tokenizer in devise configuration file <code class="language-plaintext highlighter-rouge">devise.rb</code> , add a mailer for magic link, and add the necessary translation strings in <code class="language-plaintext highlighter-rouge">devise.en.yml</code> file.</p>

<h2 id="setting-up-devise-user-resource">Setting up devise User resource</h2>

<p>Now, let’s generate the User model:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails generate devise User
</code></pre></div></div>

<p>You can name whatever is apt for your application for instead of User model here, like Customer, Admin etc.</p>

<p>The above command will generate a User resource and create relevant files.</p>

<p>The generated migration has various fields for the users table related to password authentication. We don’t need them, we kill remove them and keep only the email:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># frozen_string_literal: true</span>

<span class="k">class</span> <span class="nc">DeviseCreateUsers</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Migration</span><span class="p">[</span><span class="mf">8.1</span><span class="p">]</span>
  <span class="k">def</span> <span class="nf">change</span>
    <span class="n">create_table</span> <span class="ss">:users</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span>
      <span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:email</span><span class="p">,</span>              <span class="ss">null: </span><span class="kp">false</span><span class="p">,</span> <span class="ss">default: </span><span class="s2">""</span>
      <span class="n">t</span><span class="p">.</span><span class="nf">timestamps</span> <span class="ss">null: </span><span class="kp">false</span>
    <span class="k">end</span>

    <span class="n">add_index</span> <span class="ss">:users</span><span class="p">,</span> <span class="ss">:email</span><span class="p">,</span>                <span class="ss">unique: </span><span class="kp">true</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>We can now run the db:migrate command to run the migration:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails db:migrate
</code></pre></div></div>

<p>Now we will update the User model to just include the concern related to password less login. We will remove all other devise concerns added by the generator:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">User</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">devise</span> <span class="ss">:magic_link_authenticatable</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The user model generator command has also added all the devise authentication related routes to <code class="language-plaintext highlighter-rouge">routes.rb</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">devise_for</span> <span class="ss">:users</span>
</code></pre></div></div>

<p>The above is a short cut method provided by devise gem to add all the session routes to our application instead of listing them. You can see the routes added by devise along with other routes in you application, when you execute <code class="language-plaintext highlighter-rouge">rails routes</code> command:</p>

<p><img src="/assets/images/devise-routes.png" alt="Devise routes" /></p>

<p>We will override these routes to use the one from <code class="language-plaintext highlighter-rouge">devise-passwordless</code> for password-less authentication. Replace <code class="language-plaintext highlighter-rouge">devise_for :users</code> with the one given below:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span>
  <span class="n">devise_for</span> <span class="ss">:users</span><span class="p">,</span>
    <span class="ss">controllers: </span><span class="p">{</span> <span class="ss">sessions: </span><span class="s2">"devise/passwordless/sessions"</span> <span class="p">}</span>

  <span class="c1"># remaining routes</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now, if you visit http://localhost:3100/users/sign_in, you will see a login page.</p>

<p><img src="/assets/images/devise-users-sign-in-form.png" alt="Devise users sign in form" /></p>

<p>Before testing out the authentication, lets first mount a React component instead of the devise ERB view for the users sign in page.</p>

<h2 id="adding-a-react-component-as-login-form">Adding a react component as login form</h2>

<p>First we need to expose the devise sessions controller:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails g devise:controllers <span class="nb">users</span>
</code></pre></div></div>

<p><img src="/assets/images/devise-expose-controllers.png" alt="Expose Devise controllers" /></p>

<p>This will generate some controllers in users namespace in our application. We only need sessions_controller. So we will go ahead and remove the remaining controllers.</p>

<p>The generated session controller inherits the devise session controller. Since we are using <code class="language-plaintext highlighter-rouge">devise-passwordless</code> gem, we need to inherit the passwordless gem’s session controller:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># frozen_string_literal: true</span>

<span class="k">class</span> <span class="nc">Users::SessionsController</span> <span class="o">&lt;</span> <span class="no">Devise</span><span class="o">::</span><span class="no">Passwordless</span><span class="o">::</span><span class="no">SessionsController</span>
	<span class="c1"># some comments generated by the command, which can be removed safely</span>
<span class="k">end</span>
</code></pre></div></div>

<p>We also need to replace passwordless’s session controller with our users sessions controller:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span>
  <span class="n">devise_for</span> <span class="ss">:users</span><span class="p">,</span>
    <span class="ss">controllers: </span><span class="p">{</span> <span class="ss">sessions: </span><span class="s2">"users/sessions"</span> <span class="p">}</span>
<span class="c1"># rest of the routes</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now, lets add a new action to session’s controller to render an inertia page component:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># frozen_string_literal: true</span>

<span class="k">class</span> <span class="nc">Users::SessionsController</span> <span class="o">&lt;</span> <span class="no">Devise</span><span class="o">::</span><span class="no">Passwordless</span><span class="o">::</span><span class="no">SessionsController</span>
  <span class="k">def</span> <span class="nf">new</span>
    <span class="n">render</span> <span class="ss">inertia: </span><span class="p">{}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>At last we can add a component at <code class="language-plaintext highlighter-rouge">javascript/pages/users/sessions/new.tsx</code> to show the sign in form:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Head</span><span class="p">,</span> <span class="nx">Form</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@inertiajs/react</span><span class="dl">'</span>

<span class="kd">const</span> <span class="nx">SignIn</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">return </span><span class="p">(</span>
    <span class="o">&lt;&gt;</span>
      <span class="o">&lt;</span><span class="nx">Head</span> <span class="nx">title</span><span class="o">=</span><span class="dl">"</span><span class="s2">Sign in to Magic Link Example</span><span class="dl">"</span> <span class="o">/&gt;</span>
      <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">flex min-h-screen items-center justify-center bg-linear-to-br from-slate-50 to-slate-200 px-4 py-12</span><span class="dl">"</span><span class="o">&gt;</span>
        <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">w-full max-w-md rounded-2xl bg-white p-8 text-center shadow-xl sm:p-12</span><span class="dl">"</span><span class="o">&gt;</span>
          <span class="o">&lt;</span><span class="nx">h1</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl</span><span class="dl">"</span><span class="o">&gt;</span>
            <span class="nx">Sign</span> <span class="k">in</span>
          <span class="o">&lt;</span><span class="sr">/h1</span><span class="err">&gt;
</span>          <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">mt-3 text-base text-slate-500 sm:text-lg</span><span class="dl">"</span><span class="o">&gt;</span>
            <span class="nx">Enter</span> <span class="nx">your</span> <span class="nx">email</span> <span class="nx">to</span> <span class="nx">receive</span> <span class="nx">a</span> <span class="nx">magic</span> <span class="nx">link</span>
          <span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt;
</span>          <span class="o">&lt;</span><span class="nx">Form</span>
            <span class="nx">action</span><span class="o">=</span><span class="dl">"</span><span class="s2">/users/sign_in</span><span class="dl">"</span>
            <span class="nx">method</span><span class="o">=</span><span class="dl">"</span><span class="s2">post</span><span class="dl">"</span>
            <span class="nx">disableWhileProcessing</span>
            <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">mt-7 inert:pointer-events-none inert:opacity-50</span><span class="dl">"</span>
          <span class="o">&gt;</span>
            <span class="o">&lt;</span><span class="nx">label</span> <span class="nx">htmlFor</span><span class="o">=</span><span class="dl">"</span><span class="s2">email</span><span class="dl">"</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">sr-only</span><span class="dl">"</span><span class="o">&gt;</span>
              <span class="nx">Email</span>
            <span class="o">&lt;</span><span class="sr">/label</span><span class="err">&gt;
</span>            <span class="o">&lt;</span><span class="nx">input</span>
              <span class="nx">name</span><span class="o">=</span><span class="dl">"</span><span class="s2">user.email</span><span class="dl">"</span>
              <span class="nx">id</span><span class="o">=</span><span class="dl">"</span><span class="s2">email</span><span class="dl">"</span>
              <span class="nx">type</span><span class="o">=</span><span class="dl">"</span><span class="s2">email</span><span class="dl">"</span>
              <span class="nx">required</span>
              <span class="nx">placeholder</span><span class="o">=</span><span class="dl">"</span><span class="s2">name@email.com</span><span class="dl">"</span>
              <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">w-full rounded-2xl bg-slate-100 px-5 py-4 text-slate-900 placeholder:text-slate-400 outline-none transition focus-visible:ring-2 focus-visible:ring-slate-400/50</span><span class="dl">"</span> <span class="o">/&gt;</span>
            <span class="o">&lt;</span><span class="nx">button</span>
              <span class="nx">type</span><span class="o">=</span><span class="dl">"</span><span class="s2">submit</span><span class="dl">"</span>
              <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">mt-4 flex w-full items-center justify-center gap-2 rounded-2xl bg-slate-900 px-5 py-4 text-base font-semibold text-white transition hover:bg-slate-800 focus-visible:ring-2 focus-visible:ring-slate-900/50 focus-visible:ring-offset-2 focus-visible:ring-offset-white active:translate-y-px</span><span class="dl">"</span>
            <span class="o">&gt;</span>
              <span class="nx">Send</span> <span class="nx">magic</span> <span class="nx">link</span>
            <span class="o">&lt;</span><span class="sr">/button</span><span class="err">&gt;
</span>          <span class="o">&lt;</span><span class="sr">/Form</span><span class="err">&gt;
</span>        <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt;
</span>      <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt;
</span>    <span class="o">&lt;</span><span class="sr">/</span><span class="err">&gt;
</span>  <span class="p">);</span>
<span class="p">}</span>

<span class="k">export</span> <span class="k">default</span> <span class="nx">SignIn</span><span class="p">;</span>
</code></pre></div></div>

<p>Note that we have used the Form component from inertia and mentioned the action as <code class="language-plaintext highlighter-rouge">users/sign</code> with method <code class="language-plaintext highlighter-rouge">post</code> and also set the name for the input element to <code class="language-plaintext highlighter-rouge">user.email</code> for the backend to pick the email correctly.</p>

<p>Now, restart the server and head to http://localhost:3100/users/sign_in the form will look like this:</p>

<p><img src="/assets/images/sign_in_form.png" alt="Sign in form" /></p>

<h2 id="testing-out-authentication">Testing out authentication</h2>

<p>Let’s test out login entering an email:</p>

<p><img src="/assets/images/couldnt-find-user.png" alt="Could not find user error" /></p>

<p>Oops! We got into an error saying “Could not find a user …”.   Let’s investigate why this is happening.</p>

<h2 id="fixing-user-not-found-error">Fixing user not found error</h2>

<p>Let’s look into the the <code class="language-plaintext highlighter-rouge">Devise::Passwordless::SessionsController</code> ‘s create action:</p>

<p><img src="/assets/images/passwordless-create-action.png" alt="Passwordless create action" /></p>

<p>As you can see from the above code, devise-passwordless session controller only sends a magic link for already existing users. To solve this either we have to wire a separate user registration flow to create a user or override this behaviour to create a user and send magic link for new users. We will follow the later approach of overriding this behaviour, by adding this code to check for user record and create a user and send magic link, if the user exists we will call the super to use the default <code class="language-plaintext highlighter-rouge">Devise::Passwordless::SessionsController#create</code> behaviour:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">def</span> <span class="nf">create</span>
    <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find_or_initialize_by</span><span class="p">(</span><span class="ss">email: </span><span class="n">params</span><span class="p">[</span><span class="ss">:user</span><span class="p">][</span><span class="ss">:email</span><span class="p">])</span>
    <span class="k">if</span> <span class="n">user</span><span class="p">.</span><span class="nf">new_record?</span>
      <span class="n">user</span><span class="p">.</span><span class="nf">save</span>
      <span class="n">user</span><span class="p">.</span><span class="nf">send_magic_link</span>
      <span class="n">flash</span><span class="p">[</span><span class="ss">:notice</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"We've sent you a magic link to complete your registration. Please check your email."</span>
      <span class="n">redirect_to</span> <span class="n">after_magic_link_sent_path_for</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
    <span class="k">else</span>
      <span class="k">super</span>
    <span class="k">end</span>
  <span class="k">end</span>
</code></pre></div></div>

<p>Now , try logging in again, you should have been redirected to root path.</p>

<p>There are some issues here:</p>

<ol>
  <li>The email is not delivered</li>
  <li>After entering the email you are redirected to root. Because we haven’t define the after_magic_link_sent_path_for.</li>
  <li>The root path should not be accessible until the user logs in using the magic link.</li>
</ol>

<p>Lets’s solve them one by one</p>

<h2 id="setting-up-letter-opener-to-preview-email">Setting up letter opener to preview email</h2>

<p>Add <code class="language-plaintext highlighter-rouge">letter_opener</code> gem:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle add letter_opener
</code></pre></div></div>

<p>Add the configuration to use letter opener in <code class="language-plaintext highlighter-rouge">development.rb</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">config</span><span class="p">.</span><span class="nf">action_mailer</span><span class="p">.</span><span class="nf">delivery_method</span> <span class="o">=</span> <span class="ss">:letter_opener</span>
<span class="n">config</span><span class="p">.</span><span class="nf">action_mailer</span><span class="p">.</span><span class="nf">perform_deliveries</span> <span class="o">=</span> <span class="kp">true</span>
</code></pre></div></div>

<p>Now, restart your server and  the sign in should open the mail with magic link in the browser:</p>

<p><img src="/assets/images/sign_in_mail.png" alt="Sign in magic link email" /></p>

<p>Clicking on the link should take you to the root path and will show a signed in successfully message:</p>

<p><img src="/assets/images/signed-in-successfully.png" alt="Signed in successfully" /></p>

<h3 id="fixing-the-user-email-displayed">Fixing the user email displayed</h3>

<p>Now, we can sign but one little thing more, we need to show user email, let’s update the welcome action to send the current user email:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">HomeController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">welcome</span>
    <span class="n">render</span> <span class="ss">inertia: </span><span class="p">{</span> <span class="ss">email: </span><span class="n">current_user</span><span class="p">.</span><span class="nf">email</span> <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>We can see the current users email address on refresh:</p>

<p><img src="/assets/images/home-with-user-email.png" alt="Home page with user email" /></p>

<h2 id="setting-up-magic-link-sent-path">Setting up magic link sent path</h2>

<p>Let’s add a <code class="language-plaintext highlighter-rouge">magic_link_sent</code> controller action to render a view to inform user that the magic link is on its way:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">def</span> <span class="nf">magic_link_sent</span>
    <span class="k">return</span> <span class="n">redirect_to</span> <span class="n">root_path</span> <span class="k">if</span> <span class="n">user_signed_in?</span>

    <span class="n">render</span> <span class="ss">inertia: </span><span class="p">{</span> <span class="ss">email: </span><span class="n">params</span><span class="p">[</span><span class="ss">:email</span><span class="p">]</span> <span class="p">}</span>
  <span class="k">end</span>
</code></pre></div></div>

<p>Point the <code class="language-plaintext highlighter-rouge">after_magic_link_sent_path</code> to <code class="language-plaintext highlighter-rouge">magic_link_sent_path</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kp">protected</span>

<span class="k">def</span> <span class="nf">after_magic_link_sent_path_for</span><span class="p">(</span><span class="n">resource_or_scope</span><span class="p">)</span>
  <span class="n">magic_link_sent_path</span><span class="p">(</span><span class="ss">email: </span><span class="n">resource_or_scope</span><span class="p">.</span><span class="nf">email</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Add route for the <code class="language-plaintext highlighter-rouge">magic_link_sent</code> path to <code class="language-plaintext highlighter-rouge">routes.rb</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="n">devise_scope</span> <span class="ss">:user</span> <span class="k">do</span>
    <span class="n">get</span> <span class="s2">"users/sign_in/magic_link_sent"</span><span class="p">,</span> <span class="ss">to: </span><span class="s2">"users/sessions#magic_link_sent"</span><span class="p">,</span> <span class="ss">as: :magic_link_sent</span>
  <span class="k">end</span>
</code></pre></div></div>

<p>Add the component to render on the above path in <code class="language-plaintext highlighter-rouge">pages/users/sessions/magic_link_sent.tsx</code>:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Head</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@inertiajs/react</span><span class="dl">'</span>

<span class="kd">const</span> <span class="nx">MagicLinkSent</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">email</span> <span class="p">}:</span> <span class="p">{</span> <span class="nl">email</span><span class="p">:</span> <span class="nx">string</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">return </span><span class="p">(</span>
    <span class="o">&lt;&gt;</span>
      <span class="o">&lt;</span><span class="nx">Head</span> <span class="nx">title</span><span class="o">=</span><span class="dl">"</span><span class="s2">Magic link sent</span><span class="dl">"</span> <span class="o">/&gt;</span>
      <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">flex min-h-screen items-center justify-center bg-white px-4 py-10</span><span class="dl">"</span><span class="o">&gt;</span>
        <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">w-full max-w-2xl text-center</span><span class="dl">"</span><span class="o">&gt;</span>
          <span class="o">&lt;</span><span class="nx">h2</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">text-4xl font-bold text-black</span><span class="dl">"</span><span class="o">&gt;</span>
            <span class="nx">Check</span> <span class="nx">your</span> <span class="nx">email</span>
          <span class="o">&lt;</span><span class="sr">/h2</span><span class="err">&gt;
</span>          <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">mt-6 text-lg text-black</span><span class="dl">"</span><span class="o">&gt;</span>
            <span class="nx">We</span><span class="dl">'</span><span class="s1">ve sent a magic link to{</span><span class="dl">'</span> <span class="dl">'</span><span class="s1">}
            &lt;span className="font-bold"&gt;{email}&lt;/span&gt;. Follow the
            link in that email to sign in.
          &lt;/p&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
}

export default MagicLinkSent;
</span></code></pre></div></div>

<p>Now, clear the cookies and sessions, then check the users/sign_in path and enter the email, along with receiving email, you could see the above magic link component rendered:</p>

<p><img src="/assets/images/magic-link-sent.png" alt="Magic link sent page" /></p>

<h2 id="restrict-access-to-app-logged-in-users">Restrict access to app logged in users</h2>

<p>Add a before action to authenticate user using the <code class="language-plaintext highlighter-rouge">authenticate_scope</code> helper in application controller:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">before_action</span> <span class="ss">:authenticate_user!</span>
</code></pre></div></div>

<p>We need to skip authentication for session controller actions:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">skip_before_action</span> <span class="ss">:authenticate_user!</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[new create magic_link_sent]</span>
</code></pre></div></div>

<p>Let’s wrap this up by adding a logout feature as well:</p>

<h2 id="logout-feature">Logout feature</h2>

<p>Update the welcome component to include a logout button:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Link</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@inertiajs/react</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">Home</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">email</span> <span class="p">}:</span> <span class="p">{</span> <span class="nl">email</span><span class="p">:</span> <span class="nx">string</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">return </span><span class="p">(</span>
    <span class="o">&lt;&gt;</span>
      <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">flex justify-end border-b border-slate-200 bg-white px-6 py-3</span><span class="dl">"</span><span class="o">&gt;</span>
        <span class="o">&lt;</span><span class="nx">Link</span>
          <span class="nx">href</span><span class="o">=</span><span class="dl">"</span><span class="s2">/users/sign_out</span><span class="dl">"</span>
          <span class="nx">method</span><span class="o">=</span><span class="dl">"</span><span class="s2">delete</span><span class="dl">"</span>
          <span class="nx">as</span><span class="o">=</span><span class="dl">"</span><span class="s2">button</span><span class="dl">"</span>
          <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">rounded-2xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 transition hover:bg-slate-100 focus-visible:ring-2 focus-visible:ring-slate-400/50 active:translate-y-px</span><span class="dl">"</span>
        <span class="o">&gt;</span>
          <span class="nx">Logout</span>
        <span class="o">&lt;</span><span class="sr">/Link</span><span class="err">&gt;
</span>      <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt;
</span>      <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">flex min-h-screen items-center justify-center bg-linear-to-br from-slate-50 to-slate-200 px-4 py-12</span><span class="dl">"</span><span class="o">&gt;</span>
        <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">w-full max-w-md rounded-2xl bg-white p-8 text-center shadow-xl sm:p-12</span><span class="dl">"</span><span class="o">&gt;</span>
          <span class="o">&lt;</span><span class="nx">h1</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl</span><span class="dl">"</span><span class="o">&gt;</span>
            <span class="nx">Welcome</span>
          <span class="o">&lt;</span><span class="sr">/h1</span><span class="err">&gt;
</span>          <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">mt-3 wrap-break-word text-base text-slate-500 sm:text-lg</span><span class="dl">"</span><span class="o">&gt;</span>
            <span class="p">{</span><span class="nx">email</span><span class="p">}</span>
          <span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt;
</span>        <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt;
</span>      <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt;
</span>    <span class="o">&lt;</span><span class="sr">/</span><span class="err">&gt;
</span>  <span class="p">);</span>
<span class="p">};</span>

<span class="k">export</span> <span class="k">default</span> <span class="nx">Home</span><span class="p">;</span>
</code></pre></div></div>

<p>To redirect user to sign in path after logout, we will configure after sign out path in session’s controller, under <code class="language-plaintext highlighter-rouge">protected</code> methods:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">def</span> <span class="nf">after_sign_out_path_for</span><span class="p">(</span><span class="n">resource_or_scope</span><span class="p">)</span>
    <span class="n">new_user_session_path</span>
  <span class="k">end</span>
</code></pre></div></div>

<p>Hooray! 🎉 We have added a login feature using magic link.</p>]]></content><author><name>Sruthy J Mallya</name></author><category term="ruby-on-rails" /><category term="devise" /><category term="authentication" /><category term="reactjs" /><category term="how-to" /><summary type="html"><![CDATA[A step-by-step guide by Sruthy J Mallya to adding magic-link authentication to an Inertia Rails and React app with Devise.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://sruthyjmallya.com/assets/images/profile_home_background.jpg" /><media:content medium="image" url="https://sruthyjmallya.com/assets/images/profile_home_background.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>