Skip to content

Commit 33b519e

Browse files
committed
Merge branch 'master' of github.com:onelogin/ruby-saml
2 parents 3d3367f + d11a467 commit 33b519e

File tree

13 files changed

+89
-52
lines changed

13 files changed

+89
-52
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ lib/Lib.iml
99
test/Test.iml
1010
.rvmrc
1111
*.gem
12+
.bundle

Gemfile

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ gemspec
44

55
group :test do
66
gem "ruby-debug", "~> 0.10.4", :require => nil, :platforms => :ruby_18
7-
gem "debugger", "~> 1.1.1", :require => nil, :platforms => :ruby_19
8-
gem "shoulda"
9-
gem "rake"
10-
gem "mocha"
11-
gem "nokogiri", ">= 1.5.0"
12-
gem "timecop"
7+
gem "debugger", "~> 1.1", :require => nil, :platforms => :ruby_19
8+
gem "shoulda", "~> 2.11"
9+
gem "rake", "~> 10"
10+
gem "mocha", "~> 0.14"
11+
gem "nokogiri", "~> 1.5"
12+
gem "timecop", "<= 0.6.0"
13+
gem "systemu", "~> 2"
14+
gem "rspec", "~> 2"
1315
end

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,20 @@ to the IdP settings.
118118
end
119119
```
120120

121+
## Clock Drift
122+
123+
Server clocks tend to drift naturally. If during validation of the response you get the error "Current time is earlier than NotBefore condition" then this may be due to clock differences between your system and that of the Identity Provider.
124+
125+
First, ensure that both systems synchronize their clocks, using for example the industry standard [Network Time Protocol (NTP)](http://en.wikipedia.org/wiki/Network_Time_Protocol).
126+
127+
Even then you may experience intermittent issues though, because the clock of the Identity Provider may drift slightly ahead of your system clocks. To allow for a small amount of clock drift you can initialize the response passing in an option named `:allowed_clock_drift`. Its value must be given in a number (and/or fraction) of seconds. The value given is added to the current time at which the response is validated before it's tested against the `NotBefore` assertion. For example:
128+
129+
```ruby
130+
response = Onelogin::Saml::Response.new(params[:SAMLResponse], :allowed_clock_drift => 1)
131+
```
132+
133+
Make sure to keep the value as comfortably small as possible to keep security risks to a minimum.
134+
121135
## Note on Patches/Pull Requests
122136

123137
* Fork the project.

lib/onelogin/ruby-saml/authrequest.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def create(settings, params = {})
3636
def create_authentication_xml_doc(settings)
3737
uuid = "_" + UUID.new.generate
3838
time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
39-
# Create AuthnRequest root element using REXML
39+
# Create AuthnRequest root element using REXML
4040
request_doc = REXML::Document.new
4141

4242
root = request_doc.add_element "samlp:AuthnRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol" }
@@ -45,6 +45,7 @@ def create_authentication_xml_doc(settings)
4545
root.attributes['Version'] = "2.0"
4646
root.attributes['Destination'] = settings.idp_sso_target_url unless settings.idp_sso_target_url.nil?
4747
root.attributes['IsPassive'] = settings.passive unless settings.passive.nil?
48+
root.attributes['ProtocolBinding'] = settings.protocol_binding unless settings.protocol_binding.nil?
4849

4950
# Conditionally defined elements based on settings
5051
if settings.assertion_consumer_service_url != nil
@@ -55,7 +56,7 @@ def create_authentication_xml_doc(settings)
5556
issuer.text = settings.issuer
5657
end
5758
if settings.name_identifier_format != nil
58-
root.add_element "samlp:NameIDPolicy", {
59+
root.add_element "samlp:NameIDPolicy", {
5960
"xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
6061
# Might want to make AllowCreate a setting?
6162
"AllowCreate" => "true",
@@ -64,14 +65,14 @@ def create_authentication_xml_doc(settings)
6465
end
6566

6667
# BUG fix here -- if an authn_context is defined, add the tags with an "exact"
67-
# match required for authentication to succeed. If this is not defined,
68+
# match required for authentication to succeed. If this is not defined,
6869
# the IdP will choose default rules for authentication. (Shibboleth IdP)
6970
if settings.authn_context != nil
70-
requested_context = root.add_element "samlp:RequestedAuthnContext", {
71+
requested_context = root.add_element "samlp:RequestedAuthnContext", {
7172
"xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
7273
"Comparison" => "exact",
7374
}
74-
class_ref = requested_context.add_element "saml:AuthnContextClassRef", {
75+
class_ref = requested_context.add_element "saml:AuthnContextClassRef", {
7576
"xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
7677
}
7778
class_ref.text = settings.authn_context

lib/onelogin/ruby-saml/response.rb

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def session_expires_at
7878
parse_time(node, "SessionNotOnOrAfter")
7979
end
8080
end
81-
81+
8282
# Checks the status of the response for a "Success" code
8383
def success?
8484
@status_code ||= begin
@@ -92,6 +92,14 @@ def conditions
9292
@conditions ||= xpath_first_from_signed_assertion('/a:Conditions')
9393
end
9494

95+
def not_before
96+
@not_before ||= parse_time(conditions, "NotBefore")
97+
end
98+
99+
def not_on_or_after
100+
@not_on_or_after ||= parse_time(conditions, "NotOnOrAfter")
101+
end
102+
95103
def issuer
96104
@issuer ||= begin
97105
node = REXML::XPath.first(document, "/p:Response/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
@@ -110,7 +118,7 @@ def validate(soft = true)
110118
validate_structure(soft) &&
111119
validate_response_state(soft) &&
112120
validate_conditions(soft) &&
113-
document.validate(get_fingerprint, soft) &&
121+
document.validate_document(get_fingerprint, soft) &&
114122
success?
115123
end
116124

@@ -161,16 +169,14 @@ def validate_conditions(soft = true)
161169
return true if conditions.nil?
162170
return true if options[:skip_conditions]
163171

164-
if (not_before = parse_time(conditions, "NotBefore"))
165-
if Time.now.utc < not_before
166-
return soft ? false : validation_error("Current time is earlier than NotBefore condition")
167-
end
172+
now = Time.now.utc
173+
174+
if not_before && (now + (options[:allowed_clock_drift] || 0)) < not_before
175+
return soft ? false : validation_error("Current time is earlier than NotBefore condition")
168176
end
169177

170-
if (not_on_or_after = parse_time(conditions, "NotOnOrAfter"))
171-
if Time.now.utc >= not_on_or_after
172-
return soft ? false : validation_error("Current time is on or after NotOnOrAfter condition")
173-
end
178+
if not_on_or_after && now >= not_on_or_after
179+
return soft ? false : validation_error("Current time is on or after NotOnOrAfter condition")
174180
end
175181

176182
true

lib/onelogin/ruby-saml/settings.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ def initialize(overrides = {})
1818
attr_accessor :compress_request
1919
attr_accessor :double_quote_xml_attribute_values
2020
attr_accessor :passive
21+
attr_accessor :protocol_binding
2122

2223
private
23-
24+
2425
DEFAULTS = {:compress_request => true, :double_quote_xml_attribute_values => false}
2526
end
2627
end

lib/onelogin/ruby-saml/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module Onelogin
22
module Saml
3-
VERSION = '0.7.2'
3+
VERSION = '0.7.3'
44
end
55
end

lib/xml_security.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def initialize(response)
4444
extract_signed_element_id
4545
end
4646

47-
def validate(idp_cert_fingerprint, soft = true)
47+
def validate_document(idp_cert_fingerprint, soft = true)
4848
# get cert from response
4949
cert_element = REXML::XPath.first(self, "//ds:X509Certificate", { "ds"=>DSIG })
5050
raise Onelogin::Saml::ValidationError.new("Certificate element missing in response (ds:X509Certificate)") unless cert_element
@@ -59,10 +59,10 @@ def validate(idp_cert_fingerprint, soft = true)
5959
return soft ? false : (raise Onelogin::Saml::ValidationError.new("Fingerprint mismatch"))
6060
end
6161

62-
validate_doc(base64_cert, soft)
62+
validate_signature(base64_cert, soft)
6363
end
6464

65-
def validate_doc(base64_cert, soft = true)
65+
def validate_signature(base64_cert, soft = true)
6666
# validate references
6767

6868
# check for inclusive namespaces

ruby-saml.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,5 @@ Gem::Specification.new do |s|
2424
s.test_files = `git ls-files test/*`.split("\n")
2525

2626
s.add_runtime_dependency("uuid", ["~> 2.3"])
27-
s.add_runtime_dependency("nokogiri", [">= 1.5.0"])
27+
s.add_runtime_dependency("nokogiri", ["~> 1.5.0"])
2828
end

test/response_test.rb

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ class RubySamlTest < Test::Unit::TestCase
120120
settings = Onelogin::Saml::Settings.new
121121
response.settings = settings
122122
settings.idp_cert_fingerprint = "28:74:9B:E8:1F:E8:10:9C:A8:7C:A9:C3:E3:C5:01:6C:92:1C:B4:BA"
123-
XMLSecurity::SignedDocument.any_instance.expects(:validate_doc).returns(true)
123+
XMLSecurity::SignedDocument.any_instance.expects(:validate_signature).returns(true)
124124
assert response.validate!
125125
end
126126

@@ -184,6 +184,17 @@ class RubySamlTest < Test::Unit::TestCase
184184
response = Onelogin::Saml::Response.new(response_document_5)
185185
assert response.send(:validate_conditions, true)
186186
end
187+
188+
should "optionally allow for clock drift" do
189+
# The NotBefore condition in the document is 2011-06-14T18:21:01.516Z
190+
Time.stubs(:now).returns(Time.parse("2011-06-14T18:21:01Z"))
191+
response = Onelogin::Saml::Response.new(response_document_5, :allowed_clock_drift => 0.515)
192+
assert !response.send(:validate_conditions, true)
193+
194+
Time.stubs(:now).returns(Time.parse("2011-06-14T18:21:01Z"))
195+
response = Onelogin::Saml::Response.new(response_document_5, :allowed_clock_drift => 0.516)
196+
assert response.send(:validate_conditions, true)
197+
end
187198
end
188199

189200
context "#attributes" do
@@ -229,13 +240,13 @@ class RubySamlTest < Test::Unit::TestCase
229240
response = Onelogin::Saml::Response.new(response_document)
230241
assert_equal "https://app.onelogin.com/saml/metadata/13590", response.issuer
231242
end
232-
243+
233244
should "return the issuer inside the response" do
234245
response = Onelogin::Saml::Response.new(response_document_2)
235246
assert_equal "wibble", response.issuer
236247
end
237248
end
238-
249+
239250
context "#success" do
240251
should "find a status code that says success" do
241252
response = Onelogin::Saml::Response.new(response_document)

0 commit comments

Comments
 (0)