saml_idp 0.2.1 → 0.3.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.
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