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 +4 -4
- data/CHANGELOG.md +7 -1
- data/lib/spid.rb +2 -0
- data/lib/spid/identity_provider_manager.rb +1 -3
- data/lib/spid/rack/login.rb +21 -10
- data/lib/spid/rack/logout.rb +21 -10
- data/lib/spid/rack/slo.rb +33 -6
- data/lib/spid/rack/sso.rb +28 -12
- data/lib/spid/saml2.rb +2 -0
- data/lib/spid/saml2/authn_request.rb +6 -3
- data/lib/spid/saml2/identity_provider.rb +0 -6
- data/lib/spid/saml2/idp_logout_request.rb +49 -0
- data/lib/spid/saml2/idp_logout_response.rb +79 -0
- data/lib/spid/saml2/idp_metadata_parser.rb +17 -101
- data/lib/spid/saml2/logout_request.rb +5 -6
- data/lib/spid/saml2/logout_response_validator.rb +30 -3
- data/lib/spid/saml2/response.rb +24 -0
- data/lib/spid/saml2/response_validator.rb +66 -9
- data/lib/spid/slo/request.rb +4 -0
- data/lib/spid/slo/response.rb +16 -8
- data/lib/spid/sso/request.rb +4 -0
- data/lib/spid/sso/response.rb +17 -5
- data/lib/spid/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1abf8d3050f0829891601311959388cab3234dc5d75edd59e6dd19bc7de26c7f
|
4
|
+
data.tar.gz: c4668cb869d0ffc65438aa4a30ee70fcf2611c7c4b07274791d0683acf215471
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
@@ -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
|
-
|
44
|
-
certificate: certificate_from_encoded_der(idp_settings[:idp_cert])
|
42
|
+
certificate: idp_settings[:idp_cert]
|
45
43
|
)
|
46
44
|
end
|
47
45
|
|
data/lib/spid/rack/login.rb
CHANGED
@@ -11,11 +11,10 @@ module Spid
|
|
11
11
|
|
12
12
|
def call(env)
|
13
13
|
@sso = LoginEnv.new(env)
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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?
|
data/lib/spid/rack/logout.rb
CHANGED
@@ -11,11 +11,10 @@ module Spid
|
|
11
11
|
|
12
12
|
def call(env)
|
13
13
|
@slo = LogoutEnv.new(env)
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
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
|
-
|
17
|
-
|
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
|
32
|
-
request.session["spid"]
|
33
|
-
|
34
|
-
|
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
|
-
|
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(
|
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
|
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" =>
|
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
|
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
|
52
|
-
:idp_slo_target_url => single_logout_service_url
|
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
|
-
|
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
|
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
|
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
|
127
|
-
binding = single_signon_service_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
|
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
|
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
|
159
|
-
binding = single_logout_service_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
|
-
|
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
|
-
|
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
|
-
|
261
|
-
|
262
|
-
|
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
|
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
|
-
|
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
|
data/lib/spid/saml2/response.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/spid/slo/request.rb
CHANGED
data/lib/spid/slo/response.rb
CHANGED
@@ -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 :
|
8
|
+
attr_reader :request_uuid
|
9
9
|
|
10
|
-
def initialize(body:, session_index:,
|
10
|
+
def initialize(body:, session_index:, request_uuid:)
|
11
11
|
@body = body
|
12
12
|
@session_index = session_index
|
13
|
-
@
|
13
|
+
@request_uuid = request_uuid
|
14
14
|
end
|
15
15
|
|
16
16
|
def valid?
|
17
|
-
|
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
|
data/lib/spid/sso/request.rb
CHANGED
data/lib/spid/sso/response.rb
CHANGED
@@ -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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
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.
|
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-
|
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
|