icn_saml_idp 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (129) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +2 -0
  3. data/LICENSE +22 -0
  4. data/README.md +238 -0
  5. data/app/controllers/saml_idp/idp_controller.rb +53 -0
  6. data/app/views/saml_idp/idp/new.html.erb +22 -0
  7. data/app/views/saml_idp/idp/saml_post.html.erb +14 -0
  8. data/lib/saml_idp.rb +92 -0
  9. data/lib/saml_idp/algorithmable.rb +19 -0
  10. data/lib/saml_idp/assertion_builder.rb +172 -0
  11. data/lib/saml_idp/attribute_decorator.rb +27 -0
  12. data/lib/saml_idp/attributeable.rb +24 -0
  13. data/lib/saml_idp/configurator.rb +48 -0
  14. data/lib/saml_idp/controller.rb +128 -0
  15. data/lib/saml_idp/default.rb +49 -0
  16. data/lib/saml_idp/encryptor.rb +86 -0
  17. data/lib/saml_idp/engine.rb +5 -0
  18. data/lib/saml_idp/hashable.rb +26 -0
  19. data/lib/saml_idp/incoming_metadata.rb +144 -0
  20. data/lib/saml_idp/logout_builder.rb +42 -0
  21. data/lib/saml_idp/logout_request_builder.rb +34 -0
  22. data/lib/saml_idp/logout_response_builder.rb +35 -0
  23. data/lib/saml_idp/metadata_builder.rb +160 -0
  24. data/lib/saml_idp/name_id_formatter.rb +68 -0
  25. data/lib/saml_idp/persisted_metadata.rb +10 -0
  26. data/lib/saml_idp/request.rb +180 -0
  27. data/lib/saml_idp/response_builder.rb +62 -0
  28. data/lib/saml_idp/saml_response.rb +79 -0
  29. data/lib/saml_idp/service_provider.rb +76 -0
  30. data/lib/saml_idp/signable.rb +131 -0
  31. data/lib/saml_idp/signature_builder.rb +42 -0
  32. data/lib/saml_idp/signed_info_builder.rb +88 -0
  33. data/lib/saml_idp/version.rb +4 -0
  34. data/lib/saml_idp/xml_security.rb +181 -0
  35. data/saml_idp.gemspec +65 -0
  36. data/spec/acceptance/acceptance_helper.rb +9 -0
  37. data/spec/acceptance/idp_controller_spec.rb +16 -0
  38. data/spec/lib/saml_idp/algorithmable_spec.rb +48 -0
  39. data/spec/lib/saml_idp/assertion_builder_spec.rb +106 -0
  40. data/spec/lib/saml_idp/attribute_decorator_spec.rb +53 -0
  41. data/spec/lib/saml_idp/configurator_spec.rb +43 -0
  42. data/spec/lib/saml_idp/controller_spec.rb +94 -0
  43. data/spec/lib/saml_idp/encryptor_spec.rb +27 -0
  44. data/spec/lib/saml_idp/logout_request_builder_spec.rb +41 -0
  45. data/spec/lib/saml_idp/logout_response_builder_spec.rb +41 -0
  46. data/spec/lib/saml_idp/metadata_builder_spec.rb +19 -0
  47. data/spec/lib/saml_idp/name_id_formatter_spec.rb +42 -0
  48. data/spec/lib/saml_idp/request_spec.rb +106 -0
  49. data/spec/lib/saml_idp/response_builder_spec.rb +42 -0
  50. data/spec/lib/saml_idp/saml_response_spec.rb +68 -0
  51. data/spec/lib/saml_idp/service_provider_spec.rb +27 -0
  52. data/spec/lib/saml_idp/signable_spec.rb +77 -0
  53. data/spec/lib/saml_idp/signature_builder_spec.rb +19 -0
  54. data/spec/lib/saml_idp/signed_info_builder_spec.rb +25 -0
  55. data/spec/rails_app/.gitignore +15 -0
  56. data/spec/rails_app/README.rdoc +261 -0
  57. data/spec/rails_app/Rakefile +7 -0
  58. data/spec/rails_app/app/assets/images/rails.png +0 -0
  59. data/spec/rails_app/app/assets/javascripts/application.js +15 -0
  60. data/spec/rails_app/app/assets/stylesheets/application.css +13 -0
  61. data/spec/rails_app/app/controllers/application_controller.rb +3 -0
  62. data/spec/rails_app/app/controllers/saml_controller.rb +8 -0
  63. data/spec/rails_app/app/controllers/saml_idp_controller.rb +11 -0
  64. data/spec/rails_app/app/helpers/application_helper.rb +2 -0
  65. data/spec/rails_app/app/mailers/.gitkeep +0 -0
  66. data/spec/rails_app/app/models/.gitkeep +0 -0
  67. data/spec/rails_app/app/views/layouts/application.html.erb +14 -0
  68. data/spec/rails_app/config.ru +4 -0
  69. data/spec/rails_app/config/application.rb +60 -0
  70. data/spec/rails_app/config/boot.rb +6 -0
  71. data/spec/rails_app/config/database.yml +25 -0
  72. data/spec/rails_app/config/environment.rb +5 -0
  73. data/spec/rails_app/config/environments/development.rb +37 -0
  74. data/spec/rails_app/config/environments/production.rb +67 -0
  75. data/spec/rails_app/config/environments/test.rb +37 -0
  76. data/spec/rails_app/config/initializers/backtrace_silencers.rb +7 -0
  77. data/spec/rails_app/config/initializers/inflections.rb +15 -0
  78. data/spec/rails_app/config/initializers/mime_types.rb +5 -0
  79. data/spec/rails_app/config/initializers/secret_token.rb +7 -0
  80. data/spec/rails_app/config/initializers/session_store.rb +8 -0
  81. data/spec/rails_app/config/initializers/wrap_parameters.rb +14 -0
  82. data/spec/rails_app/config/locales/en.yml +5 -0
  83. data/spec/rails_app/config/routes.rb +6 -0
  84. data/spec/rails_app/db/seeds.rb +7 -0
  85. data/spec/rails_app/doc/README_FOR_APP +2 -0
  86. data/spec/rails_app/lib/assets/.gitkeep +0 -0
  87. data/spec/rails_app/lib/tasks/.gitkeep +0 -0
  88. data/spec/rails_app/log/.gitkeep +0 -0
  89. data/spec/rails_app/public/404.html +26 -0
  90. data/spec/rails_app/public/422.html +26 -0
  91. data/spec/rails_app/public/500.html +25 -0
  92. data/spec/rails_app/public/favicon.ico +0 -0
  93. data/spec/rails_app/public/index.html +241 -0
  94. data/spec/rails_app/public/robots.txt +5 -0
  95. data/spec/rails_app/script/rails +6 -0
  96. data/spec/rails_app/test/fixtures/.gitkeep +0 -0
  97. data/spec/rails_app/test/functional/.gitkeep +0 -0
  98. data/spec/rails_app/test/integration/.gitkeep +0 -0
  99. data/spec/rails_app/test/performance/browsing_test.rb +12 -0
  100. data/spec/rails_app/test/test_helper.rb +13 -0
  101. data/spec/rails_app/test/unit/.gitkeep +0 -0
  102. data/spec/rails_app/vendor/assets/javascripts/.gitkeep +0 -0
  103. data/spec/rails_app/vendor/assets/stylesheets/.gitkeep +0 -0
  104. data/spec/rails_app/vendor/plugins/.gitkeep +0 -0
  105. data/spec/spec_helper.rb +49 -0
  106. data/spec/support/certificates/certificate1 +12 -0
  107. data/spec/support/certificates/r1_certificate2_base64 +1 -0
  108. data/spec/support/responses/adfs_response_sha1.xml +46 -0
  109. data/spec/support/responses/adfs_response_sha256.xml +46 -0
  110. data/spec/support/responses/adfs_response_sha384.xml +46 -0
  111. data/spec/support/responses/adfs_response_sha512.xml +46 -0
  112. data/spec/support/responses/logoutresponse_fixtures.rb +67 -0
  113. data/spec/support/responses/no_signature_ns.xml +48 -0
  114. data/spec/support/responses/open_saml_response.xml +56 -0
  115. data/spec/support/responses/r1_response6.xml.base64 +1 -0
  116. data/spec/support/responses/response1.xml.base64 +1 -0
  117. data/spec/support/responses/response2.xml.base64 +79 -0
  118. data/spec/support/responses/response3.xml.base64 +66 -0
  119. data/spec/support/responses/response4.xml.base64 +93 -0
  120. data/spec/support/responses/response5.xml.base64 +102 -0
  121. data/spec/support/responses/response_with_ampersands.xml +139 -0
  122. data/spec/support/responses/response_with_ampersands.xml.base64 +93 -0
  123. data/spec/support/responses/simple_saml_php.xml +71 -0
  124. data/spec/support/responses/starfield_response.xml.base64 +1 -0
  125. data/spec/support/responses/wrapped_response_2.xml.base64 +150 -0
  126. data/spec/support/saml_request_macros.rb +38 -0
  127. data/spec/support/security_helpers.rb +61 -0
  128. data/spec/xml_security_spec.rb +137 -0
  129. metadata +465 -0
@@ -0,0 +1,68 @@
1
+ module SamlIdp
2
+ class NameIdFormatter
3
+ attr_accessor :list
4
+ def initialize(list)
5
+ self.list = (list || {})
6
+ end
7
+
8
+ def all
9
+ if split?
10
+ one_one.map { |key_val| build("1.1", key_val)[:name] } +
11
+ two_zero.map { |key_val| build("2.0", key_val)[:name] }
12
+ else
13
+ list.map { |key_val| build("2.0", key_val)[:name] }
14
+ end
15
+ end
16
+
17
+ def chosen
18
+ if split?
19
+ version, choose = "1.1", one_one.first
20
+ version, choose = "2.0", two_zero.first unless choose
21
+ version, choose = "2.0", "persistent" unless choose
22
+ build(version, choose)
23
+ else
24
+ choose = list.first || "persistent"
25
+ build("2.0", choose)
26
+ end
27
+ end
28
+
29
+ def build(version, key_val)
30
+ key_val = Array(key_val)
31
+ name = key_val.first.to_s.underscore
32
+ getter = build_getter key_val.last || name
33
+ {
34
+ name: "urn:oasis:names:tc:SAML:#{version}:nameid-format:#{name.camelize(:lower)}",
35
+ getter: getter
36
+ }
37
+ end
38
+ private :build
39
+
40
+ def build_getter(getter_val)
41
+ if getter_val.respond_to?(:call)
42
+ getter_val
43
+ else
44
+ ->(p) { p.public_send getter_val.to_s }
45
+ end
46
+ end
47
+ private :build_getter
48
+
49
+ def split?
50
+ list.is_a?(Hash) && (list.key?("2.0") || list.key?("1.1"))
51
+ end
52
+ private :split?
53
+
54
+ def one_one
55
+ list["1.1"] || {}
56
+ rescue TypeError
57
+ {}
58
+ end
59
+ private :one_one
60
+
61
+ def two_zero
62
+ list["2.0"] || {}
63
+ rescue TypeError
64
+ {}
65
+ end
66
+ private :two_zero
67
+ end
68
+ end
@@ -0,0 +1,10 @@
1
+ require 'saml_idp/attributeable'
2
+ module SamlIdp
3
+ class PersistedMetadata
4
+ include Attributeable
5
+
6
+ def sign_assertions?
7
+ !!attributes[:sign_assertions]
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,180 @@
1
+ require 'saml_idp/xml_security'
2
+ require 'saml_idp/service_provider'
3
+ module SamlIdp
4
+ class Request
5
+ def self.from_deflated_request(raw)
6
+ if raw
7
+ decoded = Base64.decode64(raw)
8
+ zstream = Zlib::Inflate.new(-Zlib::MAX_WBITS)
9
+ begin
10
+ inflated = zstream.inflate(decoded).tap do
11
+ zstream.finish
12
+ zstream.close
13
+ end
14
+ rescue Zlib::BufError, Zlib::DataError # not compressed
15
+ inflated = decoded
16
+ end
17
+ else
18
+ inflated = ""
19
+ end
20
+ new(inflated)
21
+ end
22
+
23
+ attr_accessor :raw_xml
24
+
25
+ delegate :config, to: :SamlIdp
26
+ private :config
27
+ delegate :xpath, to: :document
28
+ private :xpath
29
+
30
+ def initialize(raw_xml = "")
31
+ self.raw_xml = raw_xml
32
+ end
33
+
34
+ def logout_request?
35
+ logout_request.nil? ? false : true
36
+ end
37
+
38
+ def authn_request?
39
+ authn_request.nil? ? false : true
40
+ end
41
+
42
+ def request_id
43
+ request["ID"]
44
+ end
45
+
46
+ def request
47
+ if authn_request?
48
+ authn_request
49
+ elsif logout_request?
50
+ logout_request
51
+ end
52
+ end
53
+
54
+ def requested_authn_context
55
+ if authn_request? && authn_context_node
56
+ authn_context_node.content
57
+ else
58
+ nil
59
+ end
60
+ end
61
+
62
+ def acs_url
63
+ service_provider.acs_url ||
64
+ authn_request["AssertionConsumerServiceURL"].to_s
65
+ end
66
+
67
+ def logout_url
68
+ service_provider.assertion_consumer_logout_service_url
69
+ end
70
+
71
+ def response_url
72
+ if authn_request?
73
+ acs_url
74
+ elsif logout_request?
75
+ logout_url
76
+ end
77
+ end
78
+
79
+ def log(msg)
80
+ if Rails && Rails.logger
81
+ Rails.logger.info msg
82
+ else
83
+ puts msg
84
+ end
85
+ end
86
+
87
+ def valid?
88
+ unless service_provider?
89
+ log "Unable to find service provider for issuer #{issuer}"
90
+ return false
91
+ end
92
+
93
+ unless (authn_request? ^ logout_request?)
94
+ log "One and only one of authnrequest and logout request is required. authnrequest: #{authn_request?} logout_request: #{logout_request?} "
95
+ return false
96
+ end
97
+
98
+ unless valid_signature?
99
+ log "Signature is invalid in #{raw_xml}"
100
+ return false
101
+ end
102
+
103
+ if response_url.nil?
104
+ log "Unable to find response url for #{issuer}: #{raw_xml}"
105
+ return false
106
+ end
107
+
108
+ return true
109
+ end
110
+
111
+ def valid_signature?
112
+ # Force signatures for logout requests because there is no other
113
+ # protection against a cross-site DoS.
114
+ service_provider.valid_signature?(document, logout_request?)
115
+ end
116
+
117
+ def service_provider?
118
+ service_provider.valid?
119
+ end
120
+
121
+ def service_provider
122
+ @_service_provider ||= ServiceProvider.new((service_provider_finder[issuer] || {}).merge(identifier: issuer))
123
+ end
124
+
125
+ def issuer
126
+ @_issuer ||= xpath("//saml:Issuer", saml: assertion).first.try(:content)
127
+ @_issuer if @_issuer.present?
128
+ end
129
+
130
+ def name_id
131
+ @_name_id ||= xpath("//saml:NameID", saml: assertion).first.try(:content)
132
+ end
133
+
134
+ def session_index
135
+ @_session_index ||= xpath("//samlp:SessionIndex", samlp: samlp).first.try(:content)
136
+ end
137
+
138
+ def document
139
+ @_document ||= Saml::XML::Document.parse(raw_xml)
140
+ end
141
+ private :document
142
+
143
+ def authn_context_node
144
+ @_authn_context_node ||= xpath("//samlp:AuthnRequest/samlp:RequestedAuthnContext/saml:AuthnContextClassRef",
145
+ samlp: samlp,
146
+ saml: assertion).first
147
+ end
148
+ private :authn_context_node
149
+
150
+ def authn_request
151
+ @_authn_request ||= xpath("//samlp:AuthnRequest", samlp: samlp).first
152
+ end
153
+ private :authn_request
154
+
155
+ def logout_request
156
+ @_logout_request ||= xpath("//samlp:LogoutRequest", samlp: samlp).first
157
+ end
158
+ private :logout_request
159
+
160
+ def samlp
161
+ Saml::XML::Namespaces::PROTOCOL
162
+ end
163
+ private :samlp
164
+
165
+ def assertion
166
+ Saml::XML::Namespaces::ASSERTION
167
+ end
168
+ private :assertion
169
+
170
+ def signature_namespace
171
+ Saml::XML::Namespaces::SIGNATURE
172
+ end
173
+ private :signature_namespace
174
+
175
+ def service_provider_finder
176
+ config.service_provider.finder
177
+ end
178
+ private :service_provider_finder
179
+ end
180
+ end
@@ -0,0 +1,62 @@
1
+ require 'builder'
2
+ module SamlIdp
3
+ class ResponseBuilder
4
+ attr_accessor :response_id
5
+ attr_accessor :issuer_uri
6
+ attr_accessor :saml_acs_url
7
+ attr_accessor :saml_request_id
8
+ attr_accessor :assertion_and_signature
9
+
10
+ def initialize(response_id, issuer_uri, saml_acs_url, saml_request_id, assertion_and_signature)
11
+ self.response_id = response_id
12
+ self.issuer_uri = issuer_uri
13
+ self.saml_acs_url = saml_acs_url
14
+ self.saml_request_id = saml_request_id
15
+ self.assertion_and_signature = assertion_and_signature
16
+ end
17
+
18
+ def encoded
19
+ @encoded ||= encode
20
+ end
21
+
22
+ def raw
23
+ build
24
+ end
25
+
26
+ def encode
27
+ Base64.strict_encode64(raw)
28
+ end
29
+ private :encode
30
+
31
+ def build
32
+ resp_options = {}
33
+ resp_options[:ID] = response_id_string
34
+ resp_options[:Version] = "2.0"
35
+ resp_options[:IssueInstant] = now_iso
36
+ resp_options[:Destination] = saml_acs_url
37
+ resp_options[:Consent] = Saml::XML::Namespaces::Consents::UNSPECIFIED
38
+ resp_options[:InResponseTo] = saml_request_id unless saml_request_id.nil?
39
+ resp_options["xmlns:samlp"] = Saml::XML::Namespaces::PROTOCOL
40
+
41
+ builder = Builder::XmlMarkup.new
42
+ builder.tag! "samlp:Response", resp_options do |response|
43
+ response.Issuer issuer_uri, xmlns: Saml::XML::Namespaces::ASSERTION
44
+ response.tag! "samlp:Status" do |status|
45
+ status.tag! "samlp:StatusCode", Value: Saml::XML::Namespaces::Statuses::SUCCESS
46
+ end
47
+ response << assertion_and_signature
48
+ end
49
+ end
50
+ private :build
51
+
52
+ def response_id_string
53
+ "_#{response_id}"
54
+ end
55
+ private :response_id_string
56
+
57
+ def now_iso
58
+ Time.now.utc.iso8601
59
+ end
60
+ private :now_iso
61
+ end
62
+ end
@@ -0,0 +1,79 @@
1
+ require 'saml_idp/assertion_builder'
2
+ require 'saml_idp/response_builder'
3
+ module SamlIdp
4
+ class SamlResponse
5
+ attr_accessor :assertion_with_signature
6
+ attr_accessor :reference_id
7
+ attr_accessor :response_id
8
+ attr_accessor :issuer_uri
9
+ attr_accessor :principal
10
+ attr_accessor :audience_uri
11
+ attr_accessor :saml_request_id
12
+ attr_accessor :saml_acs_url
13
+ attr_accessor :algorithm
14
+ attr_accessor :secret_key
15
+ attr_accessor :x509_certificate
16
+ attr_accessor :authn_context_classref
17
+ attr_accessor :expiry
18
+ attr_accessor :encryption_opts
19
+
20
+ def initialize(reference_id,
21
+ response_id,
22
+ issuer_uri,
23
+ principal,
24
+ audience_uri,
25
+ saml_request_id,
26
+ saml_acs_url,
27
+ algorithm,
28
+ authn_context_classref,
29
+ expiry=60*60,
30
+ encryption_opts=nil
31
+ )
32
+ self.reference_id = reference_id
33
+ self.response_id = response_id
34
+ self.issuer_uri = issuer_uri
35
+ self.principal = principal
36
+ self.audience_uri = audience_uri
37
+ self.saml_request_id = saml_request_id
38
+ self.saml_acs_url = saml_acs_url
39
+ self.algorithm = algorithm
40
+ self.secret_key = secret_key
41
+ self.x509_certificate = x509_certificate
42
+ self.authn_context_classref = authn_context_classref
43
+ self.expiry = expiry
44
+ self.encryption_opts = encryption_opts
45
+ end
46
+
47
+ def build
48
+ @built ||= response_builder.encoded
49
+ end
50
+
51
+ def signed_assertion
52
+ if encryption_opts
53
+ assertion_builder.encrypt(sign: true)
54
+ else
55
+ assertion_builder.signed
56
+ end
57
+ end
58
+ private :signed_assertion
59
+
60
+ def response_builder
61
+ ResponseBuilder.new(response_id, issuer_uri, saml_acs_url, saml_request_id, signed_assertion)
62
+ end
63
+ private :response_builder
64
+
65
+ def assertion_builder
66
+ @assertion_builder ||= AssertionBuilder.new reference_id,
67
+ issuer_uri,
68
+ principal,
69
+ audience_uri,
70
+ saml_request_id,
71
+ saml_acs_url,
72
+ algorithm,
73
+ authn_context_classref,
74
+ expiry,
75
+ encryption_opts
76
+ end
77
+ private :assertion_builder
78
+ end
79
+ end
@@ -0,0 +1,76 @@
1
+ require 'httparty'
2
+ require 'saml_idp/attributeable'
3
+ require 'saml_idp/incoming_metadata'
4
+ require 'saml_idp/persisted_metadata'
5
+ module SamlIdp
6
+ class ServiceProvider
7
+ include Attributeable
8
+ attribute :identifier
9
+ attribute :cert
10
+ attribute :fingerprint
11
+ attribute :metadata_url
12
+ attribute :validate_signature
13
+ attribute :acs_url
14
+ attribute :assertion_consumer_logout_service_url
15
+
16
+ delegate :config, to: :SamlIdp
17
+
18
+ def valid?
19
+ attributes.present?
20
+ end
21
+
22
+ def valid_signature?(doc, require_signature = false)
23
+ if require_signature || should_validate_signature?
24
+ doc.valid_signature?(fingerprint)
25
+ else
26
+ true
27
+ end
28
+ end
29
+
30
+ def should_validate_signature?
31
+ attributes[:validate_signature] ||
32
+ current_metadata.respond_to?(:sign_assertions?) && current_metadata.sign_assertions?
33
+ end
34
+
35
+ def refresh_metadata
36
+ fresh = fresh_incoming_metadata
37
+ if valid_signature?(fresh.document)
38
+ metadata_persister[identifier, fresh]
39
+ @current_metadata = nil
40
+ fresh
41
+ end
42
+ end
43
+
44
+ def current_metadata
45
+ @current_metadata ||= get_current_or_build
46
+ end
47
+
48
+ def get_current_or_build
49
+ persisted = metadata_getter[identifier, self]
50
+ if persisted.is_a? Hash
51
+ PersistedMetadata.new(persisted)
52
+ end
53
+ end
54
+ private :get_current_or_build
55
+
56
+ def metadata_getter
57
+ config.service_provider.persisted_metadata_getter
58
+ end
59
+ private :metadata_getter
60
+
61
+ def metadata_persister
62
+ config.service_provider.metadata_persister
63
+ end
64
+ private :metadata_persister
65
+
66
+ def fresh_incoming_metadata
67
+ IncomingMetadata.new request_metadata
68
+ end
69
+ private :fresh_incoming_metadata
70
+
71
+ def request_metadata
72
+ metadata_url.present? ? HTTParty.get(metadata_url).body : ""
73
+ end
74
+ private :request_metadata
75
+ end
76
+ end