saml-kit 0.2.1 → 0.2.2
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 +5 -5
- data/README.md +1 -1
- data/exe/saml-kit-create-self-signed-certificate +21 -0
- data/lib/saml/kit.rb +9 -1
- data/lib/saml/kit/authentication_request.rb +9 -47
- data/lib/saml/kit/bindings/http_post.rb +0 -1
- data/lib/saml/kit/buildable.rb +21 -0
- data/lib/saml/kit/builders.rb +13 -0
- data/lib/saml/kit/builders/authentication_request.rb +48 -0
- data/lib/saml/kit/builders/identity_provider_metadata.rb +103 -0
- data/lib/saml/kit/builders/logout_request.rb +55 -0
- data/lib/saml/kit/builders/logout_response.rb +50 -0
- data/lib/saml/kit/builders/response.rb +182 -0
- data/lib/saml/kit/builders/service_provider_metadata.rb +89 -0
- data/lib/saml/kit/document.rb +16 -0
- data/lib/saml/kit/id.rb +9 -0
- data/lib/saml/kit/identity_provider_metadata.rb +11 -96
- data/lib/saml/kit/logout_request.rb +7 -53
- data/lib/saml/kit/logout_response.rb +1 -50
- data/lib/saml/kit/metadata.rb +9 -0
- data/lib/saml/kit/response.rb +11 -181
- data/lib/saml/kit/self_signed_certificate.rb +12 -5
- data/lib/saml/kit/service_provider_metadata.rb +5 -82
- data/lib/saml/kit/signature.rb +1 -1
- data/lib/saml/kit/trustable.rb +1 -5
- data/lib/saml/kit/version.rb +1 -1
- data/lib/saml/kit/{cryptography.rb → xml_decryption.rb} +1 -1
- data/saml-kit.gemspec +2 -2
- metadata +32 -21
@@ -0,0 +1,182 @@
|
|
1
|
+
module Saml
|
2
|
+
module Kit
|
3
|
+
module Builders
|
4
|
+
class Response
|
5
|
+
attr_reader :user, :request
|
6
|
+
attr_accessor :id, :reference_id, :now
|
7
|
+
attr_accessor :version, :status_code
|
8
|
+
attr_accessor :issuer, :sign, :destination, :encrypt
|
9
|
+
|
10
|
+
def initialize(user, request)
|
11
|
+
@user = user
|
12
|
+
@request = request
|
13
|
+
@id = Id.generate
|
14
|
+
@reference_id = Id.generate
|
15
|
+
@now = Time.now.utc
|
16
|
+
@version = "2.0"
|
17
|
+
@status_code = Namespaces::SUCCESS
|
18
|
+
@issuer = configuration.issuer
|
19
|
+
@destination = destination_for(request)
|
20
|
+
@sign = want_assertions_signed
|
21
|
+
@encrypt = false
|
22
|
+
end
|
23
|
+
|
24
|
+
def want_assertions_signed
|
25
|
+
request.provider.want_assertions_signed
|
26
|
+
rescue => error
|
27
|
+
Saml::Kit.logger.error(error)
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_xml
|
32
|
+
Signature.sign(sign: sign) do |xml, signature|
|
33
|
+
xml.Response response_options do
|
34
|
+
xml.Issuer(issuer, xmlns: Namespaces::ASSERTION)
|
35
|
+
signature.template(id)
|
36
|
+
xml.Status do
|
37
|
+
xml.StatusCode Value: status_code
|
38
|
+
end
|
39
|
+
assertion(xml, signature)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def build
|
45
|
+
Saml::Kit::Response.new(to_xml, request_id: request.id)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def assertion(xml, signature)
|
51
|
+
with_encryption(xml) do |xml|
|
52
|
+
xml.Assertion(assertion_options) do
|
53
|
+
xml.Issuer issuer
|
54
|
+
signature.template(reference_id) unless encrypt
|
55
|
+
xml.Subject do
|
56
|
+
xml.NameID user.name_id_for(request.name_id_format), Format: request.name_id_format
|
57
|
+
xml.SubjectConfirmation Method: Namespaces::BEARER do
|
58
|
+
xml.SubjectConfirmationData "", subject_confirmation_data_options
|
59
|
+
end
|
60
|
+
end
|
61
|
+
xml.Conditions conditions_options do
|
62
|
+
xml.AudienceRestriction do
|
63
|
+
xml.Audience request.issuer
|
64
|
+
end
|
65
|
+
end
|
66
|
+
xml.AuthnStatement authn_statement_options do
|
67
|
+
xml.AuthnContext do
|
68
|
+
xml.AuthnContextClassRef Namespaces::PASSWORD
|
69
|
+
end
|
70
|
+
end
|
71
|
+
assertion_attributes = user.assertion_attributes_for(request)
|
72
|
+
if assertion_attributes.any?
|
73
|
+
xml.AttributeStatement do
|
74
|
+
assertion_attributes.each do |key, value|
|
75
|
+
xml.Attribute Name: key, NameFormat: Namespaces::URI, FriendlyName: key do
|
76
|
+
xml.AttributeValue value.to_s
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def with_encryption(xml)
|
86
|
+
if encrypt
|
87
|
+
temp = ::Builder::XmlMarkup.new
|
88
|
+
yield temp
|
89
|
+
raw_xml_to_encrypt = temp.target!
|
90
|
+
|
91
|
+
encryption_certificate = request.provider.encryption_certificates.first
|
92
|
+
public_key = encryption_certificate.public_key
|
93
|
+
|
94
|
+
cipher = OpenSSL::Cipher.new('AES-256-CBC')
|
95
|
+
cipher.encrypt
|
96
|
+
key = cipher.random_key
|
97
|
+
iv = cipher.random_iv
|
98
|
+
encrypted = cipher.update(raw_xml_to_encrypt) + cipher.final
|
99
|
+
|
100
|
+
Saml::Kit.logger.debug ['+iv', iv].inspect
|
101
|
+
Saml::Kit.logger.debug ['+key', key].inspect
|
102
|
+
|
103
|
+
xml.EncryptedAssertion xmlns: Namespaces::ASSERTION do
|
104
|
+
xml.EncryptedData xmlns: Namespaces::XMLENC do
|
105
|
+
xml.EncryptionMethod Algorithm: "http://www.w3.org/2001/04/xmlenc#aes256-cbc"
|
106
|
+
xml.KeyInfo xmlns: Namespaces::XMLDSIG do
|
107
|
+
xml.EncryptedKey xmlns: Namespaces::XMLENC do
|
108
|
+
xml.EncryptionMethod Algorithm: "http://www.w3.org/2001/04/xmlenc#rsa-1_5"
|
109
|
+
xml.CipherData do
|
110
|
+
xml.CipherValue Base64.encode64(public_key.public_encrypt(key))
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
xml.CipherData do
|
115
|
+
xml.CipherValue Base64.encode64(iv + encrypted)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
else
|
120
|
+
yield xml
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def destination_for(request)
|
125
|
+
if request.signed? && request.trusted?
|
126
|
+
request.assertion_consumer_service_url || request.provider.assertion_consumer_service_for(binding: :http_post).try(:location)
|
127
|
+
else
|
128
|
+
request.provider.assertion_consumer_service_for(binding: :http_post).try(:location)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def configuration
|
133
|
+
Saml::Kit.configuration
|
134
|
+
end
|
135
|
+
|
136
|
+
def response_options
|
137
|
+
{
|
138
|
+
ID: id,
|
139
|
+
Version: version,
|
140
|
+
IssueInstant: now.iso8601,
|
141
|
+
Destination: destination,
|
142
|
+
Consent: Namespaces::UNSPECIFIED,
|
143
|
+
InResponseTo: request.id,
|
144
|
+
xmlns: Namespaces::PROTOCOL,
|
145
|
+
}
|
146
|
+
end
|
147
|
+
|
148
|
+
def assertion_options
|
149
|
+
{
|
150
|
+
ID: reference_id,
|
151
|
+
IssueInstant: now.iso8601,
|
152
|
+
Version: "2.0",
|
153
|
+
xmlns: Namespaces::ASSERTION,
|
154
|
+
}
|
155
|
+
end
|
156
|
+
|
157
|
+
def subject_confirmation_data_options
|
158
|
+
{
|
159
|
+
InResponseTo: request.id,
|
160
|
+
NotOnOrAfter: 3.hours.since(now).utc.iso8601,
|
161
|
+
Recipient: request.assertion_consumer_service_url,
|
162
|
+
}
|
163
|
+
end
|
164
|
+
|
165
|
+
def conditions_options
|
166
|
+
{
|
167
|
+
NotBefore: now.utc.iso8601,
|
168
|
+
NotOnOrAfter: Saml::Kit.configuration.session_timeout.from_now.utc.iso8601,
|
169
|
+
}
|
170
|
+
end
|
171
|
+
|
172
|
+
def authn_statement_options
|
173
|
+
{
|
174
|
+
AuthnInstant: now.iso8601,
|
175
|
+
SessionIndex: assertion_options[:ID],
|
176
|
+
SessionNotOnOrAfter: 3.hours.since(now).utc.iso8601,
|
177
|
+
}
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Saml
|
2
|
+
module Kit
|
3
|
+
module Builders
|
4
|
+
class ServiceProviderMetadata
|
5
|
+
attr_accessor :id, :entity_id, :acs_urls, :logout_urls, :name_id_formats, :sign
|
6
|
+
attr_accessor :want_assertions_signed
|
7
|
+
|
8
|
+
def initialize(configuration = Saml::Kit.configuration)
|
9
|
+
@id = Id.generate
|
10
|
+
@configuration = configuration
|
11
|
+
@entity_id = configuration.issuer
|
12
|
+
@acs_urls = []
|
13
|
+
@logout_urls = []
|
14
|
+
@name_id_formats = [Namespaces::PERSISTENT]
|
15
|
+
@sign = true
|
16
|
+
@want_assertions_signed = true
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_assertion_consumer_service(url, binding: :http_post)
|
20
|
+
@acs_urls.push(location: url, binding: Bindings.binding_for(binding))
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_single_logout_service(url, binding: :http_post)
|
24
|
+
@logout_urls.push(location: url, binding: Bindings.binding_for(binding))
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_xml
|
28
|
+
Signature.sign(sign: sign) do |xml, signature|
|
29
|
+
xml.instruct!
|
30
|
+
xml.EntityDescriptor entity_descriptor_options do
|
31
|
+
signature.template(id)
|
32
|
+
xml.SPSSODescriptor descriptor_options do
|
33
|
+
if @configuration.signing_certificate_pem.present?
|
34
|
+
xml.KeyDescriptor use: "signing" do
|
35
|
+
xml.KeyInfo "xmlns": Namespaces::XMLDSIG do
|
36
|
+
xml.X509Data do
|
37
|
+
xml.X509Certificate @configuration.stripped_signing_certificate
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
if @configuration.encryption_certificate_pem.present?
|
43
|
+
xml.KeyDescriptor use: "encryption" do
|
44
|
+
xml.KeyInfo "xmlns": Namespaces::XMLDSIG do
|
45
|
+
xml.X509Data do
|
46
|
+
xml.X509Certificate @configuration.stripped_encryption_certificate
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
logout_urls.each do |item|
|
52
|
+
xml.SingleLogoutService Binding: item[:binding], Location: item[:location]
|
53
|
+
end
|
54
|
+
name_id_formats.each do |format|
|
55
|
+
xml.NameIDFormat format
|
56
|
+
end
|
57
|
+
acs_urls.each_with_index do |item, index|
|
58
|
+
xml.AssertionConsumerService Binding: item[:binding], Location: item[:location], index: index, isDefault: index == 0 ? true : false
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def build
|
66
|
+
Saml::Kit::ServiceProviderMetadata.new(to_xml)
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def entity_descriptor_options
|
72
|
+
{
|
73
|
+
'xmlns': Namespaces::METADATA,
|
74
|
+
ID: id,
|
75
|
+
entityID: entity_id,
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
def descriptor_options
|
80
|
+
{
|
81
|
+
AuthnRequestsSigned: sign,
|
82
|
+
WantAssertionsSigned: want_assertions_signed,
|
83
|
+
protocolSupportEnumeration: Namespaces::PROTOCOL,
|
84
|
+
}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/lib/saml/kit/document.rb
CHANGED
@@ -5,6 +5,7 @@ module Saml
|
|
5
5
|
include XsdValidatable
|
6
6
|
include ActiveModel::Validations
|
7
7
|
include Trustable
|
8
|
+
include Buildable
|
8
9
|
validates_presence_of :content
|
9
10
|
validates_presence_of :id
|
10
11
|
validate :must_match_xsd
|
@@ -76,6 +77,21 @@ module Saml
|
|
76
77
|
Saml::Kit.logger.error(error)
|
77
78
|
InvalidDocument.new(xml)
|
78
79
|
end
|
80
|
+
|
81
|
+
def builder_class
|
82
|
+
case name
|
83
|
+
when Saml::Kit::Response.to_s
|
84
|
+
Saml::Kit::Builders::Response
|
85
|
+
when Saml::Kit::LogoutResponse.to_s
|
86
|
+
Saml::Kit::Builders::LogoutResponse
|
87
|
+
when Saml::Kit::AuthenticationRequest.to_s
|
88
|
+
Saml::Kit::Builders::AuthenticationRequest
|
89
|
+
when Saml::Kit::LogoutRequest.to_s
|
90
|
+
Saml::Kit::Builders::LogoutRequest
|
91
|
+
else
|
92
|
+
raise ArgumentError.new("Unknown SAML Document #{name}")
|
93
|
+
end
|
94
|
+
end
|
79
95
|
end
|
80
96
|
|
81
97
|
private
|
data/lib/saml/kit/id.rb
ADDED
@@ -29,105 +29,20 @@ module Saml
|
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
attr_accessor :want_authn_requests_signed, :sign
|
37
|
-
attr_reader :logout_urls, :single_sign_on_urls
|
38
|
-
|
39
|
-
def initialize(configuration = Saml::Kit.configuration)
|
40
|
-
@id = SecureRandom.uuid
|
41
|
-
@entity_id = configuration.issuer
|
42
|
-
@attributes = []
|
43
|
-
@name_id_formats = [Namespaces::PERSISTENT]
|
44
|
-
@single_sign_on_urls = []
|
45
|
-
@logout_urls = []
|
46
|
-
@configuration = configuration
|
47
|
-
@sign = true
|
48
|
-
@want_authn_requests_signed = true
|
49
|
-
end
|
50
|
-
|
51
|
-
def add_single_sign_on_service(url, binding: :http_post)
|
52
|
-
@single_sign_on_urls.push(location: url, binding: Bindings.binding_for(binding))
|
53
|
-
end
|
54
|
-
|
55
|
-
def add_single_logout_service(url, binding: :http_post)
|
56
|
-
@logout_urls.push(location: url, binding: Bindings.binding_for(binding))
|
57
|
-
end
|
58
|
-
|
59
|
-
def to_xml
|
60
|
-
Signature.sign(sign: sign) do |xml, signature|
|
61
|
-
xml.instruct!
|
62
|
-
xml.EntityDescriptor entity_descriptor_options do
|
63
|
-
signature.template(id)
|
64
|
-
xml.IDPSSODescriptor idp_sso_descriptor_options do
|
65
|
-
if @configuration.signing_certificate_pem.present?
|
66
|
-
xml.KeyDescriptor use: "signing" do
|
67
|
-
xml.KeyInfo "xmlns": Namespaces::XMLDSIG do
|
68
|
-
xml.X509Data do
|
69
|
-
xml.X509Certificate @configuration.stripped_signing_certificate
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
74
|
-
if @configuration.encryption_certificate_pem.present?
|
75
|
-
xml.KeyDescriptor use: "encryption" do
|
76
|
-
xml.KeyInfo "xmlns": Namespaces::XMLDSIG do
|
77
|
-
xml.X509Data do
|
78
|
-
xml.X509Certificate @configuration.stripped_encryption_certificate
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
|
-
logout_urls.each do |item|
|
84
|
-
xml.SingleLogoutService Binding: item[:binding], Location: item[:location]
|
85
|
-
end
|
86
|
-
name_id_formats.each do |format|
|
87
|
-
xml.NameIDFormat format
|
88
|
-
end
|
89
|
-
single_sign_on_urls.each do |item|
|
90
|
-
xml.SingleSignOnService Binding: item[:binding], Location: item[:location]
|
91
|
-
end
|
92
|
-
attributes.each do |attribute|
|
93
|
-
xml.tag! 'saml:Attribute', Name: attribute
|
94
|
-
end
|
95
|
-
end
|
96
|
-
xml.Organization do
|
97
|
-
xml.OrganizationName organization_name, 'xml:lang': "en"
|
98
|
-
xml.OrganizationDisplayName organization_name, 'xml:lang': "en"
|
99
|
-
xml.OrganizationURL organization_url, 'xml:lang': "en"
|
100
|
-
end
|
101
|
-
xml.ContactPerson contactType: "technical" do
|
102
|
-
xml.Company "mailto:#{contact_email}"
|
103
|
-
end
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
def build
|
109
|
-
IdentityProviderMetadata.new(to_xml)
|
110
|
-
end
|
111
|
-
|
112
|
-
private
|
113
|
-
|
114
|
-
def entity_descriptor_options
|
115
|
-
{
|
116
|
-
'xmlns': Namespaces::METADATA,
|
117
|
-
'xmlns:ds': Namespaces::XMLDSIG,
|
118
|
-
'xmlns:saml': Namespaces::ASSERTION,
|
119
|
-
ID: "_#{id}",
|
120
|
-
entityID: entity_id,
|
121
|
-
}
|
32
|
+
def login_request_for(binding:, relay_state: nil)
|
33
|
+
builder = Saml::Kit::AuthenticationRequest.builder do |x|
|
34
|
+
x.sign = want_authn_requests_signed
|
35
|
+
yield x if block_given?
|
122
36
|
end
|
37
|
+
request_binding = single_sign_on_service_for(binding: binding)
|
38
|
+
request_binding.serialize(builder, relay_state: relay_state)
|
39
|
+
end
|
123
40
|
|
124
|
-
|
125
|
-
|
126
|
-
WantAuthnRequestsSigned: want_authn_requests_signed,
|
127
|
-
protocolSupportEnumeration: Namespaces::PROTOCOL,
|
128
|
-
}
|
129
|
-
end
|
41
|
+
def self.builder_class
|
42
|
+
Saml::Kit::Builders::IdentityProviderMetadata
|
130
43
|
end
|
44
|
+
|
45
|
+
Builder = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Saml::Kit::IdentityProviderMetadata::Builder', 'Saml::Kit::Builders::IdentityProviderMetadata')
|
131
46
|
end
|
132
47
|
end
|
133
48
|
end
|
@@ -18,61 +18,15 @@ module Saml
|
|
18
18
|
urls.first
|
19
19
|
end
|
20
20
|
|
21
|
-
def response_for(user)
|
22
|
-
LogoutResponse
|
23
|
-
|
24
|
-
|
25
|
-
private
|
26
|
-
|
27
|
-
class Builder
|
28
|
-
attr_accessor :id, :destination, :issuer, :name_id_format, :now
|
29
|
-
attr_accessor :sign, :version
|
30
|
-
attr_reader :user
|
31
|
-
|
32
|
-
def initialize(user, configuration: Saml::Kit.configuration, sign: true)
|
33
|
-
@user = user
|
34
|
-
@id = SecureRandom.uuid
|
35
|
-
@issuer = configuration.issuer
|
36
|
-
@name_id_format = Saml::Kit::Namespaces::PERSISTENT
|
37
|
-
@now = Time.now.utc
|
38
|
-
@version = "2.0"
|
39
|
-
@sign = sign
|
40
|
-
end
|
41
|
-
|
42
|
-
def to_xml
|
43
|
-
Signature.sign(sign: sign) do |xml, signature|
|
44
|
-
xml.instruct!
|
45
|
-
xml.LogoutRequest logout_request_options do
|
46
|
-
xml.Issuer({ xmlns: Namespaces::ASSERTION }, issuer)
|
47
|
-
signature.template(id)
|
48
|
-
xml.NameID name_id_options, user.name_id_for(name_id_format)
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
def build
|
54
|
-
Saml::Kit::LogoutRequest.new(to_xml)
|
55
|
-
end
|
56
|
-
|
57
|
-
private
|
58
|
-
|
59
|
-
def logout_request_options
|
60
|
-
{
|
61
|
-
ID: "_#{id}",
|
62
|
-
Version: version,
|
63
|
-
IssueInstant: now.utc.iso8601,
|
64
|
-
Destination: destination,
|
65
|
-
xmlns: Namespaces::PROTOCOL,
|
66
|
-
}
|
67
|
-
end
|
68
|
-
|
69
|
-
def name_id_options
|
70
|
-
{
|
71
|
-
Format: name_id_format,
|
72
|
-
xmlns: Namespaces::ASSERTION,
|
73
|
-
}
|
21
|
+
def response_for(user, binding:, relay_state: nil)
|
22
|
+
builder = Saml::Kit::LogoutResponse.builder(user, self) do |x|
|
23
|
+
yield x if block_given?
|
74
24
|
end
|
25
|
+
response_binding = provider.single_logout_service_for(binding: binding)
|
26
|
+
response_binding.serialize(builder, relay_state: relay_state)
|
75
27
|
end
|
28
|
+
|
29
|
+
Builder = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Saml::Kit::LogoutRequest::Builder', 'Saml::Kit::Builders::LogoutRequest')
|
76
30
|
end
|
77
31
|
end
|
78
32
|
end
|