devise_saml_authenticatable 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +3 -2
- data/.rspec +2 -0
- data/.travis.yml +18 -0
- data/Gemfile +10 -0
- data/README.md +60 -47
- data/Rakefile +6 -0
- data/app/controllers/devise/saml_sessions_controller.rb +2 -0
- data/devise_saml_authenticatable.gemspec +2 -1
- data/lib/devise_saml_authenticatable.rb +11 -5
- data/lib/devise_saml_authenticatable/model.rb +20 -12
- data/lib/devise_saml_authenticatable/saml_config.rb +7 -1
- data/lib/devise_saml_authenticatable/strategy.rb +4 -4
- data/lib/devise_saml_authenticatable/version.rb +1 -1
- data/spec/controllers/devise/saml_sessions_controller_spec.rb +30 -0
- data/spec/devise_saml_authenticatable/model_spec.rb +118 -0
- data/spec/devise_saml_authenticatable/saml_config_spec.rb +100 -0
- data/spec/devise_saml_authenticatable/strategy_spec.rb +64 -0
- data/spec/features/saml_authentication_spec.rb +71 -0
- data/spec/rails_helper.rb +15 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/idp_template.rb +10 -0
- data/spec/support/rails_app.rb +57 -0
- data/spec/support/saml_idp_controller.rb.erb +54 -0
- data/spec/support/sp_template.rb +57 -0
- metadata +37 -19
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 69aceb045d8de7ed6acd66fd027fdabfe8da4f89
|
4
|
+
data.tar.gz: def4131da232ae9818379b35750b686a77e61d78
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5a8c183ecbe7dcda723e6fa459056de365390de80baf3c587719ebdf40df07741e89de14355c5b0c623755371ee0473c6be729b1dd5d044d00da7474d87a4b12
|
7
|
+
data.tar.gz: 5b3b63caf790992b2263e6ccceec1570df0af4e207c42e8d89c5eeb1726740195dd28ae4e90e2823dbf1f6d47b880fa71de338ec75f35fd550012791a3df8406
|
data/.gitignore
CHANGED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- "1.9.3"
|
4
|
+
- "2.0.0"
|
5
|
+
- "2.1.0"
|
6
|
+
- "2.2.0"
|
7
|
+
|
8
|
+
script:
|
9
|
+
- xvfb-run bundle exec rake
|
10
|
+
|
11
|
+
notifications:
|
12
|
+
hipchat:
|
13
|
+
rooms:
|
14
|
+
secure: eWahLFbtvZaJgulhDdnmbIxtSnl2lGR4zHqcJ6d7ExG0HzfxzCknHkau4YfqhANRa3+e66F4rCNoU3+db1EKE5jij7+oIOgRHAmVwLi9WRswzb7NQfWV0/PyUjdF6T7Ac/sfQRkFIDTUL2npYMr2zTQnjDcYrT7e0IkRxNA3VXWogzhqnC4sXKLr8j9zLLw5BdFfELcJD021Rpd7O1EmAt+hahxaJCipyXV2l/kDANR24jnlXgaQn8HJwgRXsBARAQe1QgApnrmbqnGnWc4T1GbI7kExsKGCUbHxYBOp+99m25T6MK66xnvRltwKaUeMXuUyWRZtmAV3LlbACRMX3n5qkDqg8P2OPNvWWua84hkRmUNUska7YrSLX7aCWvGfA6Vr1INnsueLl5sYjHhHZnaS8Wl0iC9ltxBUY4ikg1QlHSghXFQvMC5UAROJ2x7BHGV3kDRTc52ZLdy5BDuJrf/OFEhpuTp+HQALmEKkcJKFUdBn8Cu20wF0pF8o5XvYnSozuqytJKP+b/3+U0X4SEYqbkYCi/PcSnXTtSTIl0+7uxmEBCwPKpXRkkCfVsLqIVUkyVj1d1qIxG8Zbikn5NxK6USV5+Zl4KTDFUVD4r4T8fXCV09+8q0t/uyVjCDv3g1vGRcHvxQyDiLrs7q4DL3GNQY3trrEQ6WneA8nsLw=
|
15
|
+
template:
|
16
|
+
- '%{repository}<a href="%{build_url}">#%{build_number}</a> (%{branch} - <a href="%{compare_url}">%{commit}</a> : %{author}): %{message}'
|
17
|
+
format: html
|
18
|
+
on_pull_requests: false
|
data/Gemfile
CHANGED
@@ -2,3 +2,13 @@ source 'https://rubygems.org'
|
|
2
2
|
|
3
3
|
# Specify your gem's dependencies in devise_saml_authenticatable.gemspec
|
4
4
|
gemspec
|
5
|
+
|
6
|
+
group :test do
|
7
|
+
gem 'rake'
|
8
|
+
gem 'rspec', '~> 3.0'
|
9
|
+
gem 'rails'
|
10
|
+
gem 'rspec-rails'
|
11
|
+
gem 'sqlite3'
|
12
|
+
gem 'capybara-webkit'
|
13
|
+
end
|
14
|
+
|
data/README.md
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
+
[](https://travis-ci.org/apokalipto/devise_saml_authenticatable)
|
1
2
|
# DeviseSamlAuthenticatable
|
2
3
|
|
3
|
-
Devise Saml Authenticatable is a Single-Sign-On authentication strategy for devise that relies on SAML.
|
4
|
+
Devise Saml Authenticatable is a Single-Sign-On authentication strategy for devise that relies on SAML.
|
5
|
+
It uses [ruby-saml][] to handle all SAML related stuff.
|
4
6
|
|
5
7
|
## Installation
|
6
8
|
|
@@ -18,7 +20,8 @@ Or install it yourself as:
|
|
18
20
|
|
19
21
|
## Usage
|
20
22
|
|
21
|
-
In app/models/<YOUR_MODEL>.rb set the
|
23
|
+
In `app/models/<YOUR_MODEL>.rb` set the `:saml_authenticatable` strategy.
|
24
|
+
In the example the model is `user.rb`:
|
22
25
|
|
23
26
|
```ruby
|
24
27
|
class User < ActiveRecord::Base
|
@@ -33,51 +36,52 @@ In config/initializers/devise.rb
|
|
33
36
|
```ruby
|
34
37
|
Devise.setup do |config|
|
35
38
|
...
|
36
|
-
# ==>
|
39
|
+
# ==> Configuration for :saml_authenticatable
|
37
40
|
|
38
41
|
# Create user if the user does not exist. (Default is false)
|
39
42
|
config.saml_create_user = true
|
40
43
|
|
41
|
-
# Set the default user key
|
42
|
-
# the attribute
|
44
|
+
# Set the default user key. The user will be looked up by this key. Make
|
45
|
+
# sure that the Authentication Response includes the attribute.
|
43
46
|
config.saml_default_user_key = :email
|
47
|
+
|
48
|
+
# You can set this value to use Subject or SAML assertation as info to which email will be compared
|
49
|
+
# If you don't set it then email will be extracted from SAML assertation attributes
|
50
|
+
config.saml_use_subject = true
|
51
|
+
|
52
|
+
# Configure with your SAML settings (see [ruby-saml][] for more information).
|
53
|
+
config.saml_configure do |settings|
|
54
|
+
settings.assertion_consumer_service_url = "http://localhost:3000/users/saml/auth"
|
55
|
+
settings.assertion_consumer_service_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
56
|
+
settings.name_identifier_format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
|
57
|
+
settings.issuer = "http://localhost:3000"
|
58
|
+
settings.authn_context = ""
|
59
|
+
settings.idp_sso_target_url = "http://localhost/simplesaml/www/saml2/idp/SSOService.php"
|
60
|
+
settings.idp_cert = <<-CERT.chomp
|
61
|
+
-----BEGIN CERTIFICATE-----
|
62
|
+
1111111111111111111111111111111111111111111111111111111111111111
|
63
|
+
1111111111111111111111111111111111111111111111111111111111111111
|
64
|
+
1111111111111111111111111111111111111111111111111111111111111111
|
65
|
+
1111111111111111111111111111111111111111111111111111111111111111
|
66
|
+
1111111111111111111111111111111111111111111111111111111111111111
|
67
|
+
1111111111111_______IDP_CERTIFICATE________111111111111111111111
|
68
|
+
1111111111111111111111111111111111111111111111111111111111111111
|
69
|
+
1111111111111111111111111111111111111111111111111111111111111111
|
70
|
+
1111111111111111111111111111111111111111111111111111111111111111
|
71
|
+
1111111111111111111111111111111111111111111111111111111111111111
|
72
|
+
1111111111111111111111111111111111111111111111111111111111111111
|
73
|
+
1111111111111111111111111111111111111111111111111111111111111111
|
74
|
+
1111111111111111111111111111111111111111111111111111111111111111
|
75
|
+
111111111111111111
|
76
|
+
-----END CERTIFICATE-----
|
77
|
+
CERT
|
78
|
+
end
|
44
79
|
end
|
45
80
|
```
|
46
81
|
|
47
|
-
In config directory
|
48
|
-
|
49
|
-
```ruby
|
50
|
-
# idp.yaml
|
51
|
-
development:
|
52
|
-
idp_metadata: ""
|
53
|
-
idp_metadata_ttl: ""
|
54
|
-
assertion_consumer_service_url: "http://localhost:3000/users/sign_in"
|
55
|
-
assertion_consumer_service_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
56
|
-
name_identifier_format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
|
57
|
-
issuer: "http://localhost:3000"
|
58
|
-
authn_context: ""
|
59
|
-
idp_sso_target_url: "http://localhost/simplesaml/www/saml2/idp/SSOService.php"
|
60
|
-
idp_cert: |-
|
61
|
-
-----BEGIN CERTIFICATE-----
|
62
|
-
1111111111111111111111111111111111111111111111111111111111111111
|
63
|
-
1111111111111111111111111111111111111111111111111111111111111111
|
64
|
-
1111111111111111111111111111111111111111111111111111111111111111
|
65
|
-
1111111111111111111111111111111111111111111111111111111111111111
|
66
|
-
1111111111111111111111111111111111111111111111111111111111111111
|
67
|
-
1111111111111_______IDP_CERTIFICATE________111111111111111111111
|
68
|
-
1111111111111111111111111111111111111111111111111111111111111111
|
69
|
-
1111111111111111111111111111111111111111111111111111111111111111
|
70
|
-
1111111111111111111111111111111111111111111111111111111111111111
|
71
|
-
1111111111111111111111111111111111111111111111111111111111111111
|
72
|
-
1111111111111111111111111111111111111111111111111111111111111111
|
73
|
-
1111111111111111111111111111111111111111111111111111111111111111
|
74
|
-
1111111111111111111111111111111111111111111111111111111111111111
|
75
|
-
111111111111111111
|
76
|
-
-----END CERTIFICATE-----
|
77
|
-
```
|
78
|
-
In config directory create a YAML file (attribute-map.yml) that maps SAML attributes with your model's fields
|
82
|
+
In config directory create a YAML file (`attribute-map.yml`) that maps SAML attributes with your model's fields:
|
79
83
|
|
80
|
-
```
|
84
|
+
```yaml
|
81
85
|
# attribute-map.yml
|
82
86
|
|
83
87
|
"urn:mace:dir:attribute-def:uid": "user_name"
|
@@ -86,18 +90,19 @@ In config directory create a YAML file (attribute-map.yml) that maps SAML attrib
|
|
86
90
|
"urn:mace:dir:attribute-def:givenName": "name"
|
87
91
|
```
|
88
92
|
|
89
|
-
The attribute mappings are very dependent on the way the IdP encodes the attributes.
|
93
|
+
The attribute mappings are very dependent on the way the IdP encodes the attributes.
|
94
|
+
In this example the attributes are given in URN style.
|
95
|
+
Other IdPs might provide them as OID's or other means.
|
90
96
|
|
91
|
-
You are now ready to test it against an IdP.
|
97
|
+
You are now ready to test it against an IdP.
|
98
|
+
When the user goes to `/users/saml/sign_in` he will be redirected to the login page of the IdP.
|
99
|
+
Upon successful login the user is redirected to devise `user_root_path`.
|
92
100
|
|
93
101
|
## Identity Provider
|
94
102
|
|
95
103
|
If you don't have an identity provider an you would like to test the authentication against your app there are some options:
|
96
104
|
|
97
|
-
1. Use [ruby-saml-idp](https://github.com/lawrencepit/ruby-saml-idp).
|
98
|
-
|
99
|
-
You can add your own logic to your IdP, or you can also set it as a dummy IdP that always sends a valid authentication response to your app.
|
100
|
-
|
105
|
+
1. Use [ruby-saml-idp](https://github.com/lawrencepit/ruby-saml-idp). You can add your own logic to your IdP, or you can also set it as a dummy IdP that always sends a valid authentication response to your app.
|
101
106
|
2. Use an online service that can act as an IdP. Onelogin, Salesforce and some others provide you with this functionality
|
102
107
|
3. Install your own IdP.
|
103
108
|
|
@@ -107,13 +112,21 @@ There are numerous IdPs that support SAML 2.0, there are propietary (like Micros
|
|
107
112
|
|
108
113
|
## Limitations
|
109
114
|
|
110
|
-
1. At the moment there is no support for Single Logout
|
115
|
+
1. At the moment there is no support for Single Logout
|
111
116
|
2. The Authentication Requests (from your app to the IdP) are not signed and encrypted
|
112
117
|
|
118
|
+
## Thanks
|
119
|
+
|
120
|
+
The continued maintenance of this gem could not have been possible without the hard work of [Adam Stegman](https://github.com/adamstegman) and [Mitch Lindsay](https://github.com/mitch-lindsay). Thank you guys for keeping this project alive.
|
121
|
+
|
122
|
+
Thanks to all other contributors that have also helped us make this software better.
|
113
123
|
## Contributing
|
114
124
|
|
115
125
|
1. Fork it
|
116
126
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
117
127
|
3. Commit your changes (`git commit -am 'Added some feature'`)
|
118
|
-
4.
|
119
|
-
5.
|
128
|
+
4. Run the tests (`bundle exec rspec`)
|
129
|
+
5. Push to the branch (`git push origin my-new-feature`)
|
130
|
+
6. Create new Pull Request
|
131
|
+
|
132
|
+
[ruby-saml]: https://github.com/onelogin/ruby-saml
|
data/Rakefile
CHANGED
@@ -4,6 +4,8 @@ class Devise::SamlSessionsController < Devise::SessionsController
|
|
4
4
|
include DeviseSamlAuthenticatable::SamlConfig
|
5
5
|
unloadable if Rails::VERSION::MAJOR < 4
|
6
6
|
before_filter :get_saml_config
|
7
|
+
skip_before_filter :verify_authenticity_token
|
8
|
+
|
7
9
|
def new
|
8
10
|
request = OneLogin::RubySaml::Authrequest.new
|
9
11
|
action = request.create(@saml_config)
|
@@ -14,7 +14,8 @@ Gem::Specification.new do |gem|
|
|
14
14
|
gem.name = "devise_saml_authenticatable"
|
15
15
|
gem.require_paths = ["lib"]
|
16
16
|
gem.version = DeviseSamlAuthenticatable::VERSION
|
17
|
-
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
|
18
19
|
gem.add_dependency("devise","> 2.0.0")
|
19
20
|
gem.add_dependency("ruby-saml",">= 0.8.2")
|
20
21
|
end
|
@@ -20,16 +20,22 @@ module Devise
|
|
20
20
|
# Allow logging
|
21
21
|
mattr_accessor :saml_logger
|
22
22
|
@@saml_logger = true
|
23
|
-
|
23
|
+
|
24
24
|
# Add valid users to database
|
25
25
|
mattr_accessor :saml_create_user
|
26
26
|
@@saml_create_user = false
|
27
|
-
|
28
|
-
mattr_accessor :saml_config
|
29
|
-
@@saml_config = "#{Rails.root}/config/saml.yml"
|
30
|
-
|
27
|
+
|
31
28
|
mattr_accessor :saml_default_user_key
|
32
29
|
@@saml_default_user_key
|
30
|
+
|
31
|
+
mattr_accessor :saml_use_subject
|
32
|
+
@@saml_use_subject
|
33
|
+
|
34
|
+
mattr_accessor :saml_config
|
35
|
+
@@saml_config = OneLogin::RubySaml::Settings.new
|
36
|
+
def self.saml_configure
|
37
|
+
yield saml_config
|
38
|
+
end
|
33
39
|
end
|
34
40
|
|
35
41
|
# Add saml_authenticatable strategy to defaults.
|
@@ -4,7 +4,7 @@ module Devise
|
|
4
4
|
module Models
|
5
5
|
module SamlAuthenticatable
|
6
6
|
extend ActiveSupport::Concern
|
7
|
-
|
7
|
+
|
8
8
|
# Need to determine why these need to be included
|
9
9
|
included do
|
10
10
|
attr_reader :password, :current_password
|
@@ -26,37 +26,45 @@ module Devise
|
|
26
26
|
|
27
27
|
module ClassMethods
|
28
28
|
include DeviseSamlAuthenticatable::SamlConfig
|
29
|
-
def authenticate_with_saml(
|
29
|
+
def authenticate_with_saml(saml_response)
|
30
30
|
key = Devise.saml_default_user_key
|
31
|
-
|
32
|
-
|
33
|
-
|
31
|
+
attributes = saml_response.attributes
|
32
|
+
if (Devise.saml_use_subject)
|
33
|
+
auth_value = saml_response.name_id
|
34
|
+
else
|
35
|
+
inv_attr = attribute_map.invert
|
36
|
+
auth_value = attributes[inv_attr[key.to_s]]
|
37
|
+
auth_value.try(:downcase!) if Devise.case_insensitive_keys.include?(key)
|
38
|
+
end
|
34
39
|
resource = where(key => auth_value).first
|
35
40
|
if (resource.nil? && !Devise.saml_create_user)
|
36
|
-
logger.info("User(#{
|
41
|
+
logger.info("User(#{auth_value}) not found. Not configured to create the user.")
|
37
42
|
return nil
|
38
43
|
end
|
39
44
|
|
40
|
-
|
41
|
-
logger.info("Creating user(#{
|
42
|
-
|
45
|
+
if (resource.nil? && Devise.saml_create_user)
|
46
|
+
logger.info("Creating user(#{auth_value}).")
|
47
|
+
resource = new
|
43
48
|
set_user_saml_attributes(resource,attributes)
|
49
|
+
if (Devise.saml_use_subject)
|
50
|
+
resource.send "#{key}=", auth_value
|
51
|
+
end
|
44
52
|
resource.save!
|
45
53
|
end
|
46
54
|
|
47
55
|
resource
|
48
|
-
|
56
|
+
end
|
49
57
|
|
50
58
|
def find_for_shibb_authentication(conditions)
|
51
59
|
find_for_authentication(conditions)
|
52
60
|
end
|
53
|
-
|
61
|
+
|
54
62
|
def attribute_map
|
55
63
|
@attribute_map ||= YAML.load(File.read("#{Rails.root}/config/attribute-map.yml"))
|
56
64
|
end
|
57
65
|
|
58
66
|
private
|
59
|
-
|
67
|
+
|
60
68
|
def set_user_saml_attributes(user,attributes)
|
61
69
|
attribute_map.each do |k,v|
|
62
70
|
Rails.logger.info "Setting: #{v}, #{attributes[k]}"
|
@@ -2,7 +2,13 @@ require 'ruby-saml'
|
|
2
2
|
module DeviseSamlAuthenticatable
|
3
3
|
module SamlConfig
|
4
4
|
def get_saml_config
|
5
|
-
|
5
|
+
idp_config_path = "#{Rails.root}/config/idp.yml"
|
6
|
+
# Support 0.0.x-style configuration via a YAML file
|
7
|
+
if File.exists?(idp_config_path)
|
8
|
+
Devise.saml_config = OneLogin::RubySaml::Settings.new(YAML.load(File.read(idp_config_path))[Rails.env])
|
9
|
+
end
|
10
|
+
|
11
|
+
@saml_config = Devise.saml_config
|
6
12
|
end
|
7
13
|
end
|
8
14
|
end
|
@@ -8,22 +8,22 @@ module Devise
|
|
8
8
|
end
|
9
9
|
def authenticate!
|
10
10
|
@response = OneLogin::RubySaml::Response.new(params[:SAMLResponse])
|
11
|
-
|
12
|
-
|
11
|
+
@response.settings = get_saml_config
|
12
|
+
resource = mapping.to.authenticate_with_saml(@response)
|
13
13
|
if @response.is_valid?
|
14
14
|
success!(resource)
|
15
15
|
else
|
16
16
|
fail!(:invalid)
|
17
17
|
end
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
# This method should turn off storage whenever CSRF cannot be verified.
|
21
21
|
# Any known way on how to let the IdP send the CSRF token along with the SAMLResponse ?
|
22
22
|
# Please let me know!
|
23
23
|
def store?
|
24
24
|
true
|
25
25
|
end
|
26
|
-
|
26
|
+
|
27
27
|
end
|
28
28
|
end
|
29
29
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
class Devise::SessionsController < ActionController::Base
|
4
|
+
|
5
|
+
end
|
6
|
+
|
7
|
+
require_relative '../../../app/controllers/devise/saml_sessions_controller'
|
8
|
+
|
9
|
+
|
10
|
+
describe Devise::SamlSessionsController, type: :controller do
|
11
|
+
let(:saml_config) { Devise.saml_config }
|
12
|
+
|
13
|
+
describe '#new' do
|
14
|
+
it 'redirects to the SAML Auth Request endpoint' do
|
15
|
+
get :new
|
16
|
+
expect(response).to redirect_to(%r(\Ahttp://localhost:8009/saml/auth\?SAMLRequest=))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#metadata' do
|
21
|
+
it "generates metadata" do
|
22
|
+
get :metadata
|
23
|
+
|
24
|
+
# Remove ID that can vary across requests
|
25
|
+
expected_metadata = OneLogin::RubySaml::Metadata.new.generate(saml_config)
|
26
|
+
metadata_pattern = Regexp.escape(expected_metadata).gsub(/ ID='[^']+'/, " ID='[\\w-]+'")
|
27
|
+
expect(response.body).to match(Regexp.new(metadata_pattern))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Devise::Models::SamlAuthenticatable do
|
4
|
+
class Model
|
5
|
+
include Devise::Models::SamlAuthenticatable
|
6
|
+
attr_accessor :email, :name, :saved
|
7
|
+
def save!
|
8
|
+
self.saved = true
|
9
|
+
end
|
10
|
+
|
11
|
+
# Fake out ActiveRecord and Devise API to satisfy verifiable mocks
|
12
|
+
class << self
|
13
|
+
def where(*args); end
|
14
|
+
def logger; end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
before do
|
19
|
+
logger = double(:logger).as_null_object
|
20
|
+
allow(Model).to receive(:logger).and_return(logger)
|
21
|
+
allow(Rails).to receive(:logger).and_return(logger)
|
22
|
+
end
|
23
|
+
|
24
|
+
before do
|
25
|
+
allow(Devise).to receive(:saml_default_user_key).and_return(:email)
|
26
|
+
allow(Devise).to receive(:saml_create_user).and_return(false)
|
27
|
+
allow(Devise).to receive(:saml_use_subject).and_return(false)
|
28
|
+
end
|
29
|
+
|
30
|
+
before do
|
31
|
+
allow(Rails).to receive(:root).and_return("/railsroot")
|
32
|
+
allow(File).to receive(:read).with("/railsroot/config/attribute-map.yml").and_return(<<-ATTRIBUTEMAP)
|
33
|
+
---
|
34
|
+
"saml-email-format": email
|
35
|
+
"saml-name-format": name
|
36
|
+
ATTRIBUTEMAP
|
37
|
+
end
|
38
|
+
|
39
|
+
let(:response) { double(:response, attributes: attributes, name_id: name_id) }
|
40
|
+
let(:attributes) {
|
41
|
+
OneLogin::RubySaml::Attributes.new(
|
42
|
+
'saml-email-format' => ['user@example.com'],
|
43
|
+
'saml-name-format' => ['A User'],
|
44
|
+
)
|
45
|
+
}
|
46
|
+
let(:name_id) { nil }
|
47
|
+
|
48
|
+
it "looks up the user by the configured default user key" do
|
49
|
+
user = double(:user)
|
50
|
+
expect(Model).to receive(:where).with(email: 'user@example.com').and_return([user])
|
51
|
+
expect(Model.authenticate_with_saml(response)).to eq(user)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "returns nil if it cannot find a user" do
|
55
|
+
expect(Model).to receive(:where).with(email: 'user@example.com').and_return([])
|
56
|
+
expect(Model.authenticate_with_saml(response)).to be_nil
|
57
|
+
end
|
58
|
+
|
59
|
+
context "when configured to use the subject" do
|
60
|
+
let(:attributes) { OneLogin::RubySaml::Attributes.new('saml-name-format' => ['A User']) }
|
61
|
+
let(:name_id) { 'user@example.com' }
|
62
|
+
|
63
|
+
before do
|
64
|
+
allow(Devise).to receive(:saml_use_subject).and_return(true)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "looks up the user by the configured default user key" do
|
68
|
+
user = double(:user)
|
69
|
+
expect(Model).to receive(:where).with(email: 'user@example.com').and_return([user])
|
70
|
+
expect(Model.authenticate_with_saml(response)).to eq(user)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "returns nil if it cannot find a user" do
|
74
|
+
expect(Model).to receive(:where).with(email: 'user@example.com').and_return([])
|
75
|
+
expect(Model.authenticate_with_saml(response)).to be_nil
|
76
|
+
end
|
77
|
+
|
78
|
+
context "when configured to create a user and the user is not found" do
|
79
|
+
before do
|
80
|
+
allow(Devise).to receive(:saml_create_user).and_return(true)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "creates and returns a new user with the name identifier and given attributes" do
|
84
|
+
expect(Model).to receive(:where).with(email: 'user@example.com').and_return([])
|
85
|
+
model = Model.authenticate_with_saml(response)
|
86
|
+
expect(model.email).to eq('user@example.com')
|
87
|
+
expect(model.name).to eq('A User')
|
88
|
+
expect(model.saved).to be(true)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context "when configured to create a user and the user is not found" do
|
94
|
+
before do
|
95
|
+
allow(Devise).to receive(:saml_create_user).and_return(true)
|
96
|
+
end
|
97
|
+
|
98
|
+
it "creates and returns a new user with the given attributes" do
|
99
|
+
expect(Model).to receive(:where).with(email: 'user@example.com').and_return([])
|
100
|
+
model = Model.authenticate_with_saml(response)
|
101
|
+
expect(model.email).to eq('user@example.com')
|
102
|
+
expect(model.name).to eq('A User')
|
103
|
+
expect(model.saved).to be(true)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
context "when configured with a case-insensitive key" do
|
108
|
+
before do
|
109
|
+
allow(Devise).to receive(:case_insensitive_keys).and_return([:email])
|
110
|
+
end
|
111
|
+
|
112
|
+
it "looks up the user with a downcased value" do
|
113
|
+
user = double(:user)
|
114
|
+
expect(Model).to receive(:where).with(email: 'user@example.com').and_return([user])
|
115
|
+
expect(Model.authenticate_with_saml(response)).to eq(user)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe DeviseSamlAuthenticatable::SamlConfig do
|
4
|
+
subject(:saml_config) { controller.get_saml_config }
|
5
|
+
let(:controller) { Class.new { include DeviseSamlAuthenticatable::SamlConfig }.new }
|
6
|
+
|
7
|
+
# Replace global config since this test changes it
|
8
|
+
before do
|
9
|
+
@original_saml_config = Devise.saml_config
|
10
|
+
end
|
11
|
+
after do
|
12
|
+
Devise.saml_config = @original_saml_config
|
13
|
+
end
|
14
|
+
|
15
|
+
context "when config/idp.yml does not exist" do
|
16
|
+
before do
|
17
|
+
allow(Rails).to receive(:root).and_return("/railsroot")
|
18
|
+
allow(File).to receive(:exists?).with("/railsroot/config/idp.yml").and_return(false)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "is the global devise SAML config" do
|
22
|
+
Devise.saml_configure do |settings|
|
23
|
+
settings.assertion_consumer_logout_service_binding = 'test'
|
24
|
+
end
|
25
|
+
expect(saml_config).to be(Devise.saml_config)
|
26
|
+
expect(saml_config.assertion_consumer_logout_service_binding).to eq('test')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context "when config/idp.yml exists" do
|
31
|
+
before do
|
32
|
+
allow(Rails).to receive(:env).and_return("environment")
|
33
|
+
allow(Rails).to receive(:root).and_return("/railsroot")
|
34
|
+
allow(File).to receive(:exists?).with("/railsroot/config/idp.yml").and_return(true)
|
35
|
+
allow(File).to receive(:read).with("/railsroot/config/idp.yml").and_return(<<-IDP)
|
36
|
+
---
|
37
|
+
environment:
|
38
|
+
assertion_consumer_logout_service_binding: assertion_consumer_logout_service_binding
|
39
|
+
assertion_consumer_logout_service_url: assertion_consumer_logout_service_url
|
40
|
+
assertion_consumer_service_binding: assertion_consumer_service_binding
|
41
|
+
assertion_consumer_service_url: assertion_consumer_service_url
|
42
|
+
attributes_index: attributes_index
|
43
|
+
authn_context: authn_context
|
44
|
+
authn_context_comparison: authn_context_comparison
|
45
|
+
authn_context_decl_ref: authn_context_decl_ref
|
46
|
+
certificate: certificate
|
47
|
+
compress_request: compress_request
|
48
|
+
compress_response: compress_response
|
49
|
+
double_quote_xml_attribute_values: double_quote_xml_attribute_values
|
50
|
+
force_authn: force_authn
|
51
|
+
idp_cert: idp_cert
|
52
|
+
idp_cert_fingerprint: idp_cert_fingerprint
|
53
|
+
idp_cert_fingerprint_algorithm: idp_cert_fingerprint_algorithm
|
54
|
+
idp_entity_id: idp_entity_id
|
55
|
+
idp_slo_target_url: idp_slo_target_url
|
56
|
+
idp_sso_target_url: idp_sso_target_url
|
57
|
+
issuer: issuer
|
58
|
+
name_identifier_format: name_identifier_format
|
59
|
+
name_identifier_value: name_identifier_value
|
60
|
+
passive: passive
|
61
|
+
private_key: private_key
|
62
|
+
protocol_binding: protocol_binding
|
63
|
+
security: security
|
64
|
+
sessionindex: sessionindex
|
65
|
+
sp_name_qualifier: sp_name_qualifier
|
66
|
+
IDP
|
67
|
+
end
|
68
|
+
|
69
|
+
it "stores the configured IdP settings" do
|
70
|
+
expect(saml_config.assertion_consumer_logout_service_binding).to eq('assertion_consumer_logout_service_binding')
|
71
|
+
expect(saml_config.assertion_consumer_logout_service_url).to eq('assertion_consumer_logout_service_url')
|
72
|
+
expect(saml_config.assertion_consumer_service_binding).to eq('assertion_consumer_service_binding')
|
73
|
+
expect(saml_config.assertion_consumer_service_url).to eq('assertion_consumer_service_url')
|
74
|
+
expect(saml_config.attributes_index).to eq('attributes_index')
|
75
|
+
expect(saml_config.authn_context).to eq('authn_context')
|
76
|
+
expect(saml_config.authn_context_comparison).to eq('authn_context_comparison')
|
77
|
+
expect(saml_config.authn_context_decl_ref).to eq('authn_context_decl_ref')
|
78
|
+
expect(saml_config.certificate).to eq('certificate')
|
79
|
+
expect(saml_config.compress_request).to eq('compress_request')
|
80
|
+
expect(saml_config.compress_response).to eq('compress_response')
|
81
|
+
expect(saml_config.double_quote_xml_attribute_values).to eq('double_quote_xml_attribute_values')
|
82
|
+
expect(saml_config.force_authn).to eq('force_authn')
|
83
|
+
expect(saml_config.idp_cert).to eq('idp_cert')
|
84
|
+
expect(saml_config.idp_cert_fingerprint).to eq('idp_cert_fingerprint')
|
85
|
+
expect(saml_config.idp_cert_fingerprint_algorithm).to eq('idp_cert_fingerprint_algorithm')
|
86
|
+
expect(saml_config.idp_entity_id).to eq('idp_entity_id')
|
87
|
+
expect(saml_config.idp_slo_target_url).to eq('idp_slo_target_url')
|
88
|
+
expect(saml_config.idp_sso_target_url).to eq('idp_sso_target_url')
|
89
|
+
expect(saml_config.issuer).to eq('issuer')
|
90
|
+
expect(saml_config.name_identifier_format).to eq('name_identifier_format')
|
91
|
+
expect(saml_config.name_identifier_value).to eq('name_identifier_value')
|
92
|
+
expect(saml_config.passive).to eq('passive')
|
93
|
+
expect(saml_config.private_key).to eq('private_key')
|
94
|
+
expect(saml_config.protocol_binding).to eq('protocol_binding')
|
95
|
+
expect(saml_config.security).to eq('security')
|
96
|
+
expect(saml_config.sessionindex).to eq('sessionindex')
|
97
|
+
expect(saml_config.sp_name_qualifier).to eq('sp_name_qualifier')
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Devise::Strategies::SamlAuthenticatable do
|
4
|
+
subject(:strategy) { described_class.new(env, :user) }
|
5
|
+
let(:env) { {} }
|
6
|
+
|
7
|
+
let(:response) { double(:response, :settings= => nil, is_valid?: true) }
|
8
|
+
before do
|
9
|
+
allow(OneLogin::RubySaml::Response).to receive(:new).and_return(response)
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:saml_config) { OneLogin::RubySaml::Settings.new }
|
13
|
+
before do
|
14
|
+
allow(strategy).to receive(:get_saml_config).and_return(saml_config)
|
15
|
+
end
|
16
|
+
|
17
|
+
let(:mapping) { double(:mapping, to: user_class) }
|
18
|
+
let(:user_class) { double(:user_class, authenticate_with_saml: user) }
|
19
|
+
let(:user) { double(:user) }
|
20
|
+
before do
|
21
|
+
allow(strategy).to receive(:mapping).and_return(mapping)
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:params) { {} }
|
25
|
+
before do
|
26
|
+
allow(strategy).to receive(:params).and_return(params)
|
27
|
+
end
|
28
|
+
|
29
|
+
context "with a SAMLResponse parameter" do
|
30
|
+
let(:params) { {SAMLResponse: ""} }
|
31
|
+
|
32
|
+
it "is valid" do
|
33
|
+
expect(strategy).to be_valid
|
34
|
+
end
|
35
|
+
|
36
|
+
it "authenticates with the response" do
|
37
|
+
expect(OneLogin::RubySaml::Response).to receive(:new).with(params[:SAMLResponse])
|
38
|
+
expect(response).to receive(:settings=).with(saml_config)
|
39
|
+
expect(user_class).to receive(:authenticate_with_saml).with(response)
|
40
|
+
|
41
|
+
expect(strategy).to receive(:success!).with(user)
|
42
|
+
strategy.authenticate!
|
43
|
+
end
|
44
|
+
|
45
|
+
context "and the SAML response is not valid" do
|
46
|
+
before do
|
47
|
+
allow(response).to receive(:is_valid?).and_return(false)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "fails to authenticate" do
|
51
|
+
expect(strategy).to receive(:fail!).with(:invalid)
|
52
|
+
strategy.authenticate!
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
it "is not valid without a SAMLResponse parameter" do
|
58
|
+
expect(strategy).not_to be_valid
|
59
|
+
end
|
60
|
+
|
61
|
+
it "is permanent" do
|
62
|
+
expect(strategy).to be_store
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'net/http'
|
3
|
+
require 'uri'
|
4
|
+
require 'capybara/rspec'
|
5
|
+
require 'capybara/webkit'
|
6
|
+
Capybara.default_driver = :webkit
|
7
|
+
|
8
|
+
describe "SAML Authentication", type: :feature do
|
9
|
+
let(:idp_port) { 8009 }
|
10
|
+
let(:sp_port) { 8020 }
|
11
|
+
|
12
|
+
shared_examples_for "it authenticates and creates users" do
|
13
|
+
it "authenticates an existing user on a SP via an IdP" do
|
14
|
+
create_user("you@example.com")
|
15
|
+
|
16
|
+
visit 'http://localhost:8020/'
|
17
|
+
expect(current_url).to match(%r(\Ahttp://localhost:8009/saml/auth\?SAMLRequest=))
|
18
|
+
fill_in "Email", with: "you@example.com"
|
19
|
+
fill_in "Password", with: "asdf"
|
20
|
+
click_on "Sign in"
|
21
|
+
expect(page).to have_content("you@example.com")
|
22
|
+
expect(current_url).to eq("http://localhost:8020/")
|
23
|
+
end
|
24
|
+
|
25
|
+
it "creates a user on the SP from the IdP attributes" do
|
26
|
+
visit 'http://localhost:8020/'
|
27
|
+
expect(current_url).to match(%r(\Ahttp://localhost:8009/saml/auth\?SAMLRequest=))
|
28
|
+
fill_in "Email", with: "you@example.com"
|
29
|
+
fill_in "Password", with: "asdf"
|
30
|
+
click_on "Sign in"
|
31
|
+
expect(page).to have_content("you@example.com")
|
32
|
+
expect(page).to have_content("A User")
|
33
|
+
expect(current_url).to eq("http://localhost:8020/")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "when the attributes are used to authenticate" do
|
38
|
+
before(:each) do
|
39
|
+
create_app('idp', %w(y))
|
40
|
+
create_app('sp', %w(n))
|
41
|
+
@idp_pid = start_app('idp', idp_port)
|
42
|
+
@sp_pid = start_app('sp', sp_port)
|
43
|
+
end
|
44
|
+
after(:each) do
|
45
|
+
stop_app(@idp_pid)
|
46
|
+
stop_app(@sp_pid)
|
47
|
+
end
|
48
|
+
|
49
|
+
it_behaves_like "it authenticates and creates users"
|
50
|
+
end
|
51
|
+
|
52
|
+
context "when the subject is used to authenticate" do
|
53
|
+
before(:each) do
|
54
|
+
create_app('idp', %w(n))
|
55
|
+
create_app('sp', %w(y))
|
56
|
+
@idp_pid = start_app('idp', idp_port)
|
57
|
+
@sp_pid = start_app('sp', sp_port)
|
58
|
+
end
|
59
|
+
after(:each) do
|
60
|
+
stop_app(@idp_pid)
|
61
|
+
stop_app(@sp_pid)
|
62
|
+
end
|
63
|
+
|
64
|
+
it_behaves_like "it authenticates and creates users"
|
65
|
+
end
|
66
|
+
|
67
|
+
def create_user(email)
|
68
|
+
response = Net::HTTP.post_form(URI('http://localhost:8020/users'), email: email)
|
69
|
+
expect(response.code).to eq('201')
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
ENV["RAILS_ENV"] ||= 'test'
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
create_app('sp', %w(n))
|
6
|
+
require 'support/sp/config/environment'
|
7
|
+
require 'rspec/rails'
|
8
|
+
|
9
|
+
ActiveRecord::Migration.verbose = false
|
10
|
+
ActiveRecord::Base.logger = Logger.new(nil)
|
11
|
+
ActiveRecord::Migrator.migrate(File.expand_path("../support/sp/db/migrate/", __FILE__))
|
12
|
+
|
13
|
+
RSpec.configure do |config|
|
14
|
+
config.use_transactional_fixtures = true
|
15
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
RSpec.configure do |config|
|
2
|
+
config.run_all_when_everything_filtered = true
|
3
|
+
config.filter_run :focus
|
4
|
+
config.order = 'random'
|
5
|
+
|
6
|
+
config.expect_with :rspec do |expectations|
|
7
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
8
|
+
end
|
9
|
+
|
10
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
11
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
12
|
+
config.mock_with :rspec do |mocks|
|
13
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
14
|
+
# a real object. This is generally recommended, and will default to
|
15
|
+
# `true` in RSpec 4.
|
16
|
+
mocks.verify_partial_doubles = true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
require 'support/rails_app'
|
21
|
+
|
22
|
+
require 'devise_saml_authenticatable'
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# Set up a SAML IdP
|
2
|
+
|
3
|
+
@include_subject_in_attributes = ask("Include the subject in the attributes?", limit: %w(y n)) == "y"
|
4
|
+
|
5
|
+
gem 'ruby-saml-idp'
|
6
|
+
|
7
|
+
route "get '/saml/auth' => 'saml_idp#new'"
|
8
|
+
route "post '/saml/auth' => 'saml_idp#create'"
|
9
|
+
|
10
|
+
template File.expand_path('../saml_idp_controller.rb.erb', __FILE__), 'app/controllers/saml_idp_controller.rb'
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
def sh!(cmd)
|
4
|
+
unless system(cmd)
|
5
|
+
raise "[#{cmd}] failed with exit code #{$?.exitstatus}"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def app_ready?(pid, port)
|
10
|
+
Process.getpgid(pid) &&
|
11
|
+
system("lsof -i:#{port}", out: '/dev/null')
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_app(name, answers = [])
|
15
|
+
rails_new_options = %w(-T -J -S --skip-spring)
|
16
|
+
rails_new_options << "-O" if name == 'idp'
|
17
|
+
Bundler.with_clean_env do
|
18
|
+
Dir.chdir(File.expand_path('../../support', __FILE__)) do
|
19
|
+
FileUtils.rm_rf(name)
|
20
|
+
Open3.popen3("rails", "new", name, *rails_new_options, "-m", "#{name}_template.rb") do |stdin, stdout, stderr, wait_thread|
|
21
|
+
while answers.any?
|
22
|
+
question = stdout.gets
|
23
|
+
answer = answers.shift
|
24
|
+
stdin.puts answer
|
25
|
+
$stdout.puts "#{question} #{answer}"
|
26
|
+
end
|
27
|
+
wait_thread.join
|
28
|
+
|
29
|
+
$stdout.puts stdout.read
|
30
|
+
$stderr.puts stderr.read
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def start_app(name, port, options = {})
|
37
|
+
pid = nil
|
38
|
+
Bundler.with_clean_env do
|
39
|
+
Dir.chdir(File.expand_path("../../support/#{name}", __FILE__)) do
|
40
|
+
pid = Process.spawn("bundle exec rails server -p #{port}")
|
41
|
+
sleep 1 until app_ready?(pid, port)
|
42
|
+
if app_ready?(pid, port)
|
43
|
+
puts "Launched #{name} on port #{port} (pid #{pid})..."
|
44
|
+
else
|
45
|
+
raise "#{name} failed to start"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
pid
|
50
|
+
end
|
51
|
+
|
52
|
+
def stop_app(pid)
|
53
|
+
if pid
|
54
|
+
Process.kill(:INT, pid)
|
55
|
+
Process.wait(pid)
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
class SamlIdpController < SamlIdp::IdpController
|
2
|
+
def idp_authenticate(email, password)
|
3
|
+
true
|
4
|
+
end
|
5
|
+
|
6
|
+
def idp_make_saml_response(user)
|
7
|
+
attributes = {
|
8
|
+
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" => "A User",
|
9
|
+
}
|
10
|
+
if include_subject_in_attributes
|
11
|
+
attributes["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"] = "you@example.com"
|
12
|
+
end
|
13
|
+
encode_SAMLResponse("you@example.com", attributes: attributes)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def encode_SAMLResponse(nameID, opts = {})
|
19
|
+
now = Time.now.utc
|
20
|
+
response_id, reference_id = UUID.generate, UUID.generate
|
21
|
+
audience_uri = opts[:audience_uri] || saml_acs_url[/^(.*?\/\/.*?\/)/, 1]
|
22
|
+
issuer_uri = opts[:issuer_uri] || (defined?(request) && request.url) || "http://example.com"
|
23
|
+
|
24
|
+
attributes = opts.fetch(:attributes, {})
|
25
|
+
if attributes.any?
|
26
|
+
attribute_items = attributes.map { |format, value|
|
27
|
+
%[<Attribute Name="#{format}"><AttributeValue>#{value}</AttributeValue></Attribute>]
|
28
|
+
}
|
29
|
+
attribute_statement = %[<AttributeStatement>#{attribute_items.join}</AttributeStatement>]
|
30
|
+
else
|
31
|
+
attribute_statement = ""
|
32
|
+
end
|
33
|
+
|
34
|
+
assertion = %[<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="_#{reference_id}" IssueInstant="#{now.iso8601}" Version="2.0"><Issuer>#{issuer_uri}</Issuer><Subject><NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">#{nameID}</NameID><SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><SubjectConfirmationData InResponseTo="#{@saml_request_id}" NotOnOrAfter="#{(now+3*60).iso8601}" Recipient="#{@saml_acs_url}"></SubjectConfirmationData></SubjectConfirmation></Subject><Conditions NotBefore="#{(now-5).iso8601}" NotOnOrAfter="#{(now+60*60).iso8601}"><AudienceRestriction><Audience>#{audience_uri}</Audience></AudienceRestriction></Conditions>#{attribute_statement}<AuthnStatement AuthnInstant="#{now.iso8601}" SessionIndex="_#{reference_id}"><AuthnContext><AuthnContextClassRef>urn:federation:authentication:windows</AuthnContextClassRef></AuthnContext></AuthnStatement></Assertion>]
|
35
|
+
|
36
|
+
digest_value = Base64.encode64(algorithm.digest(assertion)).gsub(/\n/, '')
|
37
|
+
|
38
|
+
signed_info = %[<ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:CanonicalizationMethod><ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-#{algorithm_name}"></ds:SignatureMethod><ds:Reference URI="#_#{reference_id}"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></ds:Transform><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig##{algorithm_name}"></ds:DigestMethod><ds:DigestValue>#{digest_value}</ds:DigestValue></ds:Reference></ds:SignedInfo>]
|
39
|
+
|
40
|
+
signature_value = sign(signed_info).gsub(/\n/, '')
|
41
|
+
|
42
|
+
signature = %[<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">#{signed_info}<ds:SignatureValue>#{signature_value}</ds:SignatureValue><KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"><ds:X509Data><ds:X509Certificate>#{self.x509_certificate}</ds:X509Certificate></ds:X509Data></KeyInfo></ds:Signature>]
|
43
|
+
|
44
|
+
assertion_and_signature = assertion.sub(/Issuer\>\<Subject/, "Issuer>#{signature}<Subject")
|
45
|
+
|
46
|
+
xml = %[<samlp:Response ID="_#{response_id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{@saml_acs_url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="#{@saml_request_id}" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"><Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">#{issuer_uri}</Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status>#{assertion_and_signature}</samlp:Response>]
|
47
|
+
|
48
|
+
Base64.encode64(xml)
|
49
|
+
end
|
50
|
+
|
51
|
+
def include_subject_in_attributes
|
52
|
+
<%= @include_subject_in_attributes %>
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# Set up a SAML Service Provider
|
2
|
+
|
3
|
+
use_subject_to_authenticate = ask("Use subject to authenticate?", limit: %w(y n)) == "y"
|
4
|
+
|
5
|
+
gem 'devise_saml_authenticatable', path: '../../..'
|
6
|
+
|
7
|
+
create_file 'config/attribute-map.yml', <<-ATTRIBUTES
|
8
|
+
---
|
9
|
+
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": email
|
10
|
+
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": name
|
11
|
+
ATTRIBUTES
|
12
|
+
|
13
|
+
after_bundle do
|
14
|
+
generate :controller, 'home', 'index'
|
15
|
+
insert_into_file('app/controllers/home_controller.rb', after: "class HomeController < ApplicationController\n") {
|
16
|
+
<<-AUTHENTICATE
|
17
|
+
before_action :authenticate_user!
|
18
|
+
AUTHENTICATE
|
19
|
+
}
|
20
|
+
insert_into_file('app/views/home/index.html.erb', after: /\z/) {
|
21
|
+
"<%= current_user.email %> <%= current_user.name %>"
|
22
|
+
}
|
23
|
+
route "root to: 'home#index'"
|
24
|
+
|
25
|
+
# Configure for our SAML IdP
|
26
|
+
generate 'devise:install'
|
27
|
+
gsub_file 'config/initializers/devise.rb', /^end$/, <<-CONFIG
|
28
|
+
config.saml_default_user_key = :email
|
29
|
+
|
30
|
+
config.saml_use_subject = #{use_subject_to_authenticate}
|
31
|
+
config.saml_create_user = true
|
32
|
+
|
33
|
+
config.saml_configure do |settings|
|
34
|
+
settings.assertion_consumer_service_url = "http://localhost:8020/users/saml/auth"
|
35
|
+
settings.issuer = "http://localhost:8020/saml/metadata"
|
36
|
+
settings.idp_sso_target_url = "http://localhost:8009/saml/auth"
|
37
|
+
settings.idp_cert_fingerprint = "9E:65:2E:03:06:8D:80:F2:86:C7:6C:77:A1:D9:14:97:0A:4D:F4:4D"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
CONFIG
|
41
|
+
|
42
|
+
generate :devise, "user", "email:string", "name:string"
|
43
|
+
gsub_file 'app/models/user.rb', /database_authenticatable.*\n.*/, 'saml_authenticatable'
|
44
|
+
route "resources :users"
|
45
|
+
create_file('app/controllers/users_controller.rb', <<-USERS)
|
46
|
+
class UsersController < ApplicationController
|
47
|
+
skip_before_filter :verify_authenticity_token
|
48
|
+
def create
|
49
|
+
User.create!(email: params[:email])
|
50
|
+
render nothing: true, status: 201
|
51
|
+
end
|
52
|
+
end
|
53
|
+
USERS
|
54
|
+
|
55
|
+
rake "db:create"
|
56
|
+
rake "db:migrate"
|
57
|
+
end
|
metadata
CHANGED
@@ -1,46 +1,41 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: devise_saml_authenticatable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
5
|
-
prerelease:
|
4
|
+
version: 0.1.0
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Josef Sauter
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date: 2015-
|
11
|
+
date: 2015-06-13 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: devise
|
16
15
|
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
16
|
requirements:
|
19
|
-
- -
|
17
|
+
- - ">"
|
20
18
|
- !ruby/object:Gem::Version
|
21
19
|
version: 2.0.0
|
22
20
|
type: :runtime
|
23
21
|
prerelease: false
|
24
22
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
23
|
requirements:
|
27
|
-
- -
|
24
|
+
- - ">"
|
28
25
|
- !ruby/object:Gem::Version
|
29
26
|
version: 2.0.0
|
30
27
|
- !ruby/object:Gem::Dependency
|
31
28
|
name: ruby-saml
|
32
29
|
requirement: !ruby/object:Gem::Requirement
|
33
|
-
none: false
|
34
30
|
requirements:
|
35
|
-
- -
|
31
|
+
- - ">="
|
36
32
|
- !ruby/object:Gem::Version
|
37
33
|
version: 0.8.2
|
38
34
|
type: :runtime
|
39
35
|
prerelease: false
|
40
36
|
version_requirements: !ruby/object:Gem::Requirement
|
41
|
-
none: false
|
42
37
|
requirements:
|
43
|
-
- -
|
38
|
+
- - ">="
|
44
39
|
- !ruby/object:Gem::Version
|
45
40
|
version: 0.8.2
|
46
41
|
description: SAML Authentication for devise
|
@@ -50,7 +45,9 @@ executables: []
|
|
50
45
|
extensions: []
|
51
46
|
extra_rdoc_files: []
|
52
47
|
files:
|
53
|
-
- .gitignore
|
48
|
+
- ".gitignore"
|
49
|
+
- ".rspec"
|
50
|
+
- ".travis.yml"
|
54
51
|
- Gemfile
|
55
52
|
- LICENSE
|
56
53
|
- README.md
|
@@ -66,29 +63,50 @@ files:
|
|
66
63
|
- lib/devise_saml_authenticatable/strategy.rb
|
67
64
|
- lib/devise_saml_authenticatable/version.rb
|
68
65
|
- rails/init.rb
|
66
|
+
- spec/controllers/devise/saml_sessions_controller_spec.rb
|
67
|
+
- spec/devise_saml_authenticatable/model_spec.rb
|
68
|
+
- spec/devise_saml_authenticatable/saml_config_spec.rb
|
69
|
+
- spec/devise_saml_authenticatable/strategy_spec.rb
|
70
|
+
- spec/features/saml_authentication_spec.rb
|
71
|
+
- spec/rails_helper.rb
|
72
|
+
- spec/spec_helper.rb
|
73
|
+
- spec/support/idp_template.rb
|
74
|
+
- spec/support/rails_app.rb
|
75
|
+
- spec/support/saml_idp_controller.rb.erb
|
76
|
+
- spec/support/sp_template.rb
|
69
77
|
homepage: ''
|
70
78
|
licenses: []
|
79
|
+
metadata: {}
|
71
80
|
post_install_message:
|
72
81
|
rdoc_options: []
|
73
82
|
require_paths:
|
74
83
|
- lib
|
75
84
|
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
-
none: false
|
77
85
|
requirements:
|
78
|
-
- -
|
86
|
+
- - ">="
|
79
87
|
- !ruby/object:Gem::Version
|
80
88
|
version: '0'
|
81
89
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
-
none: false
|
83
90
|
requirements:
|
84
|
-
- -
|
91
|
+
- - ">="
|
85
92
|
- !ruby/object:Gem::Version
|
86
93
|
version: '0'
|
87
94
|
requirements: []
|
88
95
|
rubyforge_project:
|
89
|
-
rubygems_version:
|
96
|
+
rubygems_version: 2.4.6
|
90
97
|
signing_key:
|
91
|
-
specification_version:
|
98
|
+
specification_version: 4
|
92
99
|
summary: SAML Authentication for devise
|
93
|
-
test_files:
|
100
|
+
test_files:
|
101
|
+
- spec/controllers/devise/saml_sessions_controller_spec.rb
|
102
|
+
- spec/devise_saml_authenticatable/model_spec.rb
|
103
|
+
- spec/devise_saml_authenticatable/saml_config_spec.rb
|
104
|
+
- spec/devise_saml_authenticatable/strategy_spec.rb
|
105
|
+
- spec/features/saml_authentication_spec.rb
|
106
|
+
- spec/rails_helper.rb
|
107
|
+
- spec/spec_helper.rb
|
108
|
+
- spec/support/idp_template.rb
|
109
|
+
- spec/support/rails_app.rb
|
110
|
+
- spec/support/saml_idp_controller.rb.erb
|
111
|
+
- spec/support/sp_template.rb
|
94
112
|
has_rdoc:
|