devise_saml_authenticatable 1.3.2 → 1.6.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.
Files changed (37) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +0 -2
  3. data/.travis.yml +29 -22
  4. data/Gemfile +2 -2
  5. data/README.md +105 -32
  6. data/app/controllers/devise/saml_sessions_controller.rb +35 -7
  7. data/devise_saml_authenticatable.gemspec +2 -1
  8. data/lib/devise_saml_authenticatable.rb +27 -2
  9. data/lib/devise_saml_authenticatable/default_attribute_map_resolver.rb +26 -0
  10. data/lib/devise_saml_authenticatable/default_idp_entity_id_reader.rb +2 -0
  11. data/lib/devise_saml_authenticatable/exception.rb +1 -1
  12. data/lib/devise_saml_authenticatable/model.rb +16 -18
  13. data/lib/devise_saml_authenticatable/routes.rb +17 -6
  14. data/lib/devise_saml_authenticatable/saml_mapped_attributes.rb +15 -2
  15. data/lib/devise_saml_authenticatable/strategy.rb +1 -0
  16. data/lib/devise_saml_authenticatable/version.rb +1 -1
  17. data/spec/controllers/devise/saml_sessions_controller_spec.rb +118 -11
  18. data/spec/devise_saml_authenticatable/default_attribute_map_resolver_spec.rb +58 -0
  19. data/spec/devise_saml_authenticatable/model_spec.rb +68 -7
  20. data/spec/devise_saml_authenticatable/saml_mapped_attributes_spec.rb +50 -0
  21. data/spec/features/saml_authentication_spec.rb +45 -21
  22. data/spec/rails_helper.rb +6 -2
  23. data/spec/routes/routes_spec.rb +102 -0
  24. data/spec/spec_helper.rb +7 -0
  25. data/spec/support/Gemfile.rails4 +23 -6
  26. data/spec/support/Gemfile.rails5 +13 -2
  27. data/spec/support/Gemfile.rails5.1 +25 -0
  28. data/spec/support/Gemfile.rails5.2 +25 -0
  29. data/spec/support/attribute-map.yml +12 -0
  30. data/spec/support/attribute_map_resolver.rb.erb +14 -0
  31. data/spec/support/idp_settings_adapter.rb.erb +5 -5
  32. data/spec/support/idp_template.rb +6 -2
  33. data/spec/support/rails_app.rb +75 -17
  34. data/spec/support/saml_idp_controller.rb.erb +13 -6
  35. data/spec/support/sp_template.rb +45 -21
  36. metadata +25 -13
  37. data/spec/support/Gemfile.ruby-saml-1.3 +0 -24
@@ -4,21 +4,38 @@ source 'https://rubygems.org'
4
4
  gemspec path: '../..'
5
5
 
6
6
  group :test do
7
- gem 'rake'
8
7
  gem 'rspec', '~> 3.0'
9
8
  gem 'rails', '~> 4.0'
10
- gem 'rspec-rails'
11
- gem 'sqlite3'
9
+ gem 'rspec-rails', '~> 3.9'
10
+ gem 'sqlite3', '~> 1.3.6'
12
11
  gem 'capybara'
13
12
  gem 'poltergeist'
14
13
 
15
14
  # Lock down versions of gems for older versions of Ruby
16
- if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.0")
17
- gem 'addressable', '~> 2.4.0'
18
- gem 'mime-types', '~> 2.99'
15
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.1")
16
+ gem 'rake', '~> 12.2'
17
+ else
18
+ gem 'rake'
19
19
  end
20
+
20
21
  if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.1")
21
22
  gem 'devise', '~> 3.5'
23
+ gem 'minitest', '~> 5.11.0'
22
24
  gem 'nokogiri', '~> 1.6.8'
25
+ gem 'public_suffix', '~> 2.0.5'
26
+ end
27
+
28
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.1")
29
+ gem 'responders', '~> 1.0'
30
+ elsif Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.4")
31
+ gem 'responders', '~> 2.0'
32
+ end
33
+
34
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.2")
35
+ gem 'byebug', '~> 9.0'
36
+ elsif Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.3")
37
+ gem 'byebug', '~> 10.0'
38
+ elsif Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.4")
39
+ gem 'byebug', '~> 11.0.0'
23
40
  end
24
41
  end
@@ -7,8 +7,19 @@ group :test do
7
7
  gem 'rake'
8
8
  gem 'rspec', '~> 3.0'
9
9
  gem 'rails', '~> 5.0.0'
10
- gem 'rspec-rails'
11
- gem 'sqlite3'
10
+ gem 'rspec-rails', '~> 3.9'
11
+ gem 'sqlite3', '~> 1.3.6'
12
12
  gem 'capybara'
13
13
  gem 'poltergeist'
14
+
15
+ # Lock down versions of gems for older versions of Ruby
16
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.4")
17
+ gem 'responders', '~> 2.4'
18
+ end
19
+
20
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.3")
21
+ gem 'byebug', '~> 10.0'
22
+ elsif Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.4")
23
+ gem 'byebug', '~> 11.0.0'
24
+ end
14
25
  end
@@ -0,0 +1,25 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in devise_saml_authenticatable.gemspec
4
+ gemspec path: '../..'
5
+
6
+ group :test do
7
+ gem 'rake'
8
+ gem 'rspec', '~> 3.0'
9
+ gem 'rails', '~> 5.1.0'
10
+ gem 'rspec-rails', '~> 3.9'
11
+ gem 'sqlite3', '~> 1.3.6'
12
+ gem 'capybara'
13
+ gem 'poltergeist'
14
+
15
+ # Lock down versions of gems for older versions of Ruby
16
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.4")
17
+ gem 'responders', '~> 2.4'
18
+ end
19
+
20
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.3")
21
+ gem 'byebug', '~> 10.0'
22
+ elsif Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.4")
23
+ gem 'byebug', '~> 11.0.0'
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in devise_saml_authenticatable.gemspec
4
+ gemspec path: '../..'
5
+
6
+ group :test do
7
+ gem 'rake'
8
+ gem 'rspec', '~> 3.0'
9
+ gem 'rails', '~> 5.2'
10
+ gem 'rspec-rails', '~> 3.9'
11
+ gem 'sqlite3', '~> 1.3.6'
12
+ gem 'capybara'
13
+ gem 'poltergeist'
14
+
15
+ # Lock down versions of gems for older versions of Ruby
16
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.4")
17
+ gem 'responders', '~> 2.4'
18
+ end
19
+
20
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.3")
21
+ gem 'byebug', '~> 10.0'
22
+ elsif Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.4")
23
+ gem 'byebug', '~> 11.0.0'
24
+ end
25
+ end
@@ -0,0 +1,12 @@
1
+ "urn:mace:dir:attribute-def:first_name": "first_name"
2
+ "first_name": "first_name"
3
+ "firstName": "first_name"
4
+ "firstname": "first_name"
5
+ "urn:mace:dir:attribute-def:last_name": "last_name"
6
+ "last_name": "last_name"
7
+ "lastName": "last_name"
8
+ "lastname": "last_name"
9
+ "urn:mace:dir:attribute-def:email": "email"
10
+ "email_address": "email"
11
+ "emailAddress": "email"
12
+ "email": "email"
@@ -0,0 +1,14 @@
1
+ class AttributeMapResolver < DeviseSamlAuthenticatable::DefaultAttributeMapResolver
2
+ def attribute_map
3
+ issuer = saml_response.issuers.first
4
+ Rails.logger.info("[#{self.class.name}] issuer=#{issuer.inspect}")
5
+ if issuer == "http://localhost:8009/saml/auth"
6
+ {
7
+ "myemailaddress" => "email",
8
+ "myname" => "name",
9
+ }
10
+ else
11
+ {}
12
+ end
13
+ end
14
+ end
@@ -2,15 +2,15 @@ class IdpSettingsAdapter
2
2
  def self.settings(idp_entity_id)
3
3
  if idp_entity_id == "http://localhost:8020/saml/metadata"
4
4
  {
5
- assertion_consumer_service_url: "acs_url",
5
+ assertion_consumer_service_url: "http://localhost:8020/users/saml/auth",
6
6
  assertion_consumer_service_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
7
- name_identifier_format: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
7
+ name_identifier_format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
8
8
  issuer: "sp_issuer",
9
9
  idp_entity_id: "http://localhost:8020/saml/metadata",
10
10
  authn_context: "",
11
- idp_slo_target_url: "http://www.example.com",
12
- idp_sso_target_url: "http://www.example.com",
13
- idp_cert: "idp_cert"
11
+ idp_slo_target_url: "http://localhost:8010/saml/logout",
12
+ idp_sso_target_url: "http://localhost:8010/saml/auth",
13
+ idp_cert_fingerprint: "9E:65:2E:03:06:8D:80:F2:86:C7:6C:77:A1:D9:14:97:0A:4D:F4:4D"
14
14
  }
15
15
  else
16
16
  {}
@@ -1,11 +1,15 @@
1
1
  # Set up a SAML IdP
2
2
 
3
+ @email_address_attribute_key = ENV.fetch("EMAIL_ADDRESS_ATTRIBUTE_KEY", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")
4
+ @name_attribute_key = ENV.fetch("NAME_ATTRIBUTE_KEY", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name")
3
5
  @include_subject_in_attributes = ENV.fetch('INCLUDE_SUBJECT_IN_ATTRIBUTES')
4
6
  @valid_destination = ENV.fetch('VALID_DESTINATION', "true")
5
7
 
6
- gsub_file 'config/secrets.yml', /secret_key_base:.*$/, 'secret_key_base: "34814fd41f91c493b89aa01ac73c44d241a31245b5bc5542fa4b7317525e1dcfa60ba947b3d085e4e229456fdee0d8af6aac6a63cf750d807ea6fe5d853dff4a"'
8
+ if Rails::VERSION::MAJOR < 5 || (Rails::VERSION::MAJOR == 5 && Rails::VERSION::MINOR < 2)
9
+ gsub_file 'config/secrets.yml', /secret_key_base:.*$/, 'secret_key_base: "34814fd41f91c493b89aa01ac73c44d241a31245b5bc5542fa4b7317525e1dcfa60ba947b3d085e4e229456fdee0d8af6aac6a63cf750d807ea6fe5d853dff4a"'
10
+ end
7
11
 
8
- gem 'ruby-saml-idp', git: "https://github.com/lawrencepit/ruby-saml-idp.git", ref: "ec715b252e849105c7a96df27b731c6e7f725a51"
12
+ gem 'ruby-saml-idp', '~> 0.3.3'
9
13
  gem 'thin'
10
14
 
11
15
  insert_into_file('Gemfile', after: /\z/) {
@@ -1,6 +1,7 @@
1
- require 'open3'
2
- require 'socket'
3
- require 'timeout'
1
+ require "open3"
2
+ require "socket"
3
+ require "tempfile"
4
+ require "timeout"
4
5
 
5
6
  APP_READY_TIMEOUT ||= 30
6
7
 
@@ -17,25 +18,32 @@ rescue Errno::ESRCH
17
18
  end
18
19
 
19
20
  def create_app(name, env = {})
20
- rails_new_options = %w(-T -J -S --skip-spring --skip-listen)
21
- rails_new_options << "-O" if name == 'idp'
22
- Dir.chdir(File.expand_path('../../support', __FILE__)) do
23
- FileUtils.rm_rf(name)
24
- system(env, "rails", "new", name, *rails_new_options, "-m", "#{name}_template.rb")
21
+ puts "[#{name}] Creating Rails app"
22
+ rails_new_options = %w[-T -J -S --skip-spring --skip-listen --skip-bootsnap]
23
+ rails_new_options << "-O" if name == "idp"
24
+ with_clean_env do
25
+ Dir.chdir(working_directory) do
26
+ FileUtils.rm_rf(name)
27
+ puts("rails _#{Rails.version}_ new #{name} #{rails_new_options.join(" ")} -m #{File.expand_path("../#{name}_template.rb", __FILE__)}")
28
+ system(env, "rails", "_#{Rails.version}_", "new", name, *rails_new_options, "-m", File.expand_path("../#{name}_template.rb", __FILE__))
29
+ end
25
30
  end
26
31
  end
27
32
 
28
33
  def start_app(name, port, options = {})
34
+ puts "[#{name}] Starting Rails app"
29
35
  pid = nil
30
- Bundler.with_clean_env do
31
- Dir.chdir(File.expand_path("../../support/#{name}", __FILE__)) do
32
- pid = Process.spawn({"RAILS_ENV" => "production"}, "bundle exec rails server -p #{port} -e production", out: "log/#{name}.log", err: "log/#{name}.err.log")
36
+ app_bundle_install(name)
37
+
38
+ with_clean_env do
39
+ Dir.chdir(app_dir(name)) do
40
+ pid = Process.spawn(app_env(name), "bundle exec rails server -p #{port} -e production", chdir: app_dir(name), out: "log/#{name}.log", err: "log/#{name}.err.log")
33
41
  begin
34
- Timeout::timeout(APP_READY_TIMEOUT) do
42
+ Timeout.timeout(APP_READY_TIMEOUT) do
35
43
  sleep 1 until app_ready?(pid, port)
36
44
  end
37
45
  if app_ready?(pid, port)
38
- puts "Launched #{name} on port #{port} (pid #{pid})..."
46
+ puts "[#{name}] Launched #{name} on port #{port} (pid #{pid})..."
39
47
  else
40
48
  raise "#{name} failed after starting"
41
49
  end
@@ -46,16 +54,33 @@ def start_app(name, port, options = {})
46
54
  end
47
55
  pid
48
56
  rescue RuntimeError => e
49
- $stdout.puts "#{File.read(File.expand_path("../../support/#{name}/log/#{name}.log", __FILE__))}"
50
- $stderr.puts "#{File.read(File.expand_path("../../support/#{name}/log/#{name}.err.log", __FILE__))}"
57
+ warn "=== #{name}"
58
+ Dir.chdir(app_dir(name)) do
59
+ warn File.read("log/#{name}.log") if File.exist?("log/#{name}.log")
60
+ warn File.read("log/#{name}.err.log") if File.exist?("log/#{name}.err.log")
61
+ end
51
62
  raise e
52
63
  end
53
64
 
54
- def stop_app(pid)
65
+ def stop_app(name, pid)
55
66
  if pid
56
67
  Process.kill(:INT, pid)
57
68
  Process.wait(pid)
58
69
  end
70
+ Dir.chdir(app_dir(name)) do
71
+ if File.exist?("log/#{name}.log")
72
+ puts "=== [#{name}] stdout"
73
+ puts File.read("log/#{name}.log")
74
+ end
75
+ if File.exist?("log/#{name}.err.log")
76
+ warn "=== [#{name}] stderr"
77
+ warn File.read("log/#{name}.err.log")
78
+ end
79
+ if File.exist?("log/production.log")
80
+ puts "=== [#{name}] Rails logs"
81
+ puts File.read("log/production.log")
82
+ end
83
+ end
59
84
  end
60
85
 
61
86
  def port_open?(port)
@@ -64,7 +89,7 @@ def port_open?(port)
64
89
  s = TCPSocket.new('localhost', port)
65
90
  s.close
66
91
  return true
67
- rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
92
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::EADDRNOTAVAIL
68
93
  # try 127.0.0.1
69
94
  end
70
95
  begin
@@ -78,3 +103,36 @@ def port_open?(port)
78
103
  rescue Timeout::Error
79
104
  false
80
105
  end
106
+
107
+ def app_bundle_install(name)
108
+ with_clean_env do
109
+ Open3.popen3(app_env(name), "bundle install", chdir: app_dir(name)) do |stdin, stdout, stderr, thread|
110
+ stdin.close
111
+ exit_status = thread.value
112
+
113
+ puts stdout.read
114
+ warn stderr.read
115
+ raise "bundle install failed" unless exit_status.success?
116
+ end
117
+ end
118
+ end
119
+
120
+ def app_dir(name)
121
+ File.join(working_directory, name)
122
+ end
123
+
124
+ def app_env(name)
125
+ {"BUNDLE_GEMFILE" => File.join(app_dir(name), "Gemfile"), "RAILS_ENV" => "production"}
126
+ end
127
+
128
+ def working_directory
129
+ $working_directory ||= Dir.mktmpdir("dsa_test")
130
+ end
131
+
132
+ def with_clean_env(&blk)
133
+ if Bundler.respond_to?(:with_original_env)
134
+ Bundler.with_original_env(&blk)
135
+ else
136
+ Bundler.with_clean_env(&blk)
137
+ end
138
+ end
@@ -17,10 +17,10 @@ class SamlIdpController < SamlIdp::IdpController
17
17
 
18
18
  def idp_make_saml_response(_)
19
19
  attributes = {
20
- "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" => "A User",
20
+ name_attribute_key => "A User",
21
21
  }
22
22
  if include_subject_in_attributes
23
- attributes["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"] = "you@example.com"
23
+ attributes[email_address_attribute_key] = "you@example.com"
24
24
  end
25
25
  encode_SAMLResponse("you@example.com", attributes: attributes)
26
26
  end
@@ -33,6 +33,13 @@ class SamlIdpController < SamlIdp::IdpController
33
33
  }
34
34
  end
35
35
 
36
+ def email_address_attribute_key
37
+ "<%= @email_address_attribute_key %>"
38
+ end
39
+
40
+ def name_attribute_key
41
+ "<%= @name_attribute_key %>"
42
+ end
36
43
 
37
44
  def encode_SAMLResponse(nameID, opts = {})
38
45
  now = Time.now.utc
@@ -50,7 +57,7 @@ class SamlIdpController < SamlIdp::IdpController
50
57
  attribute_statement = ""
51
58
  end
52
59
 
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>]
60
+ 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:2.0:nameid-format:transient">#{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>]
54
61
 
55
62
  digest_value = Base64.encode64(algorithm.digest(assertion)).gsub(/\n/, '')
56
63
 
@@ -115,7 +122,7 @@ class SamlIdpController < SamlIdp::IdpController
115
122
  def idp_make_saml_slo_response(person)
116
123
  attributes = {}
117
124
  if include_subject_in_attributes
118
- attributes["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"] = "you@example.com"
125
+ attributes[email_address_attribute_key] = "you@example.com"
119
126
  end
120
127
  encode_SAML_SLO_Response("you@example.com", attributes: attributes)
121
128
  end
@@ -148,7 +155,7 @@ class SamlIdpController < SamlIdp::IdpController
148
155
  audience_uri = opts[:audience_uri] || (@saml_slo_acs_url && @saml_slo_acs_url[/^(.*?\/\/.*?\/)/, 1])
149
156
  issuer_uri = opts[:issuer_uri] || (defined?(request) && request.url.split("?")[0]) || "http://example.com"
150
157
 
151
- 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>]
158
+ 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:2.0:nameid-format:transient">#{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="#{email_address_attribute_key}"><AttributeValue>#{nameID}</AttributeValue></Attribute></AttributeStatement><AuthnStatement AuthnInstant="#{now.iso8601}" SessionIndex="_#{session_index}"><AuthnContext><AuthnContextClassRef>urn:federation:authentication:windows</AuthnContextClassRef></AuthnContext></AuthnStatement></Assertion>]
152
159
 
153
160
  digest_value = Base64.encode64(algorithm.digest(assertion)).gsub(/\n/, '')
154
161
 
@@ -189,7 +196,7 @@ class SamlIdpController < SamlIdp::IdpController
189
196
  Destination="#{destination(@saml_slo_acs_url)}"
190
197
  IssueInstant="#{now.iso8601}">
191
198
  <saml:Issuer >#{issuer_uri}</saml:Issuer>
192
- <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">#{nameID}</saml:NameID>
199
+ <saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">#{nameID}</saml:NameID>
193
200
  <samlp:SessionIndex>_#{session_index}</samlp:SessionIndex>
194
201
  </samlp:LogoutRequest>]
195
202
 
@@ -2,15 +2,18 @@
2
2
 
3
3
  require "onelogin/ruby-saml/version"
4
4
 
5
+ attribute_map_resolver = ENV.fetch("ATTRIBUTE_MAP_RESOLVER", "nil")
5
6
  saml_session_index_key = ENV.fetch('SAML_SESSION_INDEX_KEY', ":session_index")
6
7
  use_subject_to_authenticate = ENV.fetch('USE_SUBJECT_TO_AUTHENTICATE')
7
8
  idp_settings_adapter = ENV.fetch('IDP_SETTINGS_ADAPTER', "nil")
8
9
  idp_entity_id_reader = ENV.fetch('IDP_ENTITY_ID_READER', "DeviseSamlAuthenticatable::DefaultIdpEntityIdReader")
9
10
  saml_failed_callback = ENV.fetch('SAML_FAILED_CALLBACK', "nil")
10
11
 
11
- gsub_file 'config/secrets.yml', /secret_key_base:.*$/, 'secret_key_base: "8b5889df1fcf03f76c7d66da02d8776bcc85b06bed7d9c592f076d9c8a5455ee6d4beae45986c3c030b40208db5e612f2a6ef8283036a352e3fae83c5eda36be"'
12
+ if Rails::VERSION::MAJOR < 5 || (Rails::VERSION::MAJOR == 5 && Rails::VERSION::MINOR < 2)
13
+ gsub_file 'config/secrets.yml', /secret_key_base:.*$/, 'secret_key_base: "8b5889df1fcf03f76c7d66da02d8776bcc85b06bed7d9c592f076d9c8a5455ee6d4beae45986c3c030b40208db5e612f2a6ef8283036a352e3fae83c5eda36be"'
14
+ end
12
15
 
13
- gem 'devise_saml_authenticatable', path: '../../..'
16
+ gem 'devise_saml_authenticatable', path: File.expand_path("../../..", __FILE__)
14
17
  gem 'ruby-saml', OneLogin::RubySaml::VERSION
15
18
  gem 'thin'
16
19
 
@@ -20,17 +23,26 @@ insert_into_file('Gemfile', after: /\z/) {
20
23
  if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.1")
21
24
  gem 'devise', '~> 3.5'
22
25
  gem 'nokogiri', '~> 1.6.8'
26
+ elsif Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.4")
27
+ gem 'responders', '~> 2.4'
23
28
  end
24
29
  GEMFILE
25
30
  }
31
+ if Rails::VERSION::MAJOR < 6
32
+ # sqlite3 is hard-coded in Rails < 6 to v1.3.x
33
+ gsub_file 'Gemfile', /^gem 'sqlite3'.*$/, "gem 'sqlite3', '~> 1.3.6'"
34
+ end
26
35
 
36
+ template File.expand_path('../attribute_map_resolver.rb.erb', __FILE__), 'app/lib/attribute_map_resolver.rb'
27
37
  template File.expand_path('../idp_settings_adapter.rb.erb', __FILE__), 'app/lib/idp_settings_adapter.rb'
28
38
 
29
- create_file 'config/attribute-map.yml', <<-ATTRIBUTES
39
+ if attribute_map_resolver == "nil"
40
+ create_file 'config/attribute-map.yml', <<-ATTRIBUTES
30
41
  ---
31
42
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": email
32
43
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": name
33
- ATTRIBUTES
44
+ ATTRIBUTES
45
+ end
34
46
 
35
47
  create_file('app/lib/our_saml_failed_callback_handler.rb', <<-CALLBACKHANDLER)
36
48
 
@@ -59,22 +71,6 @@ end
59
71
  READER
60
72
 
61
73
  after_bundle do
62
- generate :controller, 'home', 'index'
63
- insert_into_file('app/controllers/home_controller.rb', after: "class HomeController < ApplicationController\n") {
64
- <<-AUTHENTICATE
65
- before_action :authenticate_user!
66
- AUTHENTICATE
67
- }
68
- insert_into_file('app/views/home/index.html.erb', after: /\z/) {
69
- <<-HOME
70
- <%= current_user.email %> <%= current_user.name %>
71
- <%= form_tag destroy_user_session_path, method: :delete do %>
72
- <%= submit_tag "Log out" %>
73
- <% end %>
74
- HOME
75
- }
76
- route "root to: 'home#index'"
77
-
78
74
  # Configure for our SAML IdP
79
75
  generate 'devise:install'
80
76
  gsub_file 'config/initializers/devise.rb', /^end$/, <<-CONFIG
@@ -83,6 +79,9 @@ after_bundle do
83
79
  config.saml_default_user_key = :email
84
80
  config.saml_session_index_key = #{saml_session_index_key}
85
81
 
82
+ if #{attribute_map_resolver}
83
+ config.saml_attribute_map_resolver = #{attribute_map_resolver}
84
+ end
86
85
  config.saml_use_subject = #{use_subject_to_authenticate}
87
86
  config.saml_create_user = true
88
87
  config.saml_update_user = true
@@ -96,11 +95,33 @@ after_bundle do
96
95
  settings.idp_slo_target_url = "http://localhost:8009/saml/logout"
97
96
  settings.idp_sso_target_url = "http://localhost:8009/saml/auth"
98
97
  settings.idp_cert_fingerprint = "9E:65:2E:03:06:8D:80:F2:86:C7:6C:77:A1:D9:14:97:0A:4D:F4:4D"
98
+ settings.name_identifier_format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
99
99
  end
100
100
  end
101
101
  CONFIG
102
102
 
103
- generate :devise, "user", "email:string", "name:string", "session_index:string"
103
+ generate :controller, 'home', 'index'
104
+ insert_into_file('app/controllers/home_controller.rb', after: "class HomeController < ApplicationController\n") {
105
+ <<-AUTHENTICATE
106
+ before_action :authenticate_user!
107
+ AUTHENTICATE
108
+ }
109
+ insert_into_file('app/views/home/index.html.erb', after: /\z/) {
110
+ <<-HOME
111
+ <%= current_user.email %> <%= current_user.name %>
112
+ <%= form_tag destroy_user_session_path(entity_id: "http://localhost:8020/saml/metadata"), method: :delete do %>
113
+ <%= submit_tag "Log out" %>
114
+ <% end %>
115
+ HOME
116
+ }
117
+ route "root to: 'home#index'"
118
+
119
+ if Rails::VERSION::MAJOR < 6
120
+ generate :devise, "user", "email:string", "name:string", "session_index:string"
121
+ else
122
+ # devise seems to add `email` by default in Rails 6
123
+ generate :devise, "user", "name:string", "session_index:string"
124
+ end
104
125
  gsub_file 'app/models/user.rb', /database_authenticatable.*\n.*/, 'saml_authenticatable'
105
126
  route "resources :users, only: [:create]"
106
127
  create_file('app/controllers/users_controller.rb', <<-USERS)
@@ -117,6 +138,9 @@ end
117
138
  rake "db:migrate"
118
139
  rake "db:create", env: "production"
119
140
  rake "db:migrate", env: "production"
141
+
142
+ # Remove any specs so that future RSpec runs don't try to also run these
143
+ run 'rm -rf spec'
120
144
  end
121
145
 
122
146
  create_file 'public/stylesheets/application.css', ''