samlurai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/LICENSE ADDED
@@ -0,0 +1,50 @@
1
+ Samlurai is adapted from ruby-saml, licensed as follows:
2
+
3
+ -----
4
+
5
+ Copyright (c) 2010 OneLogin, LLC
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in
15
+ all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ THE SOFTWARE.
24
+
25
+ -----
26
+
27
+ To what extent Samlurai has extended and modified ruby-saml, that software is
28
+ licensed as follows:
29
+
30
+ -----
31
+
32
+ Copyright (c) 2013 Instructure, Inc.
33
+
34
+ Permission is hereby granted, free of charge, to any person obtaining a copy
35
+ of this software and associated documentation files (the "Software"), to deal
36
+ in the Software without restriction, including without limitation the rights
37
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
38
+ copies of the Software, and to permit persons to whom the Software is
39
+ furnished to do so, subject to the following conditions:
40
+
41
+ The above copyright notice and this permission notice shall be included in
42
+ all copies or substantial portions of the Software.
43
+
44
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
45
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
46
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
47
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
48
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
49
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
50
+ THE SOFTWARE.
@@ -0,0 +1,5 @@
1
+ # Slice and dice yourself some SAML
2
+
3
+ [![Build Status](https://travis-ci.org/phinze/samlurai.png?branch=master)](https://travis-ci.org/phinze/samlurai)
4
+
5
+ ![Samlurai Logo](https://github.com/phinze/samlurai/raw/master/doc/samlurai.png)
@@ -0,0 +1,50 @@
1
+ require 'rubygems'
2
+ require 'xml_security'
3
+
4
+ require 'zlib'
5
+ require 'base64'
6
+ require 'xml/libxml'
7
+ require 'openssl'
8
+ require 'cgi'
9
+
10
+ module Samlurai
11
+ NAMESPACES = {
12
+ "samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
13
+ "saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
14
+ "md" => "urn:oasis:names:tc:SAML:2.0:metadata",
15
+ "xenc" => "http://www.w3.org/2001/04/xmlenc#",
16
+ "ds" => "http://www.w3.org/2000/09/xmldsig#"
17
+ }
18
+
19
+ # for SAML2 IDPs that omit the FriendlyName, map from the registered name
20
+ # http://middleware.internet2.edu/dir/edu-schema-oid-registry.html
21
+ ATTRIBUTES = {
22
+ "urn:oid:1.3.6.1.4.1.5923.1.1.2" => "eduPerson",
23
+ "urn:oid:1.3.6.1.4.1.5923.1.1.1.1" => "eduPersonAffiliation",
24
+ "urn:oid:1.3.6.1.4.1.5923.1.1.1.7" => "eduPersonEntitlement",
25
+ "urn:oid:1.3.6.1.4.1.5923.1.1.1.2" => "eduPersonNickname",
26
+ "urn:oid:1.3.6.1.4.1.5923.1.1.1.3" => "eduPersonOrgDN",
27
+ "urn:oid:1.3.6.1.4.1.5923.1.1.1.4" => "eduPersonOrgUnitDN",
28
+ "urn:oid:1.3.6.1.4.1.5923.1.1.1.5" => "eduPersonPrimaryAffiliation",
29
+ "urn:oid:1.3.6.1.4.1.5923.1.1.1.8" => "eduPersonPrimaryOrgUnitDN",
30
+ "urn:oid:1.3.6.1.4.1.5923.1.1.1.6" => "eduPersonPrincipalName",
31
+ "urn:oid:1.3.6.1.4.1.5923.1.1.1.9" => "eduPersonScopedAffiliation",
32
+ "urn:oid:1.3.6.1.4.1.5923.1.1.1.10" => "eduPersonTargetedID",
33
+ "urn:oid:1.3.6.1.4.1.5923.1.2.2" => "eduOrg",
34
+ "urn:oid:1.3.6.1.4.1.5923.1.2.1.2" => "eduOrgHomePageURI",
35
+ "urn:oid:1.3.6.1.4.1.5923.1.2.1.3" => "eduOrgIdentityAuthNPolicyURI",
36
+ "urn:oid:1.3.6.1.4.1.5923.1.2.1.4" => "eduOrgLegalName",
37
+ "urn:oid:1.3.6.1.4.1.5923.1.2.1.5" => "eduOrgSuperiorURI",
38
+ "urn:oid:1.3.6.1.4.1.5923.1.2.1.6" => "eduOrgWhitePagesURI",
39
+ }
40
+ end
41
+
42
+ require 'samlurai/auth_request'
43
+ require 'samlurai/authn_contexts.rb'
44
+ require 'samlurai/response'
45
+ require 'samlurai/settings'
46
+ require 'samlurai/name_identifiers'
47
+ require 'samlurai/status_codes'
48
+ require 'samlurai/meta_data'
49
+ require 'samlurai/log_out_request'
50
+ require 'samlurai/logout_response'
@@ -0,0 +1,53 @@
1
+ module Samlurai
2
+ class AuthRequest
3
+
4
+ attr_reader :settings, :id, :request_xml, :forward_url
5
+
6
+ def initialize(settings)
7
+ @settings = settings
8
+ end
9
+
10
+ def self.create(settings)
11
+ ar = AuthRequest.new(settings)
12
+ ar.generate_request
13
+ end
14
+
15
+ def generate_request
16
+ @id = Samlurai::AuthRequest.generate_unique_id(42)
17
+ issue_instant = Samlurai::AuthRequest.get_timestamp
18
+
19
+ @request_xml =
20
+ "<samlp:AuthnRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\"#{@id}\" Version=\"2.0\" IssueInstant=\"#{issue_instant}\" ProtocolBinding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" AssertionConsumerServiceURL=\"#{Array(settings.assertion_consumer_service_url).first}\">" +
21
+ "<saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">#{@settings.issuer}</saml:Issuer>\n" +
22
+ "<samlp:NameIDPolicy xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" Format=\"#{@settings.name_identifier_format}\" AllowCreate=\"true\"></samlp:NameIDPolicy>\n"
23
+
24
+ if @settings.requested_authn_context
25
+ @request_xml += "<samlp:RequestedAuthnContext xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" Comparison=\"exact\">"
26
+ @request_xml += "<saml:AuthnContextClassRef xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">#{@settings.requested_authn_context}</saml:AuthnContextClassRef>"
27
+ @request_xml += "</samlp:RequestedAuthnContext>\n"
28
+ end
29
+
30
+ @request_xml += "</samlp:AuthnRequest>"
31
+
32
+ deflated_request = Zlib::Deflate.deflate(@request_xml, 9)[2..-5]
33
+ base64_request = Base64.encode64(deflated_request)
34
+ encoded_request = CGI.escape(base64_request)
35
+
36
+ @forward_url = @settings.idp_sso_target_url + (@settings.idp_sso_target_url.include?("?") ? "&" : "?") + "SAMLRequest=" + encoded_request
37
+ end
38
+
39
+ private
40
+
41
+ def self.generate_unique_id(length)
42
+ chars = ("a".."f").to_a + ("0".."9").to_a
43
+ chars_len = chars.size
44
+ unique_id = ("a".."f").to_a[rand(6)]
45
+ 2.upto(length) { |i| unique_id << chars[rand(chars_len)] }
46
+ unique_id
47
+ end
48
+
49
+ def self.get_timestamp
50
+ Time.new.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,35 @@
1
+ module Samlurai
2
+ module AuthnContexts
3
+ # see section 3.4 of http://docs.oasis-open.org/security/saml/v2.0/saml-authn-context-2.0-os.pdf
4
+ INTERNET_PROTOCOL = "urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocol"
5
+ INTERNET_PROTOCOL_PASSWORD = "urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword"
6
+ KERBEROS = "urn:oasis:names:tc:SAML:2.0:ac:classes:Kerberos"
7
+ MOBILE_ONE_FACTOR_UNREGISTERED = "urn:oasis:names:tc:SAML:2.0:ac:classes:MobileOneFactorUnregistered"
8
+ MOBILE_TWO_FACTOR_UNREGISTERED = "urn:oasis:names:tc:SAML:2.0:ac:classes:MobileTwoFactorUnregistered"
9
+ MOBILE_ONE_FACTOR_CONTRACT = "urn:oasis:names:tc:SAML:2.0:ac:classes:MobileOneFactorContract"
10
+ MOBILE_TWO_FACTOR_CONTRACT = "urn:oasis:names:tc:SAML:2.0:ac:classes:MobileTwoFactorContract"
11
+ PASSWORD = "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
12
+ PASSWORD_PROTECTED_TRANSPORT = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
13
+ PREVIOUS_SESSION = "urn:oasis:names:tc:SAML:2.0:ac:classes:PreviousSession"
14
+ X509 = "urn:oasis:names:tc:SAML:2.0:ac:classes:X509"
15
+ PGP = "urn:oasis:names:tc:SAML:2.0:ac:classes:PGP"
16
+ SPKI = "urn:oasis:names:tc:SAML:2.0:ac:classes:SPKI"
17
+ XMLD_SIG = "urn:oasis:names:tc:SAML:2.0:ac:classes:XMLDSig"
18
+ SMARTCARD_PKI = "urn:oasis:names:tc:SAML:2.0:ac:classes:SmartcardPKI"
19
+ SOFTWARE_PKI = "urn:oasis:names:tc:SAML:2.0:ac:classes:SoftwarePKI"
20
+ TELEPHONY = "urn:oasis:names:tc:SAML:2.0:ac:classes:Telephony"
21
+ NOMAD_TELEPHONY = "urn:oasis:names:tc:SAML:2.0:ac:classes:NomadTelephony"
22
+ PERSONAL_TELEPHONY = "urn:oasis:names:tc:SAML:2.0:ac:classes:PersonalTelephony"
23
+ AUTHENTICATED_TELEPHONY = "urn:oasis:names:tc:SAML:2.0:ac:classes:AuthenticatedTelephony"
24
+ SECURE_REMOTE_PASSWORD = "urn:oasis:names:tc:SAML:2.0:ac:classes:SecureRemotePassword"
25
+ TLS_CLIENT = "urn:oasis:names:tc:SAML:2.0:ac:classes:TLSClient"
26
+ TIME_SYNC_TOKEN = "urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken"
27
+ UNSPECIFIED = "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified"
28
+
29
+ ALL_CONTEXTS = [INTERNET_PROTOCOL_PASSWORD, KERBEROS, MOBILE_ONE_FACTOR_UNREGISTERED,
30
+ MOBILE_TWO_FACTOR_UNREGISTERED, MOBILE_ONE_FACTOR_CONTRACT, MOBILE_TWO_FACTOR_CONTRACT,
31
+ PASSWORD, PASSWORD_PROTECTED_TRANSPORT, PREVIOUS_SESSION, X509, PGP, SPKI, XMLD_SIG,
32
+ SMARTCARD_PKI, SOFTWARE_PKI, TELEPHONY, NOMAD_TELEPHONY, PERSONAL_TELEPHONY,
33
+ AUTHENTICATED_TELEPHONY, SECURE_REMOTE_PASSWORD, TLS_CLIENT, TIME_SYNC_TOKEN, UNSPECIFIED]
34
+ end
35
+ end
@@ -0,0 +1,52 @@
1
+ module Samlurai
2
+ class LogOutRequest
3
+ attr_reader :settings, :id, :request_xml, :forward_url
4
+
5
+ def initialize(settings, session)
6
+ @settings = settings
7
+ @session = session
8
+ end
9
+
10
+ def self.create(settings, session)
11
+ ar = LogOutRequest.new(settings, session)
12
+ ar.generate_request
13
+ end
14
+
15
+ def generate_request
16
+ @id = Samlurai::AuthRequest.generate_unique_id(42)
17
+ issue_instant = Samlurai::AuthRequest.get_timestamp
18
+
19
+ @request_xml = "<samlp:LogoutRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\"#{@id}\" Version=\"2.0\" IssueInstant=\"#{issue_instant}\" Destination=\"#{@settings.idp_slo_target_url}\">" +
20
+ "<saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">#{@settings.issuer}</saml:Issuer>" +
21
+ "<saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" NameQualifier=\"#{@session[:name_qualifier]}\" SPNameQualifier=\"#{@settings.issuer}\" Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:transient\">#{@session[:name_id]}</saml:NameID>" +
22
+ "<samlp:SessionIndex xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\">#{@session[:session_index]}</samlp:SessionIndex>" +
23
+ "</samlp:LogoutRequest>"
24
+
25
+ if settings.sign?
26
+ @request_xml = XMLSecurity.sign(@request_xml, @settings.xmlsec_privatekey)
27
+ end
28
+
29
+ deflated_logout_request = Zlib::Deflate.deflate(@request_xml, 9)[2..-5]
30
+ base64_logout_request = Base64.encode64(deflated_logout_request)
31
+ encoded_logout_request = CGI.escape(base64_logout_request)
32
+
33
+ query_string = "SAMLRequest=" + encoded_logout_request
34
+
35
+ if settings.sign?
36
+ query_string += '&SigAlg=' + CGI.escape('http://www.w3.org/2000/09/xmldsig#rsa-sha1')
37
+ signature = _generate_signature(query_string, @settings.xmlsec_privatekey)
38
+ query_string += "&Signature=#{CGI.escape(signature)}"
39
+ end
40
+
41
+ @forward_url = @settings.idp_slo_target_url + (@settings.idp_slo_target_url.include?("?") ? "&" : "?") + query_string
42
+
43
+ @forward_url
44
+ end
45
+
46
+ def _generate_signature(string, private_key)
47
+ pkey = OpenSSL::PKey::RSA.new(File.read(private_key))
48
+ sign = pkey.sign(OpenSSL::Digest::SHA1.new, string)
49
+ Base64.encode64(sign).gsub(/\s/, '')
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,38 @@
1
+ module Samlurai
2
+ class LogoutResponse
3
+
4
+ attr_reader :settings, :document, :xml, :response
5
+ attr_reader :status_code, :status_message, :issuer
6
+ attr_reader :in_response_to, :destination, :request_id
7
+ def initialize(response, settings=nil)
8
+ @response = response
9
+
10
+ @xml = Base64.decode64(@response)
11
+ zlib = Zlib::Inflate.new(-Zlib::MAX_WBITS)
12
+ @xml = zlib.inflate(@xml)
13
+ @document = LibXML::XML::Document.string(@xml)
14
+
15
+ @request_id = @document.find_first("/samlp:LogoutResponse", Samlurai::NAMESPACES)['ID'] rescue nil
16
+ @issuer = @document.find_first("/samlp:LogoutResponse/saml:Issuer", Samlurai::NAMESPACES).content rescue nil
17
+ @in_response_to = @document.find_first("/samlp:LogoutResponse", Samlurai::NAMESPACES)['InResponseTo'] rescue nil
18
+ @destination = @document.find_first("/samlp:LogoutResponse", Samlurai::NAMESPACES)['Destination'] rescue nil
19
+ @status_code = @document.find_first("/samlp:LogoutResponse/samlp:Status/samlp:StatusCode", Samlurai::NAMESPACES)['Value'] rescue nil
20
+ @status_message = @document.find_first("/samlp:LogoutResponse/samlp:Status/samlp:StatusMessage", Samlurai::NAMESPACES).content rescue nil
21
+
22
+ process(settings) if settings
23
+ end
24
+
25
+ def process(settings)
26
+ @settings = settings
27
+ return unless @response
28
+ end
29
+
30
+ def logger=(val)
31
+ @logger = val
32
+ end
33
+
34
+ def success_status?
35
+ @status_code == Samlurai::StatusCodes::SUCCESS_URI
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,43 @@
1
+ module Samlurai
2
+ class MetaData
3
+ def self.create(settings)
4
+ xml = %{<?xml version="1.0"?>
5
+ <EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="#{settings.issuer}">
6
+ <SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
7
+ }
8
+ if settings.encryption_configured?
9
+ xml += %{
10
+ <KeyDescriptor>
11
+ <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
12
+ <X509Data>
13
+ <X509Certificate>
14
+ #{File.read(settings.xmlsec_certificate).gsub(/\w*-+(BEGIN|END) CERTIFICATE-+\w*/, "").strip}
15
+ </X509Certificate>
16
+ </X509Data>
17
+ </KeyInfo>
18
+ <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc">
19
+ <KeySize xmlns="http://www.w3.org/2001/04/xmlenc#">128</KeySize>
20
+ </EncryptionMethod>
21
+ </KeyDescriptor>
22
+ }
23
+ end
24
+ xml += %{
25
+ <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="#{settings.sp_slo_url}"/>
26
+ }
27
+ Array(settings.assertion_consumer_service_url).each_with_index do |url, index|
28
+ xml += %{
29
+ <AssertionConsumerService index="#{index}" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="#{url}"/>
30
+ }
31
+ end
32
+ xml += %{
33
+ </SPSSODescriptor>
34
+ <ContactPerson contactType="technical">
35
+ <SurName>#{settings.tech_contact_name}</SurName>
36
+ <EmailAddress>mailto:#{settings.tech_contact_email}</EmailAddress>
37
+ </ContactPerson>
38
+ </EntityDescriptor>
39
+ }
40
+ xml
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,15 @@
1
+ module Samlurai
2
+ module NameIdentifiers
3
+ # See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf section 8.3 for further documentation
4
+ EMAIL = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
5
+ ENTITY = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
6
+ KERBEROS = "urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos"
7
+ PERSISTENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
8
+ TRANSIENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
9
+ UNSPECIFIED = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
10
+ WINDOWS_DOMAIN = "urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName"
11
+ X509 = "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName"
12
+
13
+ ALL_IDENTIFIERS = [EMAIL, ENTITY, KERBEROS, PERSISTENT, TRANSIENT, UNSPECIFIED, WINDOWS_DOMAIN, X509]
14
+ end
15
+ end
@@ -0,0 +1,134 @@
1
+ module Samlurai
2
+ class Response
3
+ attr_accessor :settings
4
+ attr_reader :document, :decrypted_document, :xml, :response
5
+ attr_reader :name_id, :name_qualifier, :session_index, :saml_attributes
6
+ attr_reader :status_code, :status_message
7
+ attr_reader :in_response_to, :destination, :issuer
8
+ attr_reader :validation_error
9
+
10
+ def initialize(response, settings=nil, validation_options={})
11
+ @response = response
12
+ @validation_options = validation_options
13
+
14
+ begin
15
+ @xml = Base64.decode64(@response)
16
+ @document = LibXML::XML::Document.string(@xml)
17
+ rescue => e
18
+ # could not parse document, everything is invalid
19
+ @response = nil
20
+ return
21
+ end
22
+
23
+ @issuer = @document.find_first("/samlp:Response/saml:Issuer", Samlurai::NAMESPACES).content rescue nil
24
+ @status_code = @document.find_first("/samlp:Response/samlp:Status/samlp:StatusCode", Samlurai::NAMESPACES)["Value"] rescue nil
25
+
26
+ process(settings) if settings
27
+ end
28
+
29
+ def process(settings)
30
+ @settings = settings
31
+ return unless @response
32
+
33
+ @decrypted_document = self.class.decrypt(@document, @settings)
34
+
35
+ @in_response_to = @decrypted_document.find_first("/samlp:Response", Samlurai::NAMESPACES)['InResponseTo'] rescue nil
36
+ @destination = @decrypted_document.find_first("/samlp:Response", Samlurai::NAMESPACES)['Destination'] rescue nil
37
+ @name_id = @decrypted_document.find_first("/samlp:Response/saml:Assertion/saml:Subject/saml:NameID", Samlurai::NAMESPACES).content rescue nil
38
+ @saml_attributes = {}
39
+ @decrypted_document.find("//saml:Attribute", Samlurai::NAMESPACES).each do |attr|
40
+ attrname = attr['FriendlyName'] || Samlurai::ATTRIBUTES[attr['Name']] || attr['Name']
41
+ @saml_attributes[attrname] = attr.content.strip rescue nil
42
+ end
43
+ @name_qualifier = @decrypted_document.find_first("/samlp:Response//saml:Assertion/saml:Subject/saml:NameID", Samlurai::NAMESPACES)["NameQualifier"] rescue nil
44
+ @session_index = @decrypted_document.find_first("/samlp:Response//saml:Assertion/saml:AuthnStatement", Samlurai::NAMESPACES)["SessionIndex"] rescue nil
45
+ @status_message = @decrypted_document.find_first("/samlp:Response/samlp:Status/samlp:StatusCode", Samlurai::NAMESPACES).content rescue nil
46
+ end
47
+
48
+ def logger=(val)
49
+ @logger = val
50
+ end
51
+
52
+ def is_valid?
53
+ if @response.nil? || @response == ""
54
+ @validation_error = "No response to validate"
55
+ return false
56
+ end
57
+
58
+ if !@settings.idp_cert_fingerprint
59
+ @validation_error = "No fingerprint configured in SAML settings"
60
+ return false
61
+ end
62
+
63
+ # Verify the original document if it has a signature, otherwise verify the signature
64
+ # in the encrypted portion. If there is no signature, then we can't verify.
65
+ verified = false
66
+ if @document.find_first("//ds:Signature", Samlurai::NAMESPACES)
67
+ verified = self.class.validate(@document, @validation_options.merge(:cert_fingerprint => @settings.idp_cert_fingerprint, :logger => @logger))
68
+ if !verified
69
+ return false
70
+ end
71
+ end
72
+
73
+ if !verified && @decrypted_document.find_first("//ds:Signature", Samlurai::NAMESPACES)
74
+ verified = self.class.validate(@decrypted_document, @validation_options.merge(:cert_fingerprint => @settings.idp_cert_fingerprint, :logger => @logger))
75
+ if !verified
76
+ return false
77
+ end
78
+ end
79
+
80
+ if !verified
81
+ @validation_error = "No signature found in the response"
82
+ return false
83
+ end
84
+
85
+ true
86
+ end
87
+
88
+ def success_status?
89
+ @status_code == Samlurai::StatusCodes::SUCCESS_URI
90
+ end
91
+
92
+ def auth_failure?
93
+ @status_code == Samlurai::StatusCodes::AUTHN_FAILED_URI
94
+ end
95
+
96
+ def no_authn_context?
97
+ @status_code == Samlurai::StatusCodes::NO_AUTHN_CONTEXT_URI
98
+ end
99
+
100
+ def fingerprint_from_idp
101
+ if base64_cert = @decrypted_document.find_first("//ds:X509Certificate", Samlurai::NAMESPACES)
102
+ cert_text = Base64.decode64(base64_cert.content)
103
+ cert = OpenSSL::X509::Certificate.new(cert_text)
104
+ Digest::SHA1.hexdigest(cert.to_der)
105
+ else
106
+ nil
107
+ end
108
+ end
109
+
110
+ def self.validate(document, options={})
111
+ auth_statement = document.find_first('//samlp:Response', Samlurai::NAMESPACES)
112
+ if auth_statement
113
+ options[:as_of] ||= auth_statement["IssueInstant"]
114
+ end
115
+ XMLSecurity.verify_signature(document.to_s(:indent => false), options)
116
+ end
117
+
118
+ # replaces EncryptedData nodes with decrypted copies
119
+ def self.decrypt(encrypted_document, settings)
120
+ if settings.encryption_configured?
121
+ decrypted_xml = nil
122
+ begin
123
+ decrypted_xml = XMLSecurity.decrypt(encrypted_document.to_s(:indent => false), settings.xmlsec_privatekey)
124
+ rescue Exception => e
125
+ # rescuing all exceptions here; bad. need to create superclass in xmlsecurity
126
+ end
127
+ if decrypted_xml
128
+ return LibXML::XML::Document.string(decrypted_xml)
129
+ end
130
+ end
131
+ encrypted_document
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,64 @@
1
+ module Samlurai
2
+ class Settings
3
+
4
+ def initialize(atts={})
5
+ atts.each do |key, val|
6
+ if self.respond_to? "#{key}="
7
+ self.send "#{key}=", val
8
+ end
9
+ end
10
+ end
11
+
12
+ # The URL at which the SAML assertion should be received.
13
+ attr_accessor :assertion_consumer_service_url
14
+
15
+ # The name of your application.
16
+ attr_accessor :issuer
17
+
18
+ #
19
+ attr_accessor :sp_name_qualifier
20
+
21
+ # The IdP URL to which the authentication request should be sent.
22
+ attr_accessor :idp_sso_target_url
23
+
24
+ # The IdP URL to which the logout request should be sent.
25
+ attr_accessor :idp_slo_target_url
26
+
27
+ # The certificate fingerprint. This is provided from the identity provider when setting up the relationship.
28
+ attr_accessor :idp_cert_fingerprint
29
+
30
+ # Describes the format of the username required by this application.
31
+ # For email: Samlurai::NameIdentifiers::EMAIL
32
+ attr_accessor :name_identifier_format
33
+
34
+ # The type of authentication requested (see Samlurai::AuthnContexts)
35
+ attr_accessor :requested_authn_context
36
+
37
+ ## Attributes for the metadata
38
+
39
+ # The logout url of your application
40
+ attr_accessor :sp_slo_url
41
+
42
+ # The name of the technical contact for your application
43
+ attr_accessor :tech_contact_name
44
+
45
+ # The email of the technical contact for your application
46
+ attr_accessor :tech_contact_email
47
+
48
+ ## Attributes for xml encryption
49
+
50
+ # The PEM-encoded certificate
51
+ attr_accessor :xmlsec_certificate
52
+
53
+ # The PEM-encoded private key
54
+ attr_accessor :xmlsec_privatekey
55
+
56
+ def sign?
57
+ !!self.xmlsec_privatekey
58
+ end
59
+
60
+ def encryption_configured?
61
+ !!self.xmlsec_privatekey
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,28 @@
1
+ module Samlurai
2
+ module StatusCodes
3
+ # See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf section 3.2.2 for further documentation
4
+ SUCCESS_URI = "urn:oasis:names:tc:SAML:2.0:status:Success"
5
+ REQUESTER_URI = "urn:oasis:names:tc:SAML:2.0:status:Requester"
6
+ RESPONDER_URI = "urn:oasis:names:tc:SAML:2.0:status:Responder"
7
+ VERSION_MISMATCH_URI = "urn:oasis:names:tc:SAML:2.0:status:VersionMismatch"
8
+ AUTHN_FAILED_URI = "urn:oasis:names:tc:SAML:2.0:status:AuthnFailed"
9
+ INVALID_ATTR_NAME_VALUE_URI = "urn:oasis:names:tc:SAML:2.0:status:InvalidAttrNameOrValue"
10
+ INVALID_NAMEID_POLICY_URI = "urn:oasis:names:tc:SAML:2.0:status:InvalidNameIDPolicy"
11
+ NO_AUTHN_CONTEXT_URI = "urn:oasis:names:tc:SAML:2.0:status:NoAuthnContext"
12
+ NO_AVAILABLE_IDP_URI = "urn:oasis:names:tc:SAML:2.0:status:NoAvailableIDP"
13
+ NO_PASSIVE_URI = "urn:oasis:names:tc:SAML:2.0:status:NoPassive"
14
+ NO_SUPPORTED_IDP_URI = "urn:oasis:names:tc:SAML:2.0:status:NoSupportedIDP"
15
+ PARTIAL_LOGOUT_URI = "urn:oasis:names:tc:SAML:2.0:status:PartialLogout"
16
+ PROXY_COUNT_EXCEEDED_URI = "urn:oasis:names:tc:SAML:2.0:status:ProxyCountExceeded"
17
+ REQUEST_DENIED_URI = "urn:oasis:names:tc:SAML:2.0:status:RequestDenied"
18
+ REQUEST_UNSUPPORTED_URI = "urn:oasis:names:tc:SAML:2.0:status:RequestUnsupported"
19
+ REQUEST_VERSION_DEPRECATED_URI = "urn:oasis:names:tc:SAML:2.0:status:RequestVersionDeprecated"
20
+ REQUEST_VERSION_TOO_HIGH_URI = "urn:oasis:names:tc:SAML:2.0:status:RequestVersionTooHigh"
21
+ REQUEST_VERSION_TOO_LOW_URI = "urn:oasis:names:tc:SAML:2.0:status:RequestVersionTooLow"
22
+ RESOURCE_NOT_RECOGNIZED_URI = "urn:oasis:names:tc:SAML:2.0:status:ResourceNotRecognized"
23
+ TOO_MANY_RESPONSES = "urn:oasis:names:tc:SAML:2.0:status:TooManyResponses"
24
+ UNKNOWN_ATTR_PROFILE_URI = "urn:oasis:names:tc:SAML:2.0:status:UnknownAttrProfile"
25
+ UNKNOWN_PRINCIPAL_URI = "urn:oasis:names:tc:SAML:2.0:status:UnknownPrincipal"
26
+ UNSUPPORTED_BINDING_URI = "urn:oasis:names:tc:SAML:2.0:status:UnsupportedBinding"
27
+ end
28
+ end
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: samlurai
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Bracken
9
+ - Zach
10
+ - Cody
11
+ - Jeremy
12
+ - Paul
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+ date: 2013-02-11 00:00:00.000000000 Z
17
+ dependencies:
18
+ - !ruby/object:Gem::Dependency
19
+ name: libxml-ruby
20
+ requirement: !ruby/object:Gem::Requirement
21
+ none: false
22
+ requirements:
23
+ - - ! '>='
24
+ - !ruby/object:Gem::Version
25
+ version: 2.3.0
26
+ type: :runtime
27
+ prerelease: false
28
+ version_requirements: !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: 2.3.0
34
+ - !ruby/object:Gem::Dependency
35
+ name: xml_security
36
+ requirement: !ruby/object:Gem::Requirement
37
+ none: false
38
+ requirements:
39
+ - - '='
40
+ - !ruby/object:Gem::Version
41
+ version: 0.0.3
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ none: false
46
+ requirements:
47
+ - - '='
48
+ - !ruby/object:Gem::Version
49
+ version: 0.0.3
50
+ description: See http://github.com/phinze/samlurai
51
+ email: paulh@instructure.com
52
+ executables: []
53
+ extensions: []
54
+ extra_rdoc_files: []
55
+ files:
56
+ - LICENSE
57
+ - README.md
58
+ - lib/samlurai/auth_request.rb
59
+ - lib/samlurai/authn_contexts.rb
60
+ - lib/samlurai/log_out_request.rb
61
+ - lib/samlurai/logout_response.rb
62
+ - lib/samlurai/meta_data.rb
63
+ - lib/samlurai/name_identifiers.rb
64
+ - lib/samlurai/response.rb
65
+ - lib/samlurai/settings.rb
66
+ - lib/samlurai/status_codes.rb
67
+ - lib/samlurai.rb
68
+ homepage: http://github.com/phinze/samlurai
69
+ licenses: []
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ! '>='
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ requirements: []
87
+ rubyforge_project:
88
+ rubygems_version: 1.8.23
89
+ signing_key:
90
+ specification_version: 3
91
+ summary: A ruby library for SAML service providers
92
+ test_files: []
93
+ has_rdoc: