spid 0.13.0 → 0.14.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
  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