fix: skip session tracking and use short-lived token for impersonation (CW-7169) (#14622)
## Description SuperAdmin impersonation SSO logins no longer create UserSession rows visible to the customer. Impersonation tokens use a 2-day lifespan instead of ~2 months, so they naturally evict first and don't linger in the user's token list. Server-side detection via Redis value (`'impersonation'` vs `'normal'`) without changing the `valid_sso_auth_token?` signature. Backward compatible with in-flight tokens. Depends on #14556. ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? Specs cover: impersonation login skips UserSession creation, impersonation token has short lifespan, normal SSO login still creates UserSession row. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules
This commit is contained in:
@@ -20,7 +20,7 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
|
||||
end
|
||||
|
||||
def render_create_success
|
||||
track_user_session
|
||||
track_user_session unless @impersonation
|
||||
render partial: 'devise/auth', formats: [:json], locals: { resource: @resource }
|
||||
end
|
||||
|
||||
@@ -66,7 +66,10 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
|
||||
end
|
||||
|
||||
def authenticate_resource_with_sso_token
|
||||
@token = @resource.create_token
|
||||
# DTA evicts the earliest-expiring token after save when at max_number_of_devices.
|
||||
# The short-lived impersonation token would always be that one, so pre-evict to make room.
|
||||
make_room_for_impersonation_token if @impersonation
|
||||
@token = @resource.create_token(lifespan: @impersonation ? 2.days.to_i : nil)
|
||||
@resource.save!
|
||||
|
||||
sign_in(:user, @resource, store: false, bypass: false)
|
||||
@@ -74,11 +77,21 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
|
||||
@resource.invalidate_sso_auth_token(params[:sso_auth_token])
|
||||
end
|
||||
|
||||
def make_room_for_impersonation_token
|
||||
return if @resource.tokens.size < DeviseTokenAuth.max_number_of_devices
|
||||
|
||||
oldest_client_id = @resource.tokens.min_by { |_, v| v['expiry'].to_i }&.first
|
||||
@resource.tokens.delete(oldest_client_id) if oldest_client_id
|
||||
end
|
||||
|
||||
def process_sso_auth_token
|
||||
return if params[:email].blank?
|
||||
|
||||
user = User.from_email(params[:email])
|
||||
@resource = user if user&.valid_sso_auth_token?(params[:sso_auth_token])
|
||||
return unless user&.valid_sso_auth_token?(params[:sso_auth_token])
|
||||
|
||||
@resource = user
|
||||
@impersonation = user.sso_auth_token_impersonation?(params[:sso_auth_token])
|
||||
end
|
||||
|
||||
def handle_mfa_required(user)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
module SsoAuthenticatable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def generate_sso_auth_token
|
||||
def generate_sso_auth_token(impersonation: false)
|
||||
token = SecureRandom.hex(32)
|
||||
::Redis::Alfred.setex(sso_token_key(token), true, 5.minutes)
|
||||
::Redis::Alfred.setex(sso_token_key(token), impersonation ? 'impersonation' : 'normal', 5.minutes)
|
||||
token
|
||||
end
|
||||
|
||||
@@ -20,8 +20,14 @@ module SsoAuthenticatable
|
||||
"#{ENV.fetch('FRONTEND_URL', nil)}/app/login?email=#{encoded_email}&sso_auth_token=#{generate_sso_auth_token}"
|
||||
end
|
||||
|
||||
def sso_auth_token_impersonation?(token)
|
||||
::Redis::Alfred.get(sso_token_key(token)) == 'impersonation'
|
||||
end
|
||||
|
||||
def generate_sso_link_with_impersonation
|
||||
"#{generate_sso_link}&impersonation=true"
|
||||
encoded_email = ERB::Util.url_encode(email)
|
||||
"#{ENV.fetch('FRONTEND_URL',
|
||||
nil)}/app/login?email=#{encoded_email}&sso_auth_token=#{generate_sso_auth_token(impersonation: true)}&impersonation=true"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -180,4 +180,56 @@ RSpec.describe DeviseOverrides::SessionsController, type: :controller do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'impersonation SSO login' do
|
||||
let(:user) { create(:user, password: 'Test@123456') }
|
||||
let(:browser_ua) { 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15' }
|
||||
|
||||
before { request.env['HTTP_USER_AGENT'] = browser_ua }
|
||||
|
||||
it 'does not create a UserSession row for impersonation login' do
|
||||
sso_token = user.generate_sso_auth_token(impersonation: true)
|
||||
|
||||
expect do
|
||||
post :create, params: { email: user.email, sso_auth_token: sso_token }
|
||||
end.not_to change(user.user_sessions, :count)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
|
||||
it 'creates a short-lived token for impersonation login' do
|
||||
sso_token = user.generate_sso_auth_token(impersonation: true)
|
||||
|
||||
post :create, params: { email: user.email, sso_auth_token: sso_token }
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
token_entry = user.reload.tokens.values.last
|
||||
# 2-day lifespan: expiry should be within ~3 days from now (token creation + lifespan)
|
||||
expect(token_entry['expiry']).to be < (3.days.from_now).to_i
|
||||
end
|
||||
|
||||
it 'creates a normal UserSession row for regular SSO login' do
|
||||
sso_token = user.generate_sso_auth_token
|
||||
|
||||
expect do
|
||||
post :create, params: { email: user.email, sso_auth_token: sso_token }
|
||||
end.to change(user.user_sessions, :count).by(1)
|
||||
end
|
||||
|
||||
it 'preserves the impersonation token when target user is at the device cap' do
|
||||
allow(DeviseTokenAuth).to receive(:max_number_of_devices).and_return(5)
|
||||
5.times do |i|
|
||||
user.tokens["existing#{i}"] = { 'token' => 'x', 'expiry' => (Time.current + (30 + i).days).to_i }
|
||||
end
|
||||
user.save!
|
||||
sso_token = user.generate_sso_auth_token(impersonation: true)
|
||||
|
||||
post :create, params: { email: user.email, sso_auth_token: sso_token }
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
new_client_id = response.headers['client']
|
||||
expect(user.reload.tokens.keys).to include(new_client_id)
|
||||
expect(user.tokens.size).to eq(5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user