zammad/spec/requests/session_spec.rb

460 lines
15 KiB
Ruby

# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
require 'rails_helper'
RSpec.describe 'Sessions endpoints', type: :request do
describe 'GET /' do
let(:headers) { {} }
let(:session_key) { Zammad::Application::Initializer::SessionStore::SESSION_KEY }
before do
Setting.set('http_type', http_type)
get '/', headers: headers
end
context "when Setting 'http_type' is set to 'https'" do
let(:http_type) { 'https' }
context "when it's not an HTTPS request" do
it 'sets no Cookie' do
expect(response.header['Set-Cookie']).to be_nil
end
end
context "when it's an HTTPS request" do
let(:headers) do
{
'X-Forwarded-Proto' => 'https'
}
end
it "sets Cookie with 'secure' flag" do
expect(response.header['Set-Cookie']).to include(session_key).and include('; secure;')
end
end
end
context "when Setting 'http_type' is set to 'http'" do
let(:http_type) { 'http' }
context "when it's not an HTTPS request" do
it 'sets Cookie' do
expect(response.header['Set-Cookie']).to include(session_key).and not_include('; secure;')
end
end
context "when it's an HTTPS request" do
let(:headers) do
{
'X-Forwarded-Proto' => 'https'
}
end
it "sets Cookie without 'secure' flag" do
expect(response.header['Set-Cookie']).to include(session_key).and not_include('; secure;')
end
end
end
end
describe 'GET /signshow' do
context 'user logged in' do
subject(:user) { create(:agent, password: password) }
let(:password) { SecureRandom.urlsafe_base64(20) }
let(:fingerprint) { SecureRandom.urlsafe_base64(40) }
before do
setting if defined?(setting)
params = {
fingerprint: fingerprint,
username: user.login,
password: password
}
post '/api/v1/signin', params: params, as: :json
end
it 'leaks no sensitive data' do
params = { fingerprint: fingerprint }
get '/api/v1/signshow', params: params, as: :json
expect(json_response['session']).not_to include('password')
end
context 'when after auth modules are triggered' do
subject(:user) { create(:customer, roles: [role], password: password) }
let(:role) { create(:role, name: '2FA') }
context 'with no enforcing roles' do
it 'returns nil' do
expect(json_response['after_auth']).to be_nil
end
end
context 'with enforcing roles' do
let(:setting) do
Setting.set('two_factor_authentication_enforce_role_ids', [role.id])
Setting.set('two_factor_authentication_method_authenticator_app', true)
end
it 'returns the after auth information' do
expect(json_response['after_auth']).to include({ 'data' => { 'token' => be_a(String) }, 'type' => 'TwoFactorConfiguration' })
end
end
end
end
context 'user not logged in' do
subject(:user) { nil }
it 'contains only user related object manager attributes' do
get '/api/v1/signshow', params: {}, as: :json
expect(json_response['models'].keys).to match_array(%w[User])
end
it 'does not contain fields with permission admin.*' do
get '/api/v1/signshow', params: {}, as: :json
expect(json_response['models']['User']).not_to include(hash_including('name' => 'role_ids'))
end
end
end
describe 'GET /auth/sso (single sign-on)' do
before do
Setting.set('auth_sso', true)
end
context 'when SSO is disabled' do
before do
Setting.set('auth_sso', false)
end
let(:headers) { { 'X-Forwarded-User' => login } }
let(:login) { User.last.login }
it 'returns a new user-session response' do
get '/auth/sso', as: :json, headers: headers
expect(response).to have_http_status(:forbidden)
end
end
context 'with invalid user login' do
let(:login) { User.pluck(:login).max.next }
context 'in "REMOTE_USER" request env var' do
let(:env) { { 'REMOTE_USER' => login } }
it 'returns unauthorized response' do
get '/auth/sso', as: :json, env: env
expect(response).to have_http_status(:unauthorized)
end
end
context 'in "HTTP_REMOTE_USER" request env var' do
let(:env) { { 'HTTP_REMOTE_USER' => login } }
it 'returns unauthorized response' do
get '/auth/sso', as: :json, env: env
expect(response).to have_http_status(:unauthorized)
end
end
context 'in "X-Forwarded-User" request header' do
let(:headers) { { 'X-Forwarded-User' => login } }
it 'returns unauthorized response' do
get '/auth/sso', as: :json, headers: headers
expect(response).to have_http_status(:unauthorized)
end
end
end
context 'with valid user login' do
let(:user) { create(:agent) }
let(:login) { user.login }
context 'in Maintenance Mode' do
before { Setting.set('maintenance_mode', true) }
context 'in "REMOTE_USER" request env var' do
let(:env) { { 'REMOTE_USER' => login } }
it 'returns 403 Forbidden' do
get '/auth/sso', as: :json, env: env
expect(response).to have_http_status(:forbidden)
expect(json_response).to include('error' => 'Maintenance mode enabled!')
end
end
context 'in "HTTP_REMOTE_USER" request env var' do
let(:env) { { 'HTTP_REMOTE_USER' => login } }
it 'returns 403 Forbidden' do
get '/auth/sso', as: :json, env: env
expect(response).to have_http_status(:forbidden)
expect(json_response).to include('error' => 'Maintenance mode enabled!')
end
end
context 'in "X-Forwarded-User" request header' do
let(:headers) { { 'X-Forwarded-User' => login } }
it 'returns 403 Forbidden' do
get '/auth/sso', as: :json, headers: headers
expect(response).to have_http_status(:forbidden)
expect(json_response).to include('error' => 'Maintenance mode enabled!')
end
end
end
context 'in "REMOTE_USER" request env var' do
let(:env) { { 'REMOTE_USER' => login } }
it 'returns a new user-session response' do
get '/auth/sso', as: :json, env: env
expect(response).to redirect_to('/#')
end
it 'sets the :user_id session parameter' do
expect { get '/auth/sso', as: :json, env: env }
.to change { request&.session&.fetch(:user_id) }.to(user.id)
end
end
context 'in "HTTP_REMOTE_USER" request env var' do
let(:env) { { 'HTTP_REMOTE_USER' => login } }
it 'returns a new user-session response' do
get '/auth/sso', as: :json, env: env
expect(response).to redirect_to('/#')
end
it 'sets the :user_id session parameter' do
expect { get '/auth/sso', as: :json, env: env }
.to change { request&.session&.fetch(:user_id) }.to(user.id)
end
end
context 'in "X-Forwarded-User" request header' do
let(:headers) { { 'X-Forwarded-User' => login } }
it 'returns a new user-session response' do
get '/auth/sso', as: :json, headers: headers
expect(response).to redirect_to('/#')
end
it 'sets the :user_id session parameter on the client' do
expect { get '/auth/sso', as: :json, headers: headers }
.to change { request&.session&.fetch(:user_id) }.to(user.id)
end
end
end
context 'with trusted proxy IPs configured' do
before do
Setting.set('auth_sso_trusted_ips', '192.168.1.1, 10.0.0.0/8')
end
let(:user) { create(:agent) }
let(:headers) { { 'X-Forwarded-User' => user.login } }
context 'when request comes from a trusted IP address' do
it 'allows the SSO login' do
get '/auth/sso', as: :json, headers: headers, env: { 'REMOTE_ADDR' => '192.168.1.1' }
expect(response).to redirect_to('/#')
end
end
context 'when request comes from an IP within a trusted CIDR range' do
it 'allows the SSO login' do
get '/auth/sso', as: :json, headers: headers, env: { 'REMOTE_ADDR' => '10.1.2.3' }
expect(response).to redirect_to('/#')
end
end
context 'when request comes from an untrusted IP address' do
it 'returns 403 Forbidden' do
get '/auth/sso', as: :json, headers: headers, env: { 'REMOTE_ADDR' => '1.2.3.4' }
expect(response).to have_http_status(:forbidden)
expect(json_response).to include('error' => 'SSO request from untrusted IP address.')
end
end
end
end
describe 'POST /api/v1/signin - Doorkeeper OAuth resume via AfterAuth' do
let(:user) { create(:agent, password: password) }
let(:password) { SecureRandom.urlsafe_base64(20) }
let(:fingerprint) { SecureRandom.urlsafe_base64(40) }
let!(:oauth_app) { Doorkeeper::Application.create!(name: 'Test', redirect_uri: 'https://localhost', scopes: '') }
let(:oauth_path) { "/oauth/authorize?client_id=#{oauth_app.uid}&redirect_uri=https%3A%2F%2Flocalhost&response_type=code" }
context 'when session has a pending doorkeeper OAuth URL' do
before do
# Hit OAuth authorize endpoint to set doorkeeper_return_to in the session.
get oauth_path
# Now sign in - the session still has doorkeeper_return_to set.
post '/api/v1/signin', params: { fingerprint: fingerprint, username: user.login, password: password }, as: :json
end
it 'returns DoorkeeperReturnTo after_auth with the OAuth URL' do
expect(json_response['after_auth']).to eq({
'type' => 'DoorkeeperReturnTo',
'data' => { 'url' => oauth_path },
})
end
end
context 'when session has a pending doorkeeper OAuth URL and 2FA setup is required' do
before do
Setting.set('two_factor_authentication_enforce_role_ids', [Role.find_by(name: 'Agent').id])
Setting.set('two_factor_authentication_method_authenticator_app', true)
# Hit OAuth authorize endpoint to set doorkeeper_return_to in the session.
get oauth_path
# Now sign in - the session still has doorkeeper_return_to set.
post '/api/v1/signin', params: { fingerprint: fingerprint, username: user.login, password: password }, as: :json
end
it 'returns TwoFactorConfiguration after_auth instead of DoorkeeperReturnTo' do
expect(json_response['after_auth']).to include('type' => 'TwoFactorConfiguration')
end
it 'preserves doorkeeper_return_to in the session for later' do
# Simulate completing 2FA setup: disabling enforcement makes two_factor_setup_required? false.
Setting.set('two_factor_authentication_enforce_role_ids', [])
# After 2FA setup, the next session show should trigger DoorkeeperReturnTo
get '/api/v1/signshow', as: :json
expect(json_response['after_auth']).to eq({
'type' => 'DoorkeeperReturnTo',
'data' => { 'url' => oauth_path },
})
end
end
context 'when session has no pending doorkeeper OAuth URL' do
before do
post '/api/v1/signin', params: { fingerprint: fingerprint, username: user.login, password: password }, as: :json
end
it 'does not return DoorkeeperReturnTo after_auth' do
expect(json_response['after_auth']).to be_nil
end
end
end
describe 'GET /auth/sso - Doorkeeper OAuth resume' do
let(:user) { create(:agent) }
let(:login) { user.login }
let(:env) { { 'REMOTE_USER' => login } }
let!(:oauth_app) { Doorkeeper::Application.create!(name: 'Test', redirect_uri: 'https://localhost', scopes: '') }
let(:oauth_path) { "/oauth/authorize?client_id=#{oauth_app.uid}&redirect_uri=https%3A%2F%2Flocalhost&response_type=code" }
before do
Setting.set('auth_sso', true)
end
context 'when session has a pending doorkeeper OAuth URL' do
it 'redirects to the OAuth authorize URL' do
# Hit OAuth authorize endpoint to set doorkeeper_return_to in the session.
get oauth_path
# Now SSO login - the session still has doorkeeper_return_to set.
get '/auth/sso', as: :json, env: env
expect(response).to redirect_to(oauth_path)
end
end
context 'when session has no pending doorkeeper OAuth URL' do
it 'redirects to the default app route' do
get '/auth/sso', as: :json, env: env
expect(response).to redirect_to('/#')
end
end
end
describe 'POST /auth/two_factor_itwo_factor_method_enablednitiate_authentication/:method' do
let(:user) { create(:user, password: 'dummy') }
let(:params) { {} }
let(:method) { 'security_keys' }
let(:user_two_factor_preference) { nil }
let(:two_factor_method_enabled) { true }
before do
Setting.set('two_factor_authentication_method_security_keys', two_factor_method_enabled)
if defined?(user_two_factor_preference)
user_two_factor_preference
user.reload
end
post "/api/v1/auth/two_factor_initiate_authentication/#{method}", params: params, as: :json
end
context 'with missing params' do
it 'returns an error' do
expect(response).to have_http_status(:unprocessable_content)
end
end
context 'with valid params' do
let(:user_two_factor_preference) { create(:user_two_factor_preference, :security_keys, user: user) }
let(:params) { { username: user.login, password: password, method: method } }
context 'with invalid user/password' do
let(:password) { 'invalid' }
it 'returns an error' do
expect(response).to have_http_status(:unprocessable_content)
end
end
context 'with valid user/password' do
let(:password) { 'dummy' }
it 'returns options for initiation phase', :aggregate_failures do
expect(response).to have_http_status(:ok)
expect(json_response).to include('challenge')
end
context 'with disabled authenticator method' do
let(:two_factor_method_enabled) { false }
it 'returns an error' do
expect(response).to have_http_status(:unprocessable_content)
end
end
end
end
end
end