saml_idp 0.0.1

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.
Files changed (122) hide show
  1. checksums.yaml +15 -0
  2. data/Gemfile +2 -0
  3. data/LICENSE +22 -0
  4. data/README.md +197 -0
  5. data/app/controllers/saml_idp/idp_controller.rb +42 -0
  6. data/app/views/saml_idp/idp/new.html.erb +21 -0
  7. data/app/views/saml_idp/idp/saml_post.html.erb +13 -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 +144 -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 +45 -0
  14. data/lib/saml_idp/controller.rb +71 -0
  15. data/lib/saml_idp/default.rb +28 -0
  16. data/lib/saml_idp/engine.rb +5 -0
  17. data/lib/saml_idp/hashable.rb +26 -0
  18. data/lib/saml_idp/incoming_metadata.rb +144 -0
  19. data/lib/saml_idp/metadata_builder.rb +158 -0
  20. data/lib/saml_idp/name_id_formatter.rb +68 -0
  21. data/lib/saml_idp/persisted_metadata.rb +10 -0
  22. data/lib/saml_idp/request.rb +79 -0
  23. data/lib/saml_idp/response_builder.rb +60 -0
  24. data/lib/saml_idp/saml_response.rb +63 -0
  25. data/lib/saml_idp/service_provider.rb +68 -0
  26. data/lib/saml_idp/signable.rb +131 -0
  27. data/lib/saml_idp/signature_builder.rb +42 -0
  28. data/lib/saml_idp/signed_info_builder.rb +51 -0
  29. data/lib/saml_idp/version.rb +4 -0
  30. data/lib/saml_idp/xml_security.rb +168 -0
  31. data/saml_idp.gemspec +38 -0
  32. data/spec/acceptance/acceptance_helper.rb +9 -0
  33. data/spec/acceptance/idp_controller_spec.rb +16 -0
  34. data/spec/lib/saml_idp/algorithmable_spec.rb +48 -0
  35. data/spec/lib/saml_idp/assertion_builder_spec.rb +27 -0
  36. data/spec/lib/saml_idp/attribute_decorator_spec.rb +31 -0
  37. data/spec/lib/saml_idp/configurator_spec.rb +30 -0
  38. data/spec/lib/saml_idp/controller_spec.rb +51 -0
  39. data/spec/lib/saml_idp/metadata_builder_spec.rb +9 -0
  40. data/spec/lib/saml_idp/name_id_formatter_spec.rb +39 -0
  41. data/spec/lib/saml_idp/request_spec.rb +14 -0
  42. data/spec/lib/saml_idp/response_builder_spec.rb +32 -0
  43. data/spec/lib/saml_idp/saml_response_spec.rb +27 -0
  44. data/spec/lib/saml_idp/service_provider_spec.rb +21 -0
  45. data/spec/lib/saml_idp/signable_spec.rb +74 -0
  46. data/spec/lib/saml_idp/signature_builder_spec.rb +19 -0
  47. data/spec/lib/saml_idp/signed_info_builder_spec.rb +25 -0
  48. data/spec/rails_app/.gitignore +15 -0
  49. data/spec/rails_app/README.rdoc +261 -0
  50. data/spec/rails_app/Rakefile +7 -0
  51. data/spec/rails_app/app/assets/images/rails.png +0 -0
  52. data/spec/rails_app/app/assets/javascripts/application.js +15 -0
  53. data/spec/rails_app/app/assets/stylesheets/application.css +13 -0
  54. data/spec/rails_app/app/controllers/application_controller.rb +3 -0
  55. data/spec/rails_app/app/controllers/saml_controller.rb +8 -0
  56. data/spec/rails_app/app/controllers/saml_idp_controller.rb +11 -0
  57. data/spec/rails_app/app/helpers/application_helper.rb +2 -0
  58. data/spec/rails_app/app/mailers/.gitkeep +0 -0
  59. data/spec/rails_app/app/models/.gitkeep +0 -0
  60. data/spec/rails_app/app/views/layouts/application.html.erb +14 -0
  61. data/spec/rails_app/config.ru +4 -0
  62. data/spec/rails_app/config/application.rb +60 -0
  63. data/spec/rails_app/config/boot.rb +6 -0
  64. data/spec/rails_app/config/database.yml +25 -0
  65. data/spec/rails_app/config/environment.rb +5 -0
  66. data/spec/rails_app/config/environments/development.rb +37 -0
  67. data/spec/rails_app/config/environments/production.rb +67 -0
  68. data/spec/rails_app/config/environments/test.rb +37 -0
  69. data/spec/rails_app/config/initializers/backtrace_silencers.rb +7 -0
  70. data/spec/rails_app/config/initializers/inflections.rb +15 -0
  71. data/spec/rails_app/config/initializers/mime_types.rb +5 -0
  72. data/spec/rails_app/config/initializers/secret_token.rb +7 -0
  73. data/spec/rails_app/config/initializers/session_store.rb +8 -0
  74. data/spec/rails_app/config/initializers/wrap_parameters.rb +14 -0
  75. data/spec/rails_app/config/locales/en.yml +5 -0
  76. data/spec/rails_app/config/routes.rb +6 -0
  77. data/spec/rails_app/db/seeds.rb +7 -0
  78. data/spec/rails_app/doc/README_FOR_APP +2 -0
  79. data/spec/rails_app/lib/assets/.gitkeep +0 -0
  80. data/spec/rails_app/lib/tasks/.gitkeep +0 -0
  81. data/spec/rails_app/log/.gitkeep +0 -0
  82. data/spec/rails_app/public/404.html +26 -0
  83. data/spec/rails_app/public/422.html +26 -0
  84. data/spec/rails_app/public/500.html +25 -0
  85. data/spec/rails_app/public/favicon.ico +0 -0
  86. data/spec/rails_app/public/index.html +241 -0
  87. data/spec/rails_app/public/robots.txt +5 -0
  88. data/spec/rails_app/script/rails +6 -0
  89. data/spec/rails_app/test/fixtures/.gitkeep +0 -0
  90. data/spec/rails_app/test/functional/.gitkeep +0 -0
  91. data/spec/rails_app/test/integration/.gitkeep +0 -0
  92. data/spec/rails_app/test/performance/browsing_test.rb +12 -0
  93. data/spec/rails_app/test/test_helper.rb +13 -0
  94. data/spec/rails_app/test/unit/.gitkeep +0 -0
  95. data/spec/rails_app/vendor/assets/javascripts/.gitkeep +0 -0
  96. data/spec/rails_app/vendor/assets/stylesheets/.gitkeep +0 -0
  97. data/spec/rails_app/vendor/plugins/.gitkeep +0 -0
  98. data/spec/spec_helper.rb +49 -0
  99. data/spec/support/certificates/certificate1 +12 -0
  100. data/spec/support/certificates/r1_certificate2_base64 +1 -0
  101. data/spec/support/responses/adfs_response_sha1.xml +46 -0
  102. data/spec/support/responses/adfs_response_sha256.xml +46 -0
  103. data/spec/support/responses/adfs_response_sha384.xml +46 -0
  104. data/spec/support/responses/adfs_response_sha512.xml +46 -0
  105. data/spec/support/responses/logoutresponse_fixtures.rb +67 -0
  106. data/spec/support/responses/no_signature_ns.xml +48 -0
  107. data/spec/support/responses/open_saml_response.xml +56 -0
  108. data/spec/support/responses/r1_response6.xml.base64 +1 -0
  109. data/spec/support/responses/response1.xml.base64 +1 -0
  110. data/spec/support/responses/response2.xml.base64 +79 -0
  111. data/spec/support/responses/response3.xml.base64 +66 -0
  112. data/spec/support/responses/response4.xml.base64 +93 -0
  113. data/spec/support/responses/response5.xml.base64 +102 -0
  114. data/spec/support/responses/response_with_ampersands.xml +139 -0
  115. data/spec/support/responses/response_with_ampersands.xml.base64 +93 -0
  116. data/spec/support/responses/simple_saml_php.xml +71 -0
  117. data/spec/support/responses/starfield_response.xml.base64 +1 -0
  118. data/spec/support/responses/wrapped_response_2.xml.base64 +150 -0
  119. data/spec/support/saml_request_macros.rb +19 -0
  120. data/spec/support/security_helpers.rb +61 -0
  121. data/spec/xml_security_spec.rb +136 -0
  122. metadata +407 -0
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ YmY2NmYwOWVjNjFlNDcxMDg3MGEyNDRmN2ZlNTM3MDk2MWNiOWVjYg==
5
+ data.tar.gz: !binary |-
6
+ ODQ2MDYxY2VjNWRmNjc1MzUwY2FjZmFmMGQyNTY5NDMxNWU1YTU2NA==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ MDFkYjUwMjRkZmIxMThmOWFkZmQxYjFmYzc0YjFlOGY0MGYxM2E1NzVlMzAy
10
+ NzY2MzE1N2NhYjY1MWE0N2MxM2I1YTI0MjViZjAzMzk3ZjZiNGYzOTA2MmJj
11
+ NTFmMmI3ZmE4YzQwMzg0OGJkNmQwMzUyZTFmZDk4MmNiM2MyNmQ=
12
+ data.tar.gz: !binary |-
13
+ NTEyMzQxNWEzYzA5ZGJhNDk3MTk2ZDZlMmZlYmQzYmU3OGU0NWI2ZjVlMDcy
14
+ NzMxOTYxYjJiMjEzMTk5YmQyOTg3NDRmNWUxOGZjOGE0MDFjNjM5OGMzNmYy
15
+ NjUzNTZkMWI1M2U4NGE0ZjIzNWFiMzM1MWJiYjAxYTZiOGIyZjc=
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Sport Ngin (http://sportngin.com)
2
+ Portions Copyright (c) 2010 OneLogin, LLC
3
+ Portions Copyright (c) 2012 Lawrence Pit (http://lawrencepit.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,197 @@
1
+ # Ruby SAML Identity Provider (IdP)
2
+ Forked from https://github.com/lawrencepit/ruby-saml-idp
3
+
4
+ [![Build Status](https://travis-ci.org/sportngin/saml_idp.png)](https://travis-ci.org/sportngin/saml_idp)
5
+
6
+ The ruby SAML Identity Provider library is for implementing the server side of SAML authentication. It allows
7
+ your application to act as an IdP (Identity Provider) using the
8
+ [SAML v2.0](http://en.wikipedia.org/wiki/Security_Assertion_Markup_Language)
9
+ protocol. It provides a means for managing authentication requests and confirmation responses for SPs (Service Providers).
10
+
11
+ This was originally setup by @lawrencepit to test SAML Clients. I took it closer to a real
12
+ SAML IDP implementation.
13
+
14
+ # Installation and Usage
15
+
16
+ Add this to your Gemfile:
17
+
18
+ gem 'saml_idp'
19
+
20
+ ## Not using rails?
21
+ Include `SamlIdp::Controller` and see the examples that use rails. It should be straightforward for you.
22
+
23
+ Basically you call `decode_request(params[:SAMLRequest])` on an incoming request and then use the value
24
+ `saml_acs_url` to determine the source for which you need to authenticate a user. How you authenticate
25
+ a user is entirely up to you.
26
+
27
+ Once a user has successfully authenticated on your system send the Service Provider a SAMLReponse by
28
+ posting to `saml_acs_url` the parameter `SAMLResponse` with the return value from a call to
29
+ `encode_response(user_email)`.
30
+
31
+ ## Using rails?
32
+ Add to your `routes.rb` file, for example:
33
+
34
+ ``` ruby
35
+ get '/saml/auth' => 'saml_idp#new'
36
+ get '/saml/metadata' => 'saml_idp#show'
37
+ post '/saml/auth' => 'saml_idp#create'
38
+ ```
39
+
40
+ Create a controller that looks like this, customize to your own situation:
41
+
42
+ ``` ruby
43
+ class SamlIdpController < SamlIdp::IdpController
44
+ def idp_authenticate(email, password) # not using params intentionally
45
+ user = User.by_email(email).first
46
+ user && user.valid_password?(password) ? user : nil
47
+ end
48
+ private :idp_authenticate
49
+
50
+ def idp_make_saml_response(found_user) # not using params intentionally
51
+ encode_response found_user
52
+ end
53
+ private :idp_make_saml_response
54
+ end
55
+ ```
56
+
57
+ ## Configuration
58
+
59
+ Be sure to load a file like this during your app initialization:
60
+
61
+ ```ruby
62
+ SamlIdp.configure do |config|
63
+ base = "http://example.com"
64
+
65
+ config.x509_certificate = <<-CERT
66
+ -----BEGIN CERTIFICATE-----
67
+ CERTIFICATE DATA
68
+ -----END CERTIFICATE-----
69
+ CERT
70
+
71
+ config.secret_key = <<-CERT
72
+ -----BEGIN RSA PRIVATE KEY-----
73
+ KEY DATA
74
+ -----END RSA PRIVATE KEY-----
75
+ CERT
76
+
77
+ # config.algorithm = :sha256
78
+ # config.organization_name = "Your Organization"
79
+ # config.organization_url = "http://example.com"
80
+ # config.base_saml_location = "#{base}/saml"
81
+ # config.reference_id_generator # Default: -> { UUID.generate }
82
+ # config.attribute_service_location = "#{base}/saml/attributes"
83
+ # config.single_service_post_location = "#{base}/saml/auth"
84
+
85
+ # Principal is passed in when you `encode_response`
86
+ #
87
+ # config.name_id_formats # =>
88
+ # { # All 2.0
89
+ # email_address: -> (principal) { principal.email_address },
90
+ # transient: -> (principal) { principal.id },
91
+ # persistent: -> (p) { p.id },
92
+ # }
93
+ # OR
94
+ #
95
+ # {
96
+ # "1.1" => {
97
+ # email_address: -> (principal) { principal.email_address },
98
+ # },
99
+ # "2.0" => {
100
+ # transient: -> (principal) { principal.email_address },
101
+ # persistent: -> (p) { p.id },
102
+ # },
103
+ # }
104
+
105
+ # config.attributes # =>
106
+ # {
107
+ # <friendly_name> => { # required (ex "eduPersonAffiliation")
108
+ # "name" => <attrname> # required (ex "urn:oid:1.3.6.1.4.1.5923.1.1.1.1")
109
+ # "name_format" => "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", # not required
110
+ # "getter" => ->(principal) { # not required
111
+ # principal.get_eduPersonAffiliation # If no "getter" defined, will try
112
+ # } # `principal.eduPersonAffiliation`, or no values will
113
+ # } # be output
114
+ #
115
+ ## EXAMPLE ##
116
+ # config.attributes = {
117
+ # GivenName: {
118
+ # getter: :first_name,
119
+ # },
120
+ # SurName: {
121
+ # getter: :last_name,
122
+ # },
123
+ # }
124
+ ## EXAMPLE ##
125
+
126
+ # config.technical_contact.company = "Example"
127
+ # config.technical_contact.given_name = "Jonny"
128
+ # config.technical_contact.sur_name = "Support"
129
+ # config.technical_contact.telephone = "55555555555"
130
+ # config.technical_contact.email_address = "example@example.com"
131
+
132
+ service_providers = {
133
+ "some-issuer-url.com/saml" => {
134
+ fingerprint: "9E:65:2E:03:06:8D:80:F2:86:C7:6C:77:A1:D9:14:97:0A:4D:F4:4D",
135
+ metadata_url: "http://some-issuer-url.com/saml/metadata"
136
+ },
137
+ }
138
+
139
+ # `identifier` is the entity_id or issuer of the Service Provider,
140
+ # settings is an IncomingMetadata object which has a to_h method that needs to be persisted
141
+ config.service_provider.metadata_persister = ->(identifier, settings) {
142
+ fname = identifier.to_s.gsub(/\/|:/,"_")
143
+ `mkdir -p #{Rails.root.join("cache/saml/metadata")}`
144
+ File.open Rails.root.join("cache/saml/metadata/#{fname}"), "r+b" do |f|
145
+ Marshal.dump settings.to_h, f
146
+ end
147
+ }
148
+
149
+ # `identifier` is the entity_id or issuer of the Service Provider,
150
+ # `service_provider` is a ServiceProvider object. Based on the `identifier` or the
151
+ # `service_provider` you should return the settings.to_h from above
152
+ config.service_provider.persisted_metadata_getter = ->(identifier, service_provider){
153
+ fname = identifier.to_s.gsub(/\/|:/,"_")
154
+ `mkdir -p #{Rails.root.join("cache/saml/metadata")}`
155
+ File.open Rails.root.join("cache/saml/metadata/#{fname}"), "rb" do |f|
156
+ Marshal.load f
157
+ end
158
+ }
159
+
160
+ # Find ServiceProvider metadata_url and fingerprint based on our settings
161
+ config.service_provider.finder = ->(issuer_or_entity_id) do
162
+ service_providers[issuer_or_entity_id]
163
+ end
164
+ end
165
+ ```
166
+
167
+ # Keys and Secrets
168
+ To generate the SAML Response it uses a default X.509 certificate and secret key... which isn't so secret.
169
+ You can find them in `SamlIdp::Default`. The X.509 certificate is valid until year 2032.
170
+ Obviously you shouldn't use these if you intend to use this in production environments. In that case,
171
+ within the controller set the properties `x509_certificate` and `secret_key` using a `prepend_before_filter`
172
+ callback within the current request context or set them globally via the `SamlIdp.config.x509_certificate`
173
+ and `SamlIdp.config.secret_key` properties.
174
+
175
+ The fingerprint to use, if you use the default X.509 certificate of this gem, is:
176
+
177
+ ```
178
+ 9E:65:2E:03:06:8D:80:F2:86:C7:6C:77:A1:D9:14:97:0A:4D:F4:4D
179
+ ```
180
+
181
+
182
+ # Service Providers
183
+ To act as a Service Provider which generates SAML Requests and can react to SAML Responses use the
184
+ excellent [ruby-saml](https://github.com/onelogin/ruby-saml) gem.
185
+
186
+
187
+ # Author
188
+ Jon Phenow, jon.phenow@sportngin.com
189
+
190
+ Lawrence Pit, lawrence.pit@gmail.com, lawrencepit.com, @lawrencepit
191
+
192
+ # Copyright
193
+ Copyright (c) 2012 Sport Ngin.
194
+ Portions Copyright (c) 2010 OneLogin, LLC
195
+ Portions Copyright (c) 2012 Lawrence Pit (http://lawrencepit.com)
196
+
197
+ See LICENSE for details.
@@ -0,0 +1,42 @@
1
+ # encoding: utf-8
2
+ module SamlIdp
3
+ class IdpController < ActionController::Base
4
+ include SamlIdp::Controller
5
+
6
+ unloadable
7
+ protect_from_forgery
8
+ before_filter :validate_saml_request, only: [:new, :create]
9
+
10
+ def new
11
+ render template: "saml_idp/idp/new"
12
+ end
13
+
14
+ def show
15
+ render xml: SamlIdp.metadata.signed
16
+ end
17
+
18
+ def create
19
+ unless params[:email].blank? && params[:password].blank?
20
+ person = idp_authenticate(params[:email], params[:password])
21
+ if person.nil?
22
+ @saml_idp_fail_msg = "Incorrect email or password."
23
+ else
24
+ @saml_response = idp_make_saml_response(person)
25
+ render :template => "saml_idp/idp/saml_post", :layout => false
26
+ return
27
+ end
28
+ end
29
+ render :template => "saml_idp/idp/new"
30
+ end
31
+
32
+ def idp_authenticate(email, password)
33
+ raise NotImplementedError
34
+ end
35
+ protected :idp_authenticate
36
+
37
+ def idp_make_saml_response(person)
38
+ raise NotImplementedError
39
+ end
40
+ protected :idp_make_saml_response
41
+ end
42
+ end
@@ -0,0 +1,21 @@
1
+ <% if @saml_idp_fail_msg %>
2
+ <div id="saml_idp_fail_msg" class="flash error"><%= @saml_idp_fail_msg %></div>
3
+ <% end %>
4
+
5
+ <%= form_tag do %>
6
+ <%= hidden_field_tag("SAMLRequest", params[:SAMLRequest]) %>
7
+
8
+ <p>
9
+ <%= label_tag :email %>
10
+ <%= email_field_tag :email, params[:email], :autocapitalize => "off", :autocorrect => "off", :autofocus => "autofocus", :spellcheck => "false", :size => 30, :class => "email_pwd txt" %>
11
+ </p>
12
+
13
+ <p>
14
+ <%= label_tag :password %>
15
+ <%= password_field_tag :password, params[:password], :autocapitalize => "off", :autocorrect => "off", :spellcheck => "false", :size => 30, :class => "email_pwd txt" %>
16
+ </p>
17
+
18
+ <p>
19
+ <%= submit_tag "Sign in", :class => "button big blueish" %>
20
+ </p>
21
+ <% end %>
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
6
+ </head>
7
+ <body onload="document.forms[0].submit();" style="visibility:hidden;">
8
+ <%= form_tag(saml_acs_url) do %>
9
+ <%= hidden_field_tag("SAMLResponse", @saml_response) %>
10
+ <%= submit_tag "Submit" %>
11
+ <% end %>
12
+ </body>
13
+ </html>
@@ -0,0 +1,92 @@
1
+ # encoding: utf-8
2
+ module SamlIdp
3
+ require 'active_support/all'
4
+ require 'saml_idp/saml_response'
5
+ require 'saml_idp/xml_security'
6
+ require 'saml_idp/configurator'
7
+ require 'saml_idp/controller'
8
+ require 'saml_idp/default'
9
+ require 'saml_idp/metadata_builder'
10
+ require 'saml_idp/version'
11
+ require 'saml_idp/engine' if defined?(::Rails) && Rails::VERSION::MAJOR > 2
12
+
13
+ def self.config
14
+ @config ||= SamlIdp::Configurator.new
15
+ end
16
+
17
+ def self.configure
18
+ yield config
19
+ end
20
+
21
+ def self.metadata
22
+ @metadata ||= MetadataBuilder.new(config)
23
+ end
24
+ end
25
+
26
+ # TODO Needs extraction out
27
+ module Saml
28
+ module XML
29
+ module Namespaces
30
+ METADATA = "urn:oasis:names:tc:SAML:2.0:metadata"
31
+ ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
32
+ SIGNATURE = "http://www.w3.org/2000/09/xmldsig#"
33
+ PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
34
+
35
+ module Statuses
36
+ SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success"
37
+ end
38
+
39
+ module Consents
40
+ UNSPECIFIED = "urn:oasis:names:tc:SAML:2.0:consent:unspecified"
41
+ end
42
+
43
+ module AuthnContext
44
+ module ClassRef
45
+ PASSWORD = "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
46
+ PASSWORD_PROTECTED = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
47
+ end
48
+ end
49
+
50
+ module Methods
51
+ BEARER = "urn:oasis:names:tc:SAML:2.0:cm:bearer"
52
+ end
53
+
54
+ module Formats
55
+ module Attr
56
+ URI = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
57
+ end
58
+
59
+ module NameId
60
+ EMAIL_ADDRESS = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
61
+ TRANSIENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
62
+ PERSISTENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
63
+ end
64
+ end
65
+ end
66
+
67
+ class Document < Nokogiri::XML::Document
68
+ def signed?
69
+ !!xpath("//ds:Signature", ds: signature_namespace).first
70
+ end
71
+
72
+ def valid_signature?(fingerprint)
73
+ signed? &&
74
+ signed_document.validate(fingerprint, :soft)
75
+ end
76
+
77
+ def signed_document
78
+ XMLSecurity::SignedDocument.new(to_xml)
79
+ end
80
+
81
+ def signature_namespace
82
+ Namespaces::SIGNATURE
83
+ end
84
+
85
+ def to_xml
86
+ super(
87
+ save_with: Nokogiri::XML::Node::SaveOptions::AS_XML | Nokogiri::XML::Node::SaveOptions::NO_DECLARATION
88
+ ).strip
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,19 @@
1
+ module SamlIdp
2
+ module Algorithmable
3
+ def algorithm
4
+ algorithm_check = raw_algorithm || SamlIdp.config.algorithm
5
+ return algorithm_check if algorithm_check.respond_to?(:digest)
6
+ begin
7
+ OpenSSL::Digest.const_get(algorithm_check.to_s.upcase)
8
+ rescue NameError
9
+ OpenSSL::Digest::SHA1
10
+ end
11
+ end
12
+ private :algorithm
13
+
14
+ def algorithm_name
15
+ algorithm.to_s.split('::').last.downcase
16
+ end
17
+ private :algorithm_name
18
+ end
19
+ end
@@ -0,0 +1,144 @@
1
+ require 'builder'
2
+ require 'saml_idp/algorithmable'
3
+ require 'saml_idp/signable'
4
+ module SamlIdp
5
+ class AssertionBuilder
6
+ include Algorithmable
7
+ include Signable
8
+ attr_accessor :reference_id
9
+ attr_accessor :issuer_uri
10
+ attr_accessor :principal
11
+ attr_accessor :audience_uri
12
+ attr_accessor :saml_request_id
13
+ attr_accessor :saml_acs_url
14
+ attr_accessor :raw_algorithm
15
+
16
+ delegate :config, to: :SamlIdp
17
+
18
+ def initialize(reference_id, issuer_uri, principal, audience_uri, saml_request_id, saml_acs_url, raw_algorithm)
19
+ self.reference_id = reference_id
20
+ self.issuer_uri = issuer_uri
21
+ self.principal = principal
22
+ self.audience_uri = audience_uri
23
+ self.saml_request_id = saml_request_id
24
+ self.saml_acs_url = saml_acs_url
25
+ self.raw_algorithm = raw_algorithm
26
+ end
27
+
28
+ def fresh
29
+ builder = Builder::XmlMarkup.new
30
+ builder.Assertion xmlns: Saml::XML::Namespaces::ASSERTION,
31
+ ID: reference_string,
32
+ IssueInstant: now_iso,
33
+ Version: "2.0" do |assertion|
34
+ assertion.Issuer issuer_uri
35
+ sign assertion
36
+ assertion.Subject do |subject|
37
+ subject.NameID name_id, Format: name_id_format[:name]
38
+ subject.SubjectConfirmation Method: Saml::XML::Namespaces::Methods::BEARER do |confirmation|
39
+ confirmation.SubjectConfirmationData "", InResponseTo: saml_request_id,
40
+ NotOnOrAfter: not_on_or_after_subject,
41
+ Recipient: saml_acs_url
42
+ end
43
+ end
44
+ assertion.Conditions NotBefore: not_before, NotOnOrAfter: not_on_or_after_condition do |conditions|
45
+ conditions.AudienceRestriction do |restriction|
46
+ restriction.Audience audience_uri
47
+ end
48
+ end
49
+ assertion.AttributeStatement do |attr_statement|
50
+ config.attributes.each do |friendly_name, attrs|
51
+ attrs = (attrs || {}).with_indifferent_access
52
+ attr_statement.Attribute Name: attrs[:name] || friendly_name,
53
+ NameFormat: attrs[:name_format] || Saml::XML::Namespaces::Formats::Attr::URI,
54
+ FriendlyName: friendly_name.to_s do |attr|
55
+ values = get_values_for friendly_name, attrs[:getter]
56
+ values.each do |val|
57
+ attr.AttributeValue val.to_s
58
+ end
59
+ end
60
+ end
61
+ end
62
+ assertion.AuthnStatement AuthnInstant: now_iso, SessionIndex: reference_string do |statement|
63
+ statement.AuthnContext do |context|
64
+ context.AuthnContextClassRef Saml::XML::Namespaces::AuthnContext::ClassRef::PASSWORD
65
+ end
66
+ end
67
+ end
68
+ end
69
+ alias_method :raw, :fresh
70
+ private :fresh
71
+
72
+ def get_values_for(friendly_name, getter)
73
+ result = nil
74
+ if getter.present?
75
+ if getter.respond_to?(:call)
76
+ result = getter.call(principal)
77
+ else
78
+ message = getter.to_s.underscore
79
+ result = principal.public_send(message) if principal.respond_to?(message)
80
+ end
81
+ elsif getter.nil?
82
+ message = friendly_name.to_s.underscore
83
+ result = principal.public_send(message) if principal.respond_to?(message)
84
+ end
85
+ Array(result)
86
+ end
87
+ private :get_values_for
88
+
89
+ def name_id
90
+ name_id_getter.call principal
91
+ end
92
+ private :name_id
93
+
94
+ def name_id_getter
95
+ getter = name_id_format[:getter]
96
+ if getter.respond_to? :call
97
+ getter
98
+ else
99
+ ->(principal) { principal.public_send getter.to_s }
100
+ end
101
+ end
102
+ private :name_id_getter
103
+
104
+ def name_id_format
105
+ @name_id_format ||= NameIdFormatter.new(config.name_id.formats).chosen
106
+ end
107
+ private :name_id_format
108
+
109
+ def reference_string
110
+ "_#{reference_id}"
111
+ end
112
+ private :reference_string
113
+
114
+ def now
115
+ @now ||= Time.now.utc
116
+ end
117
+ private :now
118
+
119
+ def now_iso
120
+ iso { now }
121
+ end
122
+ private :now_iso
123
+
124
+ def not_before
125
+ iso { now - 5 }
126
+ end
127
+ private :not_before
128
+
129
+ def not_on_or_after_condition
130
+ iso { now + 60 * 60 }
131
+ end
132
+ private :not_on_or_after_condition
133
+
134
+ def not_on_or_after_subject
135
+ iso { now + 3 * 60 }
136
+ end
137
+ private :not_on_or_after_subject
138
+
139
+ def iso
140
+ yield.iso8601
141
+ end
142
+ private :iso
143
+ end
144
+ end