ruby-saml-mod 0.1.30 → 0.2.0

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