Skip to content

Commit a0833fb

Browse files
committed
eIDAS SAML extensions including SPType and RequestedAttribute/RequestedAttributes
1 parent 449dd6b commit a0833fb

7 files changed

Lines changed: 187 additions & 64 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ test/Test.iml
1212
*.gem
1313
.bundle
1414
*.patch
15+
/vendor

lib/onelogin/ruby-saml/authrequest.rb

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44
require "onelogin/ruby-saml/saml_message"
55
require "onelogin/ruby-saml/utils"
66
require "onelogin/ruby-saml/setting_error"
7+
require "onelogin/ruby-saml/requested_attribute"
78

89
# Only supports SAML 2.0
910
module OneLogin
1011
module RubySaml
11-
include REXML
12+
include REXML
1213

1314
# SAML2 Authentication. AuthNRequest (SSO SP initiated, Builder)
1415
#
@@ -46,7 +47,7 @@ def create(settings, params = {})
4647
# @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
4748
# @return [Hash] Parameters
4849
#
49-
def create_params(settings, params={})
50+
def create_params(settings, params = {})
5051
# The method expects :RelayState but sometimes we get 'RelayState' instead.
5152
# Based on the HashWithIndifferentAccess value in Rails we could experience
5253
# conflicts so this line will solve them.
@@ -70,12 +71,12 @@ def create_params(settings, params={})
7071
request_params = {"SAMLRequest" => base64_request}
7172

7273
if settings.security[:authn_requests_signed] && !settings.security[:embed_sign] && settings.private_key
73-
params['SigAlg'] = settings.security[:signature_method]
74+
params['SigAlg'] = settings.security[:signature_method]
7475
url_string = OneLogin::RubySaml::Utils.build_query(
75-
:type => 'SAMLRequest',
76-
:data => base64_request,
77-
:relay_state => relay_state,
78-
:sig_alg => params['SigAlg']
76+
:type => 'SAMLRequest',
77+
:data => base64_request,
78+
:relay_state => relay_state,
79+
:sig_alg => params['SigAlg']
7980
)
8081
sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method])
8182
signature = settings.get_sp_key.sign(sign_algorithm.new, url_string)
@@ -104,7 +105,7 @@ def create_xml_document(settings)
104105
request_doc = XMLSecurity::Document.new
105106
request_doc.uuid = uuid
106107

107-
root = request_doc.add_element "samlp:AuthnRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol", "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" }
108+
root = request_doc.add_element "samlp:AuthnRequest", {"xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol", "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion", "xmlns:eidas" => "http://eidas.europa.eu/saml-extensions"}
108109
root.attributes['ID'] = uuid
109110
root.attributes['IssueInstant'] = time
110111
root.attributes['Version'] = "2.0"
@@ -151,7 +152,7 @@ def create_xml_document(settings)
151152
end
152153

153154
requested_context = root.add_element "samlp:RequestedAuthnContext", {
154-
"Comparison" => comparison,
155+
"Comparison" => comparison,
155156
}
156157

157158
if settings.authn_context != nil
@@ -171,6 +172,27 @@ def create_xml_document(settings)
171172
end
172173
end
173174

175+
if settings.extensions[:sptype] != false || settings.extensions[:requested_attributes] != false
176+
req_extensions = root.add_element "samlp:Extensions"
177+
178+
sptype_value = settings.extensions[:sptype] != nil ? settings.extensions[:sptype] : 'public'
179+
eidas_sptype = req_extensions.add_element 'eidas:SPType'
180+
eidas_sptype.text = sptype_value
181+
182+
unless settings.extensions[:requested_attributes].empty?
183+
req_attributes = req_extensions.add_element 'eidas:RequestedAttributes'
184+
settings.extensions[:requested_attributes].each do |requested_attr|
185+
next unless requested_attr.is_a? RequestedAttribute
186+
187+
el_attr = req_attributes.add_element 'eidas:RequestedAttribute', requested_attr.stringify_attribute_keys
188+
next unless requested_attr.value
189+
190+
el_attr_val = el_attr.add_element 'eidas:AttributeValue'
191+
el_attr_val.text = requested_attr.value.to_s
192+
end
193+
end
194+
end
195+
174196
request_doc
175197
end
176198

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
module OneLogin
2+
module RubySaml
3+
4+
# Class simplifying management of eidas:RequestedAttribute from eIDAS saml-extensions
5+
# It's implementation is intentionally vague to allow custom attributes of RequestedAttribute element and accept any kind of value
6+
class RequestedAttribute
7+
8+
attr_accessor :attributes
9+
attr_accessor :value
10+
11+
DEFAULTS = {
12+
:NameFormat => 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri'.freeze,
13+
:isRequired => false,
14+
:FriendlyName => false
15+
}.freeze
16+
17+
# @param attrs [Hash] The +attrs+ must be Hash of known attributes, ie:
18+
# RequestedAttribute.new({
19+
# :Name => "http://eidas.europa.eu/attributes/naturalperson/DateOfBirth",
20+
# :FriendlyName => "DoB",
21+
# :isRequired => false,
22+
# :NameFormat => "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
23+
# })
24+
# or if above mentioned defaults suit your needs, you can provide only RequestedAttribute Name
25+
# RequestedAttribute.new({
26+
# :Name = "http://www.stork.gov.eu/1.0/isAgeOver"
27+
# }, 18)
28+
# @param [String|Object] val value of eidas:AttributeValue or nil if you don't want the element to be provided
29+
def initialize(attrs = {}, val = nil)
30+
@attributes = DEFAULTS.merge(attrs)
31+
@value = val
32+
end
33+
34+
# @return [Hash]
35+
def stringify_attribute_keys
36+
Hash[attributes.collect { |k, v| [k.to_s, v] }]
37+
end
38+
39+
end
40+
41+
end
42+
end

lib/onelogin/ruby-saml/settings.rb

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@ module RubySaml
1010
# SAML2 Toolkit Settings
1111
#
1212
class Settings
13-
def initialize(overrides = {}, keep_security_attributes = false)
13+
def initialize(overrides = {}, keep_security_attributes = false, keep_extensions_attributes = true)
14+
config = DEFAULTS.merge(overrides)
1415
if keep_security_attributes
1516
security_attributes = overrides.delete(:security) || {}
16-
config = DEFAULTS.merge(overrides)
1717
config[:security] = DEFAULTS[:security].merge(security_attributes)
18-
else
19-
config = DEFAULTS.merge(overrides)
18+
end
19+
if keep_extensions_attributes
20+
extensions_attributes = overrides.delete(:extensions) || {}
21+
config[:extensions] = DEFAULTS[:extensions].merge(extensions_attributes)
2022
end
2123

22-
config.each do |k,v|
24+
config.each do |k, v|
2325
acc = "#{k.to_s}=".to_sym
2426
if respond_to? acc
2527
value = v.is_a?(Hash) ? v.dup : v
@@ -69,6 +71,8 @@ def initialize(overrides = {}, keep_security_attributes = false)
6971
attr_accessor :assertion_consumer_logout_service_url
7072
attr_accessor :assertion_consumer_logout_service_binding
7173
attr_accessor :issuer
74+
# EIDAS / samlp:Extensions
75+
attr_accessor :extensions
7276

7377
# @return [String] SP Entity ID
7478
#
@@ -164,7 +168,7 @@ def get_idp_cert_multi
164168

165169
raise ArgumentError.new("Invalid value for idp_cert_multi") if not idp_cert_multi.is_a?(Hash)
166170

167-
certs = {:signing => [], :encryption => [] }
171+
certs = {:signing => [], :encryption => []}
168172

169173
if idp_cert_multi.key?(:signing) and not idp_cert_multi[:signing].empty?
170174
idp_cert_multi[:signing].each do |idp_cert|
@@ -221,27 +225,31 @@ def get_sp_key
221225
private
222226

223227
DEFAULTS = {
224-
:assertion_consumer_service_binding => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST".freeze,
225-
:single_logout_service_binding => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect".freeze,
226-
:idp_cert_fingerprint_algorithm => XMLSecurity::Document::SHA1,
227-
:compress_request => true,
228-
:compress_response => true,
229-
:soft => true,
230-
:double_quote_xml_attribute_values => false,
231-
:security => {
232-
:authn_requests_signed => false,
233-
:logout_requests_signed => false,
234-
:logout_responses_signed => false,
235-
:want_assertions_signed => false,
236-
:want_assertions_encrypted => false,
237-
:want_name_id => false,
238-
:metadata_signed => false,
239-
:embed_sign => false,
240-
:digest_method => XMLSecurity::Document::SHA1,
241-
:signature_method => XMLSecurity::Document::RSA_SHA1,
242-
:check_idp_cert_expiration => false,
243-
:check_sp_cert_expiration => false
244-
}.freeze
228+
:assertion_consumer_service_binding => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST".freeze,
229+
:single_logout_service_binding => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect".freeze,
230+
:idp_cert_fingerprint_algorithm => XMLSecurity::Document::SHA1,
231+
:compress_request => true,
232+
:compress_response => true,
233+
:soft => true,
234+
:double_quote_xml_attribute_values => false,
235+
:extensions => {
236+
:sptype => false,
237+
:requested_attributes => false
238+
}.freeze,
239+
:security => {
240+
:authn_requests_signed => false,
241+
:logout_requests_signed => false,
242+
:logout_responses_signed => false,
243+
:want_assertions_signed => false,
244+
:want_assertions_encrypted => false,
245+
:want_name_id => false,
246+
:metadata_signed => false,
247+
:embed_sign => false,
248+
:digest_method => XMLSecurity::Document::SHA1,
249+
:signature_method => XMLSecurity::Document::RSA_SHA1,
250+
:check_idp_cert_expiration => false,
251+
:check_sp_cert_expiration => false
252+
}.freeze
245253
}.freeze
246254
end
247255
end
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<xsd:schema
3+
xmlns="http://eidas.europa.eu/saml-extensions"
4+
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
5+
targetNamespace="http://eidas.europa.eu/saml-extensions"
6+
elementFormDefault="qualified"
7+
attributeFormDefault="unqualified"
8+
xmlns:eidas="http://eidas.europa.eu/saml-extensions"
9+
version="1">
10+
11+
<xsd:element name="SPType" type="SPTypeType"/>
12+
13+
<xsd:simpleType name="SPTypeType">
14+
<xsd:restriction base="xsd:string">
15+
<xsd:enumeration value="public"/>
16+
<xsd:enumeration value="private"/>
17+
</xsd:restriction>
18+
</xsd:simpleType>
19+
<xsd:element name="RequestedAttributes" type="eidas:RequestedAttributesType" />
20+
<xsd:complexType name="RequestedAttributesType">
21+
<xsd:sequence>
22+
<xsd:element minOccurs="0" maxOccurs="unbounded" ref="eidas:RequestedAttribute"/>
23+
</xsd:sequence>
24+
</xsd:complexType>
25+
<xsd:element name="RequestedAttribute" type="eidas:RequestedAttributeType" />
26+
<xsd:complexType name="RequestedAttributeType">
27+
<xsd:sequence>
28+
<xsd:element minOccurs="0" maxOccurs="unbounded" ref="eidas:AttributeValue"/>
29+
</xsd:sequence>
30+
<xsd:attribute name="Name" use="required" type="xsd:string"/>
31+
<xsd:attribute name="NameFormat" use="required" type="xsd:anyURI"/>
32+
<xsd:attribute name="FriendlyName" use="optional" type="xsd:string"/>
33+
<xsd:attribute name="isRequired" use="optional" type="xsd:boolean"/>
34+
<xsd:anyAttribute namespace="##other" processContents="lax"/>
35+
</xsd:complexType>
36+
<xsd:element name="AttributeValue" type="xsd:anyType" />
37+
38+
</xsd:schema>

0 commit comments

Comments
 (0)