Skip to content

Commit 90b2394

Browse files
committedJun 27, 2023
feat: support for storing ip address countries
1 parent 51d17a4 commit 90b2394

File tree

5 files changed

+66
-0
lines changed

5 files changed

+66
-0
lines changed
 

‎README.md

+10
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,16 @@ class LoginController < ApplicationController
292292
end
293293
```
294294

295+
## Storing IP address countries
296+
297+
Authie has support for storing the country that an IP address is located in whenever they are saved to the database. To use this, you need to specify a backend to use in the Authie configuration. The backend should respond to `#call(ip_address)`.
298+
299+
```ruby
300+
Authie.config.lookup_ip_country_backend = proc do |ip_address|
301+
SomeService.lookup_country_from_ip(ip_address)
302+
end
303+
```
304+
295305
## Instrumentation/Notification
296306

297307
Authie will publish events to the ActiveSupport::Notification instrumentation system. The following events are published
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# frozen_string_literal: true
2+
3+
class AddCountriesToAuthieSessions < ActiveRecord::Migration[6.1]
4+
def change
5+
add_column :authie_sessions, :login_ip_country, :string
6+
add_column :authie_sessions, :two_factored_ip_country, :string
7+
add_column :authie_sessions, :last_activity_ip_country, :string
8+
end
9+
end

‎lib/authie/config.rb

+8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class Config
88
attr_accessor :browser_id_cookie_name
99
attr_accessor :session_token_length
1010
attr_accessor :extend_session_expiry_on_touch
11+
attr_accessor :ip_lookup
1112

1213
def initialize
1314
@session_inactivity_timeout = 12.hours
@@ -16,6 +17,13 @@ def initialize
1617
@browser_id_cookie_name = :browser_id
1718
@session_token_length = 64
1819
@extend_session_expiry_on_touch = false
20+
@lookup_ip_country_backend = nil
21+
end
22+
23+
def lookup_ip_country(ip)
24+
return nil if @lookup_ip_country_backend.nil?
25+
26+
@lookup_ip_country_backend.call(ip)
1927
end
2028
end
2129

‎lib/authie/session.rb

+9
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,11 @@ def invalidate
9696
# @return [Authie::Session]
9797
def touch
9898
@session.last_activity_at = Time.now
99+
if @controller.request.ip != @session.last_activity_ip
100+
@session.last_activity_ip_country = Authie.config.lookup_ip_country(@controller.request.ip)
101+
end
99102
@session.last_activity_ip = @controller.request.ip
103+
100104
@session.last_activity_path = @controller.request.path
101105
@session.requests += 1
102106
extend_session_expiry_if_appropriate
@@ -124,6 +128,7 @@ def see_password
124128
def mark_as_two_factored(skip: nil)
125129
@session.two_factored_at = Time.now
126130
@session.two_factored_ip = @controller.request.ip
131+
@session.two_factored_ip_country = Authie.config.lookup_ip_country(@controller.request.ip)
127132
@session.skip_two_factor = skip unless skip.nil?
128133
@session.save!
129134
Authie.notify(:mark_as_two_factor, session: self)
@@ -244,6 +249,7 @@ def start(controller, user:, persistent: false, see_password: false, **params)
244249
session.browser_id = cookies[:browser_id]
245250
session.login_at = Time.now
246251
session.login_ip = controller.request.ip
252+
session.login_ip_country = Authie.config.lookup_ip_country(session.login_ip)
247253
session.host = controller.request.host
248254
session.user_agent = controller.request.user_agent
249255
session.expires_at = Time.now + Authie.config.persistent_session_length if persistent
@@ -298,9 +304,11 @@ def get_session(controller)
298304
delegate :invalidate_others!, to: :session
299305
delegate :last_activity_at, to: :session
300306
delegate :last_activity_ip, to: :session
307+
delegate :last_activity_ip_country, to: :session
301308
delegate :last_activity_path, to: :session
302309
delegate :login_at, to: :session
303310
delegate :login_ip, to: :session
311+
delegate :login_ip_country, to: :session
304312
delegate :password_seen_at, to: :session
305313
delegate :persisted?, to: :session
306314
delegate :persistent?, to: :session
@@ -311,6 +319,7 @@ def get_session(controller)
311319
delegate :token_hash, to: :session
312320
delegate :two_factored_at, to: :session
313321
delegate :two_factored_ip, to: :session
322+
delegate :two_factored_ip_country, to: :session
314323
delegate :two_factored?, to: :session
315324
delegate :skip_two_factor?, to: :session
316325
delegate :update, to: :session

‎spec/lib/session_spec.rb

+30
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,21 @@
142142
expect(session.last_activity_ip).to eq '1.2.3.4'
143143
end
144144

145+
it 'sets the last activity IP country' do
146+
allow(controller.request).to receive(:ip).and_return('1.2.3.4')
147+
allow(Authie.config).to receive(:lookup_ip_country).with('1.2.3.4').and_return('FR')
148+
session.touch
149+
expect(session.last_activity_ip_country).to eq 'FR'
150+
end
151+
152+
it 'does not lookup an IP if the last activity IP does not change' do
153+
allow(controller.request).to receive(:ip).and_return('1.2.3.4')
154+
session.update!(last_activity_ip: '1.2.3.4', last_activity_ip_country: 'FR')
155+
expect(Authie.config).to_not receive(:lookup_ip_country)
156+
session.touch
157+
expect(session.last_activity_ip_country).to eq 'FR'
158+
end
159+
145160
it 'sets the last activity path' do
146161
allow(controller.request).to receive(:path).and_return('/blah/blah')
147162
session.touch
@@ -250,6 +265,13 @@
250265
expect(session.two_factored_ip).to eq '1.2.3.4'
251266
end
252267

268+
it 'sets the ip address country' do
269+
allow(controller.request).to receive(:ip).and_return('1.2.3.4')
270+
allow(Authie.config).to receive(:lookup_ip_country).with('1.2.3.4').and_return('AU')
271+
session.mark_as_two_factored
272+
expect(session.two_factored_ip_country).to eq 'AU'
273+
end
274+
253275
it 'it dispatched an event' do
254276
expect(Authie).to receive(:notify).with(:mark_as_two_factor, session: session)
255277
session.mark_as_two_factored
@@ -320,6 +342,14 @@
320342
end
321343
end
322344

345+
it 'adds the login IP coutnry' do
346+
allow(Authie.config).to receive(:lookup_ip_country).with('1.2.3.4').and_return('GB')
347+
allow(controller.request).to receive(:ip).and_return('1.2.3.4')
348+
session = described_class.start(controller, user: user)
349+
expect(session).to be_a Authie::Session
350+
expect(session.session.login_ip_country).to eq 'GB'
351+
end
352+
323353
it 'invalidates all other sessions for the same browser' do
324354
existing_session = Authie::SessionModel.create!(user: user, active: true, browser_id: browser_id)
325355
described_class.start(controller, user: user)

0 commit comments

Comments
 (0)
Please sign in to comment.