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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4c162a34c36999fce62a2fb2f18cbfd5ce87a488
4
- data.tar.gz: 61637ed78f97770f32d07157928127ad40032d2e
3
+ metadata.gz: 11d475877b3a4f178413861a07e7fd030c65799b
4
+ data.tar.gz: f73eaa27a49431b435b502185af337668fceaf8e
5
5
  SHA512:
6
- metadata.gz: 3eeab1c7fdac22674db47e7e71c1c6793243a0e398cf23fe51ba261cf54ee59133d9d0445ec87e804b9c39d8ffd2d893d8369194adc318e38568a206761cea30
7
- data.tar.gz: b0306658471a7d121ef6791b74633c24a9ba0e468e283594261c046da776e9d8739235952df27162269480d6f294bbb7b131ab99676a79a99fd69bc66d1f0a9c
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
 
@@ -17,5 +17,5 @@ Gem::Specification.new do |gem|
17
17
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
18
 
19
19
  gem.add_dependency("devise","> 2.0.0")
20
- gem.add_dependency("ruby-saml",">= 0.8.2")
20
+ gem.add_dependency("ruby-saml","0.9.2")
21
21
  end
@@ -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)
@@ -1,3 +1,3 @@
1
1
  module DeviseSamlAuthenticatable
2
- VERSION = "1.0"
2
+ VERSION = "1.1"
3
3
  end
@@ -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 'spec_helper'
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
@@ -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, reference_id = UUID.generate, UUID.generate
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="_#{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>]
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="#_#{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>]
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 @saml_slo_acs_url
83
- render :template => "saml_idp/idp/saml_slo_post", :layout => false
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, reference_id = UUID.generate, UUID.generate
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="_#{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_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="_#{reference_id}"><AuthnContext><AuthnContextClassRef>urn:federation:authentication:windows</AuthnContextClassRef></AuthnContext></AuthnStatement></Assertion>]
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="#_#{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>]
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 ID="_#{response_id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{@saml_slo_acs_url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="#{@saml_slo_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: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
@@ -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.0'
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-06-17 00:00:00.000000000 Z
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.8.2
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.8.2
40
+ version: 0.9.2
41
41
  description: SAML Authentication for devise
42
42
  email:
43
43
  - Josef.Sauter@gmail.com