ruby-saml-bekk 0.2.4 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|