Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1.12.0 branch #566

Merged
merged 32 commits into from
Feb 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8564110
Fetch attribute value
JDrizzy Apr 17, 2020
1ae0c82
Update README.md
JDrizzy Jun 2, 2020
09e9cac
Add more uni tests
pitbulk Jun 27, 2020
55a2389
Support AES-128-GCM, AES-192-GCM, and AES-256-GCM encryptions
silppuri Nov 27, 2020
62ac2fe
Parse & return SLO ResponseLocation in IDPMetadataParser & Settings
JCB-K Dec 8, 2020
0cb1843
Use Singlelogout response url in LogoutResponse if set
JCB-K Dec 8, 2020
2b93476
add runtime dependency
tknzk Dec 29, 2020
59a22a0
test against ruby 3.0.0
tknzk Dec 29, 2020
2bff5f6
Merge branch 'master' of github.com:onelogin/ruby-saml
pitbulk Jan 23, 2021
5987d50
Merge pull request #562 from tknzk/test-ruby3
pitbulk Jan 23, 2021
32a0aab
Fix Travis
pitbulk Jan 23, 2021
7808e03
Remove 1.8.7 and ree fom Travis because it does not support them anym…
pitbulk Jan 23, 2021
6f8046e
Adding idp_sso_service_url and idp_slo_service_url settings
pitbulk Jan 23, 2021
d3b5913
Merge pull request #536 from JDrizzy/master
pitbulk Jan 24, 2021
03d6b41
Merge Parse & use SLO Response Location
pitbulk Jan 25, 2021
206e6b8
Merge branch 'mentimeter-parse-responselocation' into 1.12.0-dev
pitbulk Jan 25, 2021
2e2e9b2
Reduce size of built gem by excluding the test folder. See #538
pitbulk Jan 25, 2021
533c84e
Minimize Zlib deflate decompression bomb. See #383
pitbulk Jan 25, 2021
4fe698c
Update single logout code sample to encourage early session terminati…
pitbulk Jan 25, 2021
92d6caf
See #563 Add ValidUntil and cacheDuration support on Metadata generat…
pitbulk Jan 26, 2021
25cbddd
Add support for cacheDuration at the IdpMetadataParser class, at the …
pitbulk Jan 27, 2021
3f89d19
Support customizable statusCode on generated LogoutResponse
pitbulk Feb 3, 2021
3783e22
Merge pull request #557 from silppuri/support-aes-gcm
pitbulk Feb 5, 2021
5e8ad48
Fix test when GCM is not supported
pitbulk Feb 8, 2021
b4cf36d
Add duration format as a constant
pitbulk Feb 8, 2021
46edfba
Make it compatible with old versions of Ruby
pitbulk Feb 8, 2021
60b0204
Use Time.now.utc instead Time.now on parse_duration
pitbulk Feb 11, 2021
b538bdf
Add request_id, response_id and assertion_id
pitbulk Feb 11, 2021
9577d86
More utc stuff for parse duration
pitbulk Feb 12, 2021
6962bf5
Pending utc changes
pitbulk Feb 15, 2021
59b9ade
See #545 More specific error messages for signature validation
pitbulk Feb 17, 2021
d49fde6
Release 1.12.0
pitbulk Feb 18, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
language: ruby
rvm:
- 1.8.7
- 1.9.3
- 2.0.0
- 2.1.10
Expand All @@ -10,7 +9,7 @@ rvm:
- 2.5.8
- 2.6.6
- 2.7.2
- ree
- 3.0.0
- jruby-1.7.27
- jruby-9.1.17.0
- jruby-9.2.13.0
Expand All @@ -21,10 +20,6 @@ before_install:
- gem update bundler
matrix:
exclude:
- rvm: 1.8.7
gemfile: Gemfile
- rvm: ree
gemfile: Gemfile
- rvm: jruby-1.7.27
gemfile: gemfiles/nokogiri-1.5.gemfile
- rvm: jruby-9.1.17.0
Expand All @@ -33,6 +28,8 @@ matrix:
gemfile: gemfiles/nokogiri-1.5.gemfile
- rvm: 2.1.5
gemfile: gemfiles/nokogiri-1.5.gemfile
- rvm: 2.1.10
gemfile: gemfiles/nokogiri-1.5.gemfile
- rvm: 2.2.10
gemfile: gemfiles/nokogiri-1.5.gemfile
- rvm: 2.3.8
Expand All @@ -45,5 +42,7 @@ matrix:
gemfile: gemfiles/nokogiri-1.5.gemfile
- rvm: 2.7.2
gemfile: gemfiles/nokogiri-1.5.gemfile
- rvm: 3.0.0
gemfile: gemfiles/nokogiri-1.5.gemfile
env:
- JRUBY_OPTS="--debug"
60 changes: 48 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Ruby SAML [![Build Status](https://secure.travis-ci.org/onelogin/ruby-saml.svg)](http://travis-ci.org/onelogin/ruby-saml) [![Coverage Status](https://coveralls.io/repos/onelogin/ruby-saml/badge.svg?branch=master)](https://coveralls.io/r/onelogin/ruby-saml?branch=master) [![Gem Version](https://badge.fury.io/rb/ruby-saml.svg)](http://badge.fury.io/rb/ruby-saml)

## Updating from 1.11.x to 1.12.0
Version `1.12.0` adds support for gcm algorithm and
change/adds specific error messages for signature validations

## Updating from 1.10.x to 1.11.0
Version `1.11.0` deprecates the use of `settings.issuer` in favour of `settings.sp_entity_id`.
There are two new security settings: `settings.security[:check_idp_cert_expiration]` and `settings.security[:check_sp_cert_expiration]` (both false by default) that check if the IdP or SP X.509 certificate has expired, respectively.
Expand Down Expand Up @@ -261,8 +265,8 @@ def saml_settings
settings.assertion_consumer_service_url = "http://#{request.host}/saml/consume"
settings.sp_entity_id = "http://#{request.host}/saml/metadata"
settings.idp_entity_id = "https://app.onelogin.com/saml/metadata/#{OneLoginAppId}"
settings.idp_sso_target_url = "https://app.onelogin.com/trust/saml2/http-post/sso/#{OneLoginAppId}"
settings.idp_slo_target_url = "https://app.onelogin.com/trust/saml2/http-redirect/slo/#{OneLoginAppId}"
settings.idp_sso_service_url = "https://app.onelogin.com/trust/saml2/http-post/sso/#{OneLoginAppId}"
settings.idp_slo_service_url = "https://app.onelogin.com/trust/saml2/http-redirect/slo/#{OneLoginAppId}"
settings.idp_cert_fingerprint = OneLoginAppCertFingerPrint
settings.idp_cert_fingerprint_algorithm = "http://www.w3.org/2000/09/xmldsig#sha1"
settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
Expand Down Expand Up @@ -327,7 +331,7 @@ class SamlController < ApplicationController

settings.assertion_consumer_service_url = "http://#{request.host}/saml/consume"
settings.sp_entity_id = "http://#{request.host}/saml/metadata"
settings.idp_sso_target_url = "https://app.onelogin.com/saml/signon/#{OneLoginAppId}"
settings.idp_sso_service_url = "https://app.onelogin.com/saml/signon/#{OneLoginAppId}"
settings.idp_cert_fingerprint = OneLoginAppCertFingerPrint
settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"

Expand Down Expand Up @@ -400,8 +404,8 @@ end
The following attributes are set:
* idp_entity_id
* name_identifier_format
* idp_sso_target_url
* idp_slo_target_url
* idp_sso_service_url
* idp_slo_service_url
* idp_attribute_names
* idp_cert
* idp_cert_fingerprint
Expand Down Expand Up @@ -467,6 +471,9 @@ Imagine this `saml:AttributeStatement`
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="1"/>
</saml:Attribute>
<saml:Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname">
<saml:AttributeValue>usersName</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
```

Expand All @@ -477,7 +484,8 @@ pp(response.attributes) # is an OneLogin::RubySaml::Attributes object
"another_value"=>["value1", "value2"],
"role"=>["role1", "role2", "role3"],
"attribute_with_nil_value"=>[nil],
"attribute_with_nils_and_empty_strings"=>["", "valuePresent", nil, nil]}>
"attribute_with_nils_and_empty_strings"=>["", "valuePresent", nil, nil]
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"=>["usersName"]}>

# Active single_value_compatibility
OneLogin::RubySaml::Attributes.single_value_compatibility = true
Expand All @@ -494,6 +502,9 @@ pp(response.attributes.single(:role))
pp(response.attributes.multi(:role))
# => ["role1", "role2", "role3"]

pp(response.attributes.fetch(:role))
# => "role1"

pp(response.attributes[:attribute_with_nil_value])
# => nil

Expand All @@ -509,6 +520,9 @@ pp(response.attributes.single(:not_exists))
pp(response.attributes.multi(:not_exists))
# => nil

pp(response.attributes.fetch(/givenname/))
# => "usersName"

# Deactive single_value_compatibility
OneLogin::RubySaml::Attributes.single_value_compatibility = false

Expand All @@ -524,6 +538,9 @@ pp(response.attributes.single(:role))
pp(response.attributes.multi(:role))
# => ["role1", "role2", "role3"]

pp(response.attributes.fetch(:role))
# => ["role1", "role2", "role3"]

pp(response.attributes[:attribute_with_nil_value])
# => [nil]

Expand All @@ -538,6 +555,9 @@ pp(response.attributes.single(:not_exists))

pp(response.attributes.multi(:not_exists))
# => nil

pp(response.attributes.fetch(/givenname/))
# => ["usersName"]
```

The `saml:AuthnContextClassRef` of the AuthNRequest can be provided by `settings.authn_context`; possible values are described at [SAMLAuthnCxt]. The comparison method can be set using `settings.authn_context_comparison` parameter. Possible values include: 'exact', 'better', 'maximum' and 'minimum' (default value is 'exact').
Expand Down Expand Up @@ -623,21 +643,27 @@ def sp_logout_request
# LogoutRequest accepts plain browser requests w/o paramters
settings = saml_settings

if settings.idp_slo_target_url.nil?
if settings.idp_slo_service_url.nil?
logger.info "SLO IdP Endpoint not found in settings, executing then a normal logout'"
delete_session
else

# Since we created a new SAML request, save the transaction_id
# to compare it with the response we get back
logout_request = OneLogin::RubySaml::Logoutrequest.new()
session[:transaction_id] = logout_request.uuid
logger.info "New SP SLO for userid '#{session[:userid]}' transactionid '#{session[:transaction_id]}'"
logger.info "New SP SLO for userid '#{session[:userid]}' transactionid '#{logout_request.uuid}'"

if settings.name_identifier_value.nil?
settings.name_identifier_value = session[:userid]
end

# Ensure user is logged out before redirect to IdP, in case anything goes wrong during single logout process (as recommended by saml2int [SDP-SP34])
logged_user = session[:userid]
logger.info "Delete session for '#{session[:userid]}'"
delete_session

# Save the transaction_id to compare it with the response we get back
session[:transaction_id] = logout_request.uuid
session[:logged_out_user] = logged_user

relayState = url_for controller: 'saml', action: 'index'
redirect_to(logout_request.create(settings, :RelayState => relayState))
end
Expand Down Expand Up @@ -665,7 +691,7 @@ def process_logout_response
logger.error "The SAML Logout Response is invalid"
else
# Actually log out this session
logger.info "Delete session for '#{session[:userid]}'"
logger.info "SLO completed for '#{session[:logged_out_user]}'"
delete_session
end
end
Expand All @@ -674,6 +700,8 @@ end
def delete_session
session[:userid] = nil
session[:attributes] = nil
session[:transaction_id] = nil
session[:logged_out_user] = nil
end
```

Expand Down Expand Up @@ -741,6 +769,14 @@ class SamlController < ApplicationController
end
```

You can add ValidUntil and CacheDuration to the XML Metadata using instead
```ruby
# Valid until => 2 days from now
# Cache duration = 604800s = 1 week
valid_until = Time.now + 172800
cache_duration = 604800
meta.generate(settings, false, valid_until, cache_duration)
```

## Clock Drift

Expand Down
18 changes: 18 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# RubySaml Changelog

### 1.12.0 (Feb 18, 2021)
* Support AES-128-GCM, AES-192-GCM, and AES-256-GCM encryptions
* Parse & return SLO ResponseLocation in IDPMetadataParser & Settings
* Adding idp_sso_service_url and idp_slo_service_url settings
* [#536](https://github.com/onelogin/ruby-saml/pull/536) Adding feth method to be able retrieve attributes based on regex
* Reduce size of built gem by excluding the test folder
* Improve protection on Zlib deflate decompression bomb attack.
* Add ValidUntil and cacheDuration support on Metadata generator
* Add support for cacheDuration at the IdpMetadataParser
* Support customizable statusCode on generated LogoutResponse
* [#545](https://github.com/onelogin/ruby-saml/pull/545) More specific error messages for signature validation
* Support Process Transform
* Raise SettingError if invoking an action with no endpoint defined on the settings
* Made IdpMetadataParser more extensible for subclasses
*[#548](https://github.com/onelogin/ruby-saml/pull/548) Add :skip_audience option
* [#555](https://github.com/onelogin/ruby-saml/pull/555) Define 'soft' variable to prevent exception when doc cert is invalid
* Improve documentation

### 1.11.0 (Jul 24, 2019)

* Deprecate settings.issuer in favor of settings.sp_entity_id
Expand Down
23 changes: 23 additions & 0 deletions lib/onelogin/ruby-saml/attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,29 @@ def ==(other)
end
end

# Fetch attribute value using name or regex
# @param name [String|Regexp] The attribute name
# @return [String|Array] Depending on the single value compatibility status this returns:
# - First value if single_value_compatibility = true
# response.attributes['mail'] # => 'user@example.com'
# - All values if single_value_compatibility = false
# response.attributes['mail'] # => ['user@example.com','user@example.net']
#
def fetch(name)
attributes.each_key do |attribute_key|
if name.is_a?(Regexp)
if name.method_exists? :match?
return self[attribute_key] if name.match?(attribute_key)
else
return self[attribute_key] if name.match(attribute_key)
end
elsif canonize_name(name) == canonize_name(attribute_key)
return self[attribute_key]
end
end
nil
end

protected

# stringifies all names so both 'email' and :email return the same result
Expand Down
12 changes: 8 additions & 4 deletions lib/onelogin/ruby-saml/authrequest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,25 @@ def initialize
@uuid = OneLogin::RubySaml::Utils.uuid
end

def request_id
@uuid
end

# Creates the AuthNRequest string.
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
# @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
# @return [String] AuthNRequest string that includes the SAMLRequest
#
def create(settings, params = {})
params = create_params(settings, params)
params_prefix = (settings.idp_sso_target_url =~ /\?/) ? '&' : '?'
params_prefix = (settings.idp_sso_service_url =~ /\?/) ? '&' : '?'
saml_request = CGI.escape(params.delete("SAMLRequest"))
request_params = "#{params_prefix}SAMLRequest=#{saml_request}"
params.each_pair do |key, value|
request_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
end
raise SettingError.new "Invalid settings, idp_sso_target_url is not set!" if settings.idp_sso_target_url.nil? or settings.idp_sso_target_url.empty?
@login_url = settings.idp_sso_target_url + request_params
raise SettingError.new "Invalid settings, idp_sso_service_url is not set!" if settings.idp_sso_service_url.nil? or settings.idp_sso_service_url.empty?
@login_url = settings.idp_sso_service_url + request_params
end

# Creates the Get parameters for the request.
Expand Down Expand Up @@ -108,7 +112,7 @@ def create_xml_document(settings)
root.attributes['ID'] = uuid
root.attributes['IssueInstant'] = time
root.attributes['Version'] = "2.0"
root.attributes['Destination'] = settings.idp_sso_target_url unless settings.idp_sso_target_url.nil? or settings.idp_sso_target_url.empty?
root.attributes['Destination'] = settings.idp_sso_service_url unless settings.idp_sso_service_url.nil? or settings.idp_sso_service_url.empty?
root.attributes['IsPassive'] = settings.passive unless settings.passive.nil?
root.attributes['ProtocolBinding'] = settings.protocol_binding unless settings.protocol_binding.nil?
root.attributes["AttributeConsumingServiceIndex"] = settings.attributes_index unless settings.attributes_index.nil?
Expand Down
40 changes: 37 additions & 3 deletions lib/onelogin/ruby-saml/idp_metadata_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,16 @@ def parse_remote_to_array(url, validate_cert = true, options = {})
def parse(idp_metadata, options = {})
parsed_metadata = parse_to_hash(idp_metadata, options)

unless parsed_metadata[:cache_duration].nil?
cache_valid_until_timestamp = OneLogin::RubySaml::Utils.parse_duration(parsed_metadata[:cache_duration])
if parsed_metadata[:valid_until].nil? || cache_valid_until_timestamp < Time.parse(parsed_metadata[:valid_until], Time.now.utc).to_i
parsed_metadata[:valid_until] = Time.at(cache_valid_until_timestamp).utc.strftime("%Y-%m-%dT%H:%M:%SZ")
end
end
# Remove the cache_duration because on the settings
# we only gonna suppot valid_until
parsed_metadata.delete(:cache_duration)

settings = options[:settings]

if settings.nil?
Expand Down Expand Up @@ -210,13 +220,15 @@ def to_hash(options = {})
{
:idp_entity_id => @entity_id,
:name_identifier_format => idp_name_id_format,
:idp_sso_target_url => single_signon_service_url(options),
:idp_slo_target_url => single_logout_service_url(options),
:idp_sso_service_url => single_signon_service_url(options),
:idp_slo_service_url => single_logout_service_url(options),
:idp_slo_response_service_url => single_logout_response_service_url(options),
:idp_attribute_names => attribute_names,
:idp_cert => nil,
:idp_cert_fingerprint => nil,
:idp_cert_multi => nil,
:valid_until => valid_until
:valid_until => valid_until,
:cache_duration => cache_duration,
}.tap do |response_hash|
merge_certificates_into(response_hash) unless certificates.nil?
end
Expand All @@ -240,6 +252,13 @@ def valid_until
root.attributes['validUntil'] if root && root.attributes
end

# @return [String|nil] 'cacheDuration' attribute of metadata
#
def cache_duration
root = @idpsso_descriptor.root
root.attributes['cacheDuration'] if root && root.attributes
end

# @param binding_priority [Array]
# @return [String|nil] SingleSignOnService binding if exists
#
Expand Down Expand Up @@ -304,6 +323,21 @@ def single_logout_service_url(options = {})
return node.value if node
end

# @param options [Hash]
# @return [String|nil] SingleLogoutService response url if exists
#
def single_logout_response_service_url(options = {})
binding = single_logout_service_binding(options[:slo_binding])
return if binding.nil?

node = REXML::XPath.first(
@idpsso_descriptor,
"md:SingleLogoutService[@Binding=\"#{binding}\"]/@ResponseLocation",
SamlMetadata::NAMESPACE
)
return node.value if node
end

# @return [String|nil] Unformatted Certificate if exists
#
def certificates
Expand Down
4 changes: 4 additions & 0 deletions lib/onelogin/ruby-saml/logoutrequest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ def initialize
@uuid = OneLogin::RubySaml::Utils.uuid
end

def request_id
@uuid
end

# Creates the Logout Request string.
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
# @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
Expand Down
4 changes: 4 additions & 0 deletions lib/onelogin/ruby-saml/logoutresponse.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ def initialize(response, settings = nil, options = {})
@document = XMLSecurity::SignedDocument.new(@response)
end

def response_id
id(document)
end

# Checks if the Status has the "Success" code
# @return [Boolean] True if the StatusCode is Sucess
# @raise [ValidationError] if soft == false and validation fails
Expand Down
Loading