spid 0.13.0 → 0.14.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
  SHA256:
3
- metadata.gz: 83dc2ec01f9adb676d89eb91c160f3aacacf30cb6473514fc5c4655b1f2c0f32
4
- data.tar.gz: ce9606fdab6a9349888111b259979269977643add8e6f68a15a26c5286d38b82
3
+ metadata.gz: 1abf8d3050f0829891601311959388cab3234dc5d75edd59e6dd19bc7de26c7f
4
+ data.tar.gz: c4668cb869d0ffc65438aa4a30ee70fcf2611c7c4b07274791d0683acf215471
5
5
  SHA512:
6
- metadata.gz: 5d5b83dcc34a604397c5d60a682081d04064777cc54cf72fc1f61e3e0a6703215856715c5bd553335ad128231ac4da0a80a2bc084063bd130860566b84369e86
7
- data.tar.gz: dcea75e1ce343c0a3e3297a02f78f889150ae0693a3a68a1258fdab1d7d24999ec99a79d1e53a1b87544fd15c144559f91f4b0f19c0633814271b35c6c0760f0
6
+ metadata.gz: f3b6676b2941c3eba7cb55f80e0a04310467a244a8c52bae620dd3c2fbcad63ab4933fb1948f80f6b6ade12502125fbddf6ff0105c2f31fec04521c0dfd1cf70
7
+ data.tar.gz: 52981e148719b87079cb6e6aba5cf583552b92306a8e9c4b10673fd699214cbaaa1967adf6c1241efa2cba6e75ec58ad8d94018b7ef434da2f151218ee37c79e
data/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.14.0] - 2018-08-30
6
+ ### Added
7
+ - IDP-Initiated SLO management
8
+ - Error Handling
9
+
5
10
  ## [0.13.0] - 2018-08-29
6
11
  ### Added
7
12
  - Validation of Response and LogoutResponse
@@ -101,7 +106,8 @@
101
106
  - Coveralls Integration
102
107
  - Rubygems version badge in README
103
108
 
104
- [Unreleased]: https://github.com/italia/spid-ruby/compare/v0.13.0...HEAD
109
+ [Unreleased]: https://github.com/italia/spid-ruby/compare/v0.14.0...HEAD
110
+ [0.14.0]: https://github.com/italia/spid-ruby/compare/v0.13.0...v0.14.0
105
111
  [0.13.0]: https://github.com/italia/spid-ruby/compare/v0.12.0...v0.13.0
106
112
  [0.12.0]: https://github.com/italia/spid-ruby/compare/v0.11.0...v0.12.0
107
113
  [0.11.0]: https://github.com/italia/spid-ruby/compare/v0.10.0...v0.11.0
data/lib/spid.rb CHANGED
@@ -71,6 +71,8 @@ module Spid # :nodoc:
71
71
  L3
72
72
  ].freeze
73
73
 
74
+ SUCCESS_CODE = "urn:oasis:names:tc:SAML:2.0:status:Success"
75
+
74
76
  ATTRIBUTES_MAP = {
75
77
  spid_code: "spidCode",
76
78
  name: "name",
@@ -4,7 +4,6 @@ require "base64"
4
4
 
5
5
  module Spid
6
6
  class IdentityProviderManager # :nodoc:
7
- extend Spid::Saml2::Utils
8
7
  include Singleton
9
8
 
10
9
  def identity_providers
@@ -40,8 +39,7 @@ module Spid
40
39
  entity_id: idp_settings[:idp_entity_id],
41
40
  sso_target_url: idp_settings[:idp_sso_target_url],
42
41
  slo_target_url: idp_settings[:idp_slo_target_url],
43
- cert_fingerprint: idp_settings[:idp_cert_fingerprint],
44
- certificate: certificate_from_encoded_der(idp_settings[:idp_cert])
42
+ certificate: idp_settings[:idp_cert]
45
43
  )
46
44
  end
47
45
 
@@ -11,11 +11,10 @@ module Spid
11
11
 
12
12
  def call(env)
13
13
  @sso = LoginEnv.new(env)
14
- if @sso.valid_request?
15
- @sso.response
16
- else
17
- app.call(env)
18
- end
14
+
15
+ return @sso.response if @sso.valid_request?
16
+
17
+ app.call(env)
19
18
  end
20
19
 
21
20
  class LoginEnv # :nodoc:
@@ -26,7 +25,12 @@ module Spid
26
25
  @request = ::Rack::Request.new(env)
27
26
  end
28
27
 
28
+ def session
29
+ request.session["spid"]
30
+ end
31
+
29
32
  def response
33
+ session["sso_request_uuid"] = sso_request.uuid
30
34
  [
31
35
  302,
32
36
  { "Location" => sso_url },
@@ -35,11 +39,18 @@ module Spid
35
39
  end
36
40
 
37
41
  def sso_url
38
- Spid::Sso::Request.new(
39
- idp_name: idp_name,
40
- relay_state: relay_state,
41
- attribute_index: attribute_consuming_service_index
42
- ).url
42
+ sso_request.url
43
+ end
44
+
45
+ def sso_request
46
+ @sso_request ||=
47
+ begin
48
+ Spid::Sso::Request.new(
49
+ idp_name: idp_name,
50
+ relay_state: relay_state,
51
+ attribute_index: attribute_consuming_service_index
52
+ )
53
+ end
43
54
  end
44
55
 
45
56
  def valid_request?
@@ -11,11 +11,10 @@ module Spid
11
11
 
12
12
  def call(env)
13
13
  @slo = LogoutEnv.new(env)
14
- if @slo.valid_request?
15
- @slo.response
16
- else
17
- app.call(env)
18
- end
14
+
15
+ return @slo.response if @slo.valid_request?
16
+
17
+ app.call(env)
19
18
  end
20
19
 
21
20
  class LogoutEnv # :nodoc:
@@ -27,6 +26,7 @@ module Spid
27
26
  end
28
27
 
29
28
  def response
29
+ session["slo_request_uuid"] = slo_request.uuid
30
30
  [
31
31
  302,
32
32
  { "Location" => slo_url },
@@ -34,12 +34,23 @@ module Spid
34
34
  ]
35
35
  end
36
36
 
37
+ def session
38
+ request.session["spid"]
39
+ end
40
+
37
41
  def slo_url
38
- Spid::Slo::Request.new(
39
- idp_name: idp_name,
40
- relay_state: relay_state,
41
- session_index: spid_session["session-index"]
42
- ).url
42
+ slo_request.url
43
+ end
44
+
45
+ def slo_request
46
+ @slo_request ||=
47
+ begin
48
+ Spid::Slo::Request.new(
49
+ idp_name: idp_name,
50
+ relay_state: relay_state,
51
+ session_index: spid_session["session_index"]
52
+ )
53
+ end
43
54
  end
44
55
 
45
56
  def valid_request?
data/lib/spid/rack/slo.rb CHANGED
@@ -11,11 +11,10 @@ module Spid
11
11
 
12
12
  def call(env)
13
13
  @slo = SloEnv.new(env)
14
- if @slo.valid_request?
15
- @slo.response
16
- else
17
- app.call(env)
18
- end
14
+
15
+ return @slo.response if @slo.valid_request?
16
+
17
+ app.call(env)
19
18
  end
20
19
 
21
20
  class SloEnv # :nodoc:
@@ -27,12 +26,24 @@ module Spid
27
26
  @request = ::Rack::Request.new(env)
28
27
  end
29
28
 
29
+ def session
30
+ request.session["spid"]
31
+ end
32
+
30
33
  def clear_session
31
34
  request.session["spid"] = {}
32
35
  end
33
36
 
37
+ def store_session_failure
38
+ session["errors"] = slo_response.errors
39
+ end
40
+
34
41
  def response
35
- clear_session
42
+ if valid_response?
43
+ clear_session
44
+ else
45
+ store_session_failure
46
+ end
36
47
  [
37
48
  302,
38
49
  { "Location" => relay_state },
@@ -67,9 +78,25 @@ module Spid
67
78
  request.path == Spid.configuration.slo_path
68
79
  end
69
80
 
81
+ def valid_response?
82
+ slo_response.valid?
83
+ end
84
+
70
85
  def valid_request?
71
86
  valid_path? && valid_http_verb?
72
87
  end
88
+
89
+ def saml_response
90
+ request.params["SAMLResponse"]
91
+ end
92
+
93
+ def slo_response
94
+ @slo_response ||= ::Spid::Slo::Response.new(
95
+ body: saml_response,
96
+ request_uuid: session["slo_request_uuid"],
97
+ session_index: session["session_index"]
98
+ )
99
+ end
73
100
  end
74
101
  end
75
102
  end
data/lib/spid/rack/sso.rb CHANGED
@@ -12,11 +12,9 @@ module Spid
12
12
  def call(env)
13
13
  @sso = SsoEnv.new(env)
14
14
 
15
- if @sso.valid_request?
16
- @sso.response
17
- else
18
- app.call(env)
19
- end
15
+ return @sso.response if @sso.valid_request?
16
+
17
+ app.call(env)
20
18
  end
21
19
 
22
20
  class SsoEnv # :nodoc:
@@ -28,15 +26,26 @@ module Spid
28
26
  @request = ::Rack::Request.new(env)
29
27
  end
30
28
 
31
- def store_session
32
- request.session["spid"] = {
33
- "attributes" => sso_response.attributes,
34
- "session_index" => sso_response.session_index
35
- }
29
+ def session
30
+ request.session["spid"]
31
+ end
32
+
33
+ def store_session_success
34
+ session["attributes"] = sso_response.attributes
35
+ session["session_index"] = sso_response.session_index
36
+ session.delete("sso_request_uuid")
37
+ end
38
+
39
+ def store_session_failure
40
+ session["errors"] = sso_response.errors
36
41
  end
37
42
 
38
43
  def response
39
- store_session
44
+ if valid_response?
45
+ store_session_success
46
+ else
47
+ store_session_failure
48
+ end
40
49
  [
41
50
  302,
42
51
  { "Location" => relay_state },
@@ -75,12 +84,19 @@ module Spid
75
84
  request.path == Spid.configuration.acs_path
76
85
  end
77
86
 
87
+ def valid_response?
88
+ sso_response.valid?
89
+ end
90
+
78
91
  def valid_request?
79
92
  valid_path? && valid_http_verb?
80
93
  end
81
94
 
82
95
  def sso_response
83
- ::Spid::Sso::Response.new(body: saml_response)
96
+ @sso_response ||= ::Spid::Sso::Response.new(
97
+ body: saml_response,
98
+ request_uuid: session["sso_request_uuid"]
99
+ )
84
100
  end
85
101
  end
86
102
  end
data/lib/spid/saml2.rb CHANGED
@@ -6,7 +6,9 @@ require "spid/saml2/settings"
6
6
  require "spid/saml2/authn_request"
7
7
  require "spid/saml2/response"
8
8
  require "spid/saml2/logout_request"
9
+ require "spid/saml2/idp_logout_request"
9
10
  require "spid/saml2/logout_response"
11
+ require "spid/saml2/idp_logout_response"
10
12
  require "spid/saml2/sp_metadata"
11
13
  require "spid/saml2/utils"
12
14
  require "spid/saml2/idp_metadata_parser"
@@ -6,12 +6,11 @@ module Spid
6
6
  module Saml2
7
7
  class AuthnRequest # :nodoc:
8
8
  attr_reader :document
9
- attr_reader :uuid
10
9
  attr_reader :settings
11
10
 
12
11
  def initialize(uuid: nil, settings:)
13
12
  @document = REXML::Document.new
14
- @uuid = uuid || SecureRandom.uuid
13
+ @uuid = uuid
15
14
  @settings = settings
16
15
  end
17
16
 
@@ -39,7 +38,7 @@ module Spid
39
38
  attributes = {
40
39
  "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
41
40
  "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
42
- "ID" => "_#{uuid}",
41
+ "ID" => uuid,
43
42
  "Version" => "2.0",
44
43
  "IssueInstant" => issue_instant,
45
44
  "Destination" => settings.idp_sso_target_url,
@@ -100,6 +99,10 @@ module Spid
100
99
  def issue_instant
101
100
  @issue_instant ||= Time.now.utc.iso8601
102
101
  end
102
+
103
+ def uuid
104
+ @uuid ||= "_#{SecureRandom.uuid}"
105
+ end
103
106
  end
104
107
  end
105
108
  end
@@ -7,26 +7,20 @@ module Spid
7
7
  attr_reader :entity_id
8
8
  attr_reader :sso_target_url
9
9
  attr_reader :slo_target_url
10
- attr_reader :cert_fingerprint
11
10
  attr_reader :certificate
12
-
13
- # rubocop:disable Metrics/ParameterLists
14
11
  def initialize(
15
12
  name:,
16
13
  entity_id:,
17
14
  sso_target_url:,
18
15
  slo_target_url:,
19
- cert_fingerprint:,
20
16
  certificate:
21
17
  )
22
18
  @name = name
23
19
  @entity_id = entity_id
24
20
  @sso_target_url = sso_target_url
25
21
  @slo_target_url = slo_target_url
26
- @cert_fingerprint = cert_fingerprint
27
22
  @certificate = certificate
28
23
  end
29
- # rubocop:enable Metrics/ParameterLists
30
24
  end
31
25
  end
32
26
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spid
4
+ module Saml2
5
+ class IdpLogoutRequest # :nodoc:
6
+ attr_reader :saml_message
7
+ attr_reader :document
8
+
9
+ def initialize(saml_message:)
10
+ @saml_message = saml_message
11
+ @document = REXML::Document.new(@saml_message)
12
+ end
13
+
14
+ def id
15
+ document.elements["/samlp:LogoutRequest/@ID"]&.value
16
+ end
17
+
18
+ def destination
19
+ document.elements["/samlp:LogoutRequest/@Destination"]&.value
20
+ end
21
+
22
+ def issue_instant
23
+ document.elements["/samlp:LogoutRequest/@IssueInstant"]&.value
24
+ end
25
+
26
+ def issuer_name_qualifier
27
+ document.elements[
28
+ "/samlp:LogoutRequest/saml:Issuer/@NameQualifier"
29
+ ]&.value
30
+ end
31
+
32
+ def name_id_name_qualifier
33
+ document.elements[
34
+ "/samlp:LogoutRequest/saml:NameID/@NameQualifier"
35
+ ]&.value
36
+ end
37
+
38
+ def issuer
39
+ document.elements["/samlp:LogoutRequest/saml:Issuer/text()"]&.value
40
+ end
41
+
42
+ def session_index
43
+ document.elements[
44
+ "/samlp:LogoutRequest/saml:SessionIndex/text()"
45
+ ]&.value
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spid
4
+ module Saml2
5
+ class IdpLogoutResponse # :nodoc:
6
+ attr_reader :document
7
+ attr_reader :settings
8
+ attr_reader :uuid
9
+ attr_reader :issue_instant
10
+ attr_reader :request_uuid
11
+
12
+ def initialize(settings:, request_uuid:, uuid: nil)
13
+ @document = REXML::Document.new
14
+ @settings = settings
15
+ @uuid = uuid || SecureRandom.uuid
16
+ @issue_instant = Time.now.utc.iso8601
17
+ @request_uuid = request_uuid
18
+ end
19
+
20
+ def to_saml
21
+ document.add_element(logout_response)
22
+ document.to_s
23
+ end
24
+
25
+ def logout_response
26
+ @logout_response ||=
27
+ begin
28
+ element = REXML::Element.new("samlp:LogoutResponse")
29
+ element.add_attributes(logout_response_attributes)
30
+ element.add_element(issuer)
31
+ element.add_element(status)
32
+ element
33
+ end
34
+ end
35
+
36
+ def logout_response_attributes
37
+ @logout_response_attributes ||= {
38
+ "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
39
+ "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
40
+ "IssueInstant" => issue_instant,
41
+ "InResponseTo" => request_uuid,
42
+ "Destination" => settings.idp_slo_target_url,
43
+ "ID" => "_#{uuid}"
44
+ }
45
+ end
46
+
47
+ def issuer
48
+ @issuer ||=
49
+ begin
50
+ element = REXML::Element.new("saml:Issuer")
51
+ element.add_attributes(
52
+ "Format" => "urn:oasis:names:tc:SAML:2.0:nameid-format:entity",
53
+ "NameQualifier" => settings.sp_entity_id
54
+ )
55
+ element.text = settings.sp_entity_id
56
+ element
57
+ end
58
+ end
59
+
60
+ def status
61
+ @status ||=
62
+ begin
63
+ element = REXML::Element.new("saml:Status")
64
+ element.add_element(status_code)
65
+ element
66
+ end
67
+ end
68
+
69
+ def status_code
70
+ @status_code ||=
71
+ begin
72
+ element = REXML::Element.new("saml:StatusCode")
73
+ element.text = "urn:oasis:names:tc:SAML:2.0:status:Success"
74
+ element
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -22,37 +22,24 @@ module Spid
22
22
 
23
23
  attr_reader :document
24
24
  attr_reader :response
25
- attr_reader :options
26
25
 
27
26
  # Parse the Identity Provider metadata and return the results as Hash
28
27
  #
29
28
  # @param idp_metadata [String]
30
29
  #
31
- # @param options [Hash] options used for parsing the metadata and the returned Settings instance
32
- # @option options [Array<String>, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used.
33
- # @option options [Array<String>, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used.
34
- # @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When ommitted, the first entity descriptor is used.
35
- #
36
30
  # @return [Hash]
37
- def parse_to_hash(idp_metadata, options = {})
31
+ def parse_to_hash(idp_metadata)
38
32
  @document = REXML::Document.new(idp_metadata)
39
- @options = options
40
33
  @entity_descriptor = nil
41
34
  @certificates = nil
42
- @fingerprint = nil
43
-
44
- if idpsso_descriptor.nil?
45
- raise ArgumentError.new("idp_metadata must contain an IDPSSODescriptor element")
46
- end
47
35
 
48
36
  {
49
37
  :idp_entity_id => idp_entity_id,
50
38
  :name_identifier_format => idp_name_id_format,
51
- :idp_sso_target_url => single_signon_service_url(options),
52
- :idp_slo_target_url => single_logout_service_url(options),
39
+ :idp_sso_target_url => single_signon_service_url,
40
+ :idp_slo_target_url => single_logout_service_url,
53
41
  :idp_attribute_names => attribute_names,
54
42
  :idp_cert => nil,
55
- :idp_cert_fingerprint => nil,
56
43
  :idp_cert_multi => nil
57
44
  }.tap do |response_hash|
58
45
  merge_certificates_into(response_hash) unless certificates.nil?
@@ -64,28 +51,11 @@ module Spid
64
51
  def entity_descriptor
65
52
  @entity_descriptor ||= REXML::XPath.first(
66
53
  document,
67
- entity_descriptor_path,
54
+ "//md:EntityDescriptor",
68
55
  namespace
69
56
  )
70
57
  end
71
58
 
72
- def entity_descriptor_path
73
- path = "//md:EntityDescriptor"
74
- entity_id = options[:entity_id]
75
- return path unless entity_id
76
- path << "[@entityID=\"#{entity_id}\"]"
77
- end
78
-
79
- def idpsso_descriptor
80
- unless entity_descriptor.nil?
81
- return REXML::XPath.first(
82
- entity_descriptor,
83
- "md:IDPSSODescriptor",
84
- namespace
85
- )
86
- end
87
- end
88
-
89
59
  # @return [String|nil] IdP Entity ID value if exists
90
60
  #
91
61
  def idp_entity_id
@@ -103,28 +73,21 @@ module Spid
103
73
  element_text(node)
104
74
  end
105
75
 
106
- # @param binding_priority [Array]
107
76
  # @return [String|nil] SingleSignOnService binding if exists
108
77
  #
109
- def single_signon_service_binding(binding_priority = nil)
78
+ def single_signon_service_binding
110
79
  nodes = REXML::XPath.match(
111
80
  entity_descriptor,
112
81
  "md:IDPSSODescriptor/md:SingleSignOnService/@Binding",
113
82
  namespace
114
83
  )
115
- if binding_priority
116
- values = nodes.map(&:value)
117
- binding_priority.detect{ |binding| values.include? binding }
118
- else
119
- nodes.first.value if nodes.any?
120
- end
84
+ nodes.first.value if nodes.any?
121
85
  end
122
86
 
123
- # @param options [Hash]
124
87
  # @return [String|nil] SingleSignOnService endpoint if exists
125
88
  #
126
- def single_signon_service_url(options = {})
127
- binding = single_signon_service_binding(options[:sso_binding])
89
+ def single_signon_service_url
90
+ binding = single_signon_service_binding
128
91
  unless binding.nil?
129
92
  node = REXML::XPath.first(
130
93
  entity_descriptor,
@@ -135,28 +98,21 @@ module Spid
135
98
  end
136
99
  end
137
100
 
138
- # @param binding_priority [Array]
139
101
  # @return [String|nil] SingleLogoutService binding if exists
140
102
  #
141
- def single_logout_service_binding(binding_priority = nil)
103
+ def single_logout_service_binding
142
104
  nodes = REXML::XPath.match(
143
105
  entity_descriptor,
144
106
  "md:IDPSSODescriptor/md:SingleLogoutService/@Binding",
145
107
  namespace
146
108
  )
147
- if binding_priority
148
- values = nodes.map(&:value)
149
- binding_priority.detect{ |binding| values.include? binding }
150
- else
151
- nodes.first.value if nodes.any?
152
- end
109
+ nodes.first.value if nodes.any?
153
110
  end
154
111
 
155
- # @param options [Hash]
156
112
  # @return [String|nil] SingleLogoutService endpoint if exists
157
113
  #
158
- def single_logout_service_url(options = {})
159
- binding = single_logout_service_binding(options[:slo_binding])
114
+ def single_logout_service_url
115
+ binding = single_logout_service_binding
160
116
  unless binding.nil?
161
117
  node = REXML::XPath.first(
162
118
  entity_descriptor,
@@ -204,20 +160,6 @@ module Spid
204
160
  end
205
161
  end
206
162
 
207
- # @return [String|nil] the fingerpint of the X509Certificate if it exists
208
- #
209
- def fingerprint(certificate, fingerprint_algorithm = ::Spid::SHA256)
210
- @fingerprint ||= begin
211
- if certificate
212
- cert = OpenSSL::X509::Certificate.new(Base64.decode64(certificate))
213
-
214
- algorithm = fingerprint_algorithm || ::Spid::SHA256
215
- fingerprint_alg = ::Spid::SIGNATURE_ALGORITHMS[algorithm]
216
- fingerprint_alg.hexdigest(cert.to_der).upcase.scan(/../).join(":")
217
- end
218
- end
219
- end
220
-
221
163
  # @return [Array] the names of all SAML attributes if any exist
222
164
  #
223
165
  def attribute_names
@@ -239,40 +181,14 @@ module Spid
239
181
  end
240
182
 
241
183
  def merge_certificates_into(parsed_metadata)
242
- if (certificates.size == 1 &&
243
- (certificates_has_one('signing') || certificates_has_one('encryption'))) ||
244
- (certificates_has_one('signing') && certificates_has_one('encryption') &&
245
- certificates["signing"][0] == certificates["encryption"][0])
246
-
247
184
  if certificates.key?("signing")
248
- parsed_metadata[:idp_cert] = certificates["signing"][0]
249
- parsed_metadata[:idp_cert_fingerprint] = fingerprint(
250
- parsed_metadata[:idp_cert],
251
- parsed_metadata[:idp_cert_fingerprint_algorithm]
252
- )
185
+ certificate = certificates["signing"][0]
253
186
  else
254
- parsed_metadata[:idp_cert] = certificates["encryption"][0]
255
- parsed_metadata[:idp_cert_fingerprint] = fingerprint(
256
- parsed_metadata[:idp_cert],
257
- parsed_metadata[:idp_cert_fingerprint_algorithm]
258
- )
187
+ certificate = certificates["encryption"][0]
259
188
  end
260
- else
261
- # symbolize keys of certificates and pass it on
262
- parsed_metadata[:idp_cert_multi] = Hash[certificates.map { |k, v| [k.to_sym, v] }]
263
- end
264
- end
265
-
266
- def certificates_has_one(key)
267
- certificates.key?(key) && certificates[key].size == 1
268
- end
269
-
270
- def merge_parsed_metadata_into(settings, parsed_metadata)
271
- parsed_metadata.each do |key, value|
272
- settings.send("#{key}=".to_sym, value)
273
- end
274
-
275
- settings
189
+ parsed_metadata[:idp_cert] = OpenSSL::X509::Certificate.new(
190
+ Base64.decode64(certificate)
191
+ )
276
192
  end
277
193
 
278
194
  def element_text(element)
@@ -4,15 +4,16 @@ module Spid
4
4
  module Saml2
5
5
  class LogoutRequest # :nodoc:
6
6
  attr_reader :settings
7
- attr_reader :uuid
8
7
  attr_reader :document
9
8
  attr_reader :session_index
9
+ attr_reader :issue_instant
10
10
 
11
11
  def initialize(uuid: nil, settings:, session_index:)
12
12
  @settings = settings
13
13
  @document = REXML::Document.new
14
14
  @session_index = session_index
15
- @uuid = uuid || SecureRandom.uuid
15
+ @uuid = uuid
16
+ @issue_instant = Time.now.utc.iso8601
16
17
  end
17
18
 
18
19
  def to_saml
@@ -78,10 +79,8 @@ module Spid
78
79
  end
79
80
  end
80
81
 
81
- private
82
-
83
- def issue_instant
84
- @issue_instant ||= Time.now.utc.iso8601
82
+ def uuid
83
+ @uuid ||= "_#{SecureRandom.uuid}"
85
84
  end
86
85
  end
87
86
  end
@@ -5,25 +5,52 @@ module Spid
5
5
  class LogoutResponseValidator # :nodoc:
6
6
  attr_reader :response
7
7
  attr_reader :settings
8
+ attr_reader :request_uuid
9
+ attr_reader :errors
8
10
 
9
- def initialize(response:, settings:)
11
+ def initialize(response:, settings:, request_uuid:)
10
12
  @response = response
11
13
  @settings = settings
14
+ @request_uuid = request_uuid
15
+ @errors = {}
12
16
  end
13
17
 
14
18
  def call
15
19
  [
20
+ matches_request_uuid,
16
21
  destination,
17
22
  issuer
18
23
  ].all?
19
24
  end
20
25
 
26
+ def matches_request_uuid
27
+ return true if response.in_response_to == request_uuid
28
+
29
+ @errors["request_uuid_mismatch"] =
30
+ "Request uuid not belongs to current session"
31
+ false
32
+ end
33
+
21
34
  def destination
22
- response.destination == settings.sp_slo_service_url
35
+ return true if response.destination == settings.sp_slo_service_url
36
+
37
+ @errors["destination"] =
38
+ begin
39
+ "Response Destination is '#{response.destination}'" \
40
+ " but was expected '#{settings.sp_slo_service_url}'"
41
+ end
42
+ false
23
43
  end
24
44
 
25
45
  def issuer
26
- response.issuer == settings.idp_entity_id
46
+ return true if response.issuer == settings.idp_entity_id
47
+
48
+ @errors["issuer"] =
49
+ begin
50
+ "Response Issuer is '#{response.issuer}'" \
51
+ " but was expected '#{settings.idp_entity_id}'"
52
+ end
53
+ false
27
54
  end
28
55
  end
29
56
  end
@@ -19,6 +19,10 @@ module Spid
19
19
  document.elements["/samlp:Response/saml:Issuer/text()"]&.value
20
20
  end
21
21
 
22
+ def in_response_to
23
+ document.elements["/samlp:Response/@InResponseTo"]&.value
24
+ end
25
+
22
26
  def name_id
23
27
  document.elements[
24
28
  "/samlp:Response/saml:Assertion/saml:Subject/saml:NameID/text()"
@@ -71,6 +75,26 @@ module Spid
71
75
  document.elements[xpath]&.value
72
76
  end
73
77
 
78
+ def status_code
79
+ document.elements[
80
+ "/samlp:Response/samlp:Status/samlp:StatusCode/@Value"
81
+ ]&.value
82
+ end
83
+
84
+ def status_message
85
+ document.elements[
86
+ "/samlp:Response/samlp:Status/samlp:StatusCode/" \
87
+ "samlp:StatusMessage/@Value"
88
+ ]&.value
89
+ end
90
+
91
+ def status_detail
92
+ document.elements[
93
+ "/samlp:Response/samlp:Status/samlp:StatusCode/" \
94
+ "samlp:StatusDetail/@Value"
95
+ ]&.value
96
+ end
97
+
74
98
  def attributes
75
99
  main_xpath = "/samlp:Response/saml:Assertion/saml:AttributeStatement"
76
100
  main_xpath = "#{main_xpath}/saml:Attribute"
@@ -7,49 +7,106 @@ module Spid
7
7
  class ResponseValidator # :nodoc:
8
8
  attr_reader :response
9
9
  attr_reader :settings
10
+ attr_reader :errors
11
+ attr_reader :request_uuid
10
12
 
11
- def initialize(response:, settings:)
13
+ def initialize(response:, settings:, request_uuid:)
12
14
  @response = response
13
15
  @settings = settings
16
+ @request_uuid = request_uuid
17
+ @errors = {}
14
18
  end
15
19
 
16
20
  def call
17
21
  [
22
+ matches_request_uuid,
18
23
  issuer,
19
24
  certificate,
20
25
  destination,
21
26
  conditions,
22
27
  audience,
23
- signature
28
+ signature,
29
+ success?
24
30
  ].all?
25
31
  end
26
32
 
33
+ def matches_request_uuid
34
+ return true if response.in_response_to == request_uuid
35
+
36
+ @errors["request_uuid_mismatch"] =
37
+ "Request uuid not belongs to current session"
38
+ false
39
+ end
40
+
41
+ def success?
42
+ return true if response.status_code == Spid::SUCCESS_CODE
43
+
44
+ @errors["authentication"] = {
45
+ "status_message" => response.status_message,
46
+ "status_detail" => response.status_detail
47
+ }
48
+ false
49
+ end
50
+
27
51
  def issuer
28
- response.assertion_issuer == settings.idp_entity_id
52
+ return true if response.assertion_issuer == settings.idp_entity_id
53
+
54
+ @errors["issuer"] =
55
+ begin
56
+ "Response Issuer is '#{response.assertion_issuer}'" \
57
+ " but was expected '#{settings.idp_entity_id}'"
58
+ end
59
+ false
29
60
  end
30
61
 
31
62
  def certificate
32
- response.certificate.to_der == settings.idp_certificate.to_der
63
+ if response.certificate.to_der == settings.idp_certificate.to_der
64
+ return true
65
+ end
66
+
67
+ @errors["certificate"] = "Certificates mismatch"
68
+ false
33
69
  end
34
70
 
35
71
  def destination
36
- response.destination == settings.sp_acs_url
72
+ return true if response.destination == settings.sp_acs_url
73
+
74
+ @errors["destination"] =
75
+ begin
76
+ "Response Destination is '#{response.destination}'" \
77
+ " but was expected '#{settings.sp_acs_url}'"
78
+ end
79
+ false
37
80
  end
38
81
 
39
82
  def conditions
40
83
  time = Time.now.iso8601
41
84
 
42
- response.conditions_not_before <= time &&
43
- response.conditions_not_on_or_after > time
85
+ if response.conditions_not_before <= time &&
86
+ response.conditions_not_on_or_after > time
87
+ return true
88
+ end
89
+
90
+ @errors["conditions"] = "Response was out of time"
91
+ false
44
92
  end
45
93
 
46
94
  def audience
47
- response.audience == settings.sp_entity_id
95
+ return true if response.audience == settings.sp_entity_id
96
+ @errors["audience"] =
97
+ begin
98
+ "Response Audience is '#{response.audience}'" \
99
+ " but was expected '#{settings.sp_entity_id}'"
100
+ end
101
+ false
48
102
  end
49
103
 
50
104
  def signature
51
105
  signed_document = Xmldsig::SignedDocument.new(response.saml_message)
52
- signed_document.validate(response.certificate)
106
+ return true if signed_document.validate(response.certificate)
107
+
108
+ @errors["signature"] = "Signature mismatch"
109
+ false
53
110
  end
54
111
  end
55
112
  end
@@ -23,6 +23,10 @@ module Spid
23
23
  ].join("?")
24
24
  end
25
25
 
26
+ def uuid
27
+ logout_request.uuid
28
+ end
29
+
26
30
  def query_params_signer
27
31
  @query_params_signer ||=
28
32
  begin
@@ -5,23 +5,31 @@ module Spid
5
5
  class Response # :nodoc:
6
6
  attr_reader :body
7
7
  attr_reader :session_index
8
- attr_reader :matches_request_id
8
+ attr_reader :request_uuid
9
9
 
10
- def initialize(body:, session_index:, matches_request_id:)
10
+ def initialize(body:, session_index:, request_uuid:)
11
11
  @body = body
12
12
  @session_index = session_index
13
- @matches_request_id = matches_request_id
13
+ @request_uuid = request_uuid
14
14
  end
15
15
 
16
16
  def valid?
17
- Spid::Saml2::LogoutResponseValidator.new(
18
- response: saml_response,
19
- settings: settings
20
- ).call
17
+ validator.call
21
18
  end
22
19
 
23
20
  def errors
24
- []
21
+ validator.errors
22
+ end
23
+
24
+ def validator
25
+ @validator ||=
26
+ begin
27
+ Spid::Saml2::LogoutResponseValidator.new(
28
+ response: saml_response,
29
+ settings: settings,
30
+ request_uuid: request_uuid
31
+ )
32
+ end
25
33
  end
26
34
 
27
35
  def identity_provider
@@ -32,6 +32,10 @@ module Spid
32
32
  ].join("?")
33
33
  end
34
34
 
35
+ def uuid
36
+ authn_request.uuid
37
+ end
38
+
35
39
  def query_params_signer
36
40
  @query_params_signer ||=
37
41
  begin
@@ -10,23 +10,35 @@ module Spid
10
10
 
11
11
  attr_reader :body
12
12
  attr_reader :saml_message
13
+ attr_reader :request_uuid
13
14
 
14
- def initialize(body:)
15
+ def initialize(body:, request_uuid:)
15
16
  @body = body
16
17
  @saml_message = decode_and_inflate(body)
18
+ @request_uuid = request_uuid
17
19
  end
18
20
 
19
21
  def valid?
20
- Spid::Saml2::ResponseValidator.new(
21
- response: saml_response,
22
- settings: settings
23
- ).call
22
+ validator.call
23
+ end
24
+
25
+ def validator
26
+ @validator ||=
27
+ Spid::Saml2::ResponseValidator.new(
28
+ response: saml_response,
29
+ settings: settings,
30
+ request_uuid: request_uuid
31
+ )
24
32
  end
25
33
 
26
34
  def issuer
27
35
  saml_response.assertion_issuer
28
36
  end
29
37
 
38
+ def errors
39
+ validator.errors
40
+ end
41
+
30
42
  def attributes
31
43
  raw_attributes.each_with_object({}) do |(key, value), acc|
32
44
  acc[normalize_key(key)] = value
data/lib/spid/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Spid
4
- VERSION = "0.13.0"
4
+ VERSION = "0.14.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Librera
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-08-29 00:00:00.000000000 Z
11
+ date: 2018-08-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -324,6 +324,8 @@ files:
324
324
  - lib/spid/saml2.rb
325
325
  - lib/spid/saml2/authn_request.rb
326
326
  - lib/spid/saml2/identity_provider.rb
327
+ - lib/spid/saml2/idp_logout_request.rb
328
+ - lib/spid/saml2/idp_logout_response.rb
327
329
  - lib/spid/saml2/idp_metadata_parser.rb
328
330
  - lib/spid/saml2/logout_request.rb
329
331
  - lib/spid/saml2/logout_response.rb