Skip to content

Commit 9c36910

Browse files
Capture request details for each new session
When logged into the application I want to be able to view all my active sessions so that I can determine if my account has been compromised based on the session data, user agent, and IP address. Issues ------ - Closes #69
1 parent 5f6990c commit 9c36910

9 files changed

+153
-2
lines changed

README.md

+85
Original file line numberDiff line numberDiff line change
@@ -1398,3 +1398,88 @@ end
13981398
> **What's Going On Here?**
13991399
>
14001400
> - 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.
1401+
1402+
## Step 19: Capture Request Details for Each New Session
1403+
1404+
1. Add new columns to the active_sessions table.
1405+
1406+
```bash
1407+
rails g migration add_request_columns_to_active_sessions user_agent:string ip_address:string
1408+
rails db:migrate
1409+
```
1410+
1411+
2. Update login method to capture request details.
1412+
1413+
```ruby
1414+
# app/controllers/concerns/authentication.rb
1415+
module Authentication
1416+
...
1417+
def login(user)
1418+
reset_session
1419+
active_session = user.active_sessions.create!(user_agent: request.user_agent, ip_address: request.ip)
1420+
session[:current_active_session_id] = active_session.id
1421+
end
1422+
...
1423+
end
1424+
```
1425+
1426+
> **What's Going On Here?**
1427+
>
1428+
> - 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.
1429+
1430+
1431+
4. Update Users Controller.
1432+
1433+
```ruby
1434+
# app/controllers/users_controller.rb
1435+
class UsersController < ApplicationController
1436+
...
1437+
def edit
1438+
@user = current_user
1439+
@active_sessions = @user.active_sessions.order(created_at: :desc)
1440+
end
1441+
...
1442+
def update
1443+
@user = current_user
1444+
@active_sessions = @user.active_sessions.order(created_at: :desc)
1445+
...
1446+
end
1447+
end
1448+
```
1449+
1450+
5. Create active session partial.
1451+
1452+
```html+ruby
1453+
<!-- app/views/active_sessions/_active_session.html.erb -->
1454+
<td><%= active_session.user_agent %></td>
1455+
<td><%= active_session.ip_address %></td>
1456+
<td><%= active_session.created_at %></td>
1457+
```
1458+
1459+
6. Update account page.
1460+
1461+
```html+ruby
1462+
<!-- app/views/users/edit.html.erb -->
1463+
...
1464+
<h2>Current Logins</h2>
1465+
<% if @active_sessions.any? %>
1466+
<table>
1467+
<thead>
1468+
<tr>
1469+
<th>User Agent</th>
1470+
<th>IP Address</th>
1471+
<th>Signed In At</th>
1472+
</tr>
1473+
</thead>
1474+
<tbody>
1475+
<%= render @active_sessions %>
1476+
</tbody>
1477+
</table>
1478+
<% end %>
1479+
```
1480+
1481+
> **What's Going On Here?**
1482+
>
1483+
> - 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.
1484+
> - Note that we also instantiate `@active_sessions` in the `update` method. This is because the `update` method renders the `edit` method during failure cases.
1485+

app/controllers/concerns/authentication.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def authenticate_user!
1414

1515
def login(user)
1616
reset_session
17-
active_session = user.active_sessions.create!
17+
active_session = user.active_sessions.create!(user_agent: request.user_agent, ip_address: request.ip)
1818
session[:current_active_session_id] = active_session.id
1919
end
2020

app/controllers/users_controller.rb

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def destroy
2020

2121
def edit
2222
@user = current_user
23+
@active_sessions = @user.active_sessions.order(created_at: :desc)
2324
end
2425

2526
def new
@@ -28,6 +29,7 @@ def new
2829

2930
def update
3031
@user = current_user
32+
@active_sessions = @user.active_sessions.order(created_at: :desc)
3133
if @user.authenticate(params[:user][:current_password])
3234
if @user.update(update_user_params)
3335
if params[:user][:unconfirmed_email].present?
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<td><%= active_session.user_agent %></td>
2+
<td><%= active_session.ip_address %></td>
3+
<td><%= active_session.created_at %></td>

app/views/users/edit.html.erb

+17
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,21 @@
2222
<%= form.password_field :current_password, required: true %>
2323
</div>
2424
<%= form.submit "Update Account" %>
25+
<% end %>
26+
<h2>Current Logins</h2>
27+
<% if @active_sessions.any? %>
28+
<table>
29+
<thead>
30+
<tr>
31+
<th>User Agent</th>
32+
<th>IP Address</th>
33+
<th>Signed In At</th>
34+
</tr>
35+
</thead>
36+
<tbody>
37+
<tr>
38+
<%= render @active_sessions %>
39+
</tr>
40+
</tbody>
41+
</table>
2542
<% end %>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# TODO: Remove
2+
class AddRequestColumnsToActiveSessions < ActiveRecord::Migration[6.1]
3+
def change
4+
add_column :active_sessions, :user_agent, :string
5+
add_column :active_sessions, :ip_address, :string
6+
end
7+
end

db/schema.rb

+3-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
require "test_helper"
2+
3+
class UserInterfaceTest < ActionDispatch::IntegrationTest
4+
setup do
5+
@confirmed_user = User.create!(email: "confirmed_user@example.com", password: "password", password_confirmation: "password", confirmed_at: Time.current)
6+
end
7+
8+
test "should render active sessions on account page" do
9+
login @confirmed_user
10+
@confirmed_user.active_sessions.last.update!(user_agent: "Mozilla", ip_address: "123.457.789")
11+
12+
get account_path
13+
14+
assert_match "Mozilla", @response.body
15+
assert_match "123.457.789", @response.body
16+
end
17+
end

test/system/logins_test.rb

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
require "application_system_test_case"
2+
3+
class LoginsTest < ApplicationSystemTestCase
4+
setup do
5+
@confirmed_user = User.create!(email: "confirmed_user@example.com", password: "password", password_confirmation: "password", confirmed_at: Time.current)
6+
end
7+
8+
test "should login and create active session if confirmed" do
9+
visit login_path
10+
11+
fill_in "Email", with: @confirmed_user.email
12+
fill_in "Password", with: @confirmed_user.password
13+
click_on "Sign In"
14+
15+
assert_not_nil @confirmed_user.active_sessions.last.user_agent
16+
assert_not_nil @confirmed_user.active_sessions.last.ip_address
17+
end
18+
end

0 commit comments

Comments
 (0)