# Checks if the SAML Response contains or not an EncryptedAssertion element# @return [Boolean] True if the SAML Response contains an EncryptedAssertion element# SAMLResponse に EncryptedAssertion 要素が含まれているかどうかを確認します# SAMLResponse に EncryptedAssertion要素が含まれている場合はTrueが返り値となりますdefassertion_encrypted?
! REXML::XPath.first(
document,
"(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
{ "p" => PROTOCOL, "a" => ASSERTION }
).nil?
end
defvalidate_issuerreturntrueif settings.idp_entity_id.nil?
begin
obtained_issuers = issuers
rescueValidationError => e
return append_error(e.message)
end
obtained_issuers.each do |issuer|
unlessOneLogin::RubySaml::Utils.uri_match?(issuer, settings.idp_entity_id)
error_msg = "Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>"return append_error(error_msg)
endendtrueend# issuersメソッドの定義はこちら# Gets the Issuers (from Response and Assertion).# (returns the first node that matches the supplied xpath from the Response and from the Assertion)# @return [Array] Array with the Issuers (REXML::Element)# issuerを取得します(SamlResponseとアサーションから)。# SAMLResponseおよびアサーションから指定されたxpathに一致する最初のノードを返します# 返り値はissuersの配列が返りますdefissuers@issuers ||= begin
issuer_response_nodes = REXML::XPath.match(
document,
"/p:Response/a:Issuer",
{ "p" => PROTOCOL, "a" => ASSERTION }
)
unless issuer_response_nodes.size == 1
error_msg = "Issuer of the Response not found or multiple."raiseValidationError.new(error_msg)
end
issuer_assertion_nodes = xpath_from_signed_assertion("/a:Issuer")
unless issuer_assertion_nodes.size == 1
error_msg = "Issuer of the Assertion not found or multiple."raiseValidationError.new(error_msg)
end
nodes = issuer_response_nodes + issuer_assertion_nodes
nodes.map { |node| Utils.element_text(node) }.compact.uniq
endend
begin end と ||= を使ってSAMLのissuerを見ながらバリデーションを行い、問題なければ@issuersをインスタンス変数で定義するメソッドでした。
# Validates the NameID elementdefvalidate_name_idif name_id_node.nil?
if settings.security[:want_name_id]
return append_error("No NameID element found in the assertion of the Response")
endelseif name_id.nil? || name_id.empty?
return append_error("An empty NameID value found")
endunless settings.sp_entity_id.nil? || settings.sp_entity_id.empty? || name_id_spnamequalifier.nil? || name_id_spnamequalifier.empty?
if name_id_spnamequalifier != settings.sp_entity_id
return append_error("The SPNameQualifier value mistmatch the SP entityID value.")
endendendtrueend# name_id_nodeメソッドの定義はこちらdefname_id_node@name_id_node ||=
begin
encrypted_node = xpath_first_from_signed_assertion('/a:Subject/a:EncryptedID')
if encrypted_node
node = decrypt_nameid(encrypted_node)
else
node = xpath_first_from_signed_assertion('/a:Subject/a:NameID')
endendend
defcreate(settings, params = {})
params = create_params(settings, params)
params_prefix = (settings.idp_sso_service_url =~ /\?/) ? '&' : '?'
saml_request = CGI.escape(params.delete("SAMLRequest"))
request_params = "#{params_prefix}SAMLRequest=#{saml_request}"
params.each_pair do |key, value|
request_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"endraiseSettingError.new "Invalid settings, idp_sso_service_url is not set!"if settings.idp_sso_service_url.nil? or settings.idp_sso_service_url.empty?
@login_url = settings.idp_sso_service_url + request_params
end
create_paramsが実際にパラメータを生成する処理っぽいので見てみます。
defcreate_params(settings, params={})
# The method expects :RelayState but sometimes we get 'RelayState' instead.# Based on the HashWithIndifferentAccess value in Rails we could experience# conflicts so this line will solve them.
relay_state = params[:RelayState] || params['RelayState']
if relay_state.nil?
params.delete(:RelayState)
params.delete('RelayState')
end
request_doc = create_authentication_xml_doc(settings)
request_doc.context[:attribute_quote] = :quoteif settings.double_quote_xml_attribute_values
request = ""
request_doc.write(request)
Logging.debug "Created AuthnRequest: #{request}"
request = deflate(request) if settings.compress_request
base64_request = encode(request)
request_params = {"SAMLRequest" => base64_request}
if settings.idp_sso_service_binding == Utils::BINDINGS[:redirect] && settings.security[:authn_requests_signed] && settings.private_key
params['SigAlg'] = settings.security[:signature_method]
url_string = OneLogin::RubySaml::Utils.build_query(
:type => 'SAMLRequest',
:data => base64_request,
:relay_state => relay_state,
:sig_alg => params['SigAlg']
)
sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method])
signature = settings.get_sp_key.sign(sign_algorithm.new, url_string)
params['Signature'] = encode(signature)
end
params.each_pair do |key, value|
request_params[key] = value.to_s
end
request_params
end
The method expects :RelayState but sometimes we get 'RelayState' instead.
Based on the HashWithIndifferentAccess value in Rails we could experience
conflicts so this line will solve them.
# config/routes.rb
resources :saml, only: :indexdo
collection do
get :sso
post :acs
get :metadata
get :logoutendend
root 'saml#index'
# app/controllers/saml_controller.rbclassSamlController < ApplicationController
skip_before_action :verify_authenticity_token, :only => [:acs, :logout]
defindex@attrs = {}
enddefsso
settings = Account.get_saml_settings(get_url_base)
if settings.nil?
render :action => :no_settingsreturnend
request = OneLogin::RubySaml::Authrequest.new
redirect_to(request.create(settings))
enddefacs
settings = Account.get_saml_settings(get_url_base)
response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], settings: settings)
if response.is_valid?
session[:nameid] = response.nameid
session[:attributes] = response.attributes
@attrs = session[:attributes]
logger.info "Sucessfully logged"
logger.info "NAMEID: #{response.nameid}"
render :action => :indexelse
logger.info "Response Invalid. Errors: #{response.errors}"@errors = response.errors
render :action => :failendenddefmetadata
settings = Account.get_saml_settings(get_url_base)
meta = OneLogin::RubySaml::Metadata.new
render :xml => meta.generate(settings, true)
end# Trigger SP and IdP initiated Logout requestsdeflogout# If we're given a logout request, handle it in the IdP logout initiated methodif params[:SAMLRequest]
return idp_logout_request
# We've been given a response back from the IdPelsif params[:SAMLResponse]
return process_logout_response
elsif params[:slo]
return sp_logout_request
else
reset_session
endend# Create an SP initiated SLOdefsp_logout_request# LogoutRequest accepts plain browser requests w/o paramters
settings = Account.get_saml_settings(get_url_base)
if settings.idp_slo_target_url.nil?
logger.info "SLO IdP Endpoint not found in settings, executing then a normal logout'"
reset_session
else# Since we created a new SAML request, save the transaction_id# to compare it with the response we get back
logout_request = OneLogin::RubySaml::Logoutrequest.new()
session[:transaction_id] = logout_request.uuid
logger.info "New SP SLO for User ID: '#{session[:nameid]}', Transaction ID: '#{session[:transaction_id]}'"if settings.name_identifier_value.nil?
settings.name_identifier_value = session[:nameid]
end
relayState = url_for controller: 'saml', action: 'index'
redirect_to(logout_request.create(settings, :RelayState => relayState))
endend# After sending an SP initiated LogoutRequest to the IdP, we need to accept# the LogoutResponse, verify it, then actually delete our session.defprocess_logout_response
settings = Account.get_saml_settings(get_url_base)
request_id = session[:transaction_id]
logout_response = OneLogin::RubySaml::Logoutresponse.new(params[:SAMLResponse], settings, :matches_request_id => request_id, :get_params => params)
logger.info "LogoutResponse is: #{logout_response.response.to_s}"# Validate the SAML Logout Responseifnot logout_response.validate
error_msg = "The SAML Logout Response is invalid. Errors: #{logout_response.errors}"
logger.error error_msg
render :inline => error_msg
else# Actually log out this sessionif logout_response.success?
logger.info "Delete session for '#{session[:nameid]}'"
reset_session
endendend# Method to handle IdP initiated logoutsdefidp_logout_request
settings = Account.get_saml_settings(get_url_base)
logout_request = OneLogin::RubySaml::SloLogoutrequest.new(params[:SAMLRequest], :settings => settings)
ifnot logout_request.is_valid?
error_msg = "IdP initiated LogoutRequest was not valid!. Errors: #{logout_request.errors}"
logger.error error_msg
render :inline => error_msg
end
logger.info "IdP initiated Logout for #{logout_request.nameid}"# Actually log out this session
reset_session
logout_response = OneLogin::RubySaml::SloLogoutresponse.new.create(settings, logout_request.id, nil, :RelayState => params[:RelayState])
redirect_to logout_response
enddefget_url_base"#{request.protocol}#{request.host_with_port}"endend
# app/views/saml/fail.html.erb
<html><body><h4>SAML Response invalid</h4><% if @errors %><% @errors.each do |error| %><p><%= error %></p><% end %><% end %></body></html>
# app/views/saml/index.html.erb
<% if session[:nameid].present? %><p>NameID: <%= session[:nameid] %></p><% if @attrs.any? %><p>Received the following attributes in the SAML Response:</p><table><thead><th>Name</th><th>Values</th></thead><tbody><% @attrs.each do |key,attr_value| %><tr><td><%= key %></td><td><% if attr_value.any? %><ul><% attr_value.each do |val| %><li><%= val %></li><% end %></ul><% end %></td></tr><% end %></tbody></table><% end %><p><%= link_to"Logout", :action => "logout" %></p><p><%= link_to"Single Logout", :action => "logout", :slo => '1' %></p><% else %><p><%= link_to"Login", :action=>"sso"%></p><% end -%>
private# Archive the last password before save and delete all to old passwords from archive# @note we check to see if an old password has already been archived because# mongoid will keep re-triggering this callback when we add an old passworddefarchive_passwordif max_old_passwords.positive?
returntrueif old_passwords.where(encrypted_password: encrypted_password_was).exists?
old_passwords.create!(encrypted_password: encrypted_password_was) if encrypted_password_was.present?
old_passwords.reorder(created_at: :desc).offset(max_old_passwords).destroy_all
else
old_passwords.destroy_all
endend
# @return [Integer] max number of old passwords to store and checkdefmax_old_passwordscase deny_old_passwords
whentrue
[1, archive_count].max
whenfalse0else
deny_old_passwords.to_i
endenddefdeny_old_passwordsself.class.deny_old_passwords
enddefarchive_countself.class.password_archiving_count
end
config/devise-security.rb
# Deny old passwords (true, false, number_of_old_passwords_to_check)# Examples:# config.deny_old_passwords = false # allow old passwords# config.deny_old_passwords = true # will deny all the old passwords# config.deny_old_passwords = 3 # will deny new passwords that matches with the last 3 passwords# config.deny_old_passwords = true
# @return [Integer] max number of old passwords to store and checkdefmax_old_passwordscase deny_old_passwords
whentrue
[1, archive_count].max
whenfalse0else
deny_old_passwords.to_i
endend
defvalidate_password_archive
errors.add(:password, :taken_in_past) if will_save_change_to_encrypted_password? && password_archive_included?
end
password_archive_included?メソッドを見る必要がありそうなので見てみる。
defpassword_archive_included?returnfalseunless max_old_passwords.positive?
old_passwords_including_cur_change = old_passwords.reorder(created_at: :desc).limit(max_old_passwords).pluck(:encrypted_password)
old_passwords_including_cur_change << encrypted_password_was # include most recent change in list, but don't save it yet!
old_passwords_including_cur_change.any? do |old_password|
# NOTE: we deliberately do not do mass assignment here so that users that# rely on `protected_attributes_continued` gem can still use this extension.# See issue #68self.class.new.tap { |object| object.encrypted_password = old_password }.valid_password?(password)
endend
This fixes a bug where devise-security skips the password history check in certain cases,
e.g. using the protected_attributes_continued gem and not having :encrypted_password in attr_accessible.
There are other instances in the codebase where there's mass assignment, but this is a start
/usr/local/lib/ruby/2.6.0/rubygems.rb:283:in`find_spec_for_exe': Could not find 'bundler' (2.1.4) required by your /app/Gemfile.lock. (Gem::GemNotFoundException)To update to the latest version installed on your system, run `bundle update --bundler`.To install the missing version, run `gem install bundler:2.1.4` from /usr/local/lib/ruby/2.6.0/rubygems.rb:302:in `activate_bin_path' from /usr/local/bin/bundle:23:in `<main>'
# Returns all the \subdomains as a string, so <tt>"dev.www"</tt> would be# returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>,# such as 2 to catch <tt>"www"</tt> instead of <tt>"www.rubyonrails"</tt># in "www.rubyonrails.co.uk".defsubdomain(tld_length = @@tld_length)
ActionDispatch::Http::URL.extract_subdomain(host, tld_length)
end
Returns all the \subdomains as a string, so "dev.www" would be returned for "dev.www.rubyonrails.org". You can specify a different tld_length, such as 2 to catch "www" instead of "www.rubyonrails" in "www.rubyonrails.co.uk".
# Returns the subdomains of a host as a String given the domain level.## # Top-level domain example# extract_subdomain('www.example.com', 1) # => "www"# # Second-level domain example# extract_subdomain('dev.www.example.co.uk', 2) # => "dev.www"defextract_subdomain(host, tld_length)
extract_subdomains(host, tld_length).join(".")
end# Returns the subdomains of a host as an Array given the domain level.## # Top-level domain example# extract_subdomains('www.example.com', 1) # => ["www"]# # Second-level domain example# extract_subdomains('dev.www.example.co.uk', 2) # => ["dev", "www"]defextract_subdomains(host, tld_length)
if named_host?(host)
extract_subdomains_from(host, tld_length)
else
[]
endend# 実際にサブドメインを算出する処理defextract_subdomains_from(host, tld_length)
parts = host.split(".")
parts[0..-(tld_length + 2)]
end
# Returns all the \subdomains as an array, so <tt>["dev", "www"]</tt> would be# returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>,# such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt># in "www.rubyonrails.co.uk".defsubdomains(tld_length = @@tld_length)
ActionDispatch::Http::URL.extract_subdomains(host, tld_length)
end
Returns all the \subdomains as an array, so ["dev", "www"] would be returned for "dev.www.rubyonrails.org". You can specify a different tld_length, such as 2 to catch ["www"] instead of ["www", "rubyonrails"] in "www.rubyonrails.co.uk".
Track information about your user sign in. It tracks the following columns:
sign_in_count - Increased every time a sign in is made (by form, openid, oauth)
current_sign_in_at - A timestamp updated when the user signs in
last_sign_in_at - Holds the timestamp of the previous sign in
current_sign_in_ip - The remote ip updated when the user sign in
last_sign_in_ip - Holds the remote ip of the previous sign in
defupdate_tracked_fields!(request)
# We have to check if the user is already persisted before running# `save` here because invalid users can be saved if we don't.# See https://github.com/heartcombo/devise/issues/4673 for more details.returnif new_record?
update_tracked_fields(request)
save(validate: false)
end
We have to check if the user is already persisted before running
`save` here because invalid users can be saved if we don't.
See https://github.com/heartcombo/devise/issues/4673 for more details.
# frozen_string_literal: true# After each sign in, update sign in time, sign in count and sign in IP.# This is only triggered when the user is explicitly set (with set_user)# and on authentication. Retrieving the user from session (:fetch) does# not trigger it.Warden::Manager.after_set_user except: :fetchdo |record, warden, options|
if record.respond_to?(:update_tracked_fields!) && warden.authenticated?(options[:scope]) && !warden.request.env['devise.skip_trackable']
record.update_tracked_fields!(warden.request)
endend