Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Capture request details for each new session #75

Merged
merged 1 commit into from
Feb 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1398,3 +1398,88 @@ end
> **What's Going On Here?**
>
> - 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.

## Step 19: Capture Request Details for Each New Session

1. Add new columns to the active_sessions table.

```bash
rails g migration add_request_columns_to_active_sessions user_agent:string ip_address:string
rails db:migrate
```

2. Update login method to capture request details.

```ruby
# app/controllers/concerns/authentication.rb
module Authentication
...
def login(user)
reset_session
active_session = user.active_sessions.create!(user_agent: request.user_agent, ip_address: request.ip)
session[:current_active_session_id] = active_session.id
end
...
end
```

> **What's Going On Here?**
>
> - 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.


4. Update Users Controller.

```ruby
# app/controllers/users_controller.rb
class UsersController < ApplicationController
...
def edit
@user = current_user
@active_sessions = @user.active_sessions.order(created_at: :desc)
end
...
def update
@user = current_user
@active_sessions = @user.active_sessions.order(created_at: :desc)
...
end
end
```

5. Create active session partial.

```html+ruby
<!-- app/views/active_sessions/_active_session.html.erb -->
<td><%= active_session.user_agent %></td>
<td><%= active_session.ip_address %></td>
<td><%= active_session.created_at %></td>
```

6. Update account page.

```html+ruby
<!-- app/views/users/edit.html.erb -->
...
<h2>Current Logins</h2>
<% if @active_sessions.any? %>
<table>
<thead>
<tr>
<th>User Agent</th>
<th>IP Address</th>
<th>Signed In At</th>
</tr>
</thead>
<tbody>
<%= render @active_sessions %>
</tbody>
</table>
<% end %>
```

> **What's Going On Here?**
>
> - 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.
> - Note that we also instantiate `@active_sessions` in the `update` method. This is because the `update` method renders the `edit` method during failure cases.

2 changes: 1 addition & 1 deletion app/controllers/concerns/authentication.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def authenticate_user!

def login(user)
reset_session
active_session = user.active_sessions.create!
active_session = user.active_sessions.create!(user_agent: request.user_agent, ip_address: request.ip)
session[:current_active_session_id] = active_session.id
end

Expand Down
2 changes: 2 additions & 0 deletions app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def destroy

def edit
@user = current_user
@active_sessions = @user.active_sessions.order(created_at: :desc)
end

def new
Expand All @@ -28,6 +29,7 @@ def new

def update
@user = current_user
@active_sessions = @user.active_sessions.order(created_at: :desc)
if @user.authenticate(params[:user][:current_password])
if @user.update(update_user_params)
if params[:user][:unconfirmed_email].present?
Expand Down
3 changes: 3 additions & 0 deletions app/views/active_sessions/_active_session.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<td><%= active_session.user_agent %></td>
<td><%= active_session.ip_address %></td>
<td><%= active_session.created_at %></td>
17 changes: 17 additions & 0 deletions app/views/users/edit.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,21 @@
<%= form.password_field :current_password, required: true %>
</div>
<%= form.submit "Update Account" %>
<% end %>
<h2>Current Logins</h2>
<% if @active_sessions.any? %>
<table>
<thead>
<tr>
<th>User Agent</th>
<th>IP Address</th>
<th>Signed In At</th>
</tr>
</thead>
<tbody>
<tr>
<%= render @active_sessions %>
</tr>
</tbody>
</table>
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class AddRequestColumnsToActiveSessions < ActiveRecord::Migration[6.1]
def change
add_column :active_sessions, :user_agent, :string
add_column :active_sessions, :ip_address, :string
end
end
4 changes: 3 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions test/integration/user_interface_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
require "test_helper"

class UserInterfaceTest < ActionDispatch::IntegrationTest
setup do
@confirmed_user = User.create!(email: "confirmed_user@example.com", password: "password", password_confirmation: "password", confirmed_at: Time.current)
end

test "should render active sessions on account page" do
login @confirmed_user
@confirmed_user.active_sessions.last.update!(user_agent: "Mozilla", ip_address: "123.457.789")

get account_path

assert_match "Mozilla", @response.body
assert_match "123.457.789", @response.body
end
end
18 changes: 18 additions & 0 deletions test/system/logins_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
require "application_system_test_case"

class LoginsTest < ApplicationSystemTestCase
setup do
@confirmed_user = User.create!(email: "confirmed_user@example.com", password: "password", password_confirmation: "password", confirmed_at: Time.current)
end

test "should login and create active session if confirmed" do
visit login_path

fill_in "Email", with: @confirmed_user.email
fill_in "Password", with: @confirmed_user.password
click_on "Sign In"

assert_not_nil @confirmed_user.active_sessions.last.user_agent
assert_not_nil @confirmed_user.active_sessions.last.ip_address
end
end