devise_saml_authenticatable 0.1.0 → 1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 69aceb045d8de7ed6acd66fd027fdabfe8da4f89
4
- data.tar.gz: def4131da232ae9818379b35750b686a77e61d78
3
+ metadata.gz: 4c162a34c36999fce62a2fb2f18cbfd5ce87a488
4
+ data.tar.gz: 61637ed78f97770f32d07157928127ad40032d2e
5
5
  SHA512:
6
- metadata.gz: 5a8c183ecbe7dcda723e6fa459056de365390de80baf3c587719ebdf40df07741e89de14355c5b0c623755371ee0473c6be729b1dd5d044d00da7474d87a4b12
7
- data.tar.gz: 5b3b63caf790992b2263e6ccceec1570df0af4e207c42e8d89c5eeb1726740195dd28ae4e90e2823dbf1f6d47b880fa71de338ec75f35fd550012791a3df8406
6
+ metadata.gz: 3eeab1c7fdac22674db47e7e71c1c6793243a0e398cf23fe51ba261cf54ee59133d9d0445ec87e804b9c39d8ffd2d893d8369194adc318e38568a206761cea30
7
+ data.tar.gz: b0306658471a7d121ef6791b74633c24a9ba0e468e283594261c046da776e9d8739235952df27162269480d6f294bbb7b131ab99676a79a99fd69bc66d1f0a9c
data/README.md CHANGED
@@ -56,6 +56,7 @@ In config/initializers/devise.rb
56
56
  settings.name_identifier_format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
57
57
  settings.issuer = "http://localhost:3000"
58
58
  settings.authn_context = ""
59
+ settings.idp_slo_target_url = "http://localhost/simplesaml/www/saml2/idp/SingleLogoutService.php"
59
60
  settings.idp_sso_target_url = "http://localhost/simplesaml/www/saml2/idp/SSOService.php"
60
61
  settings.idp_cert = <<-CERT.chomp
61
62
  -----BEGIN CERTIFICATE-----
@@ -110,10 +111,13 @@ There are numerous IdPs that support SAML 2.0, there are propietary (like Micros
110
111
 
111
112
  [SimpleSAMLphp](http://simplesamlphp.org/) was my choice for development since it is a production-ready SAML solution, that is also really easy to install, configure and use.
112
113
 
114
+ ## Logout
115
+
116
+ Logout support is included by immediately terminating the local session and then redirecting to the IdP.
117
+
113
118
  ## Limitations
114
119
 
115
- 1. At the moment there is no support for Single Logout
116
- 2. The Authentication Requests (from your app to the IdP) are not signed and encrypted
120
+ 1. The Authentication Requests (from your app to the IdP) are not signed and encrypted
117
121
 
118
122
  ## Thanks
119
123
 
@@ -16,6 +16,13 @@ class Devise::SamlSessionsController < Devise::SessionsController
16
16
  meta = OneLogin::RubySaml::Metadata.new
17
17
  render :xml => meta.generate(@saml_config)
18
18
  end
19
-
19
+
20
+ protected
21
+
22
+ # Override devise to send user to IdP logout for SLO
23
+ def after_sign_out_path_for(_)
24
+ request = OneLogin::RubySaml::Logoutrequest.new
25
+ request.create(@saml_config)
26
+ end
20
27
  end
21
28
 
@@ -60,7 +60,7 @@ module Devise
60
60
  end
61
61
 
62
62
  def attribute_map
63
- @attribute_map ||= YAML.load(File.read("#{Rails.root}/config/attribute-map.yml"))
63
+ @attribute_map ||= attribute_map_for_environment
64
64
  end
65
65
 
66
66
  private
@@ -71,6 +71,15 @@ module Devise
71
71
  user.send "#{v}=", attributes[k]
72
72
  end
73
73
  end
74
+
75
+ def attribute_map_for_environment
76
+ attribute_map = YAML.load(File.read("#{Rails.root}/config/attribute-map.yml"))
77
+ if attribute_map.has_key?(Rails.env)
78
+ attribute_map[Rails.env]
79
+ else
80
+ attribute_map
81
+ end
82
+ end
74
83
  end
75
84
  end
76
85
  end
@@ -1,3 +1,3 @@
1
1
  module DeviseSamlAuthenticatable
2
- VERSION = "0.1.0"
2
+ VERSION = "1.0"
3
3
  end
@@ -1,7 +1,11 @@
1
1
  require 'rails_helper'
2
2
 
3
3
  class Devise::SessionsController < ActionController::Base
4
-
4
+ # The important parts from devise
5
+ def destroy
6
+ sign_out
7
+ redirect_to after_sign_out_path_for(:user)
8
+ end
5
9
  end
6
10
 
7
11
  require_relative '../../../app/controllers/devise/saml_sessions_controller'
@@ -18,7 +22,7 @@ describe Devise::SamlSessionsController, type: :controller do
18
22
  end
19
23
 
20
24
  describe '#metadata' do
21
- it "generates metadata" do
25
+ it 'generates metadata' do
22
26
  get :metadata
23
27
 
24
28
  # Remove ID that can vary across requests
@@ -27,4 +31,12 @@ describe Devise::SamlSessionsController, type: :controller do
27
31
  expect(response.body).to match(Regexp.new(metadata_pattern))
28
32
  end
29
33
  end
34
+
35
+ describe '#destroy' do
36
+ it 'signs out and redirects to the IdP' do
37
+ expect(controller).to receive(:sign_out)
38
+ delete :destroy
39
+ expect(response).to redirect_to(%r(\Ahttp://localhost:8009/saml/logout\?SAMLRequest=))
40
+ end
41
+ end
30
42
  end
@@ -1,5 +1,6 @@
1
1
  require 'spec_helper'
2
2
  require 'net/http'
3
+ require 'timeout'
3
4
  require 'uri'
4
5
  require 'capybara/rspec'
5
6
  require 'capybara/webkit'
@@ -32,6 +33,21 @@ describe "SAML Authentication", type: :feature do
32
33
  expect(page).to have_content("A User")
33
34
  expect(current_url).to eq("http://localhost:8020/")
34
35
  end
36
+
37
+ it "logs a user out of the IdP via the SP" do
38
+ sign_in
39
+
40
+ # prove user is still signed in
41
+ visit 'http://localhost:8020/'
42
+ expect(page).to have_content("you@example.com")
43
+ expect(current_url).to eq("http://localhost:8020/")
44
+
45
+ click_on "Log out"
46
+
47
+ # prove user is now signed out
48
+ visit 'http://localhost:8020/'
49
+ expect(current_url).to match(%r(\Ahttp://localhost:8009/saml/auth\?SAMLRequest=))
50
+ end
35
51
  end
36
52
 
37
53
  context "when the attributes are used to authenticate" do
@@ -68,4 +84,20 @@ describe "SAML Authentication", type: :feature do
68
84
  response = Net::HTTP.post_form(URI('http://localhost:8020/users'), email: email)
69
85
  expect(response.code).to eq('201')
70
86
  end
87
+
88
+ def sign_in
89
+ visit 'http://localhost:8020/'
90
+ expect(current_url).to match(%r(\Ahttp://localhost:8009/saml/auth\?SAMLRequest=))
91
+ fill_in "Email", with: "you@example.com"
92
+ fill_in "Password", with: "asdf"
93
+ click_on "Sign in"
94
+ Timeout.timeout(Capybara.default_wait_time) do
95
+ loop do
96
+ sleep 0.1
97
+ break if current_url == "http://localhost:8020/"
98
+ end
99
+ end
100
+ rescue Timeout::Error
101
+ expect(current_url).to eq("http://localhost:8020/")
102
+ end
71
103
  end
@@ -6,5 +6,7 @@ gem 'ruby-saml-idp'
6
6
 
7
7
  route "get '/saml/auth' => 'saml_idp#new'"
8
8
  route "post '/saml/auth' => 'saml_idp#create'"
9
+ route "get '/saml/logout' => 'saml_idp#logout'"
9
10
 
10
11
  template File.expand_path('../saml_idp_controller.rb.erb', __FILE__), 'app/controllers/saml_idp_controller.rb'
12
+ copy_file File.expand_path('../saml_idp-saml_slo_post.html.erb', __FILE__), 'app/views/saml_idp/saml_slo_post.html.erb'
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
6
+ </head>
7
+ <body onload="document.forms[0].submit();" style="visibility:hidden;">
8
+ <%= form_tag(@saml_slo_acs_url) do %>
9
+ <%= hidden_field_tag("SAMLResponse", @saml_slo_response) %>
10
+ <%= submit_tag "Submit" %>
11
+ <% end %>
12
+ </body>
13
+ </html>
@@ -1,9 +1,21 @@
1
1
  class SamlIdpController < SamlIdp::IdpController
2
+ def new
3
+ if session[:user_id]
4
+ @saml_response = idp_make_saml_response(session[:user_id])
5
+ render :template => "saml_idp/idp/saml_post", :layout => false
6
+ return
7
+ end
8
+ super
9
+ end
10
+
11
+ protected
12
+
2
13
  def idp_authenticate(email, password)
14
+ session[:user_id] = "you@example.com"
3
15
  true
4
16
  end
5
17
 
6
- def idp_make_saml_response(user)
18
+ def idp_make_saml_response(_)
7
19
  attributes = {
8
20
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" => "A User",
9
21
  }
@@ -51,4 +63,77 @@ class SamlIdpController < SamlIdp::IdpController
51
63
  def include_subject_in_attributes
52
64
  <%= @include_subject_in_attributes %>
53
65
  end
66
+
67
+ # == SLO functionality, see https://github.com/lawrencepit/ruby-saml-idp/pull/10
68
+ skip_before_filter :validate_saml_request, :only => [:logout]
69
+ before_filter :validate_saml_slo_request, :only => [:logout]
70
+
71
+ public
72
+
73
+ def logout
74
+ _person, _logout = idp_slo_authenticate(params[:name_id])
75
+ if _person && _logout
76
+ @saml_slo_response = idp_make_saml_slo_response(_person)
77
+ else
78
+ @saml_idp_fail_msg = 'User not found'
79
+ logger.error "User with email #{params[:name_id]} not found"
80
+ @saml_slo_response = encode_SAML_SLO_Response(params[:name_id])
81
+ end
82
+ if @saml_slo_acs_url
83
+ render :template => "saml_idp/idp/saml_slo_post", :layout => false
84
+ else
85
+ redirect_to 'http://example.com'
86
+ end
87
+ end
88
+
89
+ def idp_slo_authenticate(email)
90
+ session.delete :user_id
91
+ true
92
+ end
93
+
94
+ def idp_make_saml_slo_response(person)
95
+ attributes = {}
96
+ if include_subject_in_attributes
97
+ attributes["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"] = "you@example.com"
98
+ end
99
+ encode_SAML_SLO_Response("you@example.com", attributes: attributes)
100
+ end
101
+
102
+ private
103
+
104
+ def validate_saml_slo_request(saml_request = params[:SAMLRequest])
105
+ decode_SAML_SLO_Request(saml_request)
106
+ end
107
+
108
+ def decode_SAML_SLO_Request(saml_request)
109
+ zstream = Zlib::Inflate.new(-Zlib::MAX_WBITS)
110
+ @saml_slo_request = zstream.inflate(Base64.decode64(saml_request))
111
+ zstream.finish
112
+ zstream.close
113
+ @saml_slo_request_id = @saml_slo_request[/ID=['"](.+?)['"]/, 1]
114
+ @saml_slo_acs_url = @saml_slo_request[/AssertionConsumerLogoutServiceURL=['"](.+?)['"]/, 1]
115
+ end
116
+
117
+ def encode_SAML_SLO_Response(nameID, opts = {})
118
+ now = Time.now.utc
119
+ response_id, reference_id = UUID.generate, UUID.generate
120
+ audience_uri = opts[:audience_uri] || (@saml_slo_acs_url && @saml_slo_acs_url[/^(.*?\/\/.*?\/)/, 1])
121
+ issuer_uri = opts[:issuer_uri] || (defined?(request) && request.url.split("?")[0]) || "http://example.com"
122
+
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>]
124
+
125
+ digest_value = Base64.encode64(algorithm.digest(assertion)).gsub(/\n/, '')
126
+
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>]
128
+
129
+ signature_value = sign(signed_info).gsub(/\n/, '')
130
+
131
+ signature = %[<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">#{signed_info}<ds:SignatureValue>#{signature_value}</ds:SignatureValue><KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"><ds:X509Data><ds:X509Certificate>#{self.x509_certificate}</ds:X509Certificate></ds:X509Data></KeyInfo></ds:Signature>]
132
+
133
+ assertion_and_signature = assertion.sub(/Issuer\>\<Subject/, "Issuer>#{signature}<Subject")
134
+
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>]
136
+
137
+ Base64.encode64(xml)
138
+ end
54
139
  end
@@ -18,7 +18,12 @@ after_bundle do
18
18
  AUTHENTICATE
19
19
  }
20
20
  insert_into_file('app/views/home/index.html.erb', after: /\z/) {
21
- "<%= current_user.email %> <%= current_user.name %>"
21
+ <<-HOME
22
+ <%= current_user.email %> <%= current_user.name %>
23
+ <%= form_tag destroy_user_session_path, method: :delete do %>
24
+ <%= submit_tag "Log out" %>
25
+ <% end %>
26
+ HOME
22
27
  }
23
28
  route "root to: 'home#index'"
24
29
 
@@ -33,6 +38,7 @@ after_bundle do
33
38
  config.saml_configure do |settings|
34
39
  settings.assertion_consumer_service_url = "http://localhost:8020/users/saml/auth"
35
40
  settings.issuer = "http://localhost:8020/saml/metadata"
41
+ settings.idp_slo_target_url = "http://localhost:8009/saml/logout"
36
42
  settings.idp_sso_target_url = "http://localhost:8009/saml/auth"
37
43
  settings.idp_cert_fingerprint = "9E:65:2E:03:06:8D:80:F2:86:C7:6C:77:A1:D9:14:97:0A:4D:F4:4D"
38
44
  end
@@ -41,7 +47,7 @@ end
41
47
 
42
48
  generate :devise, "user", "email:string", "name:string"
43
49
  gsub_file 'app/models/user.rb', /database_authenticatable.*\n.*/, 'saml_authenticatable'
44
- route "resources :users"
50
+ route "resources :users, only: [:create]"
45
51
  create_file('app/controllers/users_controller.rb', <<-USERS)
46
52
  class UsersController < ApplicationController
47
53
  skip_before_filter :verify_authenticity_token
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: 0.1.0
4
+ version: '1.0'
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-13 00:00:00.000000000 Z
11
+ date: 2015-06-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: devise
@@ -72,6 +72,7 @@ files:
72
72
  - spec/spec_helper.rb
73
73
  - spec/support/idp_template.rb
74
74
  - spec/support/rails_app.rb
75
+ - spec/support/saml_idp-saml_slo_post.html.erb
75
76
  - spec/support/saml_idp_controller.rb.erb
76
77
  - spec/support/sp_template.rb
77
78
  homepage: ''
@@ -107,6 +108,7 @@ test_files:
107
108
  - spec/spec_helper.rb
108
109
  - spec/support/idp_template.rb
109
110
  - spec/support/rails_app.rb
111
+ - spec/support/saml_idp-saml_slo_post.html.erb
110
112
  - spec/support/saml_idp_controller.rb.erb
111
113
  - spec/support/sp_template.rb
112
114
  has_rdoc: