ruby-saml-mod 0.1.30 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: de2dcfe3e3d9c2993dac7f1a06bc809fb52cfcf9
4
- data.tar.gz: 3465736cf3a7146a3abd0b573e28ddeb3003f54d
3
+ metadata.gz: c9b8339af98b853334c4cd343eff1454a9ed537a
4
+ data.tar.gz: 7702ec985b556013a1d812f646e415ec16cfe58f
5
5
  SHA512:
6
- metadata.gz: 7ad9d8d8e2c97650b4bae20abd37917c028b9c085aebd47a56e3c1b2c7aab8490db419d0f620a66622f9e0eb4cb0621f577d8f96b84b31c0d07fa07f819935f7
7
- data.tar.gz: 18d63098d79fffee7efcfe843ea5585d2c2e57ba6f92091315de12390d717b475e4eb89b32587e902ee67c3893fdf1ace5898f313d0c44d1634dc36d25dbd5e2
6
+ metadata.gz: 1e1accc268ecc2fad5023f99552d8c2a53cca7ddd548092236534bcb0d6016ec51a40d0960b8706afceacd9fe5ecfeb204751985f0b15c90e78e1bbaeeb5cade
7
+ data.tar.gz: 868e76914f77d9766ebc31449b9262c28dd689a4ea815ed23c6269ada3a0e4422ec2941e73c87bfd3e9d53671ee0e5c0c1f6b58ab2151ba969cee116a3599f7d
data/lib/onelogin/saml.rb CHANGED
@@ -35,12 +35,13 @@ module Onelogin
35
35
  }
36
36
  end
37
37
 
38
+ require 'onelogin/saml/base_assertion'
38
39
  require 'onelogin/saml/auth_request'
39
- require 'onelogin/saml/authn_contexts.rb'
40
+ require 'onelogin/saml/authn_contexts'
40
41
  require 'onelogin/saml/response'
41
42
  require 'onelogin/saml/settings'
42
43
  require 'onelogin/saml/name_identifiers'
43
44
  require 'onelogin/saml/status_codes'
44
45
  require 'onelogin/saml/meta_data'
45
- require 'onelogin/saml/log_out_request'
46
+ require 'onelogin/saml/logout_request'
46
47
  require 'onelogin/saml/logout_response'
@@ -1,53 +1,47 @@
1
1
  module Onelogin::Saml
2
- class AuthRequest
3
-
4
- attr_reader :settings, :id, :request_xml, :forward_url
5
-
6
- def initialize(settings)
7
- @settings = settings
2
+ class AuthRequest < BaseAssertion
3
+ attr_accessor :requested_authn_context,
4
+ :assertion_consumer_service_url,
5
+ :name_identifier_format
6
+
7
+ def self.parse(raw_assertion, settings = nil)
8
+ raise NotImplementedError
8
9
  end
9
-
10
- def self.create(settings)
11
- ar = AuthRequest.new(settings)
12
- ar.generate_request
10
+
11
+ def self.generate(settings)
12
+ super(settings, {
13
+ destination: settings.idp_sso_target_url,
14
+ requested_authn_context: settings.requested_authn_context,
15
+ assertion_consumer_service_url: Array(settings.assertion_consumer_service_url).first,
16
+ name_identifier_format: settings.name_identifier_format
17
+ })
13
18
  end
14
-
15
- def generate_request
16
- @id = Onelogin::Saml::AuthRequest.generate_unique_id(42)
17
- issue_instant = Onelogin::Saml::AuthRequest.get_timestamp
18
19
 
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"
20
+ def generate
21
+ if self.requested_authn_context
22
+ xml = <<-XML
23
+ <samlp:RequestedAuthnContext Comparison="exact">
24
+ <saml:AuthnContextClassRef>#{self.requested_authn_context}</saml:AuthnContextClassRef>
25
+ </samlp:RequestedAuthnContext>
26
+ XML
28
27
  end
29
-
30
- @request_xml += "</samlp:AuthnRequest>"
31
28
 
32
- deflated_request = Zlib::Deflate.deflate(@request_xml, 9)[2..-5]
33
- base64_request = Base64.strict_encode64(deflated_request)
34
- encoded_request = CGI.escape(base64_request)
29
+ <<-XML
30
+ <samlp:AuthnRequest
31
+ xmlns:samlp="#{Onelogin::NAMESPACES['samlp']}"
32
+ xmlns:saml="#{Onelogin::NAMESPACES['saml']}"
33
+ ID="#{self.id}"
34
+ Version="2.0"
35
+ ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
36
+ AssertionConsumerServiceURL=\"#{self.assertion_consumer_service_url}\"
37
+ IssueInstant="#{self.issue_instant}">
35
38
 
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")
39
+ <saml:Issuer>#{self.issuer}</saml:Issuer>
40
+ <samlp:NameIDPolicy Format="#{self.name_identifier_format}" AllowCreate="true"></samlp:NameIDPolicy>
41
+
42
+ #{xml}
43
+ </samlp:AuthnRequest>
44
+ XML
51
45
  end
52
46
  end
53
47
  end
@@ -0,0 +1,159 @@
1
+ module Onelogin::Saml
2
+ class BaseAssertion
3
+ attr_accessor :settings
4
+ attr_reader :xml
5
+ attr_writer :id,
6
+ :issuer,
7
+ :issue_instant,
8
+ :destination,
9
+ :in_response_to,
10
+ :base64_assertion
11
+ def id
12
+ @id ||= root_attribute_value('ID')
13
+ end
14
+
15
+ def issue_instant
16
+ @issue_instant ||= root_attribute_value('IssueInstance')
17
+ end
18
+
19
+ def issuer
20
+ @issuer ||= node_content('saml:Issuer')
21
+ end
22
+
23
+ def destination
24
+ @destination ||= root_attribute_value('Destination')
25
+ end
26
+
27
+ def in_response_to
28
+ @in_response_to ||= root_attribute_value('InResponseTo')
29
+ end
30
+
31
+ def document
32
+ @document ||= LibXML::XML::Document.string(xml) if xml
33
+ end
34
+
35
+ def xml=(value)
36
+ @xml = value.strip
37
+ end
38
+
39
+ def base64_assertion
40
+ @base64_assertion ||= begin
41
+ deflated_assertion = Zlib::Deflate.deflate(self.xml, 9)[2..-5]
42
+ Base64.strict_encode64(deflated_assertion)
43
+ end
44
+ end
45
+
46
+ def assertion_type
47
+ return unless document
48
+
49
+ if document.root.name =~ /Request$/
50
+ :request
51
+ elsif document.root.name =~ /Response$/
52
+ :response
53
+ end
54
+ end
55
+
56
+ def process(settings)
57
+ # TODO: Verify signature and decrypt.
58
+ end
59
+
60
+ def self.parse(raw_assertion, settings = nil)
61
+ assertion = new
62
+ assertion.base64_assertion = raw_assertion
63
+
64
+ decoded_xml = Base64.decode64(raw_assertion)
65
+ zlib = Zlib::Inflate.new(-Zlib::MAX_WBITS)
66
+
67
+ assertion.xml = zlib.inflate(decoded_xml)
68
+
69
+ assertion.process(settings) if settings
70
+ assertion
71
+ end
72
+
73
+ def self.generate(settings, attributes = {})
74
+ assertion = new
75
+
76
+ assertion.settings = settings
77
+ assertion.id = generate_unique_id
78
+ assertion.issue_instant = get_timestamp
79
+ assertion.issuer = settings.issuer
80
+
81
+ attributes.each do |key, value|
82
+ if assertion.respond_to? "#{key}="
83
+ assertion.send "#{key}=", value
84
+ end
85
+ end
86
+
87
+ assertion.xml = assertion.generate
88
+
89
+ assertion
90
+ end
91
+
92
+ def forward_url
93
+ @forward_url ||= begin
94
+ url, existing_query_string = destination.split('?')
95
+ query_string = query_string_append(existing_query_string, query_string_param, base64_assertion)
96
+
97
+ if settings.sign?
98
+ query_string = query_string_append(query_string, "SigAlg", "http://www.w3.org/2000/09/xmldsig#rsa-sha1")
99
+ signature = generate_signature(query_string, settings.xmlsec_privatekey)
100
+ query_string = query_string_append(query_string, "Signature", signature)
101
+ end
102
+
103
+ if settings.relay_state
104
+ query_string = query_string_append(query_string, "RelayState", settings.relay_state)
105
+ end
106
+
107
+ [url, query_string].join("?")
108
+ end
109
+ end
110
+
111
+ def generate
112
+ raise "Subclass does not implement abstract method generate."
113
+ end
114
+
115
+ def root_attribute_value(attribute)
116
+ document.root[attribute] if document
117
+ end
118
+
119
+ def node_attribute_value(xpath, attribute)
120
+ document.root.find_first(xpath, Onelogin::NAMESPACES)[attribute] rescue nil
121
+ end
122
+
123
+ def node_content(xpath)
124
+ document.root.find_first(xpath, Onelogin::NAMESPACES).content rescue nil
125
+ end
126
+
127
+ def self.generate_unique_id(length = 42)
128
+ chars = ("a".."f").to_a + ("0".."9").to_a
129
+ chars_len = chars.size
130
+ unique_id = ("a".."f").to_a[rand(6)]
131
+ 2.upto(length) { |i| unique_id << chars[rand(chars_len)] }
132
+ unique_id
133
+ end
134
+
135
+ def self.get_timestamp
136
+ Time.new.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
137
+ end
138
+
139
+ private
140
+
141
+ def query_string_param
142
+ if assertion_type == :request
143
+ 'SAMLRequest'
144
+ elsif assertion_type == :response
145
+ 'SAMLResponse'
146
+ end
147
+ end
148
+
149
+ def generate_signature(string, private_key)
150
+ pkey = OpenSSL::PKey::RSA.new(File.read(private_key))
151
+ sign = pkey.sign(OpenSSL::Digest::SHA1.new, string)
152
+ Base64.encode64(sign).gsub(/\s/, '')
153
+ end
154
+
155
+ def query_string_append(query_string, key, value)
156
+ [query_string, "#{CGI.escape(key)}=#{CGI.escape(value)}"].compact.join('&')
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,44 @@
1
+ module Onelogin::Saml
2
+ class LogoutRequest < BaseAssertion
3
+ attr_writer :name_id,
4
+ :session_index,
5
+ :name_qualifier,
6
+ :name_identifier_format
7
+
8
+ def name_id
9
+ @name_id ||= node_content('saml:NameID')
10
+ end
11
+
12
+ def name_identifier_format
13
+ @name_identifier_format ||= node_attribute_value('samlp:NameID', 'Format')
14
+ end
15
+
16
+ def name_qualifier
17
+ @name_qualifier ||= node_attribute_value('samlp:NameID', 'NameQualifier')
18
+ end
19
+
20
+ def session_index
21
+ @session_index ||= node_content('samlp:SessionIndex')
22
+ end
23
+
24
+ def self.generate(name_qualifier, name_id, session_index, settings)
25
+ super(settings, {
26
+ destination: settings.idp_slo_target_url,
27
+ name_identifier_format: settings.name_identifier_format,
28
+ name_id: name_id,
29
+ name_qualifier: name_qualifier,
30
+ session_index: session_index
31
+ })
32
+ end
33
+
34
+ def generate
35
+ <<-XML
36
+ <samlp:LogoutRequest xmlns:samlp="#{Onelogin::NAMESPACES['samlp']}" xmlns:saml="#{Onelogin::NAMESPACES['saml']}" ID="#{self.id}" Version="2.0" IssueInstant="#{self.issue_instant}" Destination="#{self.destination}">
37
+ <saml:Issuer>#{self.issuer}</saml:Issuer>
38
+ <saml:NameID NameQualifier="#{self.name_qualifier}" SPNameQualifier="#{self.issuer}" Format="#{self.name_identifier_format}">#{self.name_id}</saml:NameID>
39
+ <samlp:SessionIndex>#{self.session_index}</samlp:SessionIndex>
40
+ </samlp:LogoutRequest>
41
+ XML
42
+ end
43
+ end
44
+ end
@@ -1,38 +1,37 @@
1
1
  module Onelogin::Saml
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
2
+ class LogoutResponse < BaseAssertion
9
3
 
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)
4
+ STATUS_MESSAGE = 'Successfully Signed Out'
14
5
 
15
- @request_id = @document.find_first("/samlp:LogoutResponse", Onelogin::NAMESPACES)['ID'] rescue nil
16
- @issuer = @document.find_first("/samlp:LogoutResponse/saml:Issuer", Onelogin::NAMESPACES).content rescue nil
17
- @in_response_to = @document.find_first("/samlp:LogoutResponse", Onelogin::NAMESPACES)['InResponseTo'] rescue nil
18
- @destination = @document.find_first("/samlp:LogoutResponse", Onelogin::NAMESPACES)['Destination'] rescue nil
19
- @status_code = @document.find_first("/samlp:LogoutResponse/samlp:Status/samlp:StatusCode", Onelogin::NAMESPACES)['Value'] rescue nil
20
- @status_message = @document.find_first("/samlp:LogoutResponse/samlp:Status/samlp:StatusMessage", Onelogin::NAMESPACES).content rescue nil
6
+ attr_writer :status_code,
7
+ :status_message
21
8
 
22
- process(settings) if settings
9
+ def status_code
10
+ @status_code ||= node_attribute_value('samlp:Status/samlp:StatusCode', 'Value')
23
11
  end
24
12
 
25
- def process(settings)
26
- @settings = settings
27
- return unless @response
13
+ def status_message
14
+ @status_message ||= node_content("samlp:Status/samlp:StatusMessage")
28
15
  end
29
16
 
30
- def logger=(val)
31
- @logger = val
17
+ def self.generate(in_response_to, settings)
18
+ super(settings, in_response_to: in_response_to, destination: settings.idp_slo_target_url)
32
19
  end
33
-
20
+
21
+ def generate
22
+ <<-XML
23
+ <samlp:LogoutResponse xmlns:samlp="#{Onelogin::NAMESPACES['samlp']}" xmlns:saml="#{Onelogin::NAMESPACES['saml']}" ID="#{self.id}" Version="2.0" IssueInstant="#{self.issue_instant}" Destination="#{self.destination}" InResponseTo="#{self.in_response_to}">
24
+ <saml:Issuer>#{self.issuer}</saml:Issuer>
25
+ <samlp:Status>
26
+ <samlp:StatusCode Value="#{Onelogin::Saml::StatusCodes::SUCCESS_URI}"></samlp:StatusCode>
27
+ <samlp:StatusMessage>#{STATUS_MESSAGE}</samlp:StatusMessage>
28
+ </samlp:Status>
29
+ </samlp:LogoutResponse>
30
+ XML
31
+ end
32
+
34
33
  def success_status?
35
- @status_code == Onelogin::Saml::StatusCodes::SUCCESS_URI
34
+ self.status_code == Onelogin::Saml::StatusCodes::SUCCESS_URI
36
35
  end
37
36
  end
38
37
  end
@@ -7,6 +7,7 @@ module Onelogin::Saml
7
7
  attr_reader :status_code, :status_message
8
8
  attr_reader :in_response_to, :destination, :issuer
9
9
  attr_reader :validation_error
10
+
10
11
  def initialize(response, settings=nil)
11
12
  @response = response
12
13
 
@@ -28,6 +29,7 @@ module Onelogin::Saml
28
29
 
29
30
  def process(settings)
30
31
  @settings = settings
32
+ @logger = settings.logger
31
33
  return unless @response
32
34
 
33
35
  @in_response_to = untrusted_find_first("/samlp:Response")['InResponseTo'] rescue nil
@@ -72,10 +74,6 @@ module Onelogin::Saml
72
74
  end.flatten.compact
73
75
  end
74
76
 
75
- def logger=(val)
76
- @logger = val
77
- end
78
-
79
77
  def is_valid?
80
78
  @is_valid ||= validate
81
79
  end
@@ -1,6 +1,6 @@
1
1
  module Onelogin::Saml
2
2
  class Settings
3
-
3
+
4
4
  def initialize(atts={})
5
5
  atts.each do |key, val|
6
6
  if self.respond_to? "#{key}="
@@ -8,48 +8,54 @@ module Onelogin::Saml
8
8
  end
9
9
  end
10
10
  end
11
-
11
+
12
12
  # The URL at which the SAML assertion should be received.
13
13
  attr_accessor :assertion_consumer_service_url
14
-
14
+
15
15
  # The name of your application.
16
16
  attr_accessor :issuer
17
-
18
- #
17
+
18
+ # Logger
19
+ attr_accessor :logger
20
+
21
+ #
19
22
  attr_accessor :sp_name_qualifier
20
-
23
+
21
24
  # The IdP URL to which the authentication request should be sent.
22
25
  attr_accessor :idp_sso_target_url
23
-
26
+
24
27
  # The IdP URL to which the logout request should be sent.
25
28
  attr_accessor :idp_slo_target_url
26
-
29
+
27
30
  # The certificate fingerprint. This is provided from the identity provider when setting up the relationship.
28
31
  attr_accessor :idp_cert_fingerprint
29
-
32
+
30
33
  # Describes the format of the username required by this application.
31
34
  # For email: Onelogin::Saml::NameIdentifiers::EMAIL
32
35
  attr_accessor :name_identifier_format
33
-
36
+
34
37
  # The type of authentication requested (see Onelogin::Saml::AuthnContexts)
35
38
  attr_accessor :requested_authn_context
36
-
39
+
40
+ # The relay state to use when generating assertions.
41
+ attr_accessor :relay_state
42
+
37
43
  ## Attributes for the metadata
38
-
44
+
39
45
  # The logout url of your application
40
46
  attr_accessor :sp_slo_url
41
-
42
- # The name of the technical contact for your application
47
+
48
+ # The name of the technical contact for your application
43
49
  attr_accessor :tech_contact_name
44
-
50
+
45
51
  # The email of the technical contact for your application
46
52
  attr_accessor :tech_contact_email
47
-
53
+
48
54
  ## Attributes for xml encryption
49
55
 
50
56
  # The PEM-encoded certificate
51
57
  attr_accessor :xmlsec_certificate
52
-
58
+
53
59
  # The PEM-encoded private key
54
60
  attr_accessor :xmlsec_privatekey
55
61