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.
- checksums.yaml +15 -0
- data/Gemfile +2 -0
- data/LICENSE +22 -0
- data/README.md +197 -0
- data/app/controllers/saml_idp/idp_controller.rb +42 -0
- data/app/views/saml_idp/idp/new.html.erb +21 -0
- data/app/views/saml_idp/idp/saml_post.html.erb +13 -0
- data/lib/saml_idp.rb +92 -0
- data/lib/saml_idp/algorithmable.rb +19 -0
- data/lib/saml_idp/assertion_builder.rb +144 -0
- data/lib/saml_idp/attribute_decorator.rb +27 -0
- data/lib/saml_idp/attributeable.rb +24 -0
- data/lib/saml_idp/configurator.rb +45 -0
- data/lib/saml_idp/controller.rb +71 -0
- data/lib/saml_idp/default.rb +28 -0
- data/lib/saml_idp/engine.rb +5 -0
- data/lib/saml_idp/hashable.rb +26 -0
- data/lib/saml_idp/incoming_metadata.rb +144 -0
- data/lib/saml_idp/metadata_builder.rb +158 -0
- data/lib/saml_idp/name_id_formatter.rb +68 -0
- data/lib/saml_idp/persisted_metadata.rb +10 -0
- data/lib/saml_idp/request.rb +79 -0
- data/lib/saml_idp/response_builder.rb +60 -0
- data/lib/saml_idp/saml_response.rb +63 -0
- data/lib/saml_idp/service_provider.rb +68 -0
- data/lib/saml_idp/signable.rb +131 -0
- data/lib/saml_idp/signature_builder.rb +42 -0
- data/lib/saml_idp/signed_info_builder.rb +51 -0
- data/lib/saml_idp/version.rb +4 -0
- data/lib/saml_idp/xml_security.rb +168 -0
- data/saml_idp.gemspec +38 -0
- data/spec/acceptance/acceptance_helper.rb +9 -0
- data/spec/acceptance/idp_controller_spec.rb +16 -0
- data/spec/lib/saml_idp/algorithmable_spec.rb +48 -0
- data/spec/lib/saml_idp/assertion_builder_spec.rb +27 -0
- data/spec/lib/saml_idp/attribute_decorator_spec.rb +31 -0
- data/spec/lib/saml_idp/configurator_spec.rb +30 -0
- data/spec/lib/saml_idp/controller_spec.rb +51 -0
- data/spec/lib/saml_idp/metadata_builder_spec.rb +9 -0
- data/spec/lib/saml_idp/name_id_formatter_spec.rb +39 -0
- data/spec/lib/saml_idp/request_spec.rb +14 -0
- data/spec/lib/saml_idp/response_builder_spec.rb +32 -0
- data/spec/lib/saml_idp/saml_response_spec.rb +27 -0
- data/spec/lib/saml_idp/service_provider_spec.rb +21 -0
- data/spec/lib/saml_idp/signable_spec.rb +74 -0
- data/spec/lib/saml_idp/signature_builder_spec.rb +19 -0
- data/spec/lib/saml_idp/signed_info_builder_spec.rb +25 -0
- data/spec/rails_app/.gitignore +15 -0
- data/spec/rails_app/README.rdoc +261 -0
- data/spec/rails_app/Rakefile +7 -0
- data/spec/rails_app/app/assets/images/rails.png +0 -0
- data/spec/rails_app/app/assets/javascripts/application.js +15 -0
- data/spec/rails_app/app/assets/stylesheets/application.css +13 -0
- data/spec/rails_app/app/controllers/application_controller.rb +3 -0
- data/spec/rails_app/app/controllers/saml_controller.rb +8 -0
- data/spec/rails_app/app/controllers/saml_idp_controller.rb +11 -0
- data/spec/rails_app/app/helpers/application_helper.rb +2 -0
- data/spec/rails_app/app/mailers/.gitkeep +0 -0
- data/spec/rails_app/app/models/.gitkeep +0 -0
- data/spec/rails_app/app/views/layouts/application.html.erb +14 -0
- data/spec/rails_app/config.ru +4 -0
- data/spec/rails_app/config/application.rb +60 -0
- data/spec/rails_app/config/boot.rb +6 -0
- data/spec/rails_app/config/database.yml +25 -0
- data/spec/rails_app/config/environment.rb +5 -0
- data/spec/rails_app/config/environments/development.rb +37 -0
- data/spec/rails_app/config/environments/production.rb +67 -0
- data/spec/rails_app/config/environments/test.rb +37 -0
- data/spec/rails_app/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/rails_app/config/initializers/inflections.rb +15 -0
- data/spec/rails_app/config/initializers/mime_types.rb +5 -0
- data/spec/rails_app/config/initializers/secret_token.rb +7 -0
- data/spec/rails_app/config/initializers/session_store.rb +8 -0
- data/spec/rails_app/config/initializers/wrap_parameters.rb +14 -0
- data/spec/rails_app/config/locales/en.yml +5 -0
- data/spec/rails_app/config/routes.rb +6 -0
- data/spec/rails_app/db/seeds.rb +7 -0
- data/spec/rails_app/doc/README_FOR_APP +2 -0
- data/spec/rails_app/lib/assets/.gitkeep +0 -0
- data/spec/rails_app/lib/tasks/.gitkeep +0 -0
- data/spec/rails_app/log/.gitkeep +0 -0
- data/spec/rails_app/public/404.html +26 -0
- data/spec/rails_app/public/422.html +26 -0
- data/spec/rails_app/public/500.html +25 -0
- data/spec/rails_app/public/favicon.ico +0 -0
- data/spec/rails_app/public/index.html +241 -0
- data/spec/rails_app/public/robots.txt +5 -0
- data/spec/rails_app/script/rails +6 -0
- data/spec/rails_app/test/fixtures/.gitkeep +0 -0
- data/spec/rails_app/test/functional/.gitkeep +0 -0
- data/spec/rails_app/test/integration/.gitkeep +0 -0
- data/spec/rails_app/test/performance/browsing_test.rb +12 -0
- data/spec/rails_app/test/test_helper.rb +13 -0
- data/spec/rails_app/test/unit/.gitkeep +0 -0
- data/spec/rails_app/vendor/assets/javascripts/.gitkeep +0 -0
- data/spec/rails_app/vendor/assets/stylesheets/.gitkeep +0 -0
- data/spec/rails_app/vendor/plugins/.gitkeep +0 -0
- data/spec/spec_helper.rb +49 -0
- data/spec/support/certificates/certificate1 +12 -0
- data/spec/support/certificates/r1_certificate2_base64 +1 -0
- data/spec/support/responses/adfs_response_sha1.xml +46 -0
- data/spec/support/responses/adfs_response_sha256.xml +46 -0
- data/spec/support/responses/adfs_response_sha384.xml +46 -0
- data/spec/support/responses/adfs_response_sha512.xml +46 -0
- data/spec/support/responses/logoutresponse_fixtures.rb +67 -0
- data/spec/support/responses/no_signature_ns.xml +48 -0
- data/spec/support/responses/open_saml_response.xml +56 -0
- data/spec/support/responses/r1_response6.xml.base64 +1 -0
- data/spec/support/responses/response1.xml.base64 +1 -0
- data/spec/support/responses/response2.xml.base64 +79 -0
- data/spec/support/responses/response3.xml.base64 +66 -0
- data/spec/support/responses/response4.xml.base64 +93 -0
- data/spec/support/responses/response5.xml.base64 +102 -0
- data/spec/support/responses/response_with_ampersands.xml +139 -0
- data/spec/support/responses/response_with_ampersands.xml.base64 +93 -0
- data/spec/support/responses/simple_saml_php.xml +71 -0
- data/spec/support/responses/starfield_response.xml.base64 +1 -0
- data/spec/support/responses/wrapped_response_2.xml.base64 +150 -0
- data/spec/support/saml_request_macros.rb +19 -0
- data/spec/support/security_helpers.rb +61 -0
- data/spec/xml_security_spec.rb +136 -0
- metadata +407 -0
checksums.yaml
ADDED
|
@@ -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
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.
|
data/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Ruby SAML Identity Provider (IdP)
|
|
2
|
+
Forked from https://github.com/lawrencepit/ruby-saml-idp
|
|
3
|
+
|
|
4
|
+
[](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>
|
data/lib/saml_idp.rb
ADDED
|
@@ -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
|