devise_saml_authenticatable 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/apokalipto/devise_saml_authenticatable.svg?branch=master)](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:
|