Commit 5b99e6e5 authored by Jan Provaznik's avatar Jan Provaznik

Merge branch 'dblessing_known_sign_in_jwt' into 'master'

Add cookie to known sign in check

See merge request gitlab-org/gitlab!34102
parents 725a5395 25bfc290
...@@ -2,19 +2,34 @@ ...@@ -2,19 +2,34 @@
module KnownSignIn module KnownSignIn
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include CookiesHelper
KNOWN_SIGN_IN_COOKIE = :known_sign_in
KNOWN_SIGN_IN_COOKIE_EXPIRY = 14.days
private private
def verify_known_sign_in def verify_known_sign_in
return unless current_user return unless current_user
notify_user unless known_remote_ip? notify_user unless known_device? || known_remote_ip?
update_cookie
end end
def known_remote_ip? def known_remote_ip?
known_ip_addresses.include?(request.remote_ip) known_ip_addresses.include?(request.remote_ip)
end end
def known_device?
cookies.encrypted[KNOWN_SIGN_IN_COOKIE] == current_user.id
end
def update_cookie
set_secure_cookie(KNOWN_SIGN_IN_COOKIE, current_user.id,
type: COOKIE_TYPE_ENCRYPTED, httponly: true, expires: KNOWN_SIGN_IN_COOKIE_EXPIRY)
end
def sessions def sessions
strong_memoize(:session) do strong_memoize(:session) do
ActiveSession.list(current_user).reject(&:is_impersonated) ActiveSession.list(current_user).reject(&:is_impersonated)
......
...@@ -82,7 +82,7 @@ class Projects::ApplicationController < ApplicationController ...@@ -82,7 +82,7 @@ class Projects::ApplicationController < ApplicationController
end end
def apply_diff_view_cookie! def apply_diff_view_cookie!
set_secure_cookie(:diff_view, params.delete(:view), permanent: true) if params[:view].present? set_secure_cookie(:diff_view, params.delete(:view), type: COOKIE_TYPE_PERMANENT) if params[:view].present?
end end
def require_pages_enabled! def require_pages_enabled!
......
# frozen_string_literal: true # frozen_string_literal: true
module CookiesHelper module CookiesHelper
def set_secure_cookie(key, value, httponly: false, permanent: false) COOKIE_TYPE_PERMANENT = :permanent
cookie_jar = permanent ? cookies.permanent : cookies COOKIE_TYPE_ENCRYPTED = :encrypted
cookie_jar[key] = { value: value, secure: Gitlab.config.gitlab.https, httponly: httponly } def set_secure_cookie(key, value, httponly: false, expires: nil, type: nil)
cookie_jar = case type
when COOKIE_TYPE_PERMANENT
cookies.permanent
when COOKIE_TYPE_ENCRYPTED
cookies.encrypted
else
cookies
end
cookie_jar[key] = { value: value, secure: Gitlab.config.gitlab.https, httponly: httponly, expires: expires }
end end
end end
---
title: Use IP or cookie in known sign-in check
merge_request: 34102
author:
type: changed
...@@ -22,7 +22,7 @@ See the [authentication topic](../../topics/authentication/index.md) for more de ...@@ -22,7 +22,7 @@ See the [authentication topic](../../topics/authentication/index.md) for more de
### Unknown sign-in ### Unknown sign-in
GitLab will notify you if a sign-in occurs that is from an unknown IP address. GitLab will notify you if a sign-in occurs that is from an unknown IP address or device.
See [Unknown Sign-In Notification](unknown_sign_in_notification.md) for more details. See [Unknown Sign-In Notification](unknown_sign_in_notification.md) for more details.
## User profile ## User profile
......
...@@ -9,16 +9,19 @@ info: To determine the technical writer assigned to the Stage/Group associated w ...@@ -9,16 +9,19 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27211) in GitLab 13.0. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27211) in GitLab 13.0.
When a user successfully signs in from a previously unknown IP address, When a user successfully signs in from a previously unknown IP address or device,
GitLab notifies the user by email. In this way, GitLab proactively alerts users of potentially GitLab notifies the user by email. In this way, GitLab proactively alerts users of potentially
malicious or unauthorized sign-ins. malicious or unauthorized sign-ins.
There are two methods used to identify a known sign-in: There are several methods used to identify a known sign-in. All methods must fail
for a notification email to be sent.
- Last sign-in IP: The current sign-in IP address is checked against the last sign-in - Last sign-in IP: The current sign-in IP address is checked against the last sign-in
IP address. IP address.
- Current active sessions: If the user has an existing active session from the - Current active sessions: If the user has an existing active session from the
same IP address. See [Active Sessions](active_sessions.md). same IP address. See [Active Sessions](active_sessions.md).
- Cookie: After successful sign in, an encrypted cookie is stored in the browser.
This cookie is set to expire 14 days after the last successful sign in.
## Example email ## Example email
......
...@@ -18,9 +18,13 @@ RSpec.describe Gitlab::Auth::GroupSaml::FailureHandler do ...@@ -18,9 +18,13 @@ RSpec.describe Gitlab::Auth::GroupSaml::FailureHandler do
'omniauth.error.strategy' => strategy, 'omniauth.error.strategy' => strategy,
'devise.mapping' => Devise.mappings[:user], 'devise.mapping' => Devise.mappings[:user],
'warden' => warden, 'warden' => warden,
'action_dispatch.key_generator' => ActiveSupport::KeyGenerator.new('b2efbaccbdb9548217eebc73a896db73'), # necessary for setting signed cookies in lib/gitlab/experimentation.rb # The following are necessary for setting signed/encrypted cookies such as in
'action_dispatch.signed_cookie_salt' => 'a4fb52b0ccb302eaef92bda18fedf5c3', # necessary for setting signed cookies in lib/gitlab/experimentation.rb # lib/gitlab/experimentation.rb or app/controllers/concerns/known_sign_in.rb
'action_dispatch.cookies_rotations' => OpenStruct.new(signed: []) # necessary for setting signed cookies in lib/gitlab/experimentation.rb 'action_dispatch.key_generator' => ActiveSupport::KeyGenerator.new('b2efbaccbdb9548217eebc73a896db73'),
'action_dispatch.signed_cookie_salt' => 'a4fb52b0ccb302eaef92bda18fedf5c3',
'action_dispatch.encrypted_signed_cookie_salt' => 'a4fb52b0ccb302eaef92bda18fedf5c3',
'action_dispatch.encrypted_cookie_salt' => 'a4fb52b0ccb302eaef92bda18fedf5c3',
'action_dispatch.cookies_rotations' => OpenStruct.new(signed: [], encrypted: [])
} }
Rack::MockRequest.env_for(path, params) Rack::MockRequest.env_for(path, params)
end end
......
...@@ -75,7 +75,7 @@ RSpec.describe SortingPreference do ...@@ -75,7 +75,7 @@ RSpec.describe SortingPreference do
it 'sets the cookie with the right values and flags' do it 'sets the cookie with the right values and flags' do
subject subject
expect(cookies['issue_sort']).to eq(value: 'popularity', secure: false, httponly: false) expect(cookies['issue_sort']).to eq(expires: nil, value: 'popularity', secure: false, httponly: false)
end end
end end
...@@ -86,7 +86,7 @@ RSpec.describe SortingPreference do ...@@ -86,7 +86,7 @@ RSpec.describe SortingPreference do
it 'sets the cookie with the right values and flags' do it 'sets the cookie with the right values and flags' do
subject subject
expect(cookies['issue_sort']).to eq(value: 'created_asc', secure: false, httponly: false) expect(cookies['issue_sort']).to eq(expires: nil, value: 'created_asc', secure: false, httponly: false)
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe CookiesHelper do
describe '#set_secure_cookie' do
it 'creates an encrypted cookie with expected attributes' do
stub_config_setting(https: true)
expiration = 1.month.from_now
key = :secure_cookie
value = 'secure value'
expect_next_instance_of(ActionDispatch::Cookies::EncryptedKeyRotatingCookieJar) do |instance|
expect(instance).to receive(:[]=).with(key, httponly: true, secure: true, expires: expiration, value: value)
end
helper.set_secure_cookie(key, value, httponly: true, expires: expiration, type: CookiesHelper::COOKIE_TYPE_ENCRYPTED)
end
it 'creates a permanent cookie with expected attributes' do
key = :permanent_cookie
value = 'permanent value'
expect_next_instance_of(ActionDispatch::Cookies::PermanentCookieJar) do |instance|
expect(instance).to receive(:[]=).with(key, httponly: false, secure: false, expires: nil, value: value)
end
helper.set_secure_cookie(key, value, type: CookiesHelper::COOKIE_TYPE_PERMANENT)
end
it 'creates a regular cookie with expected attributes' do
key = :regular_cookie
value = 'regular value'
expect_next_instance_of(ActionDispatch::Cookies::CookieJar) do |instance|
expect(instance).to receive(:[]=).with(key, httponly: false, secure: false, expires: nil, value: value)
end
helper.set_secure_cookie(key, value)
end
end
end
...@@ -9,13 +9,38 @@ RSpec.shared_examples 'known sign in' do ...@@ -9,13 +9,38 @@ RSpec.shared_examples 'known sign in' do
user.update!(current_sign_in_ip: ip) user.update!(current_sign_in_ip: ip)
end end
context 'with a valid post' do def stub_cookie(value = user.id)
context 'when remote IP does not match user last sign in IP' do cookies.encrypted[KnownSignIn::KNOWN_SIGN_IN_COOKIE] = {
before do value: value, expires: KnownSignIn::KNOWN_SIGN_IN_COOKIE_EXPIRY
stub_user_ip('127.0.0.1') }
stub_remote_ip('169.0.0.1') end
end
context 'when the remote IP and the last sign in IP match' do
before do
stub_user_ip('169.0.0.1')
stub_remote_ip('169.0.0.1')
end
it 'does not notify the user' do
expect_any_instance_of(NotificationService).not_to receive(:unknown_sign_in)
post_action
end
it 'sets/updates the encrypted cookie' do
post_action
expect(cookies.encrypted[KnownSignIn::KNOWN_SIGN_IN_COOKIE]).to eq(user.id)
end
end
context 'when the remote IP and the last sign in IP do not match' do
before do
stub_user_ip('127.0.0.1')
stub_remote_ip('169.0.0.1')
end
context 'when the cookie is not previously set' do
it 'notifies the user' do it 'notifies the user' do
expect_next_instance_of(NotificationService) do |instance| expect_next_instance_of(NotificationService) do |instance|
expect(instance).to receive(:unknown_sign_in) expect(instance).to receive(:unknown_sign_in)
...@@ -23,37 +48,50 @@ RSpec.shared_examples 'known sign in' do ...@@ -23,37 +48,50 @@ RSpec.shared_examples 'known sign in' do
post_action post_action
end end
end
context 'when remote IP matches an active session' do it 'sets the encrypted cookie' do
before do post_action
existing_sessions = ActiveSession.session_ids_for_user(user.id)
existing_sessions.each { |sessions| ActiveSession.destroy(user, sessions) }
stub_user_ip('169.0.0.1')
stub_remote_ip('127.0.0.1')
ActiveSession.set(user, request) expect(cookies.encrypted[KnownSignIn::KNOWN_SIGN_IN_COOKIE]).to eq(user.id)
end end
end
it 'notifies the user when the cookie is expired' do
stub_cookie
it 'does not notify the user' do Timecop.freeze((KnownSignIn::KNOWN_SIGN_IN_COOKIE_EXPIRY + 1.day).from_now) do
expect_any_instance_of(NotificationService).not_to receive(:unknown_sign_in) expect_next_instance_of(NotificationService) do |instance|
expect(instance).to receive(:unknown_sign_in)
end
post_action post_action
end end
end end
context 'when remote IP address matches last sign in IP' do it 'notifies the user when the cookie is for another user' do
before do stub_cookie(create(:user).id)
stub_user_ip('127.0.0.1')
stub_remote_ip('127.0.0.1') expect_next_instance_of(NotificationService) do |instance|
expect(instance).to receive(:unknown_sign_in)
end end
it 'does not notify the user' do post_action
expect_any_instance_of(NotificationService).not_to receive(:unknown_sign_in) end
post_action it 'does not notify the user when remote IP matches an active session' do
end ActiveSession.set(user, request)
expect_any_instance_of(NotificationService).not_to receive(:unknown_sign_in)
post_action
end
it 'does not notify the user when the cookie is present and not expired' do
stub_cookie
expect_any_instance_of(NotificationService).not_to receive(:unknown_sign_in)
post_action
end end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment