# Building a Magic Login Link in Rails

A popular feature in applications is the ability to request a magic link to your account's email address that will log you into the application without a password. I guess it's known as passwordless login or magic links.

After adding the reset password feature to the authentication system in our rails application, I thought it'd be fun to add a magic login link. It turns out it was pretty easy.

## Adding Login Tokens to the User Model

The first step is setting up the `User` model to be able to store a token and an expiration for that token that we can email to the user to trigger the login flow.

`rails g migration AddLoginTokenToUsers login_token:string login_token_expires_at:datetime`

```ruby
class AddLoginTokenToUsers < ActiveRecord::Migration[7.1]
  def change
    add_column :users, :login_token, :string
    add_column :users, :login_token_expires_at, :datetime
  end
end
```

We can then build another model to encapsulate the logic for generating and expiring the token. I'm going to put the entire `LoginToken` model here at once but we'd normally build this functionality as we need it.

[`app/models/login_token.rb`](https://github.com/aviflombaum/rails-authentication/blob/main/app/models/login_token.rb)
```ruby
class LoginToken
  include ActiveModel::Model

  attr_accessor :user, :email

  def save
    @user = User.find_by(email: email)
    if @user
      @user.login_token = SecureRandom.urlsafe_base64
      @user.login_token_expires_at = 1.hour.from_now
      @user.save
      UserMailer.login_link(@user).deliver_later
    end
  end

  def expire!
    @user.update(login_token: nil, login_token_expires_at: nil)
  end

  def self.find_by_valid_token(token)
    user = User.where("login_token = ? AND login_token_expires_at > ?", token, Time.current).first
    new.tap { |l| l.user = user } if user
  end
end
```

 We'll use the methods as follows:

- `LoginToken#save` will generate a token and expiration for the user and email them a link to login. This will be called when the user requests a login link.
- `LoginToken#expire!` will remove the token and expiration from the user. This will be called when the user logs in.
- `LoginToken.find_by_valid_token` will find a user by the token if it's valid and return an instance of `LoginToken` with the user set. This will be called when the user clicks the link in the email.

With this model set, we can setup the controller and views.

## Login Links Controller

Let's generate a controller to handle requesting the magic login link. It'll have 3 actions:

- `new` will render a form to request the login link
- `create` will create the login token and email the user
- `show` will find the user by the token and log them in. This is an odd choice/name for this action but it's what I'm going with. It stays RESTful, I guess, and I couldn't come up with something better. Maybe `use` would've been better.

`rails g controller LoginLinks new create show`

We can route the actions as follows:

```ruby
Rails.application.routes.draw do
  resources :login_links, only: [:new, :create, :show]
end
```

Here's the controller:

[`app/controllers/login_links_controller.rb`](https://github.com/aviflombaum/rails-authentication/blob/main/app/controllers/login_links_controller.rb)
```ruby
class LoginLinksController < ApplicationController
  def show
    @login_token = LoginToken.find_by_valid_token(params[:id])
    if @login_token && @login_token.user
      @login_token.expire!
      sign_in(@login_token.user)
      redirect_to root_path, notice: "You have successfully logged in!"
    else
      redirect_to new_login_link_path, alert: "Your login link has expired. Please request a new one."
    end
  end

  def new
    @login_token = LoginToken.new
  end

  def create
    @login_token = LoginToken.new(login_token_params)
    if @login_token.save
      redirect_to root_path, notice: "Login link sent!"
    else
      flash.now[:alert] = "There was a problem sending the login link."
      render :new, status: 422
    end
  end

  private

  def login_token_params
    params.require(:login_token).permit(:email)
  end
end
```

The form to request a login link looks like:

[`app/views/login_links/new.html.erb`](https://github.com/aviflombaum/rails-authentication/blob/main/app/views/login_links/new.html.erb)
```erb
<%= render_form_for(@login_token, url: login_links_path) do |f| %>
  <div class="grid gap-2">
    <div class="grid gap-1">
      <%= f.label :email, class: "sr-only" %>
      <%= f.email_field :email, placeholder: "name@example.com",
                                autocomplete: :email,
                                autocorrect: "email",
                                autocapitalize: "none" %>
    </div>
    <%= f.submit "Send Login Link" %>
  </div>
<% end %>
```

That's really it, everything else is pretty standard, there's the mailer view:

```erb
<p>Hello,</p>
<p>You requested a link to login.</p>
<p><%= link_to 'Click here to login', login_link_url(@user.login_token) %></p>
<p>If you didn't request this, please ignore this email.</p>
<p>This link is valid for 1 hour.</p>
```

As you can see, it is all very similar to Reset Password functionality.

## Conclusion

Once you get the hang of dealing with these tokens, building things like confirmable, resetable, passwordless login, gets easier and easier. If you wanted you could even build a token concern that encapsulates all of this functionality and use it for all of these features.

## Refactor: Tokenable

I decided to refactor the token functionality into a concern. I'm not sure if it's better or not but it's a little cleaner.

[`app/models/concerns/tokenable.rb`](https://github.com/aviflombaum/rails-authentication/blob/refactor-tokens/app/models/concerns/tokenable.rb)
```ruby
module Tokenable
  extend ActiveSupport::Concern

  included do
    include ActiveModel::Model
    attr_accessor :user, :email
  end

  def save
    @user = User.find_by(email: email)
    if @user
      @user.send("#{self.class.token_field}=", SecureRandom.urlsafe_base64)
      @user.send("#{self.class.token_field}_expires_at=", 1.hour.from_now)
      @user.save
    end
  end

  def expire!
    @user.update("#{self.class.token_field}": nil, "#{self.class.token_field}_expires_at": nil)
  end

  class_methods do
    def find_by_valid_token(token)
      user = User.where("#{token_field} = ? AND #{token_field}_expires_at > ?", token, Time.current).first
      new.tap { |l| l.user = user } if user
    end

    def token_field
      @token_field ||= name.to_s.underscore
    end
  end
end
```git

With this the PasswordReset and LoginToken models are much simpler:

```ruby
class PasswordResetToken
  include Tokenable
end
class LoginToken
  include Tokenable
end
```

In order to use them, I had to make some naming changes to keep things introspectable and metaprogrammable but the conventions make sense.

Would love feedback on this concern.

Hope these posts have been helpful!

