saml_idp 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -36,12 +36,15 @@ Add to your `routes.rb` file, for example:
36
36
  get '/saml/auth' => 'saml_idp#new'
37
37
  get '/saml/metadata' => 'saml_idp#show'
38
38
  post '/saml/auth' => 'saml_idp#create'
39
+ match '/saml/logout' => 'saml_idp#logout', via: [:get, :post, :delete]
39
40
  ```
40
41
 
41
42
  Create a controller that looks like this, customize to your own situation:
42
43
 
43
44
  ``` ruby
44
- class SamlIdpController < SamlIdp::IdpController
45
+ class SamlIdpController
46
+ include SamlIdp::IdpController
47
+
45
48
  def idp_authenticate(email, password) # not using params intentionally
46
49
  user = User.by_email(email).first
47
50
  user && user.valid_password?(password) ? user : nil
@@ -49,9 +52,20 @@ class SamlIdpController < SamlIdp::IdpController
49
52
  private :idp_authenticate
50
53
 
51
54
  def idp_make_saml_response(found_user) # not using params intentionally
52
- encode_response found_user
55
+ # NOTE encryption is optional
56
+ encode_response found_user, encryption: {
57
+ cert: saml_request.service_provider.cert,
58
+ block_encryption: 'aes256-cbc',
59
+ key_transport: 'rsa-oaep-mgf1p'
60
+ }
53
61
  end
54
62
  private :idp_make_saml_response
63
+
64
+ def idp_logout
65
+ user = User.by_email(saml_request.name_id)
66
+ user.logout
67
+ end
68
+ private :idp_logout
55
69
  end
56
70
  ```
57
71
 
@@ -29,6 +29,17 @@ module SamlIdp
29
29
  render :template => "saml_idp/idp/new"
30
30
  end
31
31
 
32
+ def logout
33
+ idp_logout
34
+ @saml_response = idp_make_saml_response(nil)
35
+ render :template => "saml_idp/idp/saml_post", :layout => false
36
+ end
37
+
38
+ def idp_logout
39
+ raise NotImplementedError
40
+ end
41
+ private :idp_logout
42
+
32
43
  def idp_authenticate(email, password)
33
44
  raise NotImplementedError
34
45
  end
data/lib/saml_idp.rb CHANGED
@@ -71,11 +71,11 @@ module Saml
71
71
 
72
72
  def valid_signature?(fingerprint)
73
73
  signed? &&
74
- signed_document.validate_document(fingerprint, :soft)
74
+ signed_document.validate(fingerprint, :soft)
75
75
  end
76
76
 
77
77
  def signed_document
78
- XMLSecurity::SignedDocument.new(to_xml)
78
+ SamlIdp::XMLSecurity::SignedDocument.new(to_xml)
79
79
  end
80
80
 
81
81
  def signature_namespace
@@ -14,10 +14,11 @@ module SamlIdp
14
14
  attr_accessor :raw_algorithm
15
15
  attr_accessor :authn_context_classref
16
16
  attr_accessor :expiry
17
+ attr_accessor :encryption_opts
17
18
 
18
19
  delegate :config, to: :SamlIdp
19
20
 
20
- def initialize(reference_id, issuer_uri, principal, audience_uri, saml_request_id, saml_acs_url, raw_algorithm, authn_context_classref, expiry=60*60)
21
+ def initialize(reference_id, issuer_uri, principal, audience_uri, saml_request_id, saml_acs_url, raw_algorithm, authn_context_classref, expiry=60*60, encryption_opts=nil)
21
22
  self.reference_id = reference_id
22
23
  self.issuer_uri = issuer_uri
23
24
  self.principal = principal
@@ -27,6 +28,7 @@ module SamlIdp
27
28
  self.raw_algorithm = raw_algorithm
28
29
  self.authn_context_classref = authn_context_classref
29
30
  self.expiry = expiry
31
+ self.encryption_opts = encryption_opts
30
32
  end
31
33
 
32
34
  def fresh
@@ -73,6 +75,14 @@ module SamlIdp
73
75
  alias_method :raw, :fresh
74
76
  private :fresh
75
77
 
78
+ def encrypt(opts = {})
79
+ raise "Must set encryption_opts to encrypt" unless encryption_opts
80
+ raw_xml = opts[:sign] ? signed : raw
81
+ require 'saml_idp/encryptor'
82
+ encryptor = Encryptor.new encryption_opts
83
+ encryptor.encrypt(raw_xml)
84
+ end
85
+
76
86
  def get_values_for(friendly_name, getter)
77
87
  result = nil
78
88
  if getter.present?
@@ -13,6 +13,7 @@ module SamlIdp
13
13
  attr_accessor :reference_id_generator
14
14
  attr_accessor :attribute_service_location
15
15
  attr_accessor :single_service_post_location
16
+ attr_accessor :single_logout_service_post_location
16
17
  attr_accessor :attributes
17
18
  attr_accessor :service_provider
18
19
 
@@ -4,6 +4,7 @@ require 'base64'
4
4
  require 'time'
5
5
  require 'uuid'
6
6
  require 'saml_idp/request'
7
+ require 'saml_idp/logout_response_builder'
7
8
  module SamlIdp
8
9
  module Controller
9
10
  extend ActiveSupport::Concern
@@ -30,10 +31,14 @@ module SamlIdp
30
31
  Saml::XML::Namespaces::AuthnContext::ClassRef::PASSWORD
31
32
  end
32
33
 
33
- def encode_response(principal, opts = {})
34
- response_id, reference_id = get_saml_response_id, get_saml_reference_id
34
+ def encode_authn_response(principal, opts = {})
35
+ response_id = get_saml_response_id
36
+ reference_id = opts[:reference_id] || get_saml_reference_id
35
37
  audience_uri = opts[:audience_uri] || saml_request.issuer || saml_acs_url[/^(.*?\/\/.*?\/)/, 1]
36
38
  opt_issuer_uri = opts[:issuer_uri] || issuer_uri
39
+ my_authn_context_classref = opts[:authn_context_classref] || authn_context_classref
40
+ expiry = opts[:expiry] || 60*60
41
+ encryption_opts = opts[:encryption] || nil
37
42
 
38
43
  SamlResponse.new(
39
44
  reference_id,
@@ -43,11 +48,33 @@ module SamlIdp
43
48
  audience_uri,
44
49
  saml_request_id,
45
50
  saml_acs_url,
46
- algorithm,
47
- authn_context_classref
51
+ (opts[:algorithm] || algorithm || default_algorithm),
52
+ my_authn_context_classref,
53
+ expiry,
54
+ encryption_opts
48
55
  ).build
49
56
  end
50
57
 
58
+ def encode_logout_response(principal, opts = {})
59
+ SamlIdp::LogoutResponseBuilder.new(
60
+ get_saml_response_id,
61
+ (opts[:issuer_uri] || issuer_uri),
62
+ saml_logout_url,
63
+ saml_request_id,
64
+ (opts[:algorithm] || algorithm || default_algorithm)
65
+ ).signed
66
+ end
67
+
68
+ def encode_response(principal, opts = {})
69
+ if saml_request.authn_request?
70
+ encode_authn_response(principal, opts)
71
+ elsif saml_request.logout_request?
72
+ encode_logout_response(principal, opts)
73
+ else
74
+ raise "Unknown request: #{saml_request}"
75
+ end
76
+ end
77
+
51
78
  def issuer_uri
52
79
  (SamlIdp.config.base_saml_location.present? && SamlIdp.config.base_saml_location) ||
53
80
  (defined?(request) && request.url.to_s.split("?").first) ||
@@ -66,6 +93,10 @@ module SamlIdp
66
93
  saml_request.acs_url
67
94
  end
68
95
 
96
+ def saml_logout_url
97
+ saml_request.logout_url
98
+ end
99
+
69
100
  def get_saml_response_id
70
101
  UUID.generate
71
102
  end
@@ -73,5 +104,9 @@ module SamlIdp
73
104
  def get_saml_reference_id
74
105
  UUID.generate
75
106
  end
107
+
108
+ def default_algorithm
109
+ OpenSSL::Digest::SHA256
110
+ end
76
111
  end
77
112
  end
@@ -0,0 +1,86 @@
1
+ require 'xmlenc'
2
+ module SamlIdp
3
+ class Encryptor
4
+ attr_accessor :encryption_key
5
+ attr_accessor :block_encryption
6
+ attr_accessor :key_transport
7
+ attr_accessor :cert
8
+
9
+ def initialize(opts)
10
+ self.block_encryption = opts[:block_encryption]
11
+ self.key_transport = opts[:key_transport]
12
+ self.cert = opts[:cert]
13
+ end
14
+
15
+ def encrypt(raw_xml)
16
+ encryption_template = Nokogiri::XML::Document.parse(build_encryption_template).root
17
+ encrypted_data = Xmlenc::EncryptedData.new(encryption_template)
18
+ @encryption_key = encrypted_data.encrypt(raw_xml)
19
+ encrypted_key_node = encrypted_data.node.at_xpath(
20
+ '//xenc:EncryptedData/ds:KeyInfo/xenc:EncryptedKey',
21
+ Xmlenc::NAMESPACES
22
+ )
23
+ encrypted_key = Xmlenc::EncryptedKey.new(encrypted_key_node)
24
+ encrypted_key.encrypt(openssl_cert.public_key, encryption_key)
25
+ xml = Builder::XmlMarkup.new
26
+ xml.EncryptedAssertion xmlns: Saml::XML::Namespaces::ASSERTION do |enc_assert|
27
+ enc_assert << encrypted_data.node.to_s
28
+ end
29
+ end
30
+
31
+ def openssl_cert
32
+ if cert.is_a?(String)
33
+ @_openssl_cert ||= OpenSSL::X509::Certificate.new(Base64.decode64(cert))
34
+ else
35
+ @_openssl_cert ||= cert
36
+ end
37
+ end
38
+ private :openssl_cert
39
+
40
+ def block_encryption_ns
41
+ "http://www.w3.org/2001/04/xmlenc##{block_encryption}"
42
+ end
43
+ private :block_encryption_ns
44
+
45
+ def key_transport_ns
46
+ "http://www.w3.org/2001/04/xmlenc##{key_transport}"
47
+ end
48
+ private :key_transport_ns
49
+
50
+ def cipher_algorithm
51
+ Xmlenc::EncryptedData::ALGORITHMS[block_encryption_ns]
52
+ end
53
+ private :cipher_algorithm
54
+
55
+ def build_encryption_template
56
+ xml = Builder::XmlMarkup.new
57
+ xml.EncryptedData Id: 'ED', Type: 'http://www.w3.org/2001/04/xmlenc#Element',
58
+ xmlns: 'http://www.w3.org/2001/04/xmlenc#' do |enc_data|
59
+ enc_data.EncryptionMethod Algorithm: block_encryption_ns
60
+ enc_data.tag! 'ds:KeyInfo', 'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#' do |key_info|
61
+ key_info.EncryptedKey Id: 'EK', xmlns: 'http://www.w3.org/2001/04/xmlenc#' do |enc_key|
62
+ enc_key.EncryptionMethod Algorithm: key_transport_ns
63
+ enc_key.tag! 'ds:KeyInfo', 'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#' do |key_info2|
64
+ key_info2.tag! 'ds:KeyName'
65
+ key_info2.tag! 'ds:X509Data' do |x509_data|
66
+ x509_data.tag! 'ds:X509Certificate' do |x509_cert|
67
+ x509_cert << cert.to_s.gsub(/-+(BEGIN|END) CERTIFICATE-+/, '')
68
+ end
69
+ end
70
+ end
71
+ enc_key.CipherData do |cipher_data|
72
+ cipher_data.CipherValue
73
+ end
74
+ enc_key.ReferenceList do |ref_list|
75
+ ref_list.DataReference URI: 'ED'
76
+ end
77
+ end
78
+ end
79
+ enc_data.CipherData do |cipher_data|
80
+ cipher_data.CipherValue
81
+ end
82
+ end
83
+ end
84
+ private :build_encryption_template
85
+ end
86
+ end
@@ -0,0 +1,42 @@
1
+ require 'builder'
2
+ module SamlIdp
3
+ class LogoutBuilder
4
+ include Signable
5
+
6
+ # this is an abstract base class.
7
+ def build
8
+ raise "#{self.class} must implement build method"
9
+ end
10
+
11
+ def reference_id
12
+ UUID.generate
13
+ end
14
+
15
+ def digest
16
+ algorithm.hexdigest raw
17
+ end
18
+
19
+ def encoded
20
+ @encoded ||= encode
21
+ end
22
+
23
+ def raw
24
+ build
25
+ end
26
+
27
+ def encode
28
+ Base64.strict_encode64(raw)
29
+ end
30
+ private :encode
31
+
32
+ def response_id_string
33
+ "_#{response_id}"
34
+ end
35
+ private :response_id_string
36
+
37
+ def now_iso
38
+ Time.now.utc.iso8601
39
+ end
40
+ private :now_iso
41
+ end
42
+ end
@@ -0,0 +1,36 @@
1
+ require 'saml_idp/logout_builder'
2
+ module SamlIdp
3
+ class LogoutRequestBuilder < LogoutBuilder
4
+ attr_accessor :response_id
5
+ attr_accessor :issuer_uri
6
+ attr_accessor :saml_slo_url
7
+ attr_accessor :name_id
8
+ attr_accessor :session_index
9
+ attr_accessor :algorithm
10
+
11
+ def initialize(response_id, issuer_uri, saml_slo_url, name_id, session_index, algorithm)
12
+ self.response_id = response_id
13
+ self.issuer_uri = issuer_uri
14
+ self.saml_slo_url = saml_slo_url
15
+ self.name_id = name_id
16
+ self.session_index = session_index
17
+ self.algorithm = algorithm
18
+ end
19
+
20
+ def build
21
+ builder = Builder::XmlMarkup.new
22
+ builder.LogoutRequest ID: response_id_string,
23
+ Version: "2.0",
24
+ IssueInstant: now_iso,
25
+ Destination: saml_slo_url,
26
+ "xmlns" => Saml::XML::Namespaces::PROTOCOL do |request|
27
+ request.Issuer issuer_uri, xmlns: Saml::XML::Namespaces::ASSERTION
28
+ sign request
29
+ request.NameID name_id, xmlns: Saml::XML::Namespaces::ASSERTION,
30
+ Format: Saml::XML::Namespaces::Formats::NameId::PERSISTENT
31
+ request.SessionIndex session_index
32
+ end
33
+ end
34
+ private :build
35
+ end
36
+ end
@@ -0,0 +1,35 @@
1
+ require 'saml_idp/logout_builder'
2
+ module SamlIdp
3
+ class LogoutResponseBuilder < LogoutBuilder
4
+ attr_accessor :response_id
5
+ attr_accessor :issuer_uri
6
+ attr_accessor :saml_slo_url
7
+ attr_accessor :saml_request_id
8
+ attr_accessor :algorithm
9
+
10
+ def initialize(response_id, issuer_uri, saml_slo_url, saml_request_id, algorithm)
11
+ self.response_id = response_id
12
+ self.issuer_uri = issuer_uri
13
+ self.saml_slo_url = saml_slo_url
14
+ self.saml_request_id = saml_request_id
15
+ self.algorithm = algorithm
16
+ end
17
+
18
+ def build
19
+ builder = Builder::XmlMarkup.new
20
+ builder.LogoutResponse ID: response_id_string,
21
+ Version: "2.0",
22
+ IssueInstant: now_iso,
23
+ Destination: saml_slo_url,
24
+ InResponseTo: saml_request_id,
25
+ xmlns: Saml::XML::Namespaces::PROTOCOL do |response|
26
+ response.Issuer issuer_uri, xmlns: Saml::XML::Namespaces::ASSERTION
27
+ sign response
28
+ response.Status xmlns: Saml::XML::Namespaces::PROTOCOL do |status|
29
+ status.StatusCode Value: Saml::XML::Namespaces::Statuses::SUCCESS
30
+ end
31
+ end
32
+ end
33
+ private :build
34
+ end
35
+ end
@@ -25,6 +25,8 @@ module SamlIdp
25
25
  entity.IDPSSODescriptor protocolSupportEnumeration: protocol_enumeration do |descriptor|
26
26
  build_key_descriptor descriptor
27
27
  build_name_id_formats descriptor
28
+ descriptor.SingleLogoutService Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
29
+ Location: single_logout_service_post_location
28
30
  descriptor.SingleSignOnService Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
29
31
  Location: single_service_post_location
30
32
  build_attribute descriptor
@@ -146,6 +148,7 @@ module SamlIdp
146
148
  organization_url
147
149
  attribute_service_location
148
150
  single_service_post_location
151
+ single_logout_service_post_location
149
152
  technical_contact
150
153
  ].each do |delegatable|
151
154
  define_method(delegatable) do
@@ -31,8 +31,32 @@ module SamlIdp
31
31
  self.raw_xml = raw_xml
32
32
  end
33
33
 
34
+ def logout_request?
35
+ logout_request.nil? ? false : true
36
+ end
37
+
38
+ def authn_request?
39
+ authn_request.nil? ? false : true
40
+ end
41
+
34
42
  def request_id
35
- authn_request["ID"]
43
+ request["ID"]
44
+ end
45
+
46
+ def request
47
+ if authn_request?
48
+ authn_request
49
+ elsif logout_request?
50
+ logout_request
51
+ end
52
+ end
53
+
54
+ def requested_authn_context
55
+ if authn_request? && authn_context_node
56
+ authn_context_node.content
57
+ else
58
+ nil
59
+ end
36
60
  end
37
61
 
38
62
  def acs_url
@@ -40,14 +64,54 @@ module SamlIdp
40
64
  authn_request["AssertionConsumerServiceURL"].to_s
41
65
  end
42
66
 
67
+ def logout_url
68
+ service_provider.assertion_consumer_logout_service_url
69
+ end
70
+
71
+ def response_url
72
+ if authn_request?
73
+ acs_url
74
+ elsif logout_request?
75
+ logout_url
76
+ end
77
+ end
78
+
79
+ def log(msg)
80
+ if Rails && Rails.logger
81
+ Rails.logger.info msg
82
+ else
83
+ puts msg
84
+ end
85
+ end
86
+
43
87
  def valid?
44
- service_provider? &&
45
- valid_signature? &&
46
- acs_url.present?
88
+ unless service_provider?
89
+ log "Unable to find service provider for issuer #{issuer}"
90
+ return false
91
+ end
92
+
93
+ unless (authn_request? ^ logout_request?)
94
+ log "One and only one of authnrequest and logout request is required. authnrequest: #{authn_request?} logout_request: #{logout_request?} "
95
+ return false
96
+ end
97
+
98
+ unless valid_signature?
99
+ log "Signature is invalid in #{raw_xml}"
100
+ return false
101
+ end
102
+
103
+ if response_url.nil?
104
+ log "Unable to find response url for #{issuer}: #{raw_xml}"
105
+ return false
106
+ end
107
+
108
+ return true
47
109
  end
48
110
 
49
111
  def valid_signature?
50
- service_provider.valid_signature? document
112
+ # Force signatures for logout requests because there is no other
113
+ # protection against a cross-site DoS.
114
+ service_provider.valid_signature?(document, logout_request?)
51
115
  end
52
116
 
53
117
  def service_provider?
@@ -55,24 +119,44 @@ module SamlIdp
55
119
  end
56
120
 
57
121
  def service_provider
58
- @service_provider ||= ServiceProvider.new((service_provider_finder[issuer] || {}).merge(identifier: issuer))
122
+ @_service_provider ||= ServiceProvider.new((service_provider_finder[issuer] || {}).merge(identifier: issuer))
59
123
  end
60
124
 
61
125
  def issuer
62
- @content ||= xpath("//saml:Issuer", saml: assertion).first.try(:content)
63
- @content if @content.present?
126
+ @_issuer ||= xpath("//saml:Issuer", saml: assertion).first.try(:content)
127
+ @_issuer if @_issuer.present?
128
+ end
129
+
130
+ def name_id
131
+ @_name_id ||= xpath("//saml:NameID", saml: assertion).first.try(:content)
132
+ end
133
+
134
+ def session_index
135
+ @_session_index ||= xpath("//samlp:SessionIndex", samlp: samlp).first.try(:content)
64
136
  end
65
137
 
66
138
  def document
67
- @document ||= Saml::XML::Document.parse(raw_xml)
139
+ @_document ||= Saml::XML::Document.parse(raw_xml)
68
140
  end
69
141
  private :document
70
142
 
143
+ def authn_context_node
144
+ @_authn_context_node ||= xpath("//samlp:AuthnRequest/samlp:RequestedAuthnContext/saml:AuthnContextClassRef",
145
+ samlp: samlp,
146
+ saml: assertion).first
147
+ end
148
+ private :authn_context_node
149
+
71
150
  def authn_request
72
- xpath("//samlp:AuthnRequest", samlp: samlp).first
151
+ @_authn_request ||= xpath("//samlp:AuthnRequest", samlp: samlp).first
73
152
  end
74
153
  private :authn_request
75
154
 
155
+ def logout_request
156
+ @_logout_request ||= xpath("//samlp:LogoutRequest", samlp: samlp).first
157
+ end
158
+ private :logout_request
159
+
76
160
  def samlp
77
161
  Saml::XML::Namespaces::PROTOCOL
78
162
  end