Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
135 changes: 120 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Response assertions from Identity Providers (IdPs).
**Important:** This libary does not support the IdP-side of SAML authentication,
such as creating SAML Response messages to assert a user's identity.

A Rails 4 reference implemenation is avaiable at the
A Rails 4 reference implementation is available at the
[Ruby SAML Demo Project](https://github.com/saml-toolkits/ruby-saml-example).

### Vulnerability Reporting
Expand All @@ -46,9 +46,10 @@ it by email to the maintainer: sixto.martin.garcia+security@gmail.com
and from a trusted source. Ruby SAML does not perform any validation that the URL
you entered is correct and/or safe.
- **False-Positive Security Warnings:** Some tools may incorrectly report Ruby SAML as a
potential security vulnerability, due to it's dependency on Nokogiri. Such warnings can
potential security vulnerability, due to its dependency on Nokogiri. Such warnings can
be ignored; Ruby SAML uses Nokogiri in a safe way, by always disabling its DTDLOAD option
and enabling its NONET option.
- **Prevent Replay attacks:** A replay attack is when an attacker intercepts a valid SAML assertion and "replays" it at a later time to gain unauthorized access. The `ruby-saml` library provides the tools to prevent this, but **you, the developer, must implement the core logic**, see an specific section later in the README.

### Supported Ruby Versions

Expand Down Expand Up @@ -179,7 +180,7 @@ def saml_settings
end
```

The use of settings.issuer is deprecated in favour of settings.sp_entity_id since version 1.11.0
The use of settings.issuer is deprecated in favor of settings.sp_entity_id since version 1.11.0

Some assertion validations can be skipped by passing parameters to `RubySaml::Response.new()`.
For example, you can skip the `AuthnStatement`, `Conditions`, `Recipient`, or the `SubjectConfirmation`
Expand Down Expand Up @@ -255,13 +256,13 @@ Ruby SAML allows different ways to validate the signature of the SAML Response:
`idp_cert_fingerprint` and `idp_cert_fingerprint_algorithm` parameters.

In addition, you may pass the option `:relax_signature_validation` to `SloLogoutrequest` and
`Logoutresponse` if want to skip signature validation on logout.
`Logoutresponse` if you want to skip signature validation on logout.

The `idp_cert_fingerprint` option is deprecated for the following reasons. It will be
removed in Ruby SAML version 2.1.0.
1. It only works with HTTP-POST binding, not HTTP-Redirect, since the full certificate
is not sent in the Redirect URL parameters.
2. It is theoretically be susceptible to collision attacks, by which a malicious
2. It is theoretically susceptible to collision attacks, by which a malicious
actor could impersonate the IdP. (However, as of January 2025, such attacks have not
been publicly demonstrated for SHA-256.)
3. It has been removed already from several other SAML libraries in other languages.
Expand Down Expand Up @@ -365,8 +366,7 @@ Those return an Hash instead of a `Settings` object, which may be useful for con

### Validating Signature of Metadata and retrieve settings

Right now there is no method at ruby_saml to validate the signature of the metadata that gonna be parsed,
but it can be done as follows:
Right now there is no method at ruby_saml to validate the signature of the metadata that is going to be parsed, but it can be done as follows:
* Download the XML.
* Validate the Signature, providing the cert.
* Provide the XML to the parse method if the signature was validated
Expand Down Expand Up @@ -403,7 +403,7 @@ if valid
entity_id: "<entity_id_of_the_entity_to_be_retrieved>"
)
else
print "Metadata Signarture failed to be verified with the cert provided"
print "Metadata Signature failed to be verified with the cert provided"
end
```

Expand Down Expand Up @@ -632,7 +632,7 @@ settings.security[:logout_requests_signed] = true # Enable signature on Logout
settings.security[:logout_responses_signed] = true # Enable signature on Logout Response
```

Signatures will be handled automatically for both `HTTP-Redirect` and `HTTP-Redirect` Binding.
Signatures will be handled automatically for both `HTTP-POST` and `HTTP-Redirect` Binding.
Note that the RelayState parameter is used when creating the Signature on the `HTTP-Redirect` Binding.
Remember to provide it to the Signature builder if you are sending a `GET RelayState` parameter or the
signature validation process will fail at the IdP.
Expand All @@ -655,7 +655,7 @@ settings.security[:want_assertions_encrypted] = true # Invalidate SAML messages
### Verifying Signature on IdP Assertions

You may require the IdP to sign its SAML Assertions using the following setting.
With will add `<md:SPSSODescriptor WantAssertionsSigned="true">` to your SP Metadata XML.
This will add `<md:SPSSODescriptor WantAssertionsSigned="true">` to your SP Metadata XML.
The signature will be checked against the `<md:KeyDescriptor use="signing">` element
present in the IdP's metadata.

Expand Down Expand Up @@ -687,7 +687,7 @@ advanced usage scenarios:
- Specifying separate SP certificates for signing and encryption.

The `sp_cert_multi` parameter replaces `certificate` and `private_key`
(you may not specify both pparameters at the same time.) `sp_cert_multi` has the following shape:
(you may not specify both parameters at the same time.) `sp_cert_multi` has the following shape:

```ruby
settings.sp_cert_multi = {
Expand Down Expand Up @@ -729,7 +729,7 @@ JRuby cannot support ECDSA due to a [known issue](https://github.com/jruby/jruby
### Audience Validation

A service provider should only consider a SAML response valid if the IdP includes an <AudienceRestriction>
element containting an <Audience> element that uniquely identifies the service provider. Unless you specify
element containing an <Audience> element that uniquely identifies the service provider. Unless you specify
the `skip_audience` option, Ruby SAML will validate that each SAML response includes an <Audience> element
whose contents matches `settings.sp_entity_id`.

Expand Down Expand Up @@ -762,7 +762,7 @@ def sp_logout_request
settings = saml_settings

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

Expand Down Expand Up @@ -910,7 +910,7 @@ end

### Attribute Service

To request attributes from the IdP the SP needs to provide an attribute service within it's metadata and reference the index in the assertion.
To request attributes from the IdP the SP needs to provide an attribute service within its metadata and reference the index in the assertion.

```ruby
settings = RubySaml::Settings.new
Expand All @@ -936,7 +936,7 @@ or underscore, and can only contain letters, digits, underscores, hyphens, and p

### Custom Metadata Fields

Some IdPs may require to add SPs to add additional fields (Organization, ContactPerson, etc.)
Some IdPs may require SPs to add additional fields (Organization, ContactPerson, etc.)
into the SP metadata. This can be done by extending the `RubySaml::Metadata` class and
overriding the `#add_extras` method where the first arg is a
[Nokogiri::XML::Builder](https://nokogiri.org/rdoc/Nokogiri/XML/Builder.html) object as per
Expand Down Expand Up @@ -964,6 +964,111 @@ end
MyMetadata.new.generate(settings)
```

### Preventing Replay Attacks

A replay attack is when an attacker intercepts a valid SAML assertion and "replays" it at a later time to gain unauthorized access.

The library only checks the assertion's validity window (`NotBefore` and `NotOnOrAfter` conditions). An attacker can replay a valid assertion as many times as they want within this window.

A robust defense requires tracking of assertion IDs to ensure any given assertion is only accepted once.

#### 1. Extract the Assertion ID after Validation

After a response has been successfully validated, get the assertion ID. The library makes this available via `response.assertion_id`.


#### 2. Store the ID with an Expiry

You must store this ID in a persistent cache (like Redis or Memcached) that is shared across your servers. Do not store it in the user's session, as that is not a secure cache.

The ID should be stored until the assertion's validity window has passed. You will need to check how long the trusted IdPs consider the assertion valid and then add the allowed_clock_drift.

You can define a global value, or set this value dinamically based on the `not_on_or_after` value of the re + `allowed_clock_drift`.

```ruby
# In your `consume` action, after a successful validation:
if response.is_valid?
# Prevent replay of this specific assertion
assertion_id = response.assertion_id
authorize_failure("Assertion ID is mandatory") if assertion_id.nil?

assertion_not_on_or_after = response.not_on_or_after
# We set a default of 5 min expiration in case is not provided
assertion_expiry = (Time.now.utc + 300) if assertion_not_on_or_after.nil?

# `is_new_assertion?` is your application's method to check and set the ID
# in a shared, persistent cache (e.g., Redis, Memcached).
if is_new_assertion?(assertion_id, expires_at: assertion_expiry)
# This is a new assertion, so we can proceed
session[:userid] = response.nameid
session[:attributes] = response.attributes
# ...
else
# This assertion ID has been seen before. This is a REPLAY ATTACK.
# Log the security event and reject the user.
authorize_failure("Replay attack detected")
end
else
authorize_failure("Invalid response")
end
```

Your `is_new_assertion?` method would look something like this (example for Redis):

```ruby

def is_new_assertion?(assertion_id, expires_at)
ttl = (expires_at - Time.now.utc).to_i
return false if ttl <= 0 # The assertion has already expired

# The 'nx' option tells Redis to only set the key if it does not already exist.
# The command returns `true` if the key was set, `false` otherwise.
$redis.set("saml_assertion_ids:#{assertion_id}", "1", ex: ttl, nx: true)
end
```

### Enforce SP-Initiated Flow with `InResponseTo` validation

This is the best way to prevent IdP-initiated logins and ensure that you only accept assertions that you recently requested.

#### 1. Store the `AuthnRequest` ID

When you create an `AuthnRequest`, the library assigns it a unique ID. You must store this ID, for example in the user's session *before* redirecting them to the IdP.

```ruby
def init
request = OneLogin::RubySaml::Authrequest.new
# The unique ID of the request is in request.uuid
session[:saml_request_id] = request.uuid
redirect_to(request.create(saml_settings))
end
```

#### 2. Validate the `InResponseTo` value of the `Response` with the Stored ID

When you process the `SAMLResponse`, retrieve the ID from the session and pass it to the `Response` constructor. Use `session.delete` to ensure the ID can only be used once.

```ruby
def consume
request_id = session.delete(:saml_request_id) # Use delete to prevent re-use

# You can reject the response if no previous saml_request_id was stored
raise "IdP-initiaited detected" if request_id.nil?

response = OneLogin::RubySaml::Response.new(
params[:SAMLResponse],
settings: saml_settings,
matches_request_id: request_id
)

if response.is_valid?
# ... authorize user
else
# Response is invalid, errors in response.errors
end
end
```

## Contributing

### Pay it Forward: Support RubySAML and Strengthen Open-Source Security
Expand Down
32 changes: 20 additions & 12 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,23 @@ This issue is likely not critical for most IdPs, but since it is not tested, it

### Root "OneLogin" namespace changed to "RubySaml"

RubySaml version `2.0.0` changes the root namespace from `RubySaml::` to just `RubySaml::`.
Please remove `` and `onelogin/` everywhere in your codebase. Aside from this namespace change,
RubySaml version `2.0.0` changes the root namespace from `OneLogin::RubySaml::` to just `RubySaml::`.
Please remove `onelogin/` everywhere in your codebase. Aside from this namespace change,
the class names themselves have intentionally been kept the same.

Note that the project folder structure has also been updated accordingly. Notably, the directory
`lib/onelogin/schemas` is now `lib/ruby_saml/schemas`.

For backward compatibility, the alias `OneLogin = Object` has been set, so `RubySaml::` will still work
as before. This alias will be removed in RubySaml version `3.0.0`.
For backward compatibility, a module is defined at lib/ruby_saml.rb, so `RubySaml::` will still work
as before. This module will be removed in RubySaml version `3.0.0`.
```
unless defined?(::OneLogin)
module OneLogin
RubySaml = ::RubySaml
end
end
```


### Deprecation and removal of "XMLSecurity" namespace

Expand Down Expand Up @@ -75,7 +83,7 @@ settings.security[:signature_method] = RubySaml::XML::RSA_SHA1

RubySaml `1.x` used a combination of REXML and Nokogiri for XML parsing and generation.
In `2.0.0`, REXML has been replaced with Nokogiri. As a result, there are minor differences
in how XML is generated, ncluding SAML requests and SP Metadata:
in how XML is generated, including SAML requests and SP Metadata:

1. All XML namespace declarations will be on the root node of the XML. Previously,
some declarations such as `xmlns:ds` were done on child nodes.
Expand Down Expand Up @@ -121,7 +129,7 @@ The reasons for this change are:
### Removal of embed_sign setting

The deprecated `settings.security[:embed_sign]` parameter has been removed. If you were using it, please instead switch
to using both the `settings.idp_sso_service_binding` and `settings.idp_slo_service_binding` parameters as show below.
to using both the `settings.idp_sso_service_binding` and `settings.idp_slo_service_binding` parameters as shown below.
(This new syntax is supported on version 1.13.0 and later.)

```ruby
Expand Down Expand Up @@ -230,8 +238,8 @@ how validation happens in the toolkit and also the toolkit by default will check
when parsing a SAML Message (`settings.check_malformed_doc`).

The SignedDocument class defined at xml_security.rb experienced several changes.
We don't expect compatibilty issues if you use the main methods offered by ruby-saml, but if
you use a fork or customized usage, is possible that you need to adapt your code.
We don't expect compatibility issues if you use the main methods offered by ruby-saml, but if
you use a fork or customized usage, it is possible that you will need to adapt your code.

## Upgrading from 1.12.x to 1.13.0

Expand All @@ -257,7 +265,7 @@ in favor of `idp_sso_service_url` and `idp_slo_service_url`. The `IdpMetadataPar

## Upgrading 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`.
Version `1.11.0` deprecates the use of `settings.issuer` in favor 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 All @@ -268,7 +276,7 @@ Version `1.10.1` improves Ruby 1.8.7 support.

## Upgrading from 1.9.0 to 1.10.0

Version `1.10.0` improves IdpMetadataParser to allow parse multiple IDPSSODescriptor,
Version `1.10.0` improves IdpMetadataParser to allow parsing multiple IDPSSODescriptor,
Add Subject support on AuthNRequest to allow SPs provide info to the IdP about the user
to be authenticated and updates the format_cert method to accept certs with /\x0d/

Expand Down Expand Up @@ -352,7 +360,7 @@ It adds security improvements in order to prevent Signature wrapping attacks.

## Upgrading from 1.1.x to 1.2.x

Version `1.2` adds IDP metadata parsing improvements, uuid deprecation in favour of SecureRandom,
Version `1.2` adds IDP metadata parsing improvements, uuid deprecation in favor of SecureRandom,
refactor error handling and some minor improvements.

There is no compatibility issue detected.
Expand All @@ -367,7 +375,7 @@ Version `1.1` adds some improvements on signature validation and solves some nam

Version `1.0` is a recommended update for all Ruby SAML users as it includes security fixes.

Version `1.0` adds security improvements like entity expansion limitation, more SAML message validations, and other important improvements like decrypt support.
Version `1.0` adds security improvements like entity expansion limitation, more SAML message validations, and other important improvements like decryption support.

### Important Changes

Expand Down
16 changes: 14 additions & 2 deletions lib/ruby_saml.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,17 @@
require 'ruby_saml/utils'
require 'ruby_saml/version'

# @deprecated This alias adds compatibility with v1.x and will be removed in v3.0.0
OneLogin = Object
# @deprecated This module adds compatibility with v1.x and will be removed in v3.0.0
unless defined?(OneLogin)
module OneLogin
RubySaml = ::RubySaml
end
end

unless defined?(OneLogin::RubySaml::Logging)
module OneLogin
module RubySaml
Logging = ::RubySaml::Logging
end
end
end
2 changes: 1 addition & 1 deletion lib/ruby_saml/attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def add(name, values = [])

# Make comparable to another Attributes collection based on attributes
# @param other [Attributes] An Attributes object to compare with
# @return [Boolean] True if are contains the same attributes and values
# @return [Boolean] True if it contains the same attributes and values
#
def ==(other)
if other.is_a?(Attributes)
Expand Down
Loading