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

[READY] v2.x - Rework SP request UUID generation to remove mutable constant #735

Merged
merged 17 commits into from
Jan 15, 2025
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
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ AllCops:
- 'tmp/**/*'
- 'vendor/**/*'

Style/Alias:
EnforcedStyle: prefer_alias_method

Style/ModuleFunction:
EnforcedStyle: extend_self

Expand Down
2 changes: 1 addition & 1 deletion .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ Lint/UselessAssignment:
# Offense count: 42
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
Metrics/AbcSize:
Max: 100
Max: 200

# Offense count: 1
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@
* [#709](https://github.com/SAML-Toolkits/ruby-saml/pull/709) Allow passing in `Net::HTTP` `:open_timeout`, `:read_timeout`, and `:max_retries` settings to `IdpMetadataParser#parse_remote`.
* [#715](https://github.com/SAML-Toolkits/ruby-saml/pull/715) Fix typo in error when SPNameQualifier value does not match the SP entityID.
* [#718](https://github.com/SAML-Toolkits/ruby-saml/pull/718) Add support to retrieve from SAMLResponse the AuthnInstant and AuthnContextClassRef values
* [#711](https://github.com/SAML-Toolkits/ruby-saml/pull/711) Standardize how RubySaml reads and formats certificate and private_key PEM values, including the `RubySaml::Util#format_cert` and `#format_private_key` methods.
* [#711](https://github.com/SAML-Toolkits/ruby-saml/pull/711) Standardize how RubySaml reads and formats certificate and private_key PEM values, including the `RubySaml::Utils#format_cert` and `#format_private_key` methods.
* [#732](https://github.com/SAML-Toolkits/ruby-saml/pull/732) Return original certificate and private key objects from `RubySaml::Utils` build functions.
* [#732](https://github.com/SAML-Toolkits/ruby-saml/pull/732) Allow SP `certificate`, `certificate_new`, and `private_key` settings to be `OpenSSL::X509::Certificate` and `OpenSSL::PKey::PKey` objects respectively.
* [#733](https://github.com/SAML-Toolkits/ruby-saml/pull/733) Allow `RubySaml::Utils.is_cert_expired` and `is_cert_active` to accept an optional time argument.
* [#731](https://github.com/SAML-Toolkits/ruby-saml/pull/731) Add CI coverage for Ruby 3.4. Remove CI coverage for Ruby 1.x and 2.x.
* [#735](https://github.com/SAML-Toolkits/ruby-saml/pull/735) Add `Settings#sp_uuid_prefix` and deprecate `Utils#set_prefix`.

### 1.18.0 (???)
* [#718](https://github.com/SAML-Toolkits/ruby-saml/pull/718) Add support to retrieve from SAMLResponse the AuthnInstant and AuthnContextClassRef values
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,15 @@ end

The `attribute_value` option additionally accepts an array of possible values.

## SP-Originated Message IDs

Ruby SAML automatically generates message IDs for SP-originated messages (AuthNRequest, etc.)
By default, this is a UUID prefixed by the `_` character, for example `"_ea8b5fdf-0a71-4bef-9f87-5406ee746f5b"`.
To override this behavior, you may set `settings.sp_uuid_prefix` to a string of your choice.
Note that the SAML specification requires that this type (`xsd:ID`) be an
[NCName](https://www.w3.org/TR/xmlschema-2/#NCName), meaning that it must start with a letter
or underscore, and can only contain letters, digits, underscores, hyphens, and periods.

## Custom Metadata Fields

Some IdPs may require to add SPs to add additional fields (Organization, ContactPerson, etc.)
Expand Down
27 changes: 25 additions & 2 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,30 @@ settings.idp_slo_service_binding = :redirect

For clarity, the default value of both parameters is `:redirect` if they are not set.

### Change to message UUID prefix customization

On SP-originated messages (`Authrequest`, `Logoutrequest`, `Logoutresponse`), RubySaml generates the
`uuid` (aliased to `request_id` / `response_id`) using the `_` character as a default prefix,
for example `_a1b3c5d7-9f1e-3d5c-7b1a-9f1e3d5c7b1a`. In RubySaml versions prior to `2.0.0`, it was
possible to change this default prefix by either `RubySaml::Utils.set_prefix` or by mutating
the `RubySaml::Utils::UUID_PREFIX` constant (which was what `.set_prefix` did.) In RubySaml `2.0.0`,
this prefix is now set using `settings.sp_uuid_prefix`:

```ruby
# Change the default prefix from `_` to `my_id_`
settings.sp_uuid_prefix = 'my_id_'

# Create the AuthNRequest message
request = RubySaml::Authrequest.new
request.create(settings)
request.uuid #=> "my_id_a1b3c5d7-9f1e-3d5c-7b1a-9f1e3d5c7b1a"
```

A side-effect of this change is that the `uuid` of the `Authrequest`, `Logoutrequest`, and `Logoutresponse`
classes now is `nil` until the `#create` method is called (previously, it was set in the constructor.)
After calling `#create` for the first time the `uuid` will not change, even if a `Settings` object with
a different `sp_uuid_prefix` is passed-in on subsequent calls.

### Deprecation of compression settings

The `settings.compress_request` and `settings.compress_response` parameters have been deprecated
Expand All @@ -103,11 +127,10 @@ The following parameters in `RubySaml::Settings` are deprecated and will be remo

### Minor changes to Util#format_cert and #format_private_key


Version `2.0.0` standardizes how RubySaml reads and formats certificate and private key
PEM strings. In general, version `2.0.0` is more permissive than `1.x`, and the changes
are not anticipated to affect most users. Please note the change affects parameters
such `#idp_cert` and `#certificate`, as well as the `RubySaml::Util#format_cert`
such `#idp_cert` and `#certificate`, as well as the `RubySaml::Utils#format_cert`
and `#format_private_key` methods. Specifically:


Expand Down
19 changes: 7 additions & 12 deletions lib/ruby_saml/authrequest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,15 @@ class Authrequest < SamlMessage

# AuthNRequest ID
attr_accessor :uuid

# Initializes the AuthNRequest. An Authrequest Object that is an extension of the SamlMessage class.
# Asigns an ID, a random uuid.
#
def initialize
@uuid = RubySaml::Utils.uuid
super()
end

def request_id
@uuid
end
alias_method :request_id, :uuid

# Creates the AuthNRequest string.
# @param settings [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 = {})
assign_uuid(settings)
params = create_params(settings, params)
params_prefix = /\?/.match?(settings.idp_sso_service_url) ? '&' : '?'
saml_request = CGI.escape(params.delete("SAMLRequest"))
Expand Down Expand Up @@ -107,6 +97,7 @@ def create_authentication_xml_doc(settings)

def create_xml_document(settings)
time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
assign_uuid(settings)

request_doc = RubySaml::XML::Document.new
request_doc.uuid = uuid
Expand Down Expand Up @@ -190,5 +181,9 @@ def sign_document(document, settings)

document
end

def assign_uuid(settings)
@uuid ||= RubySaml::Utils.generate_uuid(settings.sp_uuid_prefix) # rubocop:disable Naming/MemoizedInstanceVariableName
end
end
end
19 changes: 7 additions & 12 deletions lib/ruby_saml/logoutrequest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,15 @@ class Logoutrequest < SamlMessage

# Logout Request ID
attr_accessor :uuid

# Initializes the Logout Request. A Logoutrequest Object that is an extension of the SamlMessage class.
# Asigns an ID, a random uuid.
#
def initialize
@uuid = RubySaml::Utils.uuid
super()
end

def request_id
@uuid
end
alias_method :request_id, :uuid

# Creates the Logout Request string.
# @param settings [RubySaml::Settings|nil] Toolkit settings
# @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
# @return [String] Logout Request string that includes the SAMLRequest
#
def create(settings, params={})
assign_uuid(settings)
params = create_params(settings, params)
params_prefix = /\?/.match?(settings.idp_slo_service_url) ? '&' : '?'
saml_request = CGI.escape(params.delete("SAMLRequest"))
Expand Down Expand Up @@ -105,6 +95,7 @@ def create_logout_request_xml_doc(settings)

def create_xml_document(settings)
time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
assign_uuid(settings)

request_doc = RubySaml::XML::Document.new
request_doc.uuid = uuid
Expand Down Expand Up @@ -149,5 +140,9 @@ def sign_document(document, settings)

document
end

def assign_uuid(settings)
@uuid ||= RubySaml::Utils.generate_uuid(settings.sp_uuid_prefix) # rubocop:disable Naming/MemoizedInstanceVariableName
end
end
end
1 change: 1 addition & 0 deletions lib/ruby_saml/settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def initialize(overrides = {}, keep_security_attributes = false)
attr_reader :assertion_consumer_service_binding
attr_accessor :single_logout_service_url
attr_reader :single_logout_service_binding
attr_accessor :sp_uuid_prefix
attr_accessor :sp_name_qualifier
attr_accessor :name_identifier_format
attr_accessor :name_identifier_value
Expand Down
20 changes: 7 additions & 13 deletions lib/ruby_saml/slo_logoutresponse.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# frozen_string_literal: true

require "ruby_saml/logging"

require "ruby_saml/saml_message"
require "ruby_saml/utils"
require "ruby_saml/setting_error"
Expand All @@ -15,18 +14,7 @@ class SloLogoutresponse < SamlMessage

# Logout Response ID
attr_accessor :uuid

# Initializes the Logout Response. A SloLogoutresponse Object that is an extension of the SamlMessage class.
# Asigns an ID, a random uuid.
#
def initialize
@uuid = RubySaml::Utils.uuid
super()
end

def response_id
@uuid
end
alias_method :response_id, :uuid

# Creates the Logout Response string.
# @param settings [RubySaml::Settings|nil] Toolkit settings
Expand All @@ -37,6 +25,7 @@ def response_id
# @return [String] Logout Request string that includes the SAMLRequest
#
def create(settings, request_id = nil, logout_message = nil, params = {}, logout_status_code = nil)
assign_uuid(settings)
params = create_params(settings, request_id, logout_message, params, logout_status_code)
params_prefix = /\?/.match?(settings.idp_slo_service_url) ? '&' : '?'
url = settings.idp_slo_response_service_url || settings.idp_slo_service_url
Expand Down Expand Up @@ -117,6 +106,7 @@ def create_logout_response_xml_doc(settings, request_id = nil, logout_message =

def create_xml_document(settings, request_id = nil, logout_message = nil, status_code = nil)
time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
assign_uuid(settings)

response_doc = RubySaml::XML::Document.new
response_doc.uuid = uuid
Expand Down Expand Up @@ -160,5 +150,9 @@ def sign_document(document, settings)

document
end

def assign_uuid(settings)
@uuid ||= RubySaml::Utils.generate_uuid(settings.sp_uuid_prefix) # rubocop:disable Naming/MemoizedInstanceVariableName
end
end
end
23 changes: 18 additions & 5 deletions lib/ruby_saml/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ module Utils # rubocop:disable Metrics/ModuleLength
(\d+)W # 8: Weeks
)
$/x
UUID_PREFIX = +'_'
UUID_DEFAULT_PREFIX = '_'

# @deprecated Use UUID_DEFAULT_PREFIX instead.
# This constant is intentionally unfrozen for backwards compatibility.
UUID_PREFIX = UUID_DEFAULT_PREFIX.dup

# Checks if the x509 cert provided is expired.
#
Expand Down Expand Up @@ -382,13 +386,22 @@ def retrieve_plaintext(cipher_text, symmetric_key, algorithm)
end
end

def set_prefix(value)
UUID_PREFIX.replace value
# @deprecated Use RubySaml::Settings#sp_uuid_prefix instead.
def set_prefix(_value)
raise NoMethodError.new('RubySaml::Util.set_prefix has been removed. Please use RubySaml::Settings#sp_uuid_prefix instead.')
end

def uuid
"#{UUID_PREFIX}#{SecureRandom.uuid}"
# Generates a UUID with a prefix.
#
# @param prefix [String|false|nil] An explicit prefix override.
# Using nil will use the default prefix, and false will use no prefix.
# @return [String] The generated UUID.
def generate_uuid(prefix = nil)
prefix = prefix.is_a?(FalseClass) ? nil : prefix || UUID_DEFAULT_PREFIX
"#{prefix}#{SecureRandom.uuid}"
end
# @deprecated Use #generate_uuid
alias_method :uuid, :generate_uuid

# Given two strings, attempt to match them as URIs using Rails' parse method. If they can be parsed,
# then the fully-qualified domain name and the host should performa a case-insensitive match, per the
Expand Down
62 changes: 42 additions & 20 deletions test/authrequest_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -161,17 +161,50 @@ class AuthrequestTest < Minitest::Test
assert auth_url.include?('&RelayState=http%3A%2F%2Fexample.com')
end

it "creates request with ID prefixed with default '_'" do
request = RubySaml::Authrequest.new
describe "uuid" do
it "uuid is initialized to nil" do
request = RubySaml::Authrequest.new

assert_match(/^_/, request.uuid)
end
assert_nil(request.uuid)
assert_equal request.request_id, request.uuid
end

it "creates request with ID prefixed with default '_'" do
request = RubySaml::Authrequest.new
request.create(settings)

assert_match(/^_/, request.uuid)
assert_equal request.uuid, request.request_id
end

it "does not change even after repeated #create calls" do
request = RubySaml::Authrequest.new
request.create(settings)

uuid = request.uuid
request.create(settings)

assert_equal uuid, request.uuid
assert_equal request.uuid, request.request_id
end

it "creates request with ID prefixed by Settings#sp_uuid_prefix" do
settings.sp_uuid_prefix = 'test'
request = RubySaml::Authrequest.new
request.create(settings)

assert_match(/^test/, request.uuid)
assert_equal request.uuid, request.request_id
end

it "creates request with ID is prefixed, when :id_prefix is passed" do
RubySaml::Utils::set_prefix("test")
request = RubySaml::Authrequest.new
assert_match(/^test/, request.uuid)
RubySaml::Utils::set_prefix("_")
it "can mutate the uuid" do
request = RubySaml::Authrequest.new
request_id = request.request_id
assert_equal request_id, request.uuid
request.uuid = "new_uuid"
assert_equal "new_uuid", request.uuid
assert_equal request.uuid, request.request_id
end
end

describe "when the target url is not set" do
Expand Down Expand Up @@ -272,17 +305,6 @@ class AuthrequestTest < Minitest::Test
assert auth_doc.to_s =~ /<saml:AuthnContextDeclRef>example\/decl\/ref<\/saml:AuthnContextDeclRef>/
end

describe "#manipulate request_id" do
it "be able to modify the request id" do
authnrequest = RubySaml::Authrequest.new
request_id = authnrequest.request_id
assert_equal request_id, authnrequest.uuid
authnrequest.uuid = "new_uuid"
assert_equal authnrequest.request_id, authnrequest.uuid
assert_equal "new_uuid", authnrequest.request_id
end
end

each_signature_algorithm do |sp_key_algo, sp_hash_algo|
describe "#create_params signing with HTTP-POST binding" do
before do
Expand Down
Loading
Loading