String, like a JSON or Ruby string
Int, like a JSON or Ruby integer
Float, like a JSON or Ruby floating point decimal
Boolean, like a JSON or Ruby boolean (true or false)
ID, which a specialized String for representing unique object identifiers
ISO8601DateTime, an ISO 8601-encoded datetime
ISO8601Date, an ISO 8601-encoded date JSON, ⚠ This returns arbitrary JSON (Ruby hashes, arrays, strings, integers, floats, booleans and nils). Take care: by using this type, you completely lose all GraphQL type safety. Consider building object types for your data instead.
BigInt, a numeric value which may exceed the size of a 32-bit integer
moduleTypesclassPhoneNumber < Types::BaseScalar
description 'PhoneNumber'defself.coerce_input(input_value, _context)
if input_value.match?(/\A\d{10,11}\z/)
input_value
elseraiseGraphQL::CoercionError, "#{input_value.inspect} is not a valid Phone Number."endenddefself.coerce_result(ruby_value, context)
ruby_value.to_s
endendend
# Retrieves the policy for the given record, initializing it with the# record and user and finally throwing an error if the user is not# authorized to perform the given action.## @param user [Object] the user that initiated the action# @param record [Object] the object we're checking permissions of# @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`)# @param policy_class [Class] the policy class we want to force use of# @raise [NotAuthorizedError] if the given query method returned false# @return [Object] Always returns the passed object recorddefauthorize(user, record, query, policy_class: nil)
policy = policy_class ? policy_class.new(user, record) : policy!(user, record)
raiseNotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)
record.is_a?(Array) ? record.last : record
end
Retrieves the policy for the given record, initializing it with the record and user and finally throwing an error
if the user is not authorized to perform the given action.
# Retrieves the policy for the given record.## @see https://github.com/varvet/pundit#policies# @param user [Object] the user that initiated the action# @param record [Object] the object we're retrieving the policy for# @raise [NotDefinedError] if the policy cannot be found# @raise [InvalidConstructorError] if the policy constructor called incorrectly# @return [Object] instance of policy class with query methodsdefpolicy!(user, record)
policy = PolicyFinder.new(record).policy!
policy.new(user, pundit_model(record))
rescueArgumentErrorraiseInvalidConstructorError, "Invalid #<#{policy}> constructor is called"end
# 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