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 +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
|