mirror of
https://github.com/zammad/zammad
synced 2026-05-24 09:48:36 +00:00
460 lines
15 KiB
Ruby
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
|