devise_saml_authenticatable 1.3.2 → 1.6.1

Sign up to get free protection for your applications and to get access to all the features.
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', ''