devise_saml_authenticatable 1.0 → 1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|