samlurai 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: