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 +4 -4
- data/README.md +6 -2
- data/app/controllers/devise/saml_sessions_controller.rb +8 -1
- data/lib/devise_saml_authenticatable/model.rb +10 -1
- data/lib/devise_saml_authenticatable/version.rb +1 -1
- data/spec/controllers/devise/saml_sessions_controller_spec.rb +14 -2
- data/spec/features/saml_authentication_spec.rb +32 -0
- data/spec/support/idp_template.rb +2 -0
- data/spec/support/saml_idp-saml_slo_post.html.erb +13 -0
- data/spec/support/saml_idp_controller.rb.erb +86 -1
- data/spec/support/sp_template.rb +8 -2
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4c162a34c36999fce62a2fb2f18cbfd5ce87a488
|
4
|
+
data.tar.gz: 61637ed78f97770f32d07157928127ad40032d2e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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 ||=
|
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,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
|
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(
|
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
|
data/spec/support/sp_template.rb
CHANGED
@@ -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
|
-
|
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:
|
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-
|
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:
|