{"id":13878727,"url":"https://github.com/stevepolitodesign/rails-authentication-from-scratch","last_synced_at":"2025-04-07T09:19:48.777Z","repository":{"id":39981376,"uuid":"426395194","full_name":"stevepolitodesign/rails-authentication-from-scratch","owner":"stevepolitodesign","description":"A step-by-step guide on how to build your own authentication system in Rails from scratch.","archived":false,"fork":false,"pushed_at":"2024-06-14T16:53:07.000Z","size":243,"stargazers_count":223,"open_issues_count":10,"forks_count":33,"subscribers_count":14,"default_branch":"main","last_synced_at":"2025-03-31T08:08:36.142Z","etag":null,"topics":["authentication","devise","rails"],"latest_commit_sha":null,"homepage":"https://stevepolito.design/blog/rails-authentication-from-scratch/","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stevepolitodesign.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-11-09T21:41:06.000Z","updated_at":"2025-03-18T14:47:24.000Z","dependencies_parsed_at":"2024-01-13T20:38:42.680Z","dependency_job_id":"ff6c77ba-11d0-4b1d-96e7-796263f8856d","html_url":"https://github.com/stevepolitodesign/rails-authentication-from-scratch","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevepolitodesign%2Frails-authentication-from-scratch","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevepolitodesign%2Frails-authentication-from-scratch/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevepolitodesign%2Frails-authentication-from-scratch/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevepolitodesign%2Frails-authentication-from-scratch/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stevepolitodesign","download_url":"https://codeload.github.com/stevepolitodesign/rails-authentication-from-scratch/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247622983,"owners_count":20968575,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["authentication","devise","rails"],"created_at":"2024-08-06T08:01:57.899Z","updated_at":"2025-04-07T09:19:48.754Z","avatar_url":"https://github.com/stevepolitodesign.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# Rails Authentication from Scratch\n\nIf you're like me then you probably take Devise for granted because you're too intimidated to roll your own authentication system. As powerful as Devise is, it's not perfect. There are plenty of cases where I've reached for it only to end up constrained by its features and design, and wished I could customize it exactly to my liking.\n\nFortunately, Rails gives you all the tools you need to roll your own authentication system from scratch without needing to depend on a gem. The challenge is just knowing how to account for edge cases while being cognizant of security and best practices.\n\n## Previous Versions\n\nThis guide is continuously updated to account for best practices. You can [view previous releases here](https://github.com/stevepolitodesign/rails-authentication-from-scratch/releases).\n\n## Local Development\n\nSimply run the setup script and follow the prompts to see the final application.\n\n```bash\n./bin/setup\n```\n\n## Step 1: Build User Model\n\n1. Generate User model.\n\n```bash\nrails g model User email:string\n```\n\n```ruby\n# db/migrate/[timestamp]_create_users.rb\nclass CreateUsers \u003c ActiveRecord::Migration[6.1]\n  def change\n    create_table :users do |t|\n      t.string :email, null: false\n\n      t.timestamps\n    end\n\n    add_index :users, :email, unique: true\n  end\nend\n```\n\n2. Run migrations.\n\n```bash\nrails db:migrate\n```\n\n3. Add validations and callbacks.\n\n```ruby\n# app/models/user.rb\nclass User \u003c ApplicationRecord\n  before_save :downcase_email\n\n  validates :email, format: {with: URI::MailTo::EMAIL_REGEXP}, presence: true, uniqueness: true\n\n  private\n\n  def downcase_email\n    self.email = email.downcase\n  end\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - We prevent empty values from being saved into the email column through a `null: false` constraint in addition to the [presence](https://guides.rubyonrails.org/active_record_validations.html#presence) validation.\n\u003e - We enforce unique email addresses at the database level through `add_index :users, :email, unique: true` in addition to a [uniqueness](https://guides.rubyonrails.org/active_record_validations.html#uniqueness) validation.\n\u003e - We ensure all emails are valid through a [format](https://guides.rubyonrails.org/active_record_validations.html#format) validation.\n\u003e - We save all emails to the database in a downcase format via a [before_save](https://api.rubyonrails.org/v6.1.4/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-before_save) callback such that the values are saved in a consistent format.\n\u003e - We use [URI::MailTo::EMAIL_REGEXP](https://ruby-doc.org/stdlib-3.0.0/libdoc/uri/rdoc/URI/MailTo.html) that comes with Ruby to validate that the email address is properly formatted.\n\n## Step 2: Add Confirmation and Password Columns to Users Table\n\n1. Create migration.\n\n```bash\nrails g migration add_confirmation_and_password_columns_to_users confirmed_at:datetime password_digest:string\n```\n\n2. Update the migration.\n\n```ruby\n# db/migrate/[timestamp]_add_confirmation_and_password_columns_to_users.rb\nclass AddConfirmationAndPasswordColumnsToUsers \u003c ActiveRecord::Migration[6.1]\n  def change\n    add_column :users, :confirmed_at, :datetime\n    add_column :users, :password_digest, :string, null: false\n  end\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - The `confirmed_at` column will be set when a user confirms their account. This will help us determine who has confirmed their account and who has not.\n\u003e - The `password_digest` column will store a hashed version of the user's password. This is provided by the [has_secure_password](https://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html#method-i-has_secure_password) method.\n\n3. Run migrations.\n\n```bash\nrails db:migrate\n```\n\n4. Enable and install BCrypt.\n\nThis is needed to use `has_secure_password`.\n\n```ruby\n# Gemfile\ngem 'bcrypt', '~\u003e 3.1.7'\n```\n\n```\nbundle install\n```\n\n5. Update the User Model.\n\n```ruby\n# app/models/user.rb\nclass User \u003c ApplicationRecord\n  CONFIRMATION_TOKEN_EXPIRATION = 10.minutes\n\n  has_secure_password\n\n  before_save :downcase_email\n\n  validates :email, format: {with: URI::MailTo::EMAIL_REGEXP}, presence: true, uniqueness: true\n\n  def confirm!\n    update_columns(confirmed_at: Time.current)\n  end\n\n  def confirmed?\n    confirmed_at.present?\n  end\n\n  def generate_confirmation_token\n    signed_id expires_in: CONFIRMATION_TOKEN_EXPIRATION, purpose: :confirm_email\n  end\n\n  def unconfirmed?\n    !confirmed?\n  end\n\n  private\n\n  def downcase_email\n    self.email = email.downcase\n  end\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - The `has_secure_password` method is added to give us an [API](https://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html#method-i-has_secure_password) to work with the `password_digest` column.\n\u003e - The `confirm!` method will be called when a user confirms their email address. We still need to build this feature.\n\u003e - The `confirmed?` and `unconfirmed?` methods allow us to tell whether a user has confirmed their email address or not.\n\u003e - The `generate_confirmation_token` method creates a [signed_id](https://api.rubyonrails.org/classes/ActiveRecord/SignedId.html#method-i-signed_id) that will be used to securely identify the user. For added security, we ensure that this ID will expire in 10 minutes (this can be controlled with the `CONFIRMATION_TOKEN_EXPIRATION` constant) and give it an explicit purpose of `:confirm_email`. This will be useful when we build the confirmation mailer.\n\n## Step 3: Create Sign Up Pages\n\n1. Create a simple home page since we'll need a place to redirect users to after they sign up.\n\n```\nrails g controller StaticPages home\n```\n\n2. Create Users Controller.\n\n```\nrails g controller Users\n```\n\n```ruby\n# app/controllers/users_controller.rb\nclass UsersController \u003c ApplicationController\n\n  def create\n    @user = User.new(user_params)\n    if @user.save\n      redirect_to root_path, notice: \"Please check your email for confirmation instructions.\"\n    else\n      render :new, status: :unprocessable_entity\n    end\n  end\n\n  def new\n    @user = User.new\n  end\n\n  private\n\n  def user_params\n    params.require(:user).permit(:email, :password, :password_confirmation)\n  end\nend\n```\n\n3. Build sign-up form.\n\n```html+ruby\n\u003c!-- app/views/shared/_form_errors.html.erb --\u003e\n\u003c% if object.errors.any? %\u003e\n  \u003cul\u003e\n    \u003c% object.errors.full_messages.each do |message| %\u003e\n      \u003cli\u003e\u003c%= message %\u003e\u003c/li\u003e\n    \u003c% end %\u003e\n  \u003c/ul\u003e\n\u003c% end %\u003e\n```\n\n```html+ruby\n\u003c!-- app/views/users/new.html.erb --\u003e\n\u003c%= form_with model: @user, url: sign_up_path do |form| %\u003e\n  \u003c%= render partial: \"shared/form_errors\", locals: { object: form.object } %\u003e\n  \u003cdiv\u003e\n    \u003c%= form.label :email %\u003e\n    \u003c%= form.email_field :email, required: true %\u003e\n  \u003c/div\u003e\n  \u003cdiv\u003e\n    \u003c%= form.label :password %\u003e\n    \u003c%= form.password_field :password, required: true %\u003e\n  \u003c/div\u003e\n  \u003cdiv\u003e\n    \u003c%= form.label :password_confirmation %\u003e\n    \u003c%= form.password_field :password_confirmation, required: true %\u003e\n  \u003c/div\u003e\n  \u003c%= form.submit \"Sign Up\" %\u003e\n\u003c% end %\u003e\n```\n\n4. Update routes.\n\n```ruby\n# config/routes.rb\nRails.application.routes.draw do\n  root \"static_pages#home\"\n  post \"sign_up\", to: \"users#create\"\n  get \"sign_up\", to: \"users#new\"\nend\n```\n## Step 4: Create Confirmation Pages\n\nUsers now have a way to sign up, but we need to verify their email address to prevent SPAM.\n\n1. Create Confirmations Controller.\n\n```\nrails g controller Confirmations\n```\n\n```ruby\n# app/controllers/confirmations_controller.rb\nclass ConfirmationsController \u003c ApplicationController\n\n  def create\n    @user = User.find_by(email: params[:user][:email].downcase)\n\n    if @user.present? \u0026\u0026 @user.unconfirmed?\n      redirect_to root_path, notice: \"Check your email for confirmation instructions.\"\n    else\n      redirect_to new_confirmation_path, alert: \"We could not find a user with that email or that email has already been confirmed.\"\n    end\n  end\n\n  def edit\n    @user = User.find_signed(params[:confirmation_token], purpose: :confirm_email)\n\n    if @user.present?\n      @user.confirm!\n      redirect_to root_path, notice: \"Your account has been confirmed.\"\n    else\n      redirect_to new_confirmation_path, alert: \"Invalid token.\"\n    end\n  end\n\n  def new\n    @user = User.new\n  end\n\nend\n```\n\n2. Build confirmation pages.\n\nThis page will be used in the case where a user did not receive their confirmation instructions and needs to have them resent.\n\n```html+ruby\n\u003c!-- app/views/confirmations/new.html.erb --\u003e\n\u003c%= form_with model: @user, url: confirmations_path do |form| %\u003e\n  \u003c%= form.email_field :email, required: true %\u003e\n  \u003c%= form.submit \"Confirm Email\" %\u003e\n\u003c% end %\u003e\n```\n\n3. Update routes.\n\n```ruby\n# config/routes.rb\nRails.application.routes.draw do\n  ...\n  resources :confirmations, only: [:create, :edit, :new], param: :confirmation_token\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - The `create` action will be used to resend confirmation instructions to an unconfirmed user. We still need to build this mailer, and we still need to send this mailer when a user initially signs up. This action will be requested via the form on `app/views/confirmations/new.html.erb`. Note that we call `downcase` on the email to account for case sensitivity when searching.\n\u003e - The `edit` action is used to confirm a user's email. This will be the page that a user lands on when they click the confirmation link in their email. We still need to build this. Note that we're looking up a user through the [find_signed](https://api.rubyonrails.org/classes/ActiveRecord/SignedId/ClassMethods.html#method-i-find_signed) method and not their email or ID. This is because The `confirmation_token` is randomly generated and can't be guessed or tampered with unlike an email or numeric ID. This is also why we added `param: :confirmation_token` as a [named route parameter](https://guides.rubyonrails.org/routing.html#overriding-named-route-parameters).\n\u003e  - You'll remember that the `confirmation_token` is a [signed_id](https://api.rubyonrails.org/classes/ActiveRecord/SignedId.html#method-i-signed_id), and is set to expire in 10 minutes. You'll also note that we need to pass the method `purpose: :confirm_email` to be consistent with the purpose that was set in the `generate_confirmation_token` method. \n\n## Step 5: Create Confirmation Mailer\n\nNow we need a way to send a confirmation email to our users for them to actually confirm their accounts.\n\n1. Create a confirmation mailer.\n\n```bash\nrails g mailer User confirmation\n```\n\n```ruby\n# app/mailers/user_mailer.rb\nclass UserMailer \u003c ApplicationMailer\n  default from: User::MAILER_FROM_EMAIL\n\n  def confirmation(user, confirmation_token)\n    @user = user\n    @confirmation_token = confirmation_token\n\n    mail to: @user.email, subject: \"Confirmation Instructions\"\n  end\nend\n```\n\n```html+erb\n\u003c!-- app/views/user_mailer/confirmation.html.erb --\u003e\n\u003ch1\u003eConfirmation Instructions\u003c/h1\u003e\n\n\u003c%= link_to \"Click here to confirm your email.\", edit_confirmation_url(@confirmation_token) %\u003e\n```\n\n```html+erb\n\u003c!-- app/views/user_mailer/confirmation.text.erb --\u003e\nConfirmation Instructions\n\n\u003c%= edit_confirmation_url(@confirmation_token) %\u003e\n```\n\n2. Update User Model.\n\n```ruby\n# app/models/user.rb\nclass User \u003c ApplicationRecord\n  ...\n  MAILER_FROM_EMAIL = \"no-reply@example.com\"\n  ...\n  def send_confirmation_email!\n    confirmation_token = generate_confirmation_token\n    UserMailer.confirmation(self, confirmation_token).deliver_now\n  end\n\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - The `MAILER_FROM_EMAIL` constant is a way for us to set the email used in the `UserMailer`. This is optional.\n\u003e - The `send_confirmation_email!` method will create a new `confirmation_token`. This is to ensure confirmation links expire and cannot be reused. It will also send the confirmation email to the user.\n\u003e - We call [update_columns](https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_columns) so that the `updated_at/updated_on` columns are not updated. This is personal preference, but those columns should typically only be updated when the user updates their email or password.\n\u003e - The links in the mailer will take the user to `ConfirmationsController#edit` at which point they'll be confirmed.\n\n3. Configure Action Mailer so that links work locally.\n\nAdd a host to the test and development (and later the production) environments so that [urls will work in mailers](https://guides.rubyonrails.org/action_mailer_basics.html#generating-urls-in-action-mailer-views).\n\n```ruby\n# config/environments/test.rb\nRails.application.configure do\n  ...\n  config.action_mailer.default_url_options = { host: \"example.com\" }\nend\n```\n\n```ruby\n# config/environments/development.rb\nRails.application.configure do\n  ...\n  config.action_mailer.default_url_options = { host: \"localhost\", port: 3000 }\nend\n```\n\n4. Update Controllers.\n\nNow we can send a confirmation email when a user signs up or if they need to have it resent.\n\n```ruby\n# app/controllers/confirmations_controller.rb\nclass ConfirmationsController \u003c ApplicationController\n\n  def create\n    @user = User.find_by(email: params[:user][:email].downcase)\n\n    if @user.present? \u0026\u0026 @user.unconfirmed?\n      @user.send_confirmation_email!\n      ...\n    end\n  end\n\nend\n```\n\n```ruby\n# app/controllers/users_controller.rb\nclass UsersController \u003c ApplicationController\n\n  def create\n    @user = User.new(user_params)\n    if @user.save\n      @user.send_confirmation_email!\n      ...\n    end\n  end\n\nend\n```\n\n## Step 6: Create Current Model and Authentication Concern\n\n1. Create a model to store the current user.\n\n```ruby\n# app/models/current.rb\nclass Current \u003c ActiveSupport::CurrentAttributes\n  attribute :user\nend\n```\n\n2. Create a Concern to store helper methods that will be shared across the application.\n\n```ruby\n# app/controllers/concerns/authentication.rb\nmodule Authentication\n  extend ActiveSupport::Concern\n\n  included do\n    before_action :current_user\n    helper_method :current_user\n    helper_method :user_signed_in?\n  end\n\n  def login(user)\n    reset_session\n    session[:current_user_id] = user.id\n  end\n\n  def logout\n    reset_session\n  end\n\n  def redirect_if_authenticated\n    redirect_to root_path, alert: \"You are already logged in.\" if user_signed_in?\n  end\n\n  private\n\n  def current_user\n    Current.user ||= session[:current_user_id] \u0026\u0026 User.find_by(id: session[:current_user_id])\n  end\n\n  def user_signed_in?\n    Current.user.present?\n  end\n\nend\n```\n\n3. Load the Authentication Concern into the Application Controller.\n\n```ruby\n# app/controllers/application_controller.rb\nclass ApplicationController \u003c ActionController::Base\n  include Authentication\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - The `Current` class inherits from [ActiveSupport::CurrentAttributes](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html) which allows us to keep all per-request attributes easily available to the whole system. In essence, this will allow us to set a current user and have access to that user during each request to the server.\n\u003e - The `Authentication` Concern provides an interface for logging the user in and out. We load it into the `ApplicationController` so that it will be used across the whole application.\n\u003e   - The `login` method first [resets the session](https://api.rubyonrails.org/classes/ActionController/Metal.html#method-i-reset_session) to account for [session fixation](https://guides.rubyonrails.org/security.html#session-fixation-countermeasures).\n\u003e   - We set the user's ID in the [session](https://guides.rubyonrails.org/action_controller_overview.html#session) so that we can have access to the user across requests. The user's ID won't be stored in plain text. The cookie data is cryptographically signed to make it tamper-proof. And it is also encrypted so anyone with access to it can't read its contents.\n\u003e   - The `logout` method simply [resets the session](https://api.rubyonrails.org/classes/ActionController/Metal.html#method-i-reset_session).\n\u003e   - The `redirect_if_authenticated` method checks to see if the user is logged in. If they are, they'll be redirected to the `root_path`. This will be useful on pages an authenticated user should not be able to access, such as the login page.\n\u003e   - The `current_user` method returns a `User` and sets it as the user on the `Current` class we created. We use [memoization](https://www.honeybadger.io/blog/ruby-rails-memoization/) to avoid fetching the User each time we call the method. We call the `before_action` [filter](https://guides.rubyonrails.org/action_controller_overview.html#filters) so that we have access to the current user before each request. We also add this as a [helper_method](https://api.rubyonrails.org/classes/AbstractController/Helpers/ClassMethods.html#method-i-helper_method) so that we have access to `current_user` in the views.\n\u003e   - The `user_signed_in?` method simply returns true or false depending on whether the user is signed in or not. This is helpful for conditionally rendering items in views.\n\n## Step 7: Create Login Page\n\n1. Generate Sessions Controller.\n\n```bash\nrails g controller Sessions\n```\n\n```ruby\n# app/controllers/sessions_controller.rb\nclass SessionsController \u003c ApplicationController\n  before_action :redirect_if_authenticated, only: [:create, :new]\n\n  def create\n    @user = User.find_by(email: params[:user][:email].downcase)\n    if @user\n      if @user.unconfirmed?\n        redirect_to new_confirmation_path, alert: \"Please confirm your email first.\"\n      elsif @user.authenticate(params[:user][:password])\n        login @user\n        redirect_to root_path, notice: \"Signed in.\"\n      else\n        flash.now[:alert] = \"Incorrect email or password.\"\n        render :new, status: :unprocessable_entity\n      end\n    else\n      flash.now[:alert] = \"Incorrect email or password.\"\n      render :new, status: :unprocessable_entity\n    end\n  end\n\n  def destroy\n    logout\n    redirect_to root_path, notice: \"Signed out.\"\n  end\n\n  def new\n  end\n\nend\n```\n\n2. Update routes.\n\n```ruby\n# config/routes.rb\nRails.application.routes.draw do\n  ...\n  post \"login\", to: \"sessions#create\"\n  delete \"logout\", to: \"sessions#destroy\"\n  get \"login\", to: \"sessions#new\"\nend\n```\n\n3. Add sign-in form.\n\n```html+ruby\n\u003c!-- app/views/sessions/new.html.erb --\u003e\n\u003c%= form_with url: login_path, scope: :user do |form| %\u003e\n  \u003cdiv\u003e\n    \u003c%= form.label :email %\u003e\n    \u003c%= form.email_field :email, required: true %\u003e\n  \u003c/div\u003e\n  \u003cdiv\u003e\n    \u003c%= form.label :password %\u003e\n    \u003c%= form.password_field :password, required: true %\u003e\n  \u003c/div\u003e\n  \u003c%= form.submit \"Sign In\" %\u003e\n\u003c% end %\u003e\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - The `create` method simply checks if the user exists and is confirmed. If they are, then we check their password. If the password is correct, we log them in via the `login` method we created in the `Authentication` Concern. Otherwise, we render an alert.\n\u003e   - We're able to call `user.authenticate` because of [has_secure_password](https://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html#method-i-has_secure_password)\n\u003e   - Note that we call `downcase` on the email to account for case sensitivity when searching.\n\u003e   - Note that we set the flash to \"Incorrect email or password.\" if the user is unconfirmed. This prevents leaking email addresses.\n\u003e - The `destroy` method simply calls the `logout` method we created in the `Authentication` Concern.\n\u003e - The login form is passed a `scope: :user` option so that the params are namespaced as `params[:user][:some_value]`. This is not required, but it helps keep things organized.\n\n## Step 8: Update Existing Controllers\n\n1. Update Controllers to prevent authenticated users from accessing pages intended for anonymous users.\n\n```ruby\n# app/controllers/confirmations_controller.rb\nclass ConfirmationsController \u003c ApplicationController\n  before_action :redirect_if_authenticated, only: [:create, :new]\n\n  def edit\n    ...\n    if @user.present?\n      @user.confirm!\n      login @user\n      ...\n    else\n    end\n    ...\n  end\nend\n```\n\nNote that we also call `login @user` once a user is confirmed. That way they'll be automatically logged in after confirming their email.\n\n```ruby\n# app/controllers/users_controller.rb\nclass UsersController \u003c ApplicationController\n  before_action :redirect_if_authenticated, only: [:create, :new]\n  ...\nend\n```\n\n## Step 9: Add Password Reset Functionality\n\n1. Update User Model.\n\n```ruby\n# app/models/user.rb\nclass User \u003c ApplicationRecord\n  ...\n  PASSWORD_RESET_TOKEN_EXPIRATION = 10.minutes\n  ...\n  def generate_password_reset_token\n    signed_id expires_in: PASSWORD_RESET_TOKEN_EXPIRATION, purpose: :reset_password\n  end\n  ...\n  def send_password_reset_email!\n    password_reset_token = generate_password_reset_token\n    UserMailer.password_reset(self, password_reset_token).deliver_now\n  end\n  ...\nend\n```\n\n2. Update User Mailer.\n\n```ruby\n# app/mailers/user_mailer.rb\nclass UserMailer \u003c ApplicationMailer\n  ...\n  def password_reset(user, password_reset_token)\n    @user = user\n    @password_reset_token = password_reset_token\n\n    mail to: @user.email, subject: \"Password Reset Instructions\"\n  end\nend\n```\n\n```html+erb\n\u003c!-- app/views/user_mailer/password_reset.html.erb --\u003e\n\u003ch1\u003ePassword Reset Instructions\u003c/h1\u003e\n\n\u003c%= link_to \"Click here to reset your password.\", edit_password_url(@password_reset_token) %\u003e\n```\n\n```text\n\u003c!-- app/views/user_mailer/password_reset.text.erb --\u003e\nPassword Reset Instructions\n\n\u003c%= edit_password_url(@password_reset_token) %\u003e\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - The `generate_password_reset_token` method creates a [signed_id](https://api.rubyonrails.org/classes/ActiveRecord/SignedId.html#method-i-signed_id) that will be used to securely identify the user. For added security, we ensure that this ID will expire in 10 minutes (this can be controlled with the `PASSWORD_RESET_TOKEN_EXPIRATION` constant) and give it an explicit purpose of `:reset_password`.\n\u003e - The `send_password_reset_email!` method will create a new `password_reset_token`. This is to ensure password reset links expire and cannot be reused. It will also send the password reset email to the user.\n\n## Step 10: Build Password Reset Forms\n\n1. Create Passwords Controller.\n\n```bash\nrails g controller Passwords\n```\n\n```ruby\n# app/controllers/passwords_controller.rb\nclass PasswordsController \u003c ApplicationController\n  before_action :redirect_if_authenticated\n\n  def create\n    @user = User.find_by(email: params[:user][:email].downcase)\n    if @user.present?\n      if @user.confirmed?\n        @user.send_password_reset_email!\n        redirect_to root_path, notice: \"If that user exists we've sent instructions to their email.\"\n      else\n        redirect_to new_confirmation_path, alert: \"Please confirm your email first.\"\n      end\n    else\n      redirect_to root_path, notice: \"If that user exists we've sent instructions to their email.\"\n    end\n  end\n\n  def edit\n    @user = User.find_signed(params[:password_reset_token], purpose: :reset_password)\n    if @user.present? \u0026\u0026 @user.unconfirmed?\n      redirect_to new_confirmation_path, alert: \"You must confirm your email before you can sign in.\"\n    elsif @user.nil?\n      redirect_to new_password_path, alert: \"Invalid or expired token.\"\n    end\n  end\n\n  def new\n  end\n\n  def update\n    @user = User.find_signed(params[:password_reset_token], purpose: :reset_password)\n    if @user\n      if @user.unconfirmed?\n        redirect_to new_confirmation_path, alert: \"You must confirm your email before you can sign in.\"\n      elsif @user.update(password_params)\n        redirect_to login_path, notice: \"Sign in.\"\n      else\n        flash.now[:alert] = @user.errors.full_messages.to_sentence\n        render :edit, status: :unprocessable_entity\n      end\n    else\n      flash.now[:alert] = \"Invalid or expired token.\"\n      render :new, status: :unprocessable_entity\n    end\n  end\n\n  private\n\n  def password_params\n    params.require(:user).permit(:password, :password_confirmation)\n  end\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - The `create` action will send an email to the user containing a link that will allow them to reset the password. The link will contain their `password_reset_token` which is unique and expires. Note that we call `downcase` on the email to account for case sensitivity when searching.\n\u003e   - You'll remember that the `password_reset_token` is a [signed_id](https://api.rubyonrails.org/classes/ActiveRecord/SignedId.html#method-i-signed_id), and is set to expire in 10 minutes. You'll also note that we need to pass the method `purpose: :reset_password` to be consistent with the purpose that was set in the `generate_password_reset_token` method. \n\u003e   - Note that we return `Invalid or expired token.` if the user is not found. This makes it difficult for a bad actor to use the reset form to see which email accounts exist on the application.\n\u003e - The `edit` action simply renders the form for the user to update their password. It attempts to find a user by their `password_reset_token`. You can think of the `password_reset_token` as a way to identify the user much like how we normally identify records by their ID. However, the `password_reset_token` is randomly generated and will expire so it's more secure.\n\u003e - The `new` action simply renders a form for the user to put their email address in to receive the password reset email.\n\u003e - The `update` also ensures the user is identified by their `password_reset_token`. It's not enough to just do this on the `edit` action since a bad actor could make a `PUT` request to the server and bypass the form.\n\u003e   - If the user exists and is confirmed we update their password to the one they will set in the form. Otherwise, we handle each failure case differently.\n\n2. Update Routes.\n\n```ruby\n# config/routes.rb\nRails.application.routes.draw do\n  ...\n  resources :passwords, only: [:create, :edit, :new, :update], param: :password_reset_token\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e -  We add `param: :password_reset_token` as a [named route parameter](https://guides.rubyonrails.org/routing.html#overriding-named-route-parameters) so that we can identify users by their `password_reset_token` and not `id`. This is similar to what we did with the confirmations routes and ensures a user cannot be identified by their ID.\n\n3. Build forms.\n\n```html+ruby\n\u003c!-- app/views/passwords/new.html.erb --\u003e\n\u003c%= form_with url: passwords_path, scope: :user do |form| %\u003e\n  \u003c%= form.email_field :email, required: true %\u003e\n  \u003c%= form.submit \"Reset Password\" %\u003e\n\u003c% end %\u003e\n```\n\n```html+ruby\n\u003c!-- app/views/passwords/edit.html.erb --\u003e\n\u003c%= form_with url: password_path(params[:password_reset_token]), scope: :user, method: :put do |form| %\u003e\n  \u003cdiv\u003e\n    \u003c%= form.label :password %\u003e\n    \u003c%= form.password_field :password, required: true %\u003e\n  \u003c/div\u003e\n  \u003cdiv\u003e\n    \u003c%= form.label :password_confirmation %\u003e\n    \u003c%= form.password_field :password_confirmation, required: true %\u003e\n  \u003c/div\u003e\n  \u003c%= form.submit \"Update Password\" %\u003e\n\u003c% end %\u003e\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - The password reset form is passed a `scope: :user` option so that the params are namespaced as `params[:user][:some_value]`. This is not required, but it helps keep things organized.\n\n## Step 11: Add Unconfirmed Email Column To Users Table\n\n1. Create and run migration.\n\n```bash\nrails g migration add_unconfirmed_email_to_users unconfirmed_email:string\nrails db:migrate\n```\n\n2. Update User Model.\n\n```ruby\n# app/models/user.rb\nclass User \u003c ApplicationRecord\n  ...\n  attr_accessor :current_password\n  ...\n  before_save :downcase_unconfirmed_email\n  ...\n  validates :unconfirmed_email, format: {with: URI::MailTo::EMAIL_REGEXP, allow_blank: true}\n\n  def confirm!\n    if unconfirmed_or_reconfirming?\n      if unconfirmed_email.present?\n        return false unless update(email: unconfirmed_email, unconfirmed_email: nil)\n      end\n      update_columns(confirmed_at: Time.current)\n    else\n      false\n    end\n  end\n  ...\n  def confirmable_email\n    if unconfirmed_email.present?\n      unconfirmed_email\n    else\n      email\n    end\n  end\n  ...\n  def reconfirming?\n    unconfirmed_email.present?\n  end\n\n  def unconfirmed_or_reconfirming?\n    unconfirmed? || reconfirming?\n  end\n\n  private\n  ...\n  def downcase_unconfirmed_email\n    return if unconfirmed_email.nil?\n    self.unconfirmed_email = unconfirmed_email.downcase\n  end\n\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - We add a `unconfirmed_email` column to the `users` table so that we have a place to store the email a user is trying to use after their account has been confirmed with their original email.\n\u003e - We add `attr_accessor :current_password` so that we'll be able to use `f.password_field :current_password` in the user form (which doesn't exist yet). This will allow us to require the user to submit their current password before they can update their account.\n\u003e - We ensure to format the `unconfirmed_email` before saving it to the database. This ensures all data is saved consistently.\n\u003e - We add validations to the `unconfirmed_email` column ensuring it's a valid email address.\n\u003e - We update the `confirm!` method to set the `email` column to the value of the `unconfirmed_email` column, and then clear out the `unconfirmed_email` column. This will only happen if a user is trying to confirm a new email address. Note that we return `false` if updating the email address fails. This could happen if a user tries to confirm an email address that has already been confirmed. \n\u003e - We add the `confirmable_email` method so that we can call the correct email in the updated `UserMailer`.\n\u003e - We add `reconfirming?` and `unconfirmed_or_reconfirming?` to help us determine what state a user is in. This will come in handy later in our controllers.\n\n3. Update User Mailer.\n\n```ruby\n# app/mailers/user_mailer.rb\nclass UserMailer \u003c ApplicationMailer\n\n  def confirmation(user, confirmation_token)\n    ...\n    mail to: @user.confirmable_email, subject: \"Confirmation Instructions\"\n  end\nend\n```\n\n3. Update Confirmations Controller.\n\n```ruby\n# app/controllers/confirmations_controller.rb\nclass ConfirmationsController \u003c ApplicationController\n  ...\n  def edit\n    ...\n    if @user.present?\n      if @user.confirm!\n        login @user\n        redirect_to root_path, notice: \"Your account has been confirmed.\"\n      else\n        redirect_to new_confirmation_path, alert: \"Something went wrong.\"\n      end\n    else\n      ...\n    end\n  end\n  ...\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - We update the `edit` method to account for the return value of `@user.confirm!`. If for some reason `@user.confirm!` returns `false` (which would most likely happen if the email has already been taken) then we render a generic error. This prevents leaking email addresses.\n\n## Step 12: Update Users Controller\n\n1. Update Authentication Concern.\n\n```ruby\n# app/controllers/concerns/authentication.rb\nmodule Authentication\n  ...\n  def authenticate_user!\n    redirect_to login_path, alert: \"You need to login to access that page.\" unless user_signed_in?\n  end\n  ...\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - The `authenticate_user!` method can be called to ensure an anonymous user cannot access a page that requires a user to be logged in. We'll need this when we build the page allowing a user to edit or delete their profile.\n\n2. Add destroy, edit and update methods. Modify create method and user_params.\n\n```ruby\n# app/controllers/users_controller.rb\nclass UsersController \u003c ApplicationController\n  before_action :authenticate_user!, only: [:edit, :destroy, :update]\n  ...\n  def create\n    @user = User.new(create_user_params)\n    ...\n  end\n\n  def destroy\n    current_user.destroy\n    reset_session\n    redirect_to root_path, notice: \"Your account has been deleted.\"\n  end\n\n  def edit\n    @user = current_user\n  end\n  ...\n  def update\n    @user = current_user\n    if @user.authenticate(params[:user][:current_password])\n      if @user.update(update_user_params)\n        if params[:user][:unconfirmed_email].present?\n          @user.send_confirmation_email!\n          redirect_to root_path, notice: \"Check your email for confirmation instructions.\"\n        else\n          redirect_to root_path, notice: \"Account updated.\"\n        end\n      else\n        render :edit, status: :unprocessable_entity\n      end\n    else\n      flash.now[:error] = \"Incorrect password\"\n      render :edit, status: :unprocessable_entity\n    end\n  end\n\n  private\n\n  def create_user_params\n    params.require(:user).permit(:email, :password, :password_confirmation)\n  end\n\n  def update_user_params\n    params.require(:user).permit(:current_password, :password, :password_confirmation, :unconfirmed_email)\n  end\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - We call `authenticate_user!` before editing, destroying, or updating a user since only an authenticated user should be able to do this.\n\u003e - We update the `create` method to accept `create_user_params` (formerly `user_params`). This is because we're going to require different parameters for creating an account vs. editing an account.\n\u003e - The `destroy` action simply deletes the user and logs them out. Note that we're calling `current_user`, so this action can only be scoped to the user who is logged in.\n\u003e - The `edit` action simply assigns `@user` to the `current_user` so that we have access to the user in the edit form.\n\u003e - The `update` action first checks if their password is correct. Note that we're passing this in as `current_password` and not `password`. This is because we still want a user to be able to change their password and therefore we need another parameter to store this value. This is also why we have a private `update_user_params` method.\n\u003e   - If the user is updating their email address (via `unconfirmed_email`) we send a confirmation email to that new email address before setting it as the `email` value.\n\u003e   - We force a user to always put in their `current_password` as an extra security measure in case someone leaves their browser open on a public computer.\n\n3. Update routes.\n\n```ruby\n# config/routes.rb\nRails.application.routes.draw do\n  ...\n  put \"account\", to: \"users#update\"\n  get \"account\", to: \"users#edit\"\n  delete \"account\", to: \"users#destroy\"\n  ...\nend\n```\n\n4. Create an edit form.\n\n```html+ruby\n\u003c!-- app/views/users/edit.html.erb --\u003e\n\u003c%= form_with model: @user, url: account_path, method: :put do |form| %\u003e\n  \u003c%= render partial: \"shared/form_errors\", locals: { object: form.object } %\u003e\n  \u003cdiv\u003e\n    \u003c%= form.label :email, \"Current Email\" %\u003e\n    \u003c%= form.email_field :email, disabled: true %\u003e\n  \u003c/div\u003e\n  \u003cdiv\u003e\n    \u003c%= form.label :unconfirmed_email, \"New Email\" %\u003e\n    \u003c%= form.text_field :unconfirmed_email %\u003e\n  \u003c/div\u003e\n  \u003cdiv\u003e\n    \u003c%= form.label :password, \"Password (leave blank if you don't want to change it)\" %\u003e\n    \u003c%= form.password_field :password %\u003e\n  \u003c/div\u003e\n  \u003cdiv\u003e\n    \u003c%= form.label :password_confirmation %\u003e\n    \u003c%= form.password_field :password_confirmation %\u003e\n  \u003c/div\u003e\n  \u003chr/\u003e\n  \u003cdiv\u003e\n    \u003c%= form.label :current_password, \"Current password (we need your current password to confirm your changes)\" %\u003e\n    \u003c%= form.password_field :current_password, required: true %\u003e\n  \u003c/div\u003e\n  \u003c%= form.submit \"Update Account\" %\u003e\n\u003c% end %\u003e\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - We `disable` the `email` field to ensure we're not passing that value back to the controller. This is just so the user can see what their current email is.\n\u003e - We `require` the `current_password` field since we'll always want a user to confirm their password before making changes.\n\u003e - The `password` and `password_confirmation` fields are there if a user wants to update their current password.\n\n## Step 13: Update Confirmations Controller\n\n1. Update edit action.\n\n```ruby\n# app/controllers/confirmations_controller.rb\nclass ConfirmationsController \u003c ApplicationController\n  ...\n  def edit\n    ...\n    if @user.present? \u0026\u0026 @user.unconfirmed_or_reconfirming?\n      ...\n    end\n  end\n  ...\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - We add `@user.unconfirmed_or_reconfirming?` to the conditional to ensure only unconfirmed users or users who are reconfirming can access this page. This is necessary since we're now allowing users to confirm new email addresses.\n\n## Step 14: Add Remember Token Column to Users Table\n\n1. Create migration.\n\n```bash\nrails g migration add_remember_token_to_users remember_token:string\n```\n\n2. Update migration.\n\n```ruby\n# db/migrate/[timestamp]_add_remember_token_to_users.rb\nclass AddRememberTokenToUsers \u003c ActiveRecord::Migration[6.1]\n  def change\n    add_column :users, :remember_token, :string, null: false\n    add_index :users, :remember_token, unique: true\n  end\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - We add `null: false` to ensure this column always has a value.\n\u003e - We add a [unique index](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Table.html#method-i-index) to ensure this column has unique data.\n\n3. Run migrations.\n\n```bash\nrails db:migrate\n```\n\n4. Update the User model.\n\n```ruby\n# app/models/user.rb\nclass User \u003c ApplicationRecord\n  ...\n  has_secure_token :remember_token\n  ...\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - We call [has_secure_token](https://api.rubyonrails.org/classes/ActiveRecord/SecureToken/ClassMethods.html#method-i-has_secure_token) on the `remember_token`. This ensures that the value for this column will be set when the record is created. This value will be used later to securely identify the user.\n\n## Step 15: Update Authentication Concern\n\n1. Add new helper methods.\n\n```ruby\n# app/controllers/concerns/authentication.rb\nmodule Authentication\n  extend ActiveSupport::Concern\n  ...\n  def forget(user)\n    cookies.delete :remember_token\n    user.regenerate_remember_token\n  end\n  ...\n  def remember(user)\n    user.regenerate_remember_token\n    cookies.permanent.encrypted[:remember_token] = user.remember_token\n  end\n  ...\n  private\n\n  def current_user\n    Current.user ||= if session[:current_user_id].present?\n      User.find_by(id: session[:current_user_id])\n    elsif cookies[:remember_token]\n      User.find_by(remember_token: cookies.encrypted[:remember_token])\n    end\n  end\n  ...\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - The `remember` method first regenerates a new `remember_token` to ensure these values are being rotated and can't be used more than once. We get the `regenerate_remember_token` method from [has_secure_token](https://api.rubyonrails.org/classes/ActiveRecord/SecureToken/ClassMethods.html#method-i-has_secure_token). Next, we assign this value to a [cookie](https://api.rubyonrails.org/classes/ActionDispatch/Cookies.html). The call to [permanent](https://api.rubyonrails.org/classes/ActionDispatch/Cookies/ChainedCookieJars.html#method-i-permanent) ensures the cookie won't expire until 20 years from now. The call to [encrypted](https://api.rubyonrails.org/classes/ActionDispatch/Cookies/ChainedCookieJars.html#method-i-encrypted) ensures the value will be encrypted. This is vital since this value is used to identify the user and is being set in the browser.\n\u003e - The `forget` method deletes the cookie and regenerates a new `remember_token` to ensure these values are being rotated and can't be used more than once.\n\u003e - We update the `current_user` method by adding a conditional to first try and find the user by the session, and then fallback to finding the user by the cookie. This is the logic that allows a user to completely exit their browser and remain logged in when they return to the website since the cookie will still be set.\n\n## Step 16: Update Sessions Controller\n\n1. Update the `create` and `destroy` methods.\n\n```ruby\n# app/controllers/sessions_controller.rb\nclass SessionsController \u003c ApplicationController\n  ...\n  before_action :authenticate_user!, only: [:destroy]\n\n  def create\n    ...\n    if @user\n      if @user.unconfirmed?\n        ...\n      elsif @user.authenticate(params[:user][:password])\n        login @user\n        remember(@user) if params[:user][:remember_me] == \"1\"\n        ...\n      else\n        ...\n      end\n    else\n      ...\n    end\n  end\n\n  def destroy\n    forget(current_user)\n    ...\n  end\n  ...\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - We conditionally call `remember(@user)` in the `create` method if the user has checked the \"Remember me\" checkbox. We still need to add this to our form.\n\u003e - We call `forget(current_user)` in the `destroy` method to ensure we delete the `remember_me` cookie and regenerate the user's `remember_token` token.\n\u003e - We also add a `before_action` to ensure only authenticated users can access the `destroy` action.\n\n2. Add the \"Remember me\" checkbox to the login form.\n\n```html+ruby\n\u003c!-- app/views/sessions/new.html.erb --\u003e\n\u003c%= form_with url: login_path, scope: :user do |form| %\u003e\n  ...\n  \u003cdiv\u003e\n    \u003c%= form.label :remember_me %\u003e\n    \u003c%= form.check_box :remember_me %\u003e\n  \u003c/div\u003e\n  \u003c%= form.submit \"Sign In\" %\u003e\n\u003c% end %\u003e\n```\n\n## Step 17: Add Friendly Redirects\n\n1. Update Authentication Concern.\n\n```ruby\n# app/controllers/concerns/authentication.rb\nmodule Authentication\n  ...\n  def authenticate_user!\n    store_location\n    ...\n  end\n  ...\n  private\n  ...\n  def store_location\n    session[:user_return_to] = request.original_url if request.get? \u0026\u0026 request.local?\n  end\n\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - The `store_location` method stores the [request.original_url](https://api.rubyonrails.org/classes/ActionDispatch/Request.html#method-i-original_url) in the [session](https://guides.rubyonrails.org/action_controller_overview.html#session) so it can be retrieved later. We only do this if the request made was a `get` request. We also call `request.local?` to ensure it was a local request. This prevents redirecting to an external application.\n\u003e - We call `store_location` in the `authenticate_user!` method so that we can save the path to the page the user was trying to visit before they were redirected to the login page. We need to do this before visiting the login page otherwise the call to `request.original_url` will always return the url to the login page.\n\n2. Update Sessions Controller.\n\n```ruby\n# app/controllers/sessions_controller.rb\nclass SessionsController \u003c ApplicationController\n  ...\n  def create\n    ...\n    if @user\n      if @user.unconfirmed?\n        ...\n      elsif @user.authenticate(params[:user][:password])\n        after_login_path = session[:user_return_to] || root_path\n        login @user\n        remember(@user) if params[:user][:remember_me] == \"1\"\n        redirect_to after_login_path, notice: \"Signed in.\"\n      else\n        ...\n      end\n    else\n      ...\n    end\n  end\n  ...\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - The `after_login_path` variable it set to be whatever is in the `session[:user_return_to]`. If there's nothing in `session[:user_return_to]` then it defaults to the `root_path`.\n\u003e - Note that we call this method before calling `login`. This is because `login` calls `reset_session` which would deleted the `session[:user_return_to]`.\n\n## Step 17: Account for Timing Attacks\n\n1. Update the User model.\n\n**[Note that this class method will be available in Rails 7.1](https://edgeapi.rubyonrails.org/classes/ActiveRecord/SecurePassword/ClassMethods.html#method-i-authenticate_by)**\n\n```ruby\n# app/models/user.rb\nclass User \u003c ApplicationRecord\n  ...\n  def self.authenticate_by(attributes)\n    passwords, identifiers = attributes.to_h.partition do |name, value|\n      !has_attribute?(name) \u0026\u0026 has_attribute?(\"#{name}_digest\")\n    end.map(\u0026:to_h)\n\n    raise ArgumentError, \"One or more password arguments are required\" if passwords.empty?\n    raise ArgumentError, \"One or more finder arguments are required\" if identifiers.empty?\n    if (record = find_by(identifiers))\n      record if passwords.count { |name, value| record.public_send(:\"authenticate_#{name}\", value) } == passwords.size\n    else\n      new(passwords)\n      nil\n    end\n  end  \n  ...\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - This class method serves to find a user using the non-password attributes (such as email), and then authenticates that record using the password attributes. Regardless of whether a user is found or authentication succeeds, `authenticate_by` will take the same amount of time. This prevents [timing-based enumeration attacks](https://en.wikipedia.org/wiki/Timing_attack), wherein an attacker can determine if a password record exists even without knowing the password.\n\n2. Update the Sessions Controller.\n\n```ruby\n# app/controllers/sessions_controller.rb\nclass SessionsController \u003c ApplicationController\n  ...\n  def create\n    @user = User.authenticate_by(email: params[:user][:email].downcase, password: params[:user][:password])\n    if @user\n      if @user.unconfirmed?\n        redirect_to new_confirmation_path, alert: \"Please confirm your email first.\"\n      else\n        after_login_path = session[:user_return_to] || root_path\n        login @user\n        remember(@user) if params[:user][:remember_me] == \"1\"\n        redirect_to after_login_path, notice: \"Signed in.\"\n      end\n    else\n      flash.now[:alert] = \"Incorrect email or password.\"\n      render :new, status: :unprocessable_entity\n    end\n  end\n  ...\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - We refactor the `create` method to always start by finding and authenticating the user. Not only does this prevent timing attacks, but it also prevents accidentally leaking email addresses. This is because we were originally checking if a user was confirmed before authenticating them. That means a bad actor could try and sign in with an email address to see if it exists on the system without needing to know the password.\n\n## Step 18: Store Session in the Database\n\nWe're currently setting the user's ID in the session. Even though that value is encrypted, the encrypted value doesn't change since it's based on the user id which doesn't change. This means that if a bad actor were to get a copy of the session they would have access to a victim's account in perpetuity. One solution is to [rotate encrypted and signed cookie configurations](https://guides.rubyonrails.org/security.html#rotating-encrypted-and-signed-cookies-configurations). Another option is to configure the [Rails session store](https://guides.rubyonrails.org/configuring.html#config-session-store) to use `mem_cache_store` to store session data. \n\nThe solution we will implement is to set a rotating value to identify the user and store that value in the database.\n\n1. Generate ActiveSession model.\n\n```bash\nrails g model active_session user:references\n```\n\n2. Update the migration.\n\n```ruby\nclass CreateActiveSessions \u003c ActiveRecord::Migration[6.1]\n  def change\n    create_table :active_sessions do |t|\n      t.references :user, null: false, foreign_key: {on_delete: :cascade}\n\n      t.timestamps\n    end\n  end\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - We update the `foreign_key` option from `true` to `{on_delete: :cascade}`. The [on_delete](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_foreign_key-label-Creating+a+cascading+foreign+key) option will delete any `active_session` record if its associated `user` is deleted from the database.\n\n3. Run migration.\n\n```bash\nrails db:migrate\n```\n\n4. Update User model.\n\n```ruby\n# app/models/user.rb\nclass User \u003c ApplicationRecord\n  ...\n  has_many :active_sessions, dependent: :destroy\n  ...\nend\n```\n\n5. Update Authentication Concern\n\n```ruby\n# app/controllers/concerns/authentication.rb\nmodule Authentication\n  ...\n  def login(user)\n    reset_session\n    active_session = user.active_sessions.create!\n    session[:current_active_session_id] = active_session.id\n  end\n  ...\n  def logout\n    active_session = ActiveSession.find_by(id: session[:current_active_session_id])\n    reset_session\n    active_session.destroy! if active_session.present?\n  end\n  ...\n  private\n\n  def current_user\n    Current.user = if session[:current_active_session_id].present?\n      ActiveSession.find_by(id: session[:current_active_session_id]).user\n    elsif cookies[:remember_token]\n      User.find_by(remember_token: cookies.encrypted[:remember_token])\n    end\n  end\n  ...\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - We update the `login` method by creating a new `active_session` record and then storing it's ID in the `session`. Note that we replaced `session[:current_user_id]` with `session[:current_active_session_id]`.\n\u003e - We update the `logout` method by first finding the `active_session` record from the `session`. After we call `reset_session` we then delete the `active_session` record if it exists. We need to check if it exists because in a future section we will allow a user to log out all current active sessions.\n\u003e - We update the `current_user` method by finding the `active_session` record from the `session`, and then returning its associated `user`. Note that we've replaced all instances of `session[:current_user_id]` with `session[:current_active_session_id]`.\n\n6. Force SSL.\n\n```ruby\n# config/environments/production.rb\nRails.application.configure do\n  ...\n  config.force_ssl = true\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - We force SSL in production to prevent [session hijacking](https://guides.rubyonrails.org/security.html#session-hijacking). Even though the session is encrypted we want to prevent the cookie from being exposed through an insecure network. If it were exposed, a bad actor could sign in as the victim.\n\n## Step 19: Capture Request Details for Each New Session\n\n1. Add new columns to the active_sessions table.\n\n```bash\nrails g migration add_request_columns_to_active_sessions user_agent:string ip_address:string\nrails db:migrate\n```\n\n2. Update login method to capture request details.\n\n```ruby\n# app/controllers/concerns/authentication.rb\nmodule Authentication\n  ...\n  def login(user)\n    reset_session\n    active_session = user.active_sessions.create!(user_agent: request.user_agent, ip_address: request.ip)\n    session[:current_active_session_id] = active_session.id\n  end\n  ...\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - We add columns to the `active_sessions` table to store data about when and where these sessions are being created. We are able to do this by tapping into the [request object](https://api.rubyonrails.org/classes/ActionDispatch/Request.html) and returning the [ip](https://api.rubyonrails.org/classes/ActionDispatch/Request.html#method-i-ip) and user agent. The user agent is simply the browser and device.\n \n\n4. Update Users Controller.\n\n```ruby\n# app/controllers/users_controller.rb\nclass UsersController \u003c ApplicationController\n  ...\n  def edit\n    @user = current_user\n    @active_sessions = @user.active_sessions.order(created_at: :desc)\n  end\n  ...\n  def update\n    @user = current_user\n    @active_sessions = @user.active_sessions.order(created_at: :desc)\n    ...\n  end\nend\n```\n\n5. Create active session partial.\n\n```html+ruby\n\u003c!-- app/views/active_sessions/_active_session.html.erb --\u003e\n\u003ctr\u003e\n  \u003ctd\u003e\u003c%= active_session.user_agent %\u003e\u003c/td\u003e\n  \u003ctd\u003e\u003c%= active_session.ip_address %\u003e\u003c/td\u003e\n  \u003ctd\u003e\u003c%= active_session.created_at %\u003e\u003c/td\u003e\n\u003c/tr\u003e\n```\n\n6. Update account page.\n\n```html+ruby\n\u003c!-- app/views/users/edit.html.erb --\u003e\n...\n\u003ch2\u003eCurrent Logins\u003c/h2\u003e\n\u003c% if @active_sessions.any? %\u003e\n  \u003ctable\u003e\n    \u003cthead\u003e\n      \u003ctr\u003e\n        \u003cth\u003eUser Agent\u003c/th\u003e\n        \u003cth\u003eIP Address\u003c/th\u003e\n        \u003cth\u003eSigned In At\u003c/th\u003e\n      \u003c/tr\u003e\n    \u003c/thead\u003e\n    \u003ctbody\u003e\n      \u003c%= render @active_sessions %\u003e\n    \u003c/tbody\u003e\n  \u003c/table\u003e\n\u003c% end %\u003e\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - We're simply showing any `active_session` associated with the `current_user`. By rendering the `user_agent`, `ip_address`, and `created_at` values we're giving the `current_user` all the information they need to know if there's any suspicious activity happening with their account. For example, if there's an `active_session` with a unfamiliar IP address or browser, this could indicate that the user's account has been compromised.\n\u003e - Note that we also instantiate `@active_sessions` in the `update` method. This is because the `update` method renders the `edit` method during failure cases.\n\n## Step 20: Allow User to Sign Out Specific Active Sessions\n\n1. Generate the Active Sessions Controller and update routes.\n\n```\nrails g controller active_sessions\n```\n\n```ruby\n# app/controllers/active_sessions_controller.rb\nclass ActiveSessionsController \u003c ApplicationController\n  before_action :authenticate_user!\n\n  def destroy\n    @active_session = current_user.active_sessions.find(params[:id])\n\n    @active_session.destroy\n\n    if current_user\n      redirect_to account_path, notice: \"Session deleted.\"\n    else\n      reset_session\n      redirect_to root_path, notice: \"Signed out.\"\n    end\n  end\n\n  def destroy_all\n    current_user.active_sessions.destroy_all\n    reset_session\n\n    redirect_to root_path, notice: \"Signed out.\"\n  end\nend\n```\n\n```ruby\n# config/routes.rb\nRails.application.routes.draw do\n  ...\n  resources :active_sessions, only: [:destroy] do\n    collection do\n      delete \"destroy_all\"\n    end\n  end\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - We ensure only users who are logged in can access these endpoints by calling `before_action :authenticate_user!`.\n\u003e - The `destroy` method simply looks for an `active_session` associated with the `current_user`. This ensures that a user can only delete sessions associated with their account.\n\u003e   - Once we destroy the `active_session` we then redirect back to the account page or to the homepage. This is because a user may not be deleting a session for the device or browser they're currently logged into. Note that we only call [reset_session](https://api.rubyonrails.org/classes/ActionDispatch/Request.html#method-i-reset_session) if the user has deleted a session for the device or browser they're currently logged into, as this is the same as logging out.\n\u003e - The `destroy_all` method is a [collection route](https://guides.rubyonrails.org/routing.html#adding-collection-routes) that will destroy all `active_session` records associated with the `current_user`. Note that we call `reset_session` because we will be logging out the `current_user` during this request.\n\n2. Update views by adding buttons to destroy sessions. \n\n```html+ruby\n\u003c!-- app/views/users/edit.html.erb --\u003e\n...\n\u003ch2\u003eCurrent Logins\u003c/h2\u003e\n\u003c% if @active_sessions.any? %\u003e \n  \u003c%= button_to \"Log out of all other sessions\", destroy_all_active_sessions_path, method: :delete %\u003e\n  \u003ctable\u003e\n    \u003cthead\u003e\n      \u003ctr\u003e\n        \u003cth\u003eUser Agent\u003c/th\u003e\n        \u003cth\u003eIP Address\u003c/th\u003e\n        \u003cth\u003eSigned In At\u003c/th\u003e\n        \u003cth\u003eSign Out\u003c/th\u003e\n      \u003c/tr\u003e\n    \u003c/thead\u003e\n    \u003ctbody\u003e\n      \u003c%= render @active_sessions %\u003e\n    \u003c/tbody\u003e\n  \u003c/table\u003e\n\u003c% end %\u003e\n```\n\n```html+ruby\n\u003c!-- app/views/active_sessions/_active_session.html.erb --\u003e\n\u003ctr\u003e\n  \u003ctd\u003e\u003c%= active_session.user_agent %\u003e\u003c/td\u003e\n  \u003ctd\u003e\u003c%= active_session.ip_address %\u003e\u003c/td\u003e\n  \u003ctd\u003e\u003c%= active_session.created_at %\u003e\u003c/td\u003e\n  \u003ctd\u003e\u003c%= button_to \"Sign Out\", active_session_path(active_session), method: :delete %\u003e\u003c/td\u003e\n\u003c/tr\u003e\n```\n\n3. Update Authentication Concern.\n\n```ruby\n# app/controllers/concerns/authentication.rb\nmodule Authentication\n  ...\n  private\n\n  def current_user\n    Current.user = if session[:current_active_session_id].present?\n      ActiveSession.find_by(id: session[:current_active_session_id])\u0026.user\n    elsif cookies[:remember_token]\n      User.find_by(remember_token: cookies.encrypted[:remember_token])\n    end\n  end\n  ...\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - This is a very subtle change, but we've added a [safe navigation operator](https://ruby-doc.org/core-2.6/doc/syntax/calling_methods_rdoc.html#label-Safe+navigation+operator) via the `\u0026.user` call. This is because `ActiveSession.find_by(id: session[:current_active_session_id])` can now return `nil` since we're able to delete other `active_session` records.\n\n## Step 21: Refactor Remember Logic\n\nSince we're now associating our sessions with an `active_session` and not a `user`, we'll want to remove the `remember_token` token from the `users` table and onto the `active_sessions`.\n\n1. Move remember_token column from users to active_sessions table.\n\n```bash\nrails g migration move_remember_token_from_users_to_active_sessions\n```\n\n```ruby\n# db/migrate/[timestamp]_move_remember_token_from_users_to_active_sessions.rb\nclass MoveRememberTokenFromUsersToActiveSessions \u003c ActiveRecord::Migration[6.1]\n  def change\n    remove_column :users, :remember_token\n    add_column :active_sessions, :remember_token, :string, null: false\n\n    add_index :active_sessions, :remember_token, unique: true\n  end\nend\n```\n\n2. Run migration.\n\n```bash\nrails db:migrate\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - We add `null: false` to ensure this column always has a value.\n\u003e - We add a [unique index](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Table.html#method-i-index) to ensure this column has unique data.\n\n3. Update User Model.\n\n```diff\n class User \u003c ApplicationRecord\n    ...\n-   has_secure_token :remember_token\n    ...\n end\n```\n\n4. Update Active Session Model.\n\n```ruby\n# app/models/active_session.rb\nclass ActiveSession \u003c ApplicationRecord\n  ...\n  has_secure_token :remember_token\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e\n\u003e - We call [has_secure_token](https://api.rubyonrails.org/classes/ActiveRecord/SecureToken/ClassMethods.html#method-i-has_secure_token) on the `remember_token`. This ensures that the value for this column will be set when the record is created. This value will be used later to securely identify the user.\n\u003e - Note that we remove this from the `user` model.\n\n5. Refactor the Authentication Concern.\n\n```ruby\n# app/controllers/concerns/authentication.rb\nmodule Authentication\n  ...\n  def login(user)\n    reset_session\n    active_session = user.active_sessions.create!(user_agent: request.user_agent, ip_address: request.ip)\n    session[:current_active_session_id] = active_session.id\n\n    active_session\n  end\n\n  def forget_active_session\n    cookies.delete :remember_token\n  end\n  ...\n  def remember(active_session)\n    cookies.encrypted[:remember_token] = active_session.remember_token\n  end\n  ...\n  private\n\n  def current_user\n    Current.user = if session[:current_active_session_id].present?\n      ActiveSession.find_by(id: session[:current_active_session_id])\u0026.user\n    elsif cookies[:remember_token]\n      ActiveSession.find_by(remember_token: cookies.encrypted[:remember_token])\u0026.user\n    end\n  end\n  ...\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e \n\u003e - The `login` method now returns the `active_session`. This will be used later when calling `SessionsController#create`.\n\u003e - The `forget` method has been renamed to `forget_active_session` and no longer takes any arguments. This method simply deletes the `cookie`. We don't need to call `active_session.regenerate_remember_token` since the `active_session` will be deleted, and therefor cannot be referenced again.\n\u003e - The `remember` method now accepts an `active_session` and not a `user`. We do not need to call `active_session.regenerate_remember_token` since a new `active_session` record will be created each time a user logs in. Note that we now save `active_session.remember_token` to the cookie.\n\u003e - The `current_user` method now finds the `active_session` record if the `remember_token` is present and returns the user via the [safe navigation operator](https://ruby-doc.org/core-2.6/doc/syntax/calling_methods_rdoc.html#label-Safe+navigation+operator).\n\n6. Refactor the Sessions Controller.\n\n```ruby\n# app/controllers/sessions_controller.rb\nclass SessionsController \u003c ApplicationController\n  def create\n    ...\n    if @user\n      if @user.unconfirmed?\n        ...\n      else\n        ...\n        active_session = login @user\n        remember(active_session) if params[:user][:remember_me] == \"1\"\n      end\n    else\n    ...\n    end\n  end\n\n   def destroy\n    forget_active_session\n    ...\n  end\nend\n```\n\n\u003e **What's Going On Here?**\n\u003e \n\u003e - Since the `login` method now returns an `active_session`, we can take that value and pass it to `remember`.\n\u003e - We replace `forget(current_user)` with `forget_active_session` to reflect changes to the method name and structure.\n\n7. Refactor Active Sessions Controller\n\n```ruby\n# app/controllers/active_sessions_controller.rb\nclass ActiveSessionsController \u003c ApplicationController\n  ...\n  def destroy\n    ...\n    if current_user\n    ...\n    else\n      forget_active_session\n      ...\n    end\n  end\n\n  def destroy_all\n    forget_active_session\n    current_user.active_sessions.destroy_all\n    ...\n  end\nend\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstevepolitodesign%2Frails-authentication-from-scratch","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstevepolitodesign%2Frails-authentication-from-scratch","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstevepolitodesign%2Frails-authentication-from-scratch/lists"}