Skip to content

Commit

Permalink
Capture request details for each new session
Browse files Browse the repository at this point in the history
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
  • Loading branch information
stevepolitodesign committed Feb 5, 2022
1 parent b055fc7 commit 1010370
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 2 deletions.
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

0 comments on commit 1010370

Please sign in to comment.