Skip to content

Commit 03a102b

Browse files
committed
Added Rails 8 authentication
This is added by running `bin/rails generate authentication`
1 parent 285c1cd commit 03a102b

20 files changed

+238
-1
lines changed

Gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ gem "stimulus-rails"
1818
gem "jbuilder"
1919

2020
# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
21-
# gem "bcrypt", "~> 3.1.7"
21+
gem "bcrypt", "~> 3.1.7"
2222

2323
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
2424
gem "tzinfo-data", platforms: %i[ windows jruby ]

Gemfile.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ GEM
7474
uri (>= 0.13.1)
7575
ast (2.4.2)
7676
base64 (0.2.0)
77+
bcrypt (3.1.20)
7778
bcrypt_pbkdf (1.1.1)
7879
bcrypt_pbkdf (1.1.1-arm64-darwin)
7980
bcrypt_pbkdf (1.1.1-x86_64-darwin)
@@ -335,6 +336,7 @@ PLATFORMS
335336
x86_64-linux-musl
336337

337338
DEPENDENCIES
339+
bcrypt (~> 3.1.7)
338340
bootsnap
339341
brakeman
340342
debug
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module ApplicationCable
2+
class Connection < ActionCable::Connection::Base
3+
identified_by :current_user
4+
5+
def connect
6+
set_current_user || reject_unauthorized_connection
7+
end
8+
9+
private
10+
def set_current_user
11+
if session = Session.find_by(id: cookies.signed[:session_id])
12+
self.current_user = session.user
13+
end
14+
end
15+
end
16+
end
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
class ApplicationController < ActionController::Base
2+
include Authentication
23
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
34
allow_browser versions: :modern
45
end
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
module Authentication
2+
extend ActiveSupport::Concern
3+
4+
included do
5+
before_action :require_authentication
6+
helper_method :authenticated?
7+
end
8+
9+
class_methods do
10+
def allow_unauthenticated_access(**options)
11+
skip_before_action :require_authentication, **options
12+
end
13+
end
14+
15+
private
16+
def authenticated?
17+
resume_session
18+
end
19+
20+
def require_authentication
21+
resume_session || request_authentication
22+
end
23+
24+
25+
def resume_session
26+
Current.session ||= find_session_by_cookie
27+
end
28+
29+
def find_session_by_cookie
30+
Session.find_by(id: cookies.signed[:session_id])
31+
end
32+
33+
34+
def request_authentication
35+
session[:return_to_after_authenticating] = request.url
36+
redirect_to new_session_path
37+
end
38+
39+
def after_authentication_url
40+
session.delete(:return_to_after_authenticating) || root_url
41+
end
42+
43+
44+
def start_new_session_for(user)
45+
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
46+
Current.session = session
47+
cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }
48+
end
49+
end
50+
51+
def terminate_session
52+
Current.session.destroy
53+
cookies.delete(:session_id)
54+
end
55+
end
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
class PasswordsController < ApplicationController
2+
allow_unauthenticated_access
3+
before_action :set_user_by_token, only: %i[ edit update ]
4+
5+
def new
6+
end
7+
8+
def create
9+
if user = User.find_by(email_address: params[:email_address])
10+
PasswordsMailer.reset(user).deliver_later
11+
end
12+
13+
redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)."
14+
end
15+
16+
def edit
17+
end
18+
19+
def update
20+
if @user.update(params.permit(:password, :password_confirmation))
21+
redirect_to new_session_path, notice: "Password has been reset."
22+
else
23+
redirect_to edit_password_path(params[:token]), alert: "Passwords did not match."
24+
end
25+
end
26+
27+
private
28+
def set_user_by_token
29+
@user = User.find_by_password_reset_token!(params[:token])
30+
rescue ActiveSupport::MessageVerifier::InvalidSignature
31+
redirect_to new_password_path, alert: "Password reset link is invalid or has expired."
32+
end
33+
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
class SessionsController < ApplicationController
2+
allow_unauthenticated_access only: %w[new create]
3+
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_url, alert: "Try again later." }
4+
5+
def new
6+
end
7+
8+
def create
9+
if user = User.authenticate_by(params.permit(:email_address, :password))
10+
start_new_session_for user
11+
redirect_to after_authentication_url
12+
else
13+
redirect_to new_session_path, alert: "Try another email address or password."
14+
end
15+
end
16+
17+
def destroy
18+
terminate_session
19+
redirect_to new_session_path
20+
end
21+
end

app/mailers/passwords_mailer.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class PasswordsMailer < ApplicationMailer
2+
def reset(user)
3+
@user = user
4+
mail subject: "Reset your password", to: user.email_address
5+
end
6+
end

app/models/current.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
class Current < ActiveSupport::CurrentAttributes
2+
attribute :session
3+
delegate :user, to: :session, allow_nil: true
4+
end

app/models/session.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class Session < ApplicationRecord
2+
belongs_to :user
3+
end

app/models/user.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class User < ApplicationRecord
2+
has_secure_password
3+
has_many :sessions, dependent: :destroy
4+
5+
normalizes :email_address, with: ->(e) { e.strip.downcase }
6+
end

app/views/passwords/edit.html.erb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<h1>Update your password</h1>
2+
3+
<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
4+
5+
<%= form_with url: password_path(params[:token]), method: :put do |form| %>
6+
<%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter new password", maxlength: 72 %><br>
7+
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72 %><br>
8+
<%= form.submit "Save" %>
9+
<% end %>

app/views/passwords/new.html.erb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<h1>Forgot your password?</h1>
2+
3+
<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
4+
5+
<%= form_with url: passwords_path do |form| %>
6+
<%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %><br>
7+
<%= form.submit "Email reset instructions" %>
8+
<% end %>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<p>
2+
You can reset your password within the next 15 minutes on
3+
<%= link_to "this password reset page", edit_password_url(@user.password_reset_token) %>.
4+
</p>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
You can reset your password within the next 15 minutes on this password reset page:
2+
<%= edit_password_url(@user.password_reset_token) %>

app/views/sessions/new.html.erb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
2+
<%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %>
3+
4+
<%= form_with url: session_path do |form| %>
5+
<%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %><br>
6+
<%= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72 %><br>
7+
<%= form.submit "Sign in" %>
8+
<% end %>
9+
<br>
10+
11+
<%= link_to "Forgot password?", new_password_path %>

config/routes.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
Rails.application.routes.draw do
2+
resource :session
3+
resources :passwords, param: :token
24
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
35

46
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class CreateUsers < ActiveRecord::Migration[8.0]
2+
def change
3+
create_table :users do |t|
4+
t.string :email_address, null: false
5+
t.string :password_digest, null: false
6+
7+
t.timestamps
8+
end
9+
add_index :users, :email_address, unique: true
10+
end
11+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class CreateSessions < ActiveRecord::Migration[8.0]
2+
def change
3+
create_table :sessions do |t|
4+
t.references :user, null: false, foreign_key: true
5+
t.string :ip_address
6+
t.string :user_agent
7+
8+
t.timestamps
9+
end
10+
end
11+
end

db/schema.rb

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)