Skip to content
Open
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
* Fix ruby 1.8.7 incompatibilities

### 1.10.0 (Mar 21, 2019)
* Add Subject support on AuthNRequest to allow SPs provide info to the IdP about the user to be authenticated
* Add Subject support on AuthnRequest to allow SPs provide info to the IdP about the user to be authenticated
* Improves IdpMetadataParser to allow parse multiple IDPSSODescriptors
* Improves format_cert method to accept certs with /\x0d/
* Forces nokogiri >= 1.8.2 when possible
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ 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').
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').
To add a `saml:AuthnContextDeclRef`, define `settings.authn_context_decl_ref`.

In a SP-initiated flow, the SP can indicate to the IdP the subject that should be authenticated. This is done by defining the `settings.name_identifier_value_requested` before
Expand Down Expand Up @@ -627,7 +627,7 @@ settings.private_key = "PRIVATE KEY TEXT WITH BEGIN/END HEADER AND FOOTER"
Next, you may specify the specific SP SAML messages you would like to sign:

```ruby
settings.security[:authn_requests_signed] = true # Enable signature on AuthNRequest
settings.security[:authn_requests_signed] = true # Enable signature on AuthnRequest
settings.security[:logout_requests_signed] = true # Enable signature on Logout Request
settings.security[:logout_responses_signed] = true # Enable signature on Logout Response
```
Expand Down Expand Up @@ -927,7 +927,7 @@ 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.)
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
Expand Down
100 changes: 98 additions & 2 deletions UPGRADING.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,101 @@
# Ruby SAML Migration Guide

## Upgrading from 2.0.x to 2.1.0

**IMPORTANT: Please read this section carefully as it contains potentially breaking changes!**

RubySaml 2.1.0 introduces a greatly simplified API and class-based errors.

We have attempted our best to "shim" the old functionality and methods to the new API,
in such a way that all tests pass and the gem should work as before. However, there are
a few minor changes to be aware.

### Before upgrading

Please ensure you have first upgraded to latest 2.0.x, and that it is running
smoothly in production. Refer to "Upgrading from 1.x to 2.0.0" below.

### Deprecation of SP message builder classes (`Authrequest`, `Logoutrequest`, `SloLogoutresponse`)

| Old Class | New Class |
|--------------------------------|------------------------------------------|
| `RubySaml::Authrequest` | `RubySaml::Builders::SP::AuthnRequest` |
| `RubySaml::Logoutrequest` | `RubySaml::Builders::SP::LogoutRequest` |
| `RubySaml::SloLogoutresponse` | `RubySaml::Builders::SP::LogoutResponse` |

For each of these, the method usage has changed:

| Old Method | New Method |
|------------------------|-------------------------------------------|
| `#create` | `#url` (or `#redirect_url` / `#post_url`) |
| `#create_params` | `#body` (or `#post_body`) |
| `#create_xml_document` | `#xml` |

### Deprecation of IdP message parser classes (`Response`, `Logoutresponse`, `SloLogoutrequest`)

| Old Class | New Class |
|------------------------------|------------------------------------------|
| `RubySaml::Response` | `RubySaml::Parsers::IdP::Response` |
| `RubySaml::Logoutresponse` | `RubySaml::Parsers::IdP::LogoutResponse` |
| `RubySaml::SloLogoutrequest` | `RubySaml::Parsers::IdP::LogoutRequest` |


### Deprecation of metadata-related classes

| Old Class | New Class |
|-------------------------------|------------------------------------|
| `RubySaml::Metadata` | `RubySaml::Builders::SP::Metadata` |
| `RubySaml::IdpMetadataParser` | `RubySaml::Parsers::IdP::Metadata` |


### New Shortcut API

```ruby
app = RubySaml::SPApplication(settings)

# Create your RubySaml::Builder::SP::AuthnRequest object
app.build('AuthnRequest', **options)

app.parse('Response', params)
```


```ruby
class MySamlController < ActionController::Base
def index
sp_app = RubySaml::SPApplication(settings)
authn = sp_app.build('AuthnRequest', **options)

if authn.binding_post?
@saml_message = authn
render 'saml_post_form'
else
redirect_to authn.redirect_url, allow_other_host: true
end
end
end
```

```html
<form id="saml-post-form" method="post" action="<%= @saml_message.post_url %>" style="display:none;">
<% @saml_message.post_body.each do |name, value| %>
<input type="hidden" name="<%= name %>" value="<%= value %>" />
<% end %>
<noscript><button type="submit">Continue to Identity Provider</button></noscript>
</form>
<script>document.getElementById('saml-post-form').submit();</script>
```


ap

authn.url
authn.url


RubySaml::Application(settings).sp.build('AuthnRequest', **options)


## Upgrading from 1.x to 2.0.0

**IMPORTANT: Please read this section carefully as it contains breaking changes!**
Expand Down Expand Up @@ -149,7 +245,7 @@ this prefix is now set using `settings.sp_uuid_prefix`:
# Change the default prefix from `_` to `my_id_`
settings.sp_uuid_prefix = 'my_id_'

# Create the AuthNRequest message
# Create the AuthnRequest message
request = RubySaml::Authrequest.new
request.create(settings)
request.uuid #=> "my_id_a1b3c5d7-9f1e-3d5c-7b1a-9f1e3d5c7b1a"
Expand Down Expand Up @@ -269,7 +365,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,
Add Subject support on AuthNRequest to allow SPs provide info to the IdP about the user
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/

## Upgrading from 1.8.0 to 1.9.0
Expand Down
35 changes: 27 additions & 8 deletions lib/ruby_saml.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,40 @@
# frozen_string_literal: true

require 'cgi'
require 'uri'
require 'zlib'
require 'base64'
require 'time'
require 'nokogiri'

require 'ruby_saml/logging'
require 'ruby_saml/xml'
require 'ruby_saml/saml_message'
require 'ruby_saml/settings'
require 'ruby_saml/memoizable'

require 'ruby_saml/authrequest'
require 'ruby_saml/logoutrequest'
require 'ruby_saml/logoutresponse'
require 'ruby_saml/attributes'
require 'ruby_saml/slo_logoutrequest'
require 'ruby_saml/slo_logoutresponse'
require 'ruby_saml/response'
require 'ruby_saml/settings'
require 'ruby_saml/attribute_service'

require 'ruby_saml/sp/builders/message_builder'
require 'ruby_saml/sp/builders/authn_request'
require 'ruby_saml/sp/builders/logout_request'
require 'ruby_saml/sp/builders/logout_response'
require 'ruby_saml/metadata'

# TODO: Extract errors to have common base class
require 'ruby_saml/setting_error'
require 'ruby_saml/http_error'
require 'ruby_saml/validation_error'
require 'ruby_saml/metadata'

require 'ruby_saml/attribute_service'
require 'ruby_saml/attributes'
require 'ruby_saml/saml_message'
require 'ruby_saml/response'
require 'ruby_saml/logoutresponse'
require 'ruby_saml/slo_logoutrequest'
require 'ruby_saml/idp_metadata_parser'

require 'ruby_saml/pem_formatter'
require 'ruby_saml/utils'
require 'ruby_saml/version'
Expand Down
146 changes: 29 additions & 117 deletions lib/ruby_saml/authrequest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,151 +6,63 @@
require "ruby_saml/setting_error"

module RubySaml
# SAML2 Authentication. AuthNRequest (SSO SP initiated, Builder)
# SAML2 Authentication. AuthnRequest (SSO SP initiated)
#
# Shim class that delegates to RubySaml::Sp::Builders::AuthnRequest
class Authrequest < SamlMessage

# AuthNRequest ID
# AuthnRequest ID
attr_accessor :uuid
alias_method :request_id, :uuid

# Creates the AuthNRequest string.
# 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
# @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"))
request_params = +"#{params_prefix}SAMLRequest=#{saml_request}"
params.each_pair do |key, value|
request_params << "&#{key}=#{CGI.escape(value.to_s)}"
end
raise SettingError.new "Invalid settings, idp_sso_service_url is not set!" if settings.idp_sso_service_url.nil? || settings.idp_sso_service_url.empty?
@login_url = settings.idp_sso_service_url + request_params
create_builder(settings, params)
@login_url = builder.url
end

# Creates the Get parameters for the request.
# @param settings [RubySaml::Settings|nil] Toolkit settings
# @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
# @return [Hash] Parameters
def create_params(settings, params={})
# The method expects :RelayState but sometimes we get 'RelayState' instead.
# Based on the HashWithIndifferentAccess value in Rails we could experience
# conflicts so this line will solve them.
binding_redirect = settings.idp_sso_service_binding == Utils::BINDINGS[:redirect]
relay_state = params[:RelayState] || params['RelayState']

if relay_state.nil?
params.delete(:RelayState)
params.delete('RelayState')
end
create_builder(settings, params)
is_redirect = settings.idp_sso_service_binding == Utils::BINDINGS[:redirect]

request_doc = create_authentication_xml_doc(settings)
# Log the request
request_doc = builder.send(:build_xml_document)
request = request_doc.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)

Logging.debug "Created AuthnRequest: #{request}"

base64_request = RubySaml::XML::Decoder.encode_message(request, compress: binding_redirect)
request_params = {"SAMLRequest" => base64_request}
sp_signing_key = settings.get_sp_signing_key

if binding_redirect && settings.security[:authn_requests_signed] && sp_signing_key
params['SigAlg'] = settings.get_sp_signature_method
url_string = RubySaml::Utils.build_query(
type: 'SAMLRequest',
data: base64_request,
relay_state: relay_state,
sig_alg: params['SigAlg']
)
sign_algorithm = RubySaml::XML.hash_algorithm(settings.get_sp_signature_method)
signature = sp_signing_key.sign(sign_algorithm.new, url_string)
params['Signature'] = Base64.strict_encode64(signature)
end

params.each_pair do |key, value|
request_params[key] = value.to_s
end

request_params
# Get payload parameters
builder.send(:build_payload, is_redirect)
end

# Creates the SAMLRequest String.
# @param settings [RubySaml::Settings|nil] Toolkit settings
# @return [String] The SAMLRequest String.
def create_authentication_xml_doc(settings)
noko = create_xml_document(settings)
sign_document(noko, settings)
def create_authentication_xml_doc(settings, params = nil)
create_builder(settings, params)
builder.send(:build_xml_document)
end

def create_xml_document(settings)
time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
assign_uuid(settings)
root_attributes = {
'xmlns:samlp' => RubySaml::XML::NS_PROTOCOL,
'xmlns:saml' => RubySaml::XML::NS_ASSERTION,
'ID' => uuid,
'IssueInstant' => time,
'Version' => '2.0',
'Destination' => settings.idp_sso_service_url,
'IsPassive' => settings.passive,
'ProtocolBinding' => settings.protocol_binding,
'AttributeConsumingServiceIndex' => settings.attributes_index,
'ForceAuthn' => settings.force_authn,
'AssertionConsumerServiceURL' => settings.assertion_consumer_service_url
}.compact.reject { |_, v| v.respond_to?(:empty?) && v.empty? }

builder = Nokogiri::XML::Builder.new do |xml|
xml['samlp'].AuthnRequest(root_attributes) do
# Add Issuer element if sp_entity_id is present
xml['saml'].Issuer(settings.sp_entity_id) if settings.sp_entity_id

# Add Subject element if name_identifier_value_requested is present
if settings.name_identifier_value_requested
xml['saml'].Subject do
nameid_attrs = {}
nameid_attrs['Format'] = settings.name_identifier_format if settings.name_identifier_format
xml['saml'].NameID(settings.name_identifier_value_requested, nameid_attrs)
xml['saml'].SubjectConfirmation(Method: 'urn:oasis:names:tc:SAML:2.0:cm:bearer')
end
end

# Add NameIDPolicy element if name_identifier_format is present
if settings.name_identifier_format
xml['samlp'].NameIDPolicy(AllowCreate: 'true', Format: settings.name_identifier_format)
end

# Add RequestedAuthnContext if authn_context or authn_context_decl_ref is present
if settings.authn_context || settings.authn_context_decl_ref
comparison = settings.authn_context_comparison || 'exact'

xml['samlp'].RequestedAuthnContext(Comparison: comparison) do
Array(settings.authn_context).each do |authn_context_class_ref|
xml['saml'].AuthnContextClassRef(authn_context_class_ref)
end

Array(settings.authn_context_decl_ref).each do |authn_context_decl_ref|
xml['saml'].AuthnContextDeclRef(authn_context_decl_ref)
end
end
end
end
end

builder.doc
def sign_document(noko, _settings = nil)
builder.send(:sign_xml_document!, noko)
end

def sign_document(noko, settings)
cert, private_key = settings.get_sp_signing_pair
if settings.idp_sso_service_binding == Utils::BINDINGS[:post] && settings.security[:authn_requests_signed] && private_key && cert
RubySaml::XML::DocumentSigner.sign_document!(noko, private_key, cert, settings.get_sp_signature_method, settings.get_sp_digest_method)
else
noko
end
end
private

attr_reader :builder

def assign_uuid(settings)
@uuid ||= RubySaml::Utils.generate_uuid(settings.sp_uuid_prefix) # rubocop:disable Naming/MemoizedInstanceVariableName
def create_builder(settings, params = {})
@uuid ||= RubySaml::Utils.generate_uuid(settings.sp_uuid_prefix)
@builder ||= RubySaml::Sp::Builders::AuthnRequest.new(
settings,
id: @uuid,
params: params
)
end
end
end
Loading
Loading