Skip to content

Commit

Permalink
[ci skip] WIP - Controller SPEC
Browse files Browse the repository at this point in the history
  • Loading branch information
zogoo committed Jan 7, 2024
1 parent d14ae6f commit 0ed50c6
Show file tree
Hide file tree
Showing 10 changed files with 289 additions and 160 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ Your application need to manage 2 things for configuration.
If you have a method called `asserted_attributes` in your Principal class,
there is no need to define it here in the config.

config.attributes =>
config.saml_attributes =>
{
<friendly_name> => { # required (ex "eduPersonAffiliation")
"name" => <attrname> # required (ex "urn:oid:1.3.6.1.4.1.5923.1.1.1.1")
Expand All @@ -139,7 +139,7 @@ Your application need to manage 2 things for configuration.
}

# EXAMPLE ##
config.attributes = {
config.saml_attributes = {
GivenName: {
getter: :first_name,
},
Expand Down
10 changes: 4 additions & 6 deletions lib/saml_idp/assertion_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ def initialize(
saml_acs_url:,
raw_algorithm:,
authn_context_classref:,
name_id_formats_opts:,
expiry: 60*60,
encryption_opts: nil,
session_expiry: nil,
name_id_formats_opts: nil,
asserted_attributes_opts: nil,
asserted_attributes_opts: nil,
x509_certificate: nil,
secret_key: nil,
password: nil
Expand All @@ -52,7 +52,7 @@ def initialize(
self.authn_context_classref = authn_context_classref
self.expiry = expiry
self.encryption_opts = encryption_opts
self.session_expiry = session_expiry.nil? ? config.session_expiry : session_expiry
self.session_expiry = session_expiry
self.name_id_formats_opts = name_id_formats_opts
self.asserted_attributes_opts = asserted_attributes_opts
self.x509_certificate = x509_certificate
Expand Down Expand Up @@ -129,8 +129,6 @@ def asserted_attributes
asserted_attributes_opts
elsif principal.respond_to?(:asserted_attributes)
principal.send(:asserted_attributes)
elsif !config.attributes.nil? && !config.attributes.empty?
config.attributes
end
end
private :asserted_attributes
Expand Down Expand Up @@ -173,7 +171,7 @@ def name_id_format
private :name_id_format

def name_id_formats
@name_id_formats ||= (name_id_formats_opts || config.name_id.formats)
@name_id_formats ||= name_id_formats_opts
end
private :name_id_formats

Expand Down
43 changes: 25 additions & 18 deletions lib/saml_idp/controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,42 +17,45 @@ module Controller
protected

def saml_request
@saml_request ||= Struct.new(:request_id) do
@saml_request ||= Struct.new(
:request_id,
:issue_url,
:acs_url
) do
def authn_request?
true
end

def issuer
nil
def idp_initiated?
true
end

def acs_url
nil
def issuer
url = URI(issue_url)
url.query = nil
url.to_s
end
end.new(nil)
end.new(nil, idp_config.issuer_uri, sp_config.assertion_consumer_services.first[:location])
end

def validate_saml_request(raw_saml_request = params[:SAMLRequest])
decode_request(raw_saml_request)
def validate_saml_request
decode_request
return true if valid_saml_request?

head :forbidden if defined?(::Rails)
false
end

def decode_request(raw_saml_request)
@saml_request = Request.from_deflated_request(raw_saml_request, sp_config)
end

def authn_context_classref
SamlIdp::XML::Namespaces::AuthnContext::ClassRef::PASSWORD
def decode_request
@saml_request ||= Request.from_deflated_request(raw_saml_request, sp_config)
sp_config.load_saml_request(@saml_request)
end

def encode_authn_response(principal)
idp_config.load_saml_request(saml_request)
SamlIdp::SamlResponse.new(
principal: principal,
idp_config: idp_config,
sp_config: sp_config,
saml_request: saml_request
).build
end
Expand All @@ -64,11 +67,11 @@ def encode_logout_response(_principal)
).signed
end

def encode_response(principal, opts = {})
def encode_response(principal)
if saml_request.authn_request?
encode_authn_response(principal, opts)
encode_authn_response(principal)
elsif saml_request.logout_request?
encode_logout_response(principal, opts)
encode_logout_response(principal)
else
raise "Unknown request: #{saml_request}"
end
Expand All @@ -88,6 +91,10 @@ def sp_config
end
end

def raw_saml_request
raise "Missing SAML SP initiated request getter implementation"
end

def sp_config_hash
nil
end
Expand Down
47 changes: 23 additions & 24 deletions lib/saml_idp/idp_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,45 @@
module SamlIdp
class IdPConfig
IDP_REQUIRED_ATTR = [
:entity_id,
:audience_uri,
:issuer_uri,
:saml_acs_url,
:base_url,
:x509_certificate,
:secret_key,
:password,
:organization_name,
:organization_url,
:attribute_service_location,
:name_id_formats,
:single_service_post_location,
:single_service_redirect_location,
:single_logout_service_post_location,
:single_logout_service_redirect_location
].freeze

IDP_OPTIONAL_ATTR = [
:entity_id,
:issuer_uri,
:reference_id,
:response_id,
:algorithm,
:attributes,
:raw_algorithm,
:saml_attributes,
:session_expiry,
:authn_context_classref,
:expiry,
:encryption,
:name_id_format,
:encryption_config,
:asserted_attributes,
:signed_message,
:signed_assertion,
:compress
:compress,
:single_logout_service_post_location,
:single_logout_service_redirect_location,
:attribute_service_location,
:organization_name,
:organization_url
].freeze

ALL_ATTRIBUTES = (IDP_REQUIRED_ATTR + IDP_OPTIONAL_ATTR).freeze

DEFAULT_VALUES = {
encryption: nil,
encryption_config: nil,
signed_message: false,
signed_assertion: true,
compress: false,
algorithm: :sha256,
raw_algorithm: :sha256,
authn_context_classref: SamlIdp::XML::Namespaces::AuthnContext::ClassRef::PASSWORD,
attributes: {},
session_expiry: 0,
Expand All @@ -60,14 +59,20 @@ def initialize(attributes = {})
instance_variable_set("@#{attr}", attributes.key?(attr) ? attributes[attr] : DEFAULT_VALUES[attr])
end

self.reference_id ||= SecureRandom.uuid
self.response_id ||= SecureRandom.uuid
@reference_id ||= SecureRandom.uuid
@response_id ||= SecureRandom.uuid
@entity_id ||= @base_url
@issuer_uri ||= @base_url
end

def single_logout_url
single_logout_service_post_location || single_logout_service_redirect_location
end

def algorithm
OpenSSL::Digest.const_get(raw_algorithm.to_s.upcase)
end

# formats
# getter
def name_id
Expand All @@ -84,12 +89,6 @@ def mail_to_string
end
end

def load_saml_request(saml_request)
self.audience_uri = idp_config.audience_uri || saml_request.issuer || saml_request.acs_url[/^(.*?\/\/.*?\/)/, 1]
self.issuer_uri = idp_config.issuer_uri || saml_request.to_s.split("?").first || "http://example.com"
self.acs_url = idp_config.acs_url || saml_request.acs_url
end

def to_hash
instance_variables.each_with_object({}) do |var, hash|
hash[var.to_s.delete("@").to_sym] = instance_variable_get(var)
Expand Down
2 changes: 1 addition & 1 deletion lib/saml_idp/metadata_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def reference_string
private :reference_string

def entity_id
configurator.entity_id.presence || configurator.base_saml_location
configurator.entity_id.presence
end
private :entity_id

Expand Down
11 changes: 6 additions & 5 deletions lib/saml_idp/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ def self.from_deflated_request(raw, sp_config)

attr_accessor :raw_xml, :sp_config, :error_msg

delegate :config, to: :SamlIdp
private :config
delegate :xpath, to: :document
private :xpath

Expand Down Expand Up @@ -63,7 +61,11 @@ def requested_authn_context
end

def acs_url
authn_request["AssertionConsumerServiceURL"].to_s || sp_config.acs_url
authn_request["AssertionConsumerServiceURL"].to_s || sp_config.assertion_consumer_services.first[:location]
end

def protocol_binding
authn_request["ProtocolBinding"].to_s || sp_config.assertion_consumer_services.first[:binding]
end

def logout_url
Expand Down Expand Up @@ -111,8 +113,7 @@ def valid?
def valid_signature?
# Force signatures for logout requests because there is no other protection against a cross-site DoS.
# Validate signature when metadata specify AuthnRequest should be signed
metadata = sp_config.current_metadata
if logout_request? || authn_request? && metadata.respond_to?(:sign_authn_request?) && metadata.sign_authn_request?
if logout_request? || authn_request? && sp_config.sign_authn_request
document.valid_signature?(sp_config.fingerprint)
else
true
Expand Down
27 changes: 14 additions & 13 deletions lib/saml_idp/saml_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
require 'saml_idp/response_builder'
module SamlIdp
class SamlResponse
attr_accessor :principal, :idp_config, :saml_request
attr_accessor :principal, :idp_config, :saml_request, :sp_config

def initialize(principal:, idp_config:, saml_request:)
def initialize(principal:, idp_config:, sp_config: ,saml_request:)
self.principal = principal
self.idp_config = idp_config
self.sp_config = sp_config
self.saml_request = saml_request
end

Expand All @@ -17,7 +18,7 @@ def build
end

def signed_assertion
if idp_config.encryption
if idp_config.encryption_config
assertion_builder.encrypt(sign: true)
elsif idp_config.signed_assertion
assertion_builder.signed
Expand All @@ -28,21 +29,21 @@ def signed_assertion
private :signed_assertion

def encoded_message
response_builder.encoded(signed_message: idp_config.signed_message, compress: idp_config.compression)
response_builder.encoded(signed_message: idp_config.signed_message, compress: idp_config.compress)
end
private :encoded_message

def response_builder
ResponseBuilder.new(
response_id: idp_config.response_id,
issuer_uri: idp_config.issuer_uri,
saml_acs_url: idp_config.saml_acs_url,
saml_request_id: saml_request.request_id,
assertion_and_signature: idp_config.assertion_and_signature,
assertion_and_signature: signed_assertion,
raw_algorithm: idp_config.raw_algorithm,
x509_certificate: idp_config.x509_certificate,
secret_key: idp_config.secret_key,
password: idp_config.password
password: idp_config.password,
saml_acs_url: sp_config.acs_url,
)
end
private :response_builder
Expand All @@ -52,19 +53,19 @@ def assertion_builder
reference_id: SecureRandom.uuid,
issuer_uri: idp_config.issuer_uri,
principal: principal,
audience_uri: idp_config.audience_uri,
saml_request_id: saml_request.request_id,
saml_acs_url: idp_config.saml_acs_url,
raw_algorithm: idp_config.raw_algorithm,
authn_context_classref: idp_config.authn_context_classref,
expiry: idp_config.expiry,
encryption_opts: idp_config.encryption_opts,
encryption_opts: idp_config.encryption_config,
session_expiry: idp_config.session_expiry,
name_id_formats_opts: idp_config.name_id_formats_opts,
asserted_attributes_opts: idp_config.asserted_attributes_opts,
name_id_formats_opts: idp_config.name_id_formats,
asserted_attributes_opts: idp_config.saml_attributes,
x509_certificate: idp_config.x509_certificate,
secret_key: idp_config.secret_key,
password: idp_config.password
password: idp_config.password,
audience_uri: sp_config.audience_uri,
saml_acs_url: sp_config.acs_url,
)
end
private :assertion_builder
Expand Down
Loading

0 comments on commit 0ed50c6

Please sign in to comment.