devise_saml_authenticatable 0.1.0 → 1.0

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: 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: