devise_saml_authenticatable 1.0 → 1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +9 -0
- data/app/controllers/devise/saml_sessions_controller.rb +25 -0
- data/devise_saml_authenticatable.gemspec +1 -1
- data/lib/devise_saml_authenticatable.rb +7 -0
- data/lib/devise_saml_authenticatable/model.rb +19 -0
- data/lib/devise_saml_authenticatable/routes.rb +1 -0
- data/lib/devise_saml_authenticatable/strategy.rb +8 -1
- data/lib/devise_saml_authenticatable/version.rb +1 -1
- data/spec/controllers/devise/saml_sessions_controller_spec.rb +67 -0
- data/spec/devise_saml_authenticatable/strategy_spec.rb +15 -4
- data/spec/features/saml_authentication_spec.rb +11 -0
- data/spec/rails_helper.rb +5 -0
- data/spec/support/idp_template.rb +9 -0
- data/spec/support/saml_idp_controller.rb.erb +56 -11
- data/spec/support/sp_template.rb +5 -1
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 11d475877b3a4f178413861a07e7fd030c65799b
|
4
|
+
data.tar.gz: f73eaa27a49431b435b502185af337668fceaf8e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6770f46c7251efd2b1bc7a9c5ed08f665e1653c2abf86de21e66a3783dc0bccde72465975ccceefca619812fc9621726ed68b11f634e1ae4691147fc023d46c4
|
7
|
+
data.tar.gz: 6ce53e6cc7a4b582e0801f595cdd7e42763841fb6c0606935886612692e2929323f29ec00f17e3625d04abdc7f2c22260dc2b3d9be4fffbc1e583cf18852ecf5
|
data/README.md
CHANGED
@@ -45,6 +45,10 @@ In config/initializers/devise.rb
|
|
45
45
|
# sure that the Authentication Response includes the attribute.
|
46
46
|
config.saml_default_user_key = :email
|
47
47
|
|
48
|
+
# Optional. This stores the session index defined by the IDP during login. If provided it will be used as a salt
|
49
|
+
# for the user's session to facilitate an IDP initiated logout request.
|
50
|
+
config.saml_session_index_key = :session_index
|
51
|
+
|
48
52
|
# You can set this value to use Subject or SAML assertation as info to which email will be compared
|
49
53
|
# If you don't set it then email will be extracted from SAML assertation attributes
|
50
54
|
config.saml_use_subject = true
|
@@ -115,6 +119,11 @@ There are numerous IdPs that support SAML 2.0, there are propietary (like Micros
|
|
115
119
|
|
116
120
|
Logout support is included by immediately terminating the local session and then redirecting to the IdP.
|
117
121
|
|
122
|
+
## Logout Request
|
123
|
+
|
124
|
+
Logout requests from the IDP are supported by the `idp_sign_out` end point. Directing logout requests to `users/saml/idp_sign_out` will logout the respective user by invalidating their current sessions.
|
125
|
+
`saml_session_index_key` must be configured to support this feature.
|
126
|
+
|
118
127
|
## Limitations
|
119
128
|
|
120
129
|
1. The Authentication Requests (from your app to the IdP) are not signed and encrypted
|
@@ -17,6 +17,26 @@ class Devise::SamlSessionsController < Devise::SessionsController
|
|
17
17
|
render :xml => meta.generate(@saml_config)
|
18
18
|
end
|
19
19
|
|
20
|
+
def idp_sign_out
|
21
|
+
if params[:SAMLRequest] && Devise.saml_session_index_key
|
22
|
+
logout_request = OneLogin::RubySaml::SloLogoutrequest.new(params[:SAMLRequest], @saml_config)
|
23
|
+
resource_class.reset_session_key_for(logout_request.name_id)
|
24
|
+
|
25
|
+
redirect_to generate_idp_logout_response(logout_request)
|
26
|
+
elsif params[:SAMLResponse]
|
27
|
+
#Currently Devise handles the session invalidation when the request is made.
|
28
|
+
#To support a true SP initiated logout response, the request ID would have to be tracked and session invalidated
|
29
|
+
#based on that.
|
30
|
+
if Devise.saml_sign_out_success_url
|
31
|
+
redirect_to Devise.saml_sign_out_success_url
|
32
|
+
else
|
33
|
+
redirect_to action: :new
|
34
|
+
end
|
35
|
+
else
|
36
|
+
head :invalid_request
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
20
40
|
protected
|
21
41
|
|
22
42
|
# Override devise to send user to IdP logout for SLO
|
@@ -24,5 +44,10 @@ class Devise::SamlSessionsController < Devise::SessionsController
|
|
24
44
|
request = OneLogin::RubySaml::Logoutrequest.new
|
25
45
|
request.create(@saml_config)
|
26
46
|
end
|
47
|
+
|
48
|
+
def generate_idp_logout_response(logout_request)
|
49
|
+
logout_request_id = logout_request.id
|
50
|
+
OneLogin::RubySaml::SloLogoutresponse.new.create(@saml_config, logout_request_id, nil)
|
51
|
+
end
|
27
52
|
end
|
28
53
|
|
@@ -5,6 +5,7 @@ require "devise_saml_authenticatable/exception"
|
|
5
5
|
require "devise_saml_authenticatable/logger"
|
6
6
|
require "devise_saml_authenticatable/routes"
|
7
7
|
require "devise_saml_authenticatable/saml_config"
|
8
|
+
|
8
9
|
begin
|
9
10
|
Rails::Engine
|
10
11
|
rescue
|
@@ -31,6 +32,12 @@ module Devise
|
|
31
32
|
mattr_accessor :saml_use_subject
|
32
33
|
@@saml_use_subject
|
33
34
|
|
35
|
+
mattr_accessor :saml_session_index_key
|
36
|
+
@@saml_session_index_key
|
37
|
+
|
38
|
+
mattr_accessor :saml_sign_out_success_url
|
39
|
+
@@saml_sign_out_success_url
|
40
|
+
|
34
41
|
mattr_accessor :saml_config
|
35
42
|
@@saml_config = OneLogin::RubySaml::Settings.new
|
36
43
|
def self.saml_configure
|
@@ -24,6 +24,20 @@ module Devise
|
|
24
24
|
result
|
25
25
|
end
|
26
26
|
|
27
|
+
def after_saml_authentication(session_index)
|
28
|
+
if self.respond_to? Devise.saml_session_index_key
|
29
|
+
self.update_attribute(Devise.saml_session_index_key, session_index)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def authenticatable_salt
|
34
|
+
if self.respond_to?(Devise.saml_session_index_key) && self.send(Devise.saml_session_index_key).present?
|
35
|
+
self.send(Devise.saml_session_index_key)
|
36
|
+
else
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
27
41
|
module ClassMethods
|
28
42
|
include DeviseSamlAuthenticatable::SamlConfig
|
29
43
|
def authenticate_with_saml(saml_response)
|
@@ -55,6 +69,11 @@ module Devise
|
|
55
69
|
resource
|
56
70
|
end
|
57
71
|
|
72
|
+
def reset_session_key_for(name_id)
|
73
|
+
resource = find_by(Devise.saml_default_user_key => name_id)
|
74
|
+
resource.update_attribute(Devise.saml_session_index_key, nil) unless resource.nil?
|
75
|
+
end
|
76
|
+
|
58
77
|
def find_for_shibb_authentication(conditions)
|
59
78
|
find_for_authentication(conditions)
|
60
79
|
end
|
@@ -6,6 +6,7 @@ ActionDispatch::Routing::Mapper.class_eval do
|
|
6
6
|
post :create, :path=>"saml/auth"
|
7
7
|
match :destroy, :path => mapping.path_names[:sign_out], :as => "destroy", :via => mapping.sign_out_via
|
8
8
|
get :metadata, :path=>"saml/metadata"
|
9
|
+
match :idp_sign_out, :path=>"saml/idp_sign_out", via: [:get, :post]
|
9
10
|
end
|
10
11
|
end
|
11
12
|
end
|
@@ -4,13 +4,20 @@ module Devise
|
|
4
4
|
class SamlAuthenticatable < Authenticatable
|
5
5
|
include DeviseSamlAuthenticatable::SamlConfig
|
6
6
|
def valid?
|
7
|
-
params[:SAMLResponse]
|
7
|
+
if params[:SAMLResponse]
|
8
|
+
response = OneLogin::RubySaml::Logoutresponse.new(params[:SAMLResponse], get_saml_config)
|
9
|
+
!(response.response.include? 'LogoutResponse')
|
10
|
+
else
|
11
|
+
false
|
12
|
+
end
|
8
13
|
end
|
14
|
+
|
9
15
|
def authenticate!
|
10
16
|
@response = OneLogin::RubySaml::Response.new(params[:SAMLResponse])
|
11
17
|
@response.settings = get_saml_config
|
12
18
|
resource = mapping.to.authenticate_with_saml(@response)
|
13
19
|
if @response.is_valid?
|
20
|
+
resource.after_saml_authentication(@response.sessionindex)
|
14
21
|
success!(resource)
|
15
22
|
else
|
16
23
|
fail!(:invalid)
|
@@ -2,10 +2,17 @@ require 'rails_helper'
|
|
2
2
|
|
3
3
|
class Devise::SessionsController < ActionController::Base
|
4
4
|
# The important parts from devise
|
5
|
+
def resource_class
|
6
|
+
User
|
7
|
+
end
|
8
|
+
|
5
9
|
def destroy
|
6
10
|
sign_out
|
7
11
|
redirect_to after_sign_out_path_for(:user)
|
8
12
|
end
|
13
|
+
|
14
|
+
def require_no_authentication
|
15
|
+
end
|
9
16
|
end
|
10
17
|
|
11
18
|
require_relative '../../../app/controllers/devise/saml_sessions_controller'
|
@@ -39,4 +46,64 @@ describe Devise::SamlSessionsController, type: :controller do
|
|
39
46
|
expect(response).to redirect_to(%r(\Ahttp://localhost:8009/saml/logout\?SAMLRequest=))
|
40
47
|
end
|
41
48
|
end
|
49
|
+
|
50
|
+
describe '#idp_sign_out' do
|
51
|
+
let(:name_id) { '12312312' }
|
52
|
+
let(:saml_request) { double(:logout_request, {
|
53
|
+
id: 42,
|
54
|
+
name_id: name_id
|
55
|
+
}) }
|
56
|
+
let(:sam_response) { double(:logout_response)}
|
57
|
+
let(:response_url) { 'http://localhost/logout_response' }
|
58
|
+
|
59
|
+
|
60
|
+
before do
|
61
|
+
allow(OneLogin::RubySaml::SloLogoutrequest).to receive(:new).and_return(saml_request)
|
62
|
+
allow(OneLogin::RubySaml::SloLogoutresponse).to receive(:new).and_return(sam_response)
|
63
|
+
allow(sam_response).to receive(:create).and_return(response_url)
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'returns invalid request if SAMLRequest is not passed' do
|
67
|
+
expect(User).not_to receive(:reset_session_key_for).with(name_id)
|
68
|
+
post :idp_sign_out
|
69
|
+
expect(response.status).to eq 500
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'accepts a LogoutResponse and redirects sign_in' do
|
73
|
+
post :idp_sign_out, SAMLResponse: 'stubbed_response'
|
74
|
+
expect(response.status).to eq 302
|
75
|
+
expect(response).to redirect_to '/users/saml/sign_in'
|
76
|
+
end
|
77
|
+
|
78
|
+
context 'when saml_sign_out_success_url is configured' do
|
79
|
+
let(:test_url) { '/test/url' }
|
80
|
+
before do
|
81
|
+
Devise.saml_sign_out_success_url = test_url
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'accepts a LogoutResponse and returns success' do
|
85
|
+
post :idp_sign_out, SAMLResponse: 'stubbed_response'
|
86
|
+
expect(response.status).to eq 302
|
87
|
+
expect(response).to redirect_to test_url
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context 'when saml_session_index_key is not configured' do
|
92
|
+
before do
|
93
|
+
Devise.saml_session_index_key = nil
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'returns invalid request' do
|
97
|
+
expect(User).not_to receive(:reset_session_key_for).with(name_id)
|
98
|
+
post :idp_sign_out, SAMLRequest: 'stubbed_request'
|
99
|
+
expect(response.status).to eq 500
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'direct the resource to reset the session key' do
|
104
|
+
expect(User).to receive(:reset_session_key_for).with(name_id)
|
105
|
+
post :idp_sign_out, SAMLRequest: 'stubbed_request'
|
106
|
+
expect(response).to redirect_to response_url
|
107
|
+
end
|
108
|
+
end
|
42
109
|
end
|
@@ -1,10 +1,10 @@
|
|
1
|
-
require '
|
1
|
+
require 'rails_helper'
|
2
2
|
|
3
3
|
describe Devise::Strategies::SamlAuthenticatable do
|
4
4
|
subject(:strategy) { described_class.new(env, :user) }
|
5
5
|
let(:env) { {} }
|
6
6
|
|
7
|
-
let(:response) { double(:response, :settings= => nil, is_valid?: true) }
|
7
|
+
let(:response) { double(:response, :settings= => nil, is_valid?: true, sessionindex: '123123123') }
|
8
8
|
before do
|
9
9
|
allow(OneLogin::RubySaml::Response).to receive(:new).and_return(response)
|
10
10
|
end
|
@@ -19,6 +19,7 @@ describe Devise::Strategies::SamlAuthenticatable do
|
|
19
19
|
let(:user) { double(:user) }
|
20
20
|
before do
|
21
21
|
allow(strategy).to receive(:mapping).and_return(mapping)
|
22
|
+
allow(user).to receive(:after_saml_authentication)
|
22
23
|
end
|
23
24
|
|
24
25
|
let(:params) { {} }
|
@@ -26,8 +27,8 @@ describe Devise::Strategies::SamlAuthenticatable do
|
|
26
27
|
allow(strategy).to receive(:params).and_return(params)
|
27
28
|
end
|
28
29
|
|
29
|
-
context "with a SAMLResponse parameter" do
|
30
|
-
let(:params) { {SAMLResponse: ""} }
|
30
|
+
context "with a login SAMLResponse parameter" do
|
31
|
+
let(:params) { {SAMLResponse: "PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIElEPSIxMjMxMjMxMjMxMjMxMjMiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE1LTA2LTMwVDE0OjQyOjI3WiIgRGVzdGluYXRpb249IntyZWNpcGllbnR9Ij48c2FtbDpJc3N1ZXI+aHR0cHM6Ly90ZXN0L3NhbWwvbWV0YWRhdGEvMTIzMTIzPC9zYW1sOklzc3Vlcj48c2FtbHA6U3RhdHVzPjxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWxwOlN0YXR1cz48c2FtbDpBc3NlcnRpb24geG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiBWZXJzaW9uPSIyLjAiIElEPSIyMzQyNDMyMzQxMjQxMjM0MTI0MyIgSXNzdWVJbnN0YW50PSIyMDE1LTA2LTMwVDE0OjQyOjI3WiI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vYXBwLm9uZWxvZ2luLmNvbS9zYW1sL21ldGFkYXRhLzQ1MzA2MTwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+PGRzOlJlZmVyZW5jZSBVUkk9IiNwZnhlOWJkZTYzNS01OWFhLTZjNTEtMWEzMS1mMzAyZjgzNGQ0ZDciPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPjZhMkxraGMyYjN6elFaQlIwYkFoQ0hrZkt1az08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+Zk1qdThCYnpqaWV2OXBpbXlvM0lpVkNEU2R4dkNMMEhHQmZ4bGxVMmJ6WHVMMXRZcnZ5bkxFcVVSVUptL3k1SlZPRWVwbjdkbVhnanNnTVVUSkl0WTg0dTJlbUU0eXRGaFN6L203UFE5MitvTkN6RFpxMy9waGQ2UlR3RC9RSEJQdzFYV0ltMUxlOE42NldSZlZwNTc5YmZQc2pXMmhWSm1kUXU1cmVRTzVpTlVad0ZwNTFVUFBiZkhNUis1QnhPWkVsL0p6TkJOWk5jQzRkT0ErMDM1SkJ6WmlBb2liK1phUWJwdDVTMWxkQjZpanoyTGJJdGFHQ2E0MzVOc1p6MkQwakxsRmU0T0hYajJqcFdOa05leTZ4SEtBZ1o0a0FDbkVIQ08yN0t1dXdac3pLSFpqOFpTRUdhRWNIVld0eC9MbGRWK2NveUNNdUE4OXh6V0lOajJ3PT08L2RzOlNpZ25hdHVyZVZhbHVlPjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUVNakNDQXhxZ0F3SUJBZ0lVY2NjNmczMU9RcnRaNHdJQkVUKzV2Z2c4Y0xNd0RRWUpLb1pJaHZjTkFRRUZCUUF3WVRFTE1Ba0dBMVVFQmhNQ1ZWTXhHakFZQmdOVkJBb01FVTl1WlNCTlpXUnBZMkZzSUVkeWIzVndNUlV3RXdZRFZRUUxEQXhQYm1WTWIyZHBiaUJKWkZBeEh6QWRCZ05WQkFNTUZrOXVaVXh2WjJsdUlFRmpZMjkxYm5RZ016Y3pPREV3SGhjTk1UUXdNVEl5TVRrek56UTJXaGNOTVRrd01USXpNVGt6TnpRMldqQmhNUXN3Q1FZRFZRUUdFd0pWVXpFYU1CZ0dBMVVFQ2d3UlQyNWxJRTFsWkdsallXd2dSM0p2ZFhBeEZUQVRCZ05WQkFzTURFOXVaVXh2WjJsdUlFbGtVREVmTUIwR0ExVUVBd3dXVDI1bFRHOW5hVzRnUVdOamIzVnVkQ0F6TnpNNE1UQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQU5BUUJqZWdFTXo3MW1yZktWUkdXUlBObU1EeXdIZ010YjFlWWJoZ3YxRnNLd1hSTWVjdFdwQmtHc0FKb3dMU2hSWEtoYUg1Vm80VE5QYzFzTTNWK3dFcWlXYjRQcWRaMW1lZDJ3YXhSeUhOUnloR3NTN1ZvbUFpR041QWpCMU1IVFJQREJzY3J5d1N5RkZBNHVaQzZ0TkVrdWFYTjVTeElZTFNHUWJRNkF5RzJOQlpZWCttelVvRm9EOFRManVHSEVlUDFpdTJvWkpsVHBRZFhsL3VvakthRXFPWGo4ZjV6VlhQWUNhVm05ejg0WkgyWFFKY25Lc01pTzJTVllzQjlVaEdJV3NPRU9nYzFMOXN4bGkvR0xSSWRoc0poZW5QbWxsY2RBQ1BuN0hOU3hQM0tuSVRjcGJoc3hsN0pSVE1wSUd6ZzVoWTFLVTBrcXRtcGlnSzBIRUNBd0VBQWFPQjRUQ0IzakFNQmdOVkhSTUJBZjhFQWpBQU1CMEdBMVVkRGdRV0JCU2pnVlpON3lPRTZmV0E0anB5RHFQamtyOC9aakNCbmdZRFZSMGpCSUdXTUlHVGdCU2pnVlpON3lPRTZmV0E0anB5RHFQamtyOC9acUZscEdNd1lURUxNQWtHQTFVRUJoTUNWVk14R2pBWUJnTlZCQW9NRVU5dVpTQk5aV1JwWTJGc0lFZHliM1Z3TVJVd0V3WURWUVFMREF4UGJtVk1iMmRwYmlCSlpGQXhIekFkQmdOVkJBTU1Gazl1WlV4dloybHVJRUZqWTI5MWJuUWdNemN6T0RHQ0ZISEhPb045VGtLN1dlTUNBUkUvdWI0SVBIQ3pNQTRHQTFVZER3RUIvd1FFQXdJSGdEQU5CZ2txaGtpRzl3MEJBUVVGQUFPQ0FRRUFOWkhFTG03NnoreHBuOXlTUXdZNGZ4bVg2SnZEWDdXTUZQaVc2ZGgwcW13MzI1UW9TbkpWcDQ1a010TUs5UXpGaldaK2cwa0VmRnlocUh2RnUrSUs1NnpmVEpvRVIyNUpBblVCb01CNGJaS1lkbHQwYS9sSFVDZDJWYzM1dStWNHR5QmhOOTRPYTg0L2NnSnRRdFd0bVh4bVlrOVE3S25DN3lQTFhTelh2OW9wODg1OUM4akswbUFwQmlEcnpsSFA2QUt6SmxzWFVBQjUzbDdnOVBUYW55alNoWE9lOXZjVzBMU3FLakRnbHNtS2p4WG0vcFhHNUE2MXFqU1MwQytObnYzVmJOcDlCbFBnekpMc1lvZGNVaGROSkpjK0h5RS9BK2o5d2h0VjhENzdNWTlTTHR6YU5kdTZUMnNqdWVUQUNMNFR2bVdISGlMWnNqY3FnZHJMNGc9PTwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj50ZXN0QHRlc3QuY29tPC9zYW1sOk5hbWVJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDE1LTA2LTMwVDE0OjQ1OjI3WiIgUmVjaXBpZW50PSJ7cmVjaXBpZW50fSIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE1LTA2LTMwVDE0OjM5OjI3WiIgTm90T25PckFmdGVyPSIyMDE1LTA2LTMwVDE0OjQ1OjI3WiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT57YXVkaWVuY2V9PC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNS0wNi0zMFQxNDo0MjoyNloiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTUtMDctMDFUMTQ6NDI6MjdaIiBTZXNzaW9uSW5kZXg9Il8xZGEzNThkMC0wMTY0LTAxMzMtMGEwMy01NGFlNTI2NTZmNzgiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZFByb3RlY3RlZFRyYW5zcG9ydDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiIE5hbWU9IkVtYWlsIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5tbGluZHNheUBvbmVtZWRpY2FsLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg0KDQo="} }
|
31
32
|
|
32
33
|
it "is valid" do
|
33
34
|
expect(strategy).to be_valid
|
@@ -37,6 +38,7 @@ describe Devise::Strategies::SamlAuthenticatable do
|
|
37
38
|
expect(OneLogin::RubySaml::Response).to receive(:new).with(params[:SAMLResponse])
|
38
39
|
expect(response).to receive(:settings=).with(saml_config)
|
39
40
|
expect(user_class).to receive(:authenticate_with_saml).with(response)
|
41
|
+
expect(user).to receive(:after_saml_authentication).with(response.sessionindex)
|
40
42
|
|
41
43
|
expect(strategy).to receive(:success!).with(user)
|
42
44
|
strategy.authenticate!
|
@@ -54,6 +56,15 @@ describe Devise::Strategies::SamlAuthenticatable do
|
|
54
56
|
end
|
55
57
|
end
|
56
58
|
|
59
|
+
context "with a logout SAMLResponse parameter" do
|
60
|
+
let(:params) { {SAMLResponse: "PHNhbWxwOkxvZ291dFJlc3BvbnNlIFZlcnNpb249JzIuMCcgSW5SZXNwb25zZVRvPSdfMTM0MjQzMjQzMjQzMicgeG1sbnM6c2FtbHA9J3VybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCcgSXNzdWVJbnN0YW50PScyMDE1LTA2LTMwVDE0OjQzOjQ0JyBJRD0nXzY5OTc2OTc5Nzk4Nzk4Nzk3OTg3Jz48c2FtbDpJc3N1ZXIgeG1sbnM6c2FtbD0ndXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbic+aHR0cHM6Ly90ZXN0L3NhbWwvbWV0YWRhdGEvMTQzMjQzMjwvc2FtbDpJc3N1ZXI+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0ndXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzJy8+PHNhbWxwOlN0YXR1c01lc3NhZ2U+U3VjY2Vzc2Z1bGx5IGxvZ2dlZCBvdXQgZnJvbSBzZXJ2aWNlIDwvc2FtbHA6U3RhdHVzTWVzc2FnZT48L3NhbWxwOlN0YXR1cz48L3NhbWxwOkxvZ291dFJlc3BvbnNlPg=="} }
|
61
|
+
|
62
|
+
it "is valid" do
|
63
|
+
expect(strategy).not_to be_valid
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
|
57
68
|
it "is not valid without a SAMLResponse parameter" do
|
58
69
|
expect(strategy).not_to be_valid
|
59
70
|
end
|
@@ -43,11 +43,22 @@ describe "SAML Authentication", type: :feature do
|
|
43
43
|
expect(current_url).to eq("http://localhost:8020/")
|
44
44
|
|
45
45
|
click_on "Log out"
|
46
|
+
#confirm the logout response redirected to the SP which in turn attempted to sign th e
|
47
|
+
expect(current_url).to match(%r(\Ahttp://localhost:8009/saml/auth\?SAMLRequest=))
|
46
48
|
|
47
49
|
# prove user is now signed out
|
48
50
|
visit 'http://localhost:8020/'
|
49
51
|
expect(current_url).to match(%r(\Ahttp://localhost:8009/saml/auth\?SAMLRequest=))
|
50
52
|
end
|
53
|
+
|
54
|
+
it 'logs a user out of the SP via the IpD' do
|
55
|
+
sign_in
|
56
|
+
|
57
|
+
visit "http://localhost:#{idp_port}/saml/sp_sign_out"
|
58
|
+
|
59
|
+
visit 'http://localhost:8020/'
|
60
|
+
expect(current_url).to match(%r(\Ahttp://localhost:8009/saml/auth\?SAMLRequest=))
|
61
|
+
end
|
51
62
|
end
|
52
63
|
|
53
64
|
context "when the attributes are used to authenticate" do
|
data/spec/rails_helper.rb
CHANGED
@@ -13,3 +13,8 @@ ActiveRecord::Migrator.migrate(File.expand_path("../support/sp/db/migrate/", __F
|
|
13
13
|
RSpec.configure do |config|
|
14
14
|
config.use_transactional_fixtures = true
|
15
15
|
end
|
16
|
+
|
17
|
+
Devise.setup do |config|
|
18
|
+
config.saml_default_user_key = :email
|
19
|
+
config.saml_session_index_key = :session_index
|
20
|
+
end
|
@@ -3,10 +3,19 @@
|
|
3
3
|
@include_subject_in_attributes = ask("Include the subject in the attributes?", limit: %w(y n)) == "y"
|
4
4
|
|
5
5
|
gem 'ruby-saml-idp'
|
6
|
+
gem 'thin'
|
6
7
|
|
7
8
|
route "get '/saml/auth' => 'saml_idp#new'"
|
8
9
|
route "post '/saml/auth' => 'saml_idp#create'"
|
9
10
|
route "get '/saml/logout' => 'saml_idp#logout'"
|
11
|
+
route "get '/saml/sp_sign_out' => 'saml_idp#sp_sign_out'"
|
10
12
|
|
11
13
|
template File.expand_path('../saml_idp_controller.rb.erb', __FILE__), 'app/controllers/saml_idp_controller.rb'
|
12
14
|
copy_file File.expand_path('../saml_idp-saml_slo_post.html.erb', __FILE__), 'app/views/saml_idp/saml_slo_post.html.erb'
|
15
|
+
create_file 'public/stylesheets/application.css', ''
|
16
|
+
|
17
|
+
gsub_file 'config/application.rb', /end[\n\w]*end$/, <<-CONFIG
|
18
|
+
config.slo_sp_url = "http://localhost:8020/users/saml/idp_sign_out"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
CONFIG
|
@@ -27,9 +27,16 @@ class SamlIdpController < SamlIdp::IdpController
|
|
27
27
|
|
28
28
|
private
|
29
29
|
|
30
|
+
def session_index
|
31
|
+
Rails.cache.fetch('session_key') {
|
32
|
+
UUID.generate
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
|
30
37
|
def encode_SAMLResponse(nameID, opts = {})
|
31
38
|
now = Time.now.utc
|
32
|
-
response_id
|
39
|
+
response_id = UUID.generate
|
33
40
|
audience_uri = opts[:audience_uri] || saml_acs_url[/^(.*?\/\/.*?\/)/, 1]
|
34
41
|
issuer_uri = opts[:issuer_uri] || (defined?(request) && request.url) || "http://example.com"
|
35
42
|
|
@@ -43,11 +50,11 @@ class SamlIdpController < SamlIdp::IdpController
|
|
43
50
|
attribute_statement = ""
|
44
51
|
end
|
45
52
|
|
46
|
-
assertion = %[<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="_#{
|
53
|
+
assertion = %[<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="_#{session_index}" 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="_#{session_index}"><AuthnContext><AuthnContextClassRef>urn:federation:authentication:windows</AuthnContextClassRef></AuthnContext></AuthnStatement></Assertion>]
|
47
54
|
|
48
55
|
digest_value = Base64.encode64(algorithm.digest(assertion)).gsub(/\n/, '')
|
49
56
|
|
50
|
-
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="#_#{
|
57
|
+
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="#_#{session_index}"><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>]
|
51
58
|
|
52
59
|
signature_value = sign(signed_info).gsub(/\n/, '')
|
53
60
|
|
@@ -65,7 +72,7 @@ class SamlIdpController < SamlIdp::IdpController
|
|
65
72
|
end
|
66
73
|
|
67
74
|
# == SLO functionality, see https://github.com/lawrencepit/ruby-saml-idp/pull/10
|
68
|
-
skip_before_filter :validate_saml_request, :only => [:logout]
|
75
|
+
skip_before_filter :validate_saml_request, :only => [:logout, :sp_sign_out]
|
69
76
|
before_filter :validate_saml_slo_request, :only => [:logout]
|
70
77
|
|
71
78
|
public
|
@@ -79,13 +86,21 @@ class SamlIdpController < SamlIdp::IdpController
|
|
79
86
|
logger.error "User with email #{params[:name_id]} not found"
|
80
87
|
@saml_slo_response = encode_SAML_SLO_Response(params[:name_id])
|
81
88
|
end
|
82
|
-
if
|
83
|
-
|
89
|
+
if Idp::Application.config.slo_sp_url
|
90
|
+
redirect_to "#{Idp::Application.config.slo_sp_url}?SAMLResponse=#{@saml_slo_response}"
|
84
91
|
else
|
85
92
|
redirect_to 'http://example.com'
|
86
93
|
end
|
87
94
|
end
|
88
95
|
|
96
|
+
def sp_sign_out
|
97
|
+
idp_slo_authenticate(params[:name_id])
|
98
|
+
saml_slo_request = encode_SAML_SLO_Request("you@example.com")
|
99
|
+
uri = URI.parse("http://localhost:8020/users/saml/idp_sign_out")
|
100
|
+
Net::HTTP.post_form(uri, {"SAMLRequest" => saml_slo_request})
|
101
|
+
head :no_content
|
102
|
+
end
|
103
|
+
|
89
104
|
def idp_slo_authenticate(email)
|
90
105
|
session.delete :user_id
|
91
106
|
true
|
@@ -111,20 +126,19 @@ class SamlIdpController < SamlIdp::IdpController
|
|
111
126
|
zstream.finish
|
112
127
|
zstream.close
|
113
128
|
@saml_slo_request_id = @saml_slo_request[/ID=['"](.+?)['"]/, 1]
|
114
|
-
@saml_slo_acs_url = @saml_slo_request[/AssertionConsumerLogoutServiceURL=['"](.+?)['"]/, 1]
|
115
129
|
end
|
116
130
|
|
117
131
|
def encode_SAML_SLO_Response(nameID, opts = {})
|
118
132
|
now = Time.now.utc
|
119
|
-
response_id
|
133
|
+
response_id = UUID.generate
|
120
134
|
audience_uri = opts[:audience_uri] || (@saml_slo_acs_url && @saml_slo_acs_url[/^(.*?\/\/.*?\/)/, 1])
|
121
135
|
issuer_uri = opts[:issuer_uri] || (defined?(request) && request.url.split("?")[0]) || "http://example.com"
|
122
136
|
|
123
|
-
assertion = %[<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="_#{
|
137
|
+
assertion = %[<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="_#{session_index}" IssueInstant="#{now.iso8601}" Version="2.0"><Issuer2>#{issuer_uri}</Issuer2><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_slo_request_id}" NotOnOrAfter="#{(now+3*60).iso8601}" Recipient="#{@saml_slo_acs_url}"></SubjectConfirmationData></SubjectConfirmation></Subject><Conditions NotBefore="#{(now-5).iso8601}" NotOnOrAfter="#{(now+60*60).iso8601}"><AudienceRestriction><Audience>#{audience_uri}</Audience></AudienceRestriction></Conditions><AttributeStatement><Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"><AttributeValue>#{nameID}</AttributeValue></Attribute></AttributeStatement><AuthnStatement AuthnInstant="#{now.iso8601}" SessionIndex="_#{session_index}"><AuthnContext><AuthnContextClassRef>urn:federation:authentication:windows</AuthnContextClassRef></AuthnContext></AuthnStatement></Assertion>]
|
124
138
|
|
125
139
|
digest_value = Base64.encode64(algorithm.digest(assertion)).gsub(/\n/, '')
|
126
140
|
|
127
|
-
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="#_#{
|
141
|
+
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="#_#{session_index}"><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>]
|
128
142
|
|
129
143
|
signature_value = sign(signed_info).gsub(/\n/, '')
|
130
144
|
|
@@ -132,7 +146,38 @@ class SamlIdpController < SamlIdp::IdpController
|
|
132
146
|
|
133
147
|
assertion_and_signature = assertion.sub(/Issuer\>\<Subject/, "Issuer>#{signature}<Subject")
|
134
148
|
|
135
|
-
xml = %[<samlp:LogoutResponse
|
149
|
+
xml = %[<samlp:LogoutResponse
|
150
|
+
ID="_#{response_id}"
|
151
|
+
Version="2.0"
|
152
|
+
IssueInstant="#{now.iso8601}"
|
153
|
+
Destination="#{@saml_slo_acs_url}"
|
154
|
+
Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified"
|
155
|
+
InResponseTo="#{@saml_slo_request_id}"
|
156
|
+
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
|
157
|
+
<Issuer2 xmlns="urn:oasis:names:tc:SAML:2.0:assertion">#{issuer_uri}</Issuer2>
|
158
|
+
<samlp:Status>
|
159
|
+
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
|
160
|
+
</samlp:Status>
|
161
|
+
#{assertion_and_signature}
|
162
|
+
</samlp:LogoutResponse>]
|
163
|
+
|
164
|
+
Base64.encode64(xml)
|
165
|
+
end
|
166
|
+
|
167
|
+
def encode_SAML_SLO_Request(nameID, opts = {})
|
168
|
+
now = Time.now.utc
|
169
|
+
response_id = UUID.generate
|
170
|
+
issuer_uri = opts[:issuer_uri] || (defined?(request) && request.url.split("?")[0]) || "http://example.com"
|
171
|
+
xml = %[<samlp:LogoutRequest
|
172
|
+
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
|
173
|
+
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
|
174
|
+
ID="_#{response_id}" Version="2.0"
|
175
|
+
Destination="#{@saml_slo_acs_url}"
|
176
|
+
IssueInstant="#{now.iso8601}">
|
177
|
+
<saml:Issuer >#{issuer_uri}</saml:Issuer>
|
178
|
+
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">#{nameID}</saml:NameID>
|
179
|
+
<samlp:SessionIndex>_#{session_index}</samlp:SessionIndex>
|
180
|
+
</samlp:LogoutRequest>]
|
136
181
|
|
137
182
|
Base64.encode64(xml)
|
138
183
|
end
|
data/spec/support/sp_template.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
use_subject_to_authenticate = ask("Use subject to authenticate?", limit: %w(y n)) == "y"
|
4
4
|
|
5
5
|
gem 'devise_saml_authenticatable', path: '../../..'
|
6
|
+
gem 'thin'
|
6
7
|
|
7
8
|
create_file 'config/attribute-map.yml', <<-ATTRIBUTES
|
8
9
|
---
|
@@ -31,6 +32,7 @@ after_bundle do
|
|
31
32
|
generate 'devise:install'
|
32
33
|
gsub_file 'config/initializers/devise.rb', /^end$/, <<-CONFIG
|
33
34
|
config.saml_default_user_key = :email
|
35
|
+
config.saml_session_index_key = :session_index
|
34
36
|
|
35
37
|
config.saml_use_subject = #{use_subject_to_authenticate}
|
36
38
|
config.saml_create_user = true
|
@@ -45,7 +47,7 @@ after_bundle do
|
|
45
47
|
end
|
46
48
|
CONFIG
|
47
49
|
|
48
|
-
generate :devise, "user", "email:string", "name:string"
|
50
|
+
generate :devise, "user", "email:string", "name:string", "session_index:string"
|
49
51
|
gsub_file 'app/models/user.rb', /database_authenticatable.*\n.*/, 'saml_authenticatable'
|
50
52
|
route "resources :users, only: [:create]"
|
51
53
|
create_file('app/controllers/users_controller.rb', <<-USERS)
|
@@ -61,3 +63,5 @@ end
|
|
61
63
|
rake "db:create"
|
62
64
|
rake "db:migrate"
|
63
65
|
end
|
66
|
+
|
67
|
+
create_file 'public/stylesheets/application.css', ''
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: devise_saml_authenticatable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '1.
|
4
|
+
version: '1.1'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josef Sauter
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-07-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: devise
|
@@ -28,16 +28,16 @@ dependencies:
|
|
28
28
|
name: ruby-saml
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - '='
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 0.
|
33
|
+
version: 0.9.2
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - '='
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 0.
|
40
|
+
version: 0.9.2
|
41
41
|
description: SAML Authentication for devise
|
42
42
|
email:
|
43
43
|
- Josef.Sauter@gmail.com
|