Last active
September 13, 2021 21:30
-
-
Save sheeley/7044243 to your computer and use it in GitHub Desktop.
SAML Controller for Rails (used when POSTing data to IDP and decrypting on return)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class SessionsController < ApplicationController | |
skip_before_filter :verify_authenticity_token, only: :saml | |
def new | |
request = Onelogin::Saml::Authrequest.new | |
settings = saml_settings | |
request_doc = request.create_authentication_xml_doc(settings) | |
request_str = request_doc.to_s #.force_encoding("UTF-8") | |
logger.debug request_str | |
request_str = Base64.strict_encode64(request_str) | |
# create a form in your view that POSTs to your IDP | |
# <input name="SAMLRequest" value="@SAMLRequest"/> | |
@SAMLRequest = request_str | |
end | |
def saml_settings | |
Onelogin::Saml::Settings.new( | |
assertion_consumer_service_url: ENV['SAML_ASSERTION_CONSUMER_SERVICE_URL'], | |
issuer: ENV['SAML_ISSUER'], | |
idp_sso_target_url: ENV['SAML_IDP_SSO_TARGET_URL'], | |
idp_cert_fingerprint: ENV['SAML_IDP_CERT_FINGERPRINT'], | |
name_identifier_format: ENV['SAML_NAME_IDENTIFIER_FORMAT'] | |
) | |
end | |
def create | |
auth = request.env["omniauth.auth"] | |
user = User.where(:provider => auth['provider'], | |
:uid => auth['uid'].to_s).first || User.create_with_omniauth(auth) | |
# Reset the session after successful login, per | |
# 2.8 Session Fixation – Countermeasures: | |
# http://guides.rubyonrails.org/security.html#session-fixation-countermeasures | |
# binding.pry | |
reset_session | |
session[:user_id] = user.id | |
user.add_role :admin if User.count == 1 # make the first user an admin | |
if user.email.blank? | |
redirect_to edit_user_path(user), :alert => "Please enter your email address." | |
else | |
redirect_to root_url, :notice => 'Signed in!' | |
end | |
end | |
def saml | |
# Onelogin handles the basic SAML response | |
response = Onelogin::Saml::Response.new(request.params['SAMLResponse']) | |
response.settings = saml_settings | |
# decrypt the document | |
enc_key = REXML::XPath.first(response.document, "//ds:KeyInfo/xenc:EncryptedKey/xenc:CipherData/xenc:CipherValue").text | |
enc_value = REXML::XPath.first(response.document, "//xenc:EncryptedData/xenc:CipherData/xenc:CipherValue").text | |
private_key = OpenSSL::PKey::RSA.new(ENV['SAML_PRIVATE_KEY']) | |
data_key = private_key.private_decrypt(Base64.decode64(enc_key), OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING) | |
actual_output = decrypt_cipher_data(data_key, enc_value) | |
# for some reason, the decrypted string has extra chars at the end, so remove them if they're there | |
output_end = '</saml:Assertion>' | |
trim_to = actual_output.index output_end | |
actual_output = actual_output[0..trim_to + output_end.length-1] if not trim_to.nil? | |
# create a new xml doc, grab the key/values from it | |
decrypted_doc = REXML::Document.new actual_output | |
values = {} | |
REXML::XPath.each(decrypted_doc, "//saml:AttributeStatement/saml:Attribute") do |elem| | |
value_name = elem.attribute 'Name' | |
if not value_name.nil? | |
value_name = value_name.value | |
values[value_name] = elem.children[0].text if not elem.children.nil? and not elem.children[0].nil? | |
end | |
end | |
request.env["omniauth.auth"] = { | |
'uid' => values['name_id'], | |
'provider' => 'saml', | |
'info' => { | |
'name' => "#{values['FirstName']} #{values['LastName']}", | |
'email' => values['Email'] | |
} | |
} | |
create | |
end | |
def destroy | |
reset_session | |
redirect_to root_url, :notice => 'Signed out!' | |
end | |
def failure | |
redirect_to root_url, :alert => "Authentication error: #{params[:message].humanize}" | |
end | |
private | |
def decrypt_cipher_data(key_cipher, cipher_data) | |
cipher_data_str = Base64.decode64(cipher_data) | |
mcrypt_iv = cipher_data_str[0..15] | |
cipher_data_str = cipher_data_str[16..-1] | |
cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc") | |
cipher.decrypt | |
cipher.padding = 0 | |
cipher.key = key_cipher | |
cipher.iv = mcrypt_iv | |
result = cipher.update(cipher_data_str) | |
result << cipher.final | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment