ruby-saml-bekk 0.2.4 → 0.3.1
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.rdoc +17 -0
- data/VERSION +1 -1
- data/lib/onelogin/saml.rb +3 -0
- data/lib/onelogin/saml/authrequest.rb +7 -6
- data/lib/onelogin/saml/codeing.rb +34 -0
- data/lib/onelogin/saml/entity_descriptor.rb +3 -0
- data/lib/onelogin/saml/entity_descriptor.xml.erb +6 -0
- data/lib/onelogin/saml/logoutrequest.rb +46 -0
- data/lib/onelogin/saml/logoutresponse.rb +33 -0
- data/lib/onelogin/saml/response.rb +2 -1
- data/lib/onelogin/saml/settings.rb +3 -0
- data/ruby-saml.gemspec +3 -3
- data/test/ruby-saml_test.rb +102 -3
- metadata +7 -4
data/README.rdoc
CHANGED
@@ -83,6 +83,23 @@ contains all the saml:AttributeStatement with its 'Name' as a indifferent key an
|
|
83
83
|
|
84
84
|
response.attributes[:username]
|
85
85
|
|
86
|
+
Logout is also supported via Onelogin::Saml::Logoutrequest and Onelogin::Saml::Logoutresponse
|
87
|
+
|
88
|
+
== EntityDescriptor generation
|
89
|
+
|
90
|
+
To install metadata about your SP(service provider) at the SAML IDP(identity provider) you
|
91
|
+
have to install a EntityDescriptor containing some metadata for your SP. To generate
|
92
|
+
thise files you can follow this example
|
93
|
+
|
94
|
+
config = {
|
95
|
+
entity_id => "https://someaddress.com/",
|
96
|
+
name_id_format => "urn:oasis:names:tc:SAML:2.0:nameid-format:transient
|
97
|
+
assertion_consumer_service_location => "https://someaddress/your/saml/consume/action"
|
98
|
+
}
|
99
|
+
entity_descriptor = Onelogin::Saml::EntityDescription.new
|
100
|
+
puts entity_descriptor.generate(config)
|
101
|
+
|
102
|
+
|
86
103
|
|
87
104
|
= Full Example
|
88
105
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.3.1
|
data/lib/onelogin/saml.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
require "base64"
|
2
2
|
require "uuid"
|
3
3
|
require "zlib"
|
4
|
-
|
4
|
+
|
5
5
|
|
6
6
|
module Onelogin::Saml
|
7
7
|
class Authrequest
|
8
|
+
include Codeing
|
8
9
|
def create(settings, params = {})
|
9
|
-
uuid = UUID.new.generate
|
10
|
+
uuid = "_".UUID.new.generate
|
10
11
|
time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
11
12
|
|
12
13
|
request =
|
@@ -17,13 +18,13 @@ module Onelogin::Saml
|
|
17
18
|
"<saml:AuthnContextClassRef xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></samlp:RequestedAuthnContext>\n" +
|
18
19
|
"</samlp:AuthnRequest>"
|
19
20
|
|
20
|
-
deflated_request =
|
21
|
-
base64_request =
|
22
|
-
encoded_request =
|
21
|
+
deflated_request = deflate(request)
|
22
|
+
base64_request = encode(deflated_request)
|
23
|
+
encoded_request = escape(base64_request)
|
23
24
|
request_params = "?SAMLRequest=" + encoded_request
|
24
25
|
|
25
26
|
params.each_pair do |key, value|
|
26
|
-
request_params << "&#{key}=#{
|
27
|
+
request_params << "&#{key}=#{escape(value.to_s)}"
|
27
28
|
end
|
28
29
|
|
29
30
|
settings.idp_sso_target_url + request_params
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "cgi"
|
2
|
+
require 'zlib'
|
3
|
+
|
4
|
+
module Onelogin
|
5
|
+
module Saml
|
6
|
+
module Codeing
|
7
|
+
def decode(encoded)
|
8
|
+
Base64.decode64(encoded)
|
9
|
+
end
|
10
|
+
|
11
|
+
def encode(encoded)
|
12
|
+
Base64.encode64(encoded)
|
13
|
+
end
|
14
|
+
|
15
|
+
def escape(unescaped)
|
16
|
+
CGI.escape(unescaped)
|
17
|
+
end
|
18
|
+
|
19
|
+
def unescape(escaped)
|
20
|
+
CGI.unescape(escaped)
|
21
|
+
end
|
22
|
+
|
23
|
+
def inflate(deflated)
|
24
|
+
zlib = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
25
|
+
zlib.inflate(deflated)
|
26
|
+
end
|
27
|
+
|
28
|
+
def deflate(inflated)
|
29
|
+
Zlib::Deflate.deflate(inflated, 9)[2..-5]
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -12,6 +12,9 @@ module Onelogin::Saml
|
|
12
12
|
name_id_format = values["name_id_format"]
|
13
13
|
assertion_consumer_service_location = values["assertion_consumer_service_location"]
|
14
14
|
|
15
|
+
single_logout_service_location = values["single_logout_service_location"]
|
16
|
+
single_logout_service_response_location = values["single_logout_service_response_location"]
|
17
|
+
|
15
18
|
erb = ERB.new(@template)
|
16
19
|
erb.result(binding)
|
17
20
|
end
|
@@ -2,6 +2,12 @@
|
|
2
2
|
|
3
3
|
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="<%= entity_id %>">
|
4
4
|
<SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
5
|
+
<% if single_logout_service_location && single_logout_service_response_location %>
|
6
|
+
<SingleLogoutService
|
7
|
+
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
8
|
+
Location="<%= single_logout_service_location %>"
|
9
|
+
ResponseLocation="<%= single_logout_service_response_location %>"/>
|
10
|
+
<% end %>
|
5
11
|
<NameIDFormat><%= name_id_format %></NameIDFormat>
|
6
12
|
<AssertionConsumerService
|
7
13
|
isDefault="true"
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'uuid'
|
3
|
+
require 'cgi'
|
4
|
+
|
5
|
+
module Onelogin::Saml
|
6
|
+
class Logoutrequest
|
7
|
+
attr_reader :transaction_id
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@transaction_id = UUID.new.generate
|
11
|
+
end
|
12
|
+
|
13
|
+
def create(settings,nameid,params={})
|
14
|
+
issue_instant = Onelogin::Saml::Logoutrequest.timestamp
|
15
|
+
|
16
|
+
request = xml(settings, nameid, issue_instant)
|
17
|
+
|
18
|
+
deflated_request = Zlib::Deflate.deflate(request, 9)[2..-5]
|
19
|
+
base64_request = Base64.encode64(deflated_request)
|
20
|
+
params["SAMLRequest"] = base64_request
|
21
|
+
query_string = params.map {|key, value| "#{key}=#{CGI.escape(value)}"}.join("&")
|
22
|
+
|
23
|
+
settings.idp_slo_target_url + "?#{query_string}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def xml(settings, nameid, issue_instant)
|
27
|
+
request = <<-EOF
|
28
|
+
<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
|
29
|
+
ID="#{transaction_id}" Version="2.0" IssueInstant="#{issue_instant}">
|
30
|
+
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">#{settings.issuer}</saml:Issuer>
|
31
|
+
<saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
|
32
|
+
NameQualifier="#{settings.sp_name_qualifier}"
|
33
|
+
Format="#{settings.name_identifier_format}">#{nameid}</saml:NameID>
|
34
|
+
</samlp:LogoutRequest>
|
35
|
+
EOF
|
36
|
+
|
37
|
+
request
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def self.timestamp
|
43
|
+
Time.new().strftime("%Y-%m-%dT%H:%M:%SZ")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
|
3
|
+
require "rexml/document"
|
4
|
+
|
5
|
+
module Onelogin
|
6
|
+
module Saml
|
7
|
+
class Logoutresponse
|
8
|
+
include Codeing
|
9
|
+
|
10
|
+
def initialize(response)
|
11
|
+
begin
|
12
|
+
@response = decode(response)
|
13
|
+
document
|
14
|
+
rescue
|
15
|
+
@response = inflate(decode(response))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def issuer
|
20
|
+
document.elements["/samlp:LogoutResponse/saml:Issuer"].text
|
21
|
+
end
|
22
|
+
|
23
|
+
def in_response_to
|
24
|
+
document.elements["/samlp:LogoutResponse"].attributes["InResponseTo"]
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
def document
|
29
|
+
REXML::Document.new(@response)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -3,12 +3,13 @@ require "time"
|
|
3
3
|
|
4
4
|
module Onelogin::Saml
|
5
5
|
class Response
|
6
|
+
include Codeing
|
6
7
|
attr_accessor :response, :document, :logger, :settings
|
7
8
|
|
8
9
|
def initialize(response)
|
9
10
|
raise ArgumentError.new("Response cannot be nil") if response.nil?
|
10
11
|
self.response = response
|
11
|
-
self.document = XMLSecurity::SignedDocument.new(
|
12
|
+
self.document = XMLSecurity::SignedDocument.new(decode(response))
|
12
13
|
end
|
13
14
|
|
14
15
|
def is_valid?
|
@@ -2,5 +2,8 @@ module Onelogin::Saml
|
|
2
2
|
class Settings
|
3
3
|
attr_accessor :assertion_consumer_service_url, :issuer, :sp_name_qualifier
|
4
4
|
attr_accessor :idp_sso_target_url, :idp_cert_fingerprint, :name_identifier_format
|
5
|
+
attr_accessor :idp_slo_target_url
|
6
|
+
|
7
|
+
SLO_ELEMENT_PATH = "/EntityDescriptor/IDPSSODescriptor/SingleLogoutService[@Binding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect']"
|
5
8
|
end
|
6
9
|
end
|
data/ruby-saml.gemspec
CHANGED
@@ -4,12 +4,12 @@
|
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
|
-
s.name = %q{ruby-saml
|
8
|
-
s.version = "0.
|
7
|
+
s.name = %q{ruby-saml}
|
8
|
+
s.version = "0.3.1"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["OneLogin LLC"]
|
12
|
-
s.date = %q{2011-
|
12
|
+
s.date = %q{2011-03-07}
|
13
13
|
s.description = %q{SAML toolkit for Ruby on Rails}
|
14
14
|
s.email = %q{support@onelogin.com}
|
15
15
|
s.extra_rdoc_files = [
|
data/test/ruby-saml_test.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
|
+
#encoding: utf-8
|
1
2
|
require 'test_helper'
|
2
3
|
|
3
4
|
class RubySamlTest < Test::Unit::TestCase
|
5
|
+
include Onelogin::Saml::Codeing
|
6
|
+
|
4
7
|
|
5
8
|
context "Settings" do
|
6
9
|
setup do
|
@@ -108,6 +111,75 @@ class RubySamlTest < Test::Unit::TestCase
|
|
108
111
|
end
|
109
112
|
end
|
110
113
|
|
114
|
+
context "Logoutrequest" do
|
115
|
+
|
116
|
+
setup do
|
117
|
+
UUID.expects(:new).returns(stub(:generate => "da64beb0-2ac4-012e-a9c9-48bcc8e9f44d"))
|
118
|
+
@settings = Onelogin::Saml::Settings.new
|
119
|
+
@settings.issuer = "issuer"
|
120
|
+
@settings.sp_name_qualifier ="sp name"
|
121
|
+
@settings.name_identifier_format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
|
122
|
+
@settings.idp_slo_target_url ="http://slotarget.com/"
|
123
|
+
|
124
|
+
Onelogin::Saml::Logoutrequest.stubs(:timestamp).returns("timestamb")
|
125
|
+
end
|
126
|
+
|
127
|
+
should "generate a correct logout request" do
|
128
|
+
logoutrequest = Onelogin::Saml::Logoutrequest.new
|
129
|
+
|
130
|
+
logout_xml = logoutrequest.xml(@settings, "demo", "test")
|
131
|
+
|
132
|
+
expected_xml = <<-EOF
|
133
|
+
<samlp:LogoutRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\"
|
134
|
+
ID=\"da64beb0-2ac4-012e-a9c9-48bcc8e9f44d\" Version=\"2.0\" IssueInstant=\"test\">
|
135
|
+
<saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">issuer</saml:Issuer>
|
136
|
+
<saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\"
|
137
|
+
NameQualifier=\"sp name\"
|
138
|
+
Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:transient\">demo</saml:NameID>
|
139
|
+
</samlp:LogoutRequest>
|
140
|
+
EOF
|
141
|
+
|
142
|
+
assert_equal expected_xml.strip, logout_xml.strip
|
143
|
+
end
|
144
|
+
|
145
|
+
|
146
|
+
should "generate a correct request" do
|
147
|
+
logoutrequest = Onelogin::Saml::Logoutrequest.new
|
148
|
+
logout_url = logoutrequest.create(@settings, "demo")
|
149
|
+
|
150
|
+
expected_url = "http://slotarget.com/?SAMLRequest=nZFbS8QwEIXf91eEea%2FWUmQbtgVhEQqroILv03S6BJqkZqbgz7cX0fW2D57H%0Aycl35jBKrdoxun7Qh3AMozzSy0gs6tX1nvXyUsIYvQ7IlrVHR6zF6Kebu4PO%0ALlI9xCDBhB426kT1voQWr%2FOGmjTJ0ORJepVRgoUpknzbGLOlosvzFtQzRbbB%0AlzDBQNXMI9WeBb2UIHZKE3QNVF%2FoH1vrxR9Ptj2%2FLDJTlCkOKrv83F2eYP4K%0AuZ849f4fIT94s2baw4i97SzFEnhQM%2BJ3722IDuV83DyxbdItVi0RPVvyAlVL%0ALrz3Wxt89lvH325ebd4A%0A"
|
151
|
+
assert_equal expected_url, logout_url
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
|
156
|
+
context "Logoutresponse" do
|
157
|
+
should "validate the response" do
|
158
|
+
|
159
|
+
issuer = "https://test-idp.test.no:443/issuer"
|
160
|
+
transaction_id = "adflkjalkfjalsdfjlaskjdf"
|
161
|
+
params = {}
|
162
|
+
logout_xml = "<samlp:LogoutResponse xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\"
|
163
|
+
ID=\"sedf164edc2121a23d4ca4b31d35261e5563dc352\" Version=\"2.0\"
|
164
|
+
IssueInstant=\"2011-03-07T14:43:34Z\" Destination=\"http://localhost:3000/saml/consume_logout\"
|
165
|
+
InResponseTo=\"#{transaction_id}\">
|
166
|
+
<saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">#{issuer}</saml:Issuer>
|
167
|
+
<samlp:Status xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\">
|
168
|
+
<samlp:StatusCode xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\"
|
169
|
+
Value=\"urn:oasis:names:tc:SAML:2.0:status:Requester\">
|
170
|
+
</samlp:StatusCode>
|
171
|
+
</samlp:Status>
|
172
|
+
</samlp:LogoutResponse>"
|
173
|
+
|
174
|
+
|
175
|
+
params["SAMLResponse"] = encode(deflate(logout_xml))
|
176
|
+
logoutresponse = Onelogin::Saml::Logoutresponse.new(params["SAMLResponse"])
|
177
|
+
|
178
|
+
assert_equal logoutresponse.issuer, issuer
|
179
|
+
assert_equal logoutresponse.in_response_to, transaction_id
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
111
183
|
context "EntityDescription" do
|
112
184
|
should "generate a correct entity descriptor" do
|
113
185
|
descriptor = Onelogin::Saml::EntityDescription.new
|
@@ -117,10 +189,11 @@ class RubySamlTest < Test::Unit::TestCase
|
|
117
189
|
"assertion_consumer_service_location" => "http://localhost:3000/saml/consume"
|
118
190
|
})
|
119
191
|
|
120
|
-
|
192
|
+
expected_xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>
|
121
193
|
|
122
194
|
<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" entityID=\"http://test.no/\">
|
123
195
|
<SPSSODescriptor AuthnRequestsSigned=\"false\" WantAssertionsSigned=\"true\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">
|
196
|
+
|
124
197
|
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
|
125
198
|
<AssertionConsumerService
|
126
199
|
isDefault=\"true\"
|
@@ -128,9 +201,35 @@ class RubySamlTest < Test::Unit::TestCase
|
|
128
201
|
Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"
|
129
202
|
Location=\"http://localhost:3000/saml/consume\"/>
|
130
203
|
</SPSSODescriptor>
|
131
|
-
<RoleDescriptor xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:query=\"urn:oasis:names:tc:SAML:metadata:ext:query\" xsi:type=\"query:AttributeQueryDescriptorType\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\"/>
|
132
|
-
<XACMLAuthzDecisionQueryDescriptor WantAssertionsSigned=\"false\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\"/>
|
204
|
+
<RoleDescriptor xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:query=\"urn:oasis:names:tc:SAML:metadata:ext:query\" xsi:type=\"query:AttributeQueryDescriptorType\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\"/>\n <XACMLAuthzDecisionQueryDescriptor WantAssertionsSigned=\"false\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\"/>
|
133
205
|
</EntityDescriptor>"
|
206
|
+
|
207
|
+
assert_equal expected_xml.gsub(" ", ""), xml.gsub(" ", "")
|
208
|
+
end
|
209
|
+
|
210
|
+
context "with slo" do
|
211
|
+
should "generate correct xml part" do
|
212
|
+
descriptor = Onelogin::Saml::EntityDescription.new
|
213
|
+
xml = descriptor.generate({
|
214
|
+
"entity_id" => "http://test.no/",
|
215
|
+
"name_id_format" => "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
|
216
|
+
"assertion_consumer_service_location" => "http://localhost:3000/saml/consume",
|
217
|
+
|
218
|
+
"single_logout_service_location" => "slo_location",
|
219
|
+
"single_logout_service_response_location" => "response_location"
|
220
|
+
})
|
221
|
+
|
222
|
+
expected_xml = " <SingleLogoutService
|
223
|
+
Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"
|
224
|
+
Location=\"slo_location\"
|
225
|
+
ResponseLocation=\"response_location\"/>"
|
226
|
+
|
227
|
+
assert xml.include?(expected_xml), "Xml does not include\nfull:#{xml}\n\nincluded: #{expected_xml}"
|
228
|
+
end
|
134
229
|
end
|
135
230
|
end
|
231
|
+
|
232
|
+
|
233
|
+
|
234
|
+
|
136
235
|
end
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
7
|
+
- 3
|
8
|
+
- 1
|
9
|
+
version: 0.3.1
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- OneLogin LLC
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2011-03-
|
17
|
+
date: 2011-03-07 00:00:00 +01:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -90,8 +90,11 @@ files:
|
|
90
90
|
- VERSION
|
91
91
|
- lib/onelogin/saml.rb
|
92
92
|
- lib/onelogin/saml/authrequest.rb
|
93
|
+
- lib/onelogin/saml/codeing.rb
|
93
94
|
- lib/onelogin/saml/entity_descriptor.rb
|
94
95
|
- lib/onelogin/saml/entity_descriptor.xml.erb
|
96
|
+
- lib/onelogin/saml/logoutrequest.rb
|
97
|
+
- lib/onelogin/saml/logoutresponse.rb
|
95
98
|
- lib/onelogin/saml/response.rb
|
96
99
|
- lib/onelogin/saml/settings.rb
|
97
100
|
- lib/ruby-saml.rb
|