Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OpenID-Connect authentication support #2855

Merged
merged 9 commits into from
Jul 13, 2018
Merged
5 changes: 4 additions & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ class ApplicationController < ActionController::Base
# This secret is reset to a value found in the miq_databases table in
# MiqWebServerWorkerMixin.configure_secret_token for rails server, UI, and
# web service worker processes.
protect_from_forgery :secret => SecureRandom.hex(64), :except => [:authenticate, :external_authenticate, :kerberos_authenticate, :saml_login, :initiate_saml_login, :csp_report], :with => :exception
protect_from_forgery(:secret => SecureRandom.hex(64),
:except => %i(authenticate external_authenticate kerberos_authenticate saml_login initiate_saml_login oidc_login initiate_oidc_login csp_report),
:with => :exception)

end

helper GtlHelper
Expand Down
84 changes: 58 additions & 26 deletions app/controllers/dashboard_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ class DashboardController < ApplicationController
@@items_per_page = 8

before_action :check_privileges, :except => %i(csp_report authenticate
external_authenticate kerberos_authenticate
logout login login_retry wait_for_task
saml_login initiate_saml_login)
external_authenticate kerberos_authenticate
logout login login_retry wait_for_task
saml_login initiate_saml_login
oidc_login initiate_oidc_login)
before_action :get_session_data, :except => %i(csp_report authenticate
external_authenticate kerberos_authenticate saml_login)
external_authenticate kerberos_authenticate saml_login oidc_login)
after_action :cleanup_action, :except => %i(csp_report)

def index
Expand Down Expand Up @@ -54,6 +55,16 @@ def saml_protected_page
end
helper_method :saml_protected_page

def oidc_protected_page
request.base_url + '/oidc_login'
end
helper_method(:oidc_protected_page)

def oidc_protected_page_logout
request.base_url + '/oidc_login/redirect_uri?logout=' + CGI.escape(request.base_url)
end
helper_method(:oidc_protected_page_logout)

def iframe
override_content_security_policy_directives(:frame_src => ['*'])
override_x_frame_options('')
Expand Down Expand Up @@ -415,6 +426,11 @@ def login
return
end

if ext_auth?(:oidc_enabled) && ext_auth?(:local_login_disabled)
redirect_to(oidc_protected_page)
return
end

if ::Settings.product.allow_passed_in_credentials # Only pre-populate credentials if setting is turned on
@user_name = params[:user_name]
@user_password = params[:user_password]
Expand Down Expand Up @@ -454,30 +470,19 @@ def initiate_saml_login
javascript_redirect(saml_protected_page)
end

# Login support for SAML - GET /saml_login
def saml_login
if @user_name.blank? && request.env.key?("HTTP_X_REMOTE_USER").present?
@user_name = params[:user_name] = request.env["HTTP_X_REMOTE_USER"].split("@").first
else
redirect_to(:action => 'logout')
return
end
# Initiate an OpenIDC Login from the main login page
def initiate_oidc_login
javascript_redirect(oidc_protected_page)
end

user = {:name => @user_name}
validation = validate_user(user, nil, request, :require_user => true, :timeout => 30)
# Login support for OpenIDC - GET /oidc_login
def oidc_login
identity_provider_login("oidc_login")
end

case validation.result
when :pass
render :template => "dashboard/saml_login",
:layout => false,
:locals => {:api_auth_token => generate_ui_api_token(@user_name),
:validation_url => validation.url}
return
when :fail
session[:user_validation_error] = validation.flash_msg || "User validation failed"
redirect_to(:action => 'logout')
return
end
# Login support for SAML - GET /saml_login
def saml_login
identity_provider_login("saml_login")
end

# Handle external-auth signon from login screen
Expand Down Expand Up @@ -651,6 +656,8 @@ def logout
# For SAML, let's do the SAML logout to clear mod_auth_mellon IdP cookies and such
if ext_auth?(:saml_enabled)
redirect_to("/saml2/logout?ReturnTo=/")
elsif ext_auth?(:oidc_enabled)
redirect_to(oidc_protected_page_logout)
else
redirect_to(:action => 'login')
end
Expand Down Expand Up @@ -821,4 +828,29 @@ def tl_gen_timeline_data
def get_session_data
@layout = "login"
end

def identity_provider_login(identity_type)
if @user_name.blank? && request.env.key?("HTTP_X_REMOTE_USER").present?
@user_name = params[:user_name] = request.env["HTTP_X_REMOTE_USER"].split("@").first
else
redirect_to(:action => 'logout')
return
end

user = {:name => @user_name}
validation = validate_user(user, nil, request, :require_user => true, :timeout => 30)

case validation.result
when :pass
render :template => "dashboard/#{identity_type}",
:layout => false,
:locals => {:api_auth_token => generate_ui_api_token(@user_name),
:validation_url => validation.url}
return
when :fail
session[:user_validation_error] = validation.flash_msg || "User validation failed"
redirect_to(:action => 'logout')
return
end
end
end
18 changes: 11 additions & 7 deletions app/controllers/ops_controller/settings/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ def settings_form_field_changed
page << set_element_visible("httpd_div", verb)
page << set_element_visible("httpd_role_div", verb)
end
if @saml_enabled_changed
verb = @edit[:new][:authentication][:saml_enabled]
page << set_element_visible("saml_local_login_div", verb)
if @provider_type_changed
verb = @edit[:new][:authentication][:provider_type] == 'none'
page << set_element_visible("none_local_login_div", !verb)
end
if @authusertype_changed
verb = @edit[:new][:authentication][:user_type] == 'samaccountname'
Expand Down Expand Up @@ -759,8 +759,11 @@ def settings_get_form_vars
@sb[:newrole] = (params[:ldap_role].to_s == "1") if params[:ldap_role]
@sb[:new_amazon_role] = (params[:amazon_role].to_s == "1") if params[:amazon_role]
@sb[:new_httpd_role] = (params[:httpd_role].to_s == "1") if params[:httpd_role]
if params[:saml_enabled] && params[:saml_enabled] != auth[:saml_enabled]
@saml_enabled_changed = true
if params[:provider_type] && params[:provider_type] != auth[:provider_type]
auth[:provider_type] = params[:provider_type]
auth[:saml_enabled] = params[:provider_type] == "saml"
auth[:oidc_enabled] = params[:provider_type] == "oidc"
@provider_type_changed = true
end
if params[:authentication_user_type] && params[:authentication_user_type] != auth[:user_type]
@authusertype_changed = true
Expand Down Expand Up @@ -820,9 +823,10 @@ def settings_get_form_vars
@authmode_changed = true
end
auth[:sso_enabled] = (params[:sso_enabled].to_s == "1") if params[:sso_enabled]
auth[:saml_enabled] = (params[:saml_enabled].to_s == "1") if params[:saml_enabled]
auth[:provider_type] = params[:provider_type] if params[:provider_type]
auth[:local_login_disabled] = (params[:local_login_disabled].to_s == "1") if params[:local_login_disabled]
auth[:default_group_for_users] = params[:authentication_default_group_for_users] if params[:authentication_default_group_for_users]

when "settings_workers" # Workers Settings tab
wb = new.config[:workers][:worker_base]
qwb = wb[:queue_worker_base]
Expand Down Expand Up @@ -975,7 +979,7 @@ def settings_set_form_vars_authentication
@edit[:current].config[:authentication][:user_proxies] ||= [{}]
@edit[:current].config[:authentication][:follow_referrals] ||= false
@edit[:current].config[:authentication][:sso_enabled] ||= false
@edit[:current].config[:authentication][:saml_enabled] ||= false
@edit[:current].config[:authentication][:provider_type] ||= "none"
@edit[:current].config[:authentication][:local_login_disabled] ||= false
@sb[:newrole] = @edit[:current].config[:authentication][:ldap_role]
@sb[:new_amazon_role] = @edit[:current].config[:authentication][:amazon_role]
Expand Down
13 changes: 9 additions & 4 deletions app/views/dashboard/login.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@
%strong= _("Invalid Single Sign-On credentials")
= render :partial => "layouts/flash_msg"

- if ext_auth?(:saml_enabled)
- if ext_auth?(:saml_enabled) || ext_auth?(:oidc_enabled)
.form-group
%label.col-md-3.control-label
.col-md-9
= link_to(_("Log In to Corporate System"), {:action => "initiate_saml_login",
:button => "saml_login"},
:id => "saml_login",
= link_to(_("Log In to Corporate System"), {:action => (ext_auth?(:saml_enabled) ? "initiate_saml_login" : "initiate_oidc_login"),
:button => (ext_auth?(:saml_enabled) ? "saml_login" : "oidc_login")},
:id => (ext_auth?(:saml_enabled) ? "saml_login" : "oidc_login"),
:class => "btn btn-primary form-control",
:alt => _("Log In"),
:title => _("Log In to Corporate System"),
Expand Down Expand Up @@ -140,6 +140,11 @@
$(function () {
$('#saml_login').click();
});
- elsif ext_auth?(:oidc_enabled)
:javascript
$(function () {
$('#oidc_login').click();
});
- else
:javascript
$(function () {
Expand Down
16 changes: 16 additions & 0 deletions app/views/dashboard/oidc_login.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
%html.login-pf{:class => ::Settings.server.custom_login_logo ? '' : 'rcue-login', :lang => I18n.locale.to_s.sub('-', '_')}
%head
= favicon_link_tag
= stylesheet_link_tag 'application'
= javascript_include_tag 'application'

%body
- if api_auth_token && validation_url
:javascript
miqFlashClearSaved();
localStorage.miq_token = '#{j_str api_auth_token}';
window.location = '#{j_str validation_url}';
- else
:javascript
miqFlashClearSaved();
window.location = '/dashboard/logout';
40 changes: 33 additions & 7 deletions app/views/ops/_settings_authentication_tab.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -239,14 +239,40 @@
= check_box_tag("sso_enabled", "1",
@edit[:new][:authentication][:sso_enabled],
"data-miq_observe_checkbox" => {:url => url}.to_json)
%h3
.form-horizontal
.form-group
%label.col-md-2.control-label
= _("Enable SAML")
.col-md-4
= check_box_tag("saml_enabled", "1",
@edit[:new][:authentication][:saml_enabled],
"data-miq_observe_checkbox" => {:url => url}.to_json)
= hidden_div_if(@edit[:new][:authentication][:saml_enabled] == false, :id => "saml_local_login_div") do
%label.control-label.col-md-2
= _("Provider Type:")
.col-md-8
%label
%input{:type => 'radio',
:name => 'provider_type',
:id => 'provider_type',
:value => 'none',
"data-miq_observe" => {:url => url}.to_json,
:checked => @edit[:new][:authentication][:provider_type] == "none"}
= _("None")
%br
%label
%input{:type => 'radio',
:name => 'provider_type',
:id => 'provider_type',
:value => 'saml',
"data-miq_observe" => {:url => url}.to_json,
:checked => @edit[:new][:authentication][:provider_type] == "saml"}
= _("Enable SAML")
%br
%label
%input{:type => 'radio',
:name => 'provider_type',
:id => 'provider_type',
:value => 'oidc',
"data-miq_observe" => {:url => url}.to_json,
:checked => @edit[:new][:authentication][:provider_type] == "oidc"}
= _("Enable OpenID-Connect")

= hidden_div_if(@edit[:new][:authentication][:provider_type] == "none", :id => "none_local_login_div") do
.form-group
%label.col-md-2.control-label
= _("Disable Local Login")
Expand Down
3 changes: 3 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1091,6 +1091,7 @@
login
logout
saml_login
oidc_login
maintab
render_csv
render_pdf
Expand All @@ -1111,6 +1112,7 @@
external_authenticate
kerberos_authenticate
initiate_saml_login
initiate_oidc_login
authenticate
change_group
csp_report
Expand Down Expand Up @@ -3196,6 +3198,7 @@
get '/pictures/:basename' => 'picture#show', :basename => /[\da-zA-Z]+\.[\da-zA-Z]+/

get '/saml_login(/*path)' => 'dashboard#saml_login'
get '/oidc_login(/*path)' => 'dashboard#oidc_login'

# ping response for load balancing
get '/ping' => 'ping#index'
Expand Down
58 changes: 32 additions & 26 deletions spec/controllers/dashboard_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,44 +92,50 @@
end
end

context "SAML support" do
context "SAML and OIDC support" do
before do
EvmSpecHelper.create_guid_miq_server_zone
end

it "SAML Login should redirect to the protected page" do
page = double("page")
allow(page).to receive(:<<).with(any_args)
expect(page).to receive(:redirect_to).with(controller.saml_protected_page)
expect(controller).to receive(:render).with(:update).and_yield(page)
controller.send(:initiate_saml_login)
%i(saml oidc).each do |protocol|
it "#{protocol.upcase} login should redirect to the protected page" do
page = double("page")
allow(page).to receive(:<<).with(any_args)
expect(page).to receive(:redirect_to).with(controller.send("#{protocol}_protected_page"))
expect(controller).to receive(:render).with(:update).and_yield(page)
controller.send("initiate_#{protocol}_login")
end
end

it "SAML protected page should redirect to logout without a valid user" do
get :saml_login
expect(response).to redirect_to(:action => "logout")
%i(saml oidc).each do |protocol|
it "#{protocol.upcase} protected page should redirect to #{protocol}_logout without a valid user" do
get "#{protocol}_login".to_sym
expect(response).to redirect_to(:action => "logout")
end
end

it "SAML protected page should render the saml_login page with the proper validation_url and api token" do
user = FactoryGirl.create(:user, :userid => "johndoe", :role => "test")
auth_token = "aabbccddeeff"
validation_url = "/user_validation_url"
%i(saml oidc).each do |protocol|
it "#{protocol.upcase} protected page should render the #{protocol}_login page with the proper validation_url and api token" do
user = FactoryGirl.create(:user, :userid => "johndoe", :role => "test")
auth_token = "aabbccddeeff"
validation_url = "/user_validation_url"

request.env["HTTP_X_REMOTE_USER"] = user.userid
skip_data_checks(validation_url)
request.env["HTTP_X_REMOTE_USER"] = user.userid
skip_data_checks(validation_url)

allow(User).to receive(:authenticate).and_return(user)
allow_any_instance_of(Api::UserTokenService).to receive(:generate_token)
.with(user.userid, "ui")
.and_return(auth_token)
allow(User).to receive(:authenticate).and_return(user)
allow_any_instance_of(Api::UserTokenService).to receive(:generate_token)
.with(user.userid, "ui")
.and_return(auth_token)

expect(controller).to receive(:render)
.with(:template => "dashboard/saml_login",
:layout => false,
:locals => {:api_auth_token => auth_token, :validation_url => validation_url})
.exactly(1).times
expect(controller).to receive(:render)
.with(:template => "dashboard/#{protocol}_login",
:layout => false,
:locals => {:api_auth_token => auth_token, :validation_url => validation_url})
.exactly(1).times

controller.send(:saml_login)
controller.send("#{protocol}_login")
end
end
end

Expand Down