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

Added options to allow IDP to dynamically change values of Name ID and attributes when creating a SAML response. #88

Merged
merged 3 commits into from
Jul 26, 2021
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
/Gemfile.lock
/gemfiles/*.gemfile.lock
/.byebug_history
/vendor
31 changes: 28 additions & 3 deletions lib/saml_idp/assertion_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,26 @@ class AssertionBuilder
attr_accessor :expiry
attr_accessor :encryption_opts
attr_accessor :session_expiry
attr_accessor :name_id_formats_opts
attr_accessor :asserted_attributes_opts

delegate :config, to: :SamlIdp

def initialize(reference_id, issuer_uri, principal, audience_uri, saml_request_id, saml_acs_url, raw_algorithm, authn_context_classref, expiry=60*60, encryption_opts=nil, session_expiry=nil)
def initialize(
reference_id,
issuer_uri,
principal,
audience_uri,
saml_request_id,
saml_acs_url,
raw_algorithm,
authn_context_classref,
expiry=60*60,
encryption_opts=nil,
session_expiry=nil,
name_id_formats_opts = nil,
asserted_attributes_opts = nil
)
self.reference_id = reference_id
self.issuer_uri = issuer_uri
self.principal = principal
Expand All @@ -31,6 +47,8 @@ def initialize(reference_id, issuer_uri, principal, audience_uri, saml_request_i
self.expiry = expiry
self.encryption_opts = encryption_opts
self.session_expiry = session_expiry.nil? ? config.session_expiry : session_expiry
self.name_id_formats_opts = name_id_formats_opts
self.asserted_attributes_opts = asserted_attributes_opts
end

def fresh
Expand Down Expand Up @@ -98,7 +116,9 @@ def encrypt(opts = {})
end

def asserted_attributes
if principal.respond_to?(:asserted_attributes)
if asserted_attributes_opts.present? && !asserted_attributes_opts.empty?
asserted_attributes_opts
elsif principal.respond_to?(:asserted_attributes)
principal.send(:asserted_attributes)
elsif !config.attributes.nil? && !config.attributes.empty?
config.attributes
Expand Down Expand Up @@ -139,10 +159,15 @@ def name_id_getter
private :name_id_getter

def name_id_format
@name_id_format ||= NameIdFormatter.new(config.name_id.formats).chosen
@name_id_format ||= NameIdFormatter.new(name_id_formats).chosen
end
private :name_id_format

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

def reference_string
"_#{reference_id}"
end
Expand Down
6 changes: 5 additions & 1 deletion lib/saml_idp/controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ def encode_authn_response(principal, opts = {})
session_expiry = opts[:session_expiry]
encryption_opts = opts[:encryption] || nil
signed_message_opts = opts[:signed_message] || false
name_id_formats_opts = opts[:name_id_formats] || nil
asserted_attributes_opts = opts[:attributes] || nil

SamlResponse.new(
reference_id,
Expand All @@ -75,7 +77,9 @@ def encode_authn_response(principal, opts = {})
expiry,
encryption_opts,
session_expiry,
signed_message_opts
signed_message_opts,
name_id_formats_opts,
asserted_attributes_opts
).build
end

Expand Down
39 changes: 24 additions & 15 deletions lib/saml_idp/saml_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,26 @@ class SamlResponse
attr_accessor :encryption_opts
attr_accessor :session_expiry
attr_accessor :signed_message_opts
attr_accessor :name_id_formats_opts
attr_accessor :asserted_attributes_opts

def initialize(reference_id,
response_id,
issuer_uri,
principal,
audience_uri,
saml_request_id,
saml_acs_url,
algorithm,
authn_context_classref,
expiry=60*60,
encryption_opts=nil,
session_expiry=0,
signed_message_opts
)
def initialize(
reference_id,
response_id,
issuer_uri,
principal,
audience_uri,
saml_request_id,
saml_acs_url,
algorithm,
authn_context_classref,
expiry=60*60,
encryption_opts=nil,
session_expiry=0,
signed_message_opts=false,
name_id_formats_opts = nil,
asserted_attributes_opts = nil
)
self.reference_id = reference_id
self.response_id = response_id
self.issuer_uri = issuer_uri
Expand All @@ -48,6 +53,8 @@ def initialize(reference_id,
self.encryption_opts = encryption_opts
self.session_expiry = session_expiry
self.signed_message_opts = signed_message_opts
self.name_id_formats_opts = name_id_formats_opts
self.asserted_attributes_opts = asserted_attributes_opts
end

def build
Expand Down Expand Up @@ -88,7 +95,9 @@ def assertion_builder
authn_context_classref,
expiry,
encryption_opts,
session_expiry
session_expiry,
name_id_formats_opts,
asserted_attributes_opts
end
private :assertion_builder
end
Expand Down
73 changes: 73 additions & 0 deletions spec/lib/saml_idp/assertion_builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ module SamlIdp
key_transport: 'rsa-oaep-mgf1p',
}
end
let(:session_expiry) { nil }
let(:name_id_formats_opt) { nil }
let(:asserted_attributes_opt) { nil }
subject { described_class.new(
reference_id,
issuer_uri,
Expand Down Expand Up @@ -103,6 +106,76 @@ module SamlIdp
expect(encrypted_xml).to_not match(audience_uri)
end

describe "with name_id_formats_opt" do
let(:name_id_formats_opt) {
{
persistent: -> (principal) {
principal.unique_identifier
}
}
}
it "delegates name_id_formats to opts" do
UserWithUniqueId = Struct.new(:unique_identifier, :email, :asserted_attributes)
principal = UserWithUniqueId.new('unique_identifier_123456', 'foo@example.com', { emailAddress: { getter: :email } })
builder = described_class.new(
reference_id,
issuer_uri,
principal,
audience_uri,
saml_request_id,
saml_acs_url,
algorithm,
authn_context_classref,
expiry,
encryption_opts,
session_expiry,
name_id_formats_opt,
asserted_attributes_opt
)
Timecop.travel(Time.zone.local(2010, 6, 1, 13, 0, 0)) do
expect(builder.raw).to eq("<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"_abc\" IssueInstant=\"2010-06-01T13:00:00Z\" Version=\"2.0\"><Issuer>http://sportngin.com</Issuer><Subject><NameID Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent\">unique_identifier_123456</NameID><SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><SubjectConfirmationData InResponseTo=\"123\" NotOnOrAfter=\"2010-06-01T13:03:00Z\" Recipient=\"http://saml.acs.url\"></SubjectConfirmationData></SubjectConfirmation></Subject><Conditions NotBefore=\"2010-06-01T12:59:55Z\" NotOnOrAfter=\"2010-06-01T16:00:00Z\"><AudienceRestriction><Audience>http://example.com</Audience></AudienceRestriction></Conditions><AuthnStatement AuthnInstant=\"2010-06-01T13:00:00Z\" SessionIndex=\"_abc\"><AuthnContext><AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef></AuthnContext></AuthnStatement><AttributeStatement><Attribute Name=\"emailAddress\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:uri\" FriendlyName=\"emailAddress\"><AttributeValue>foo@example.com</AttributeValue></Attribute></AttributeStatement></Assertion>")
end
end
end

describe "with asserted_attributes_opt" do
let(:asserted_attributes_opt) {
{
'GivenName' => {
getter: :first_name
},
'SurName' => {
getter: -> (principal) {
principal.last_name
}
}
}
}

it "delegates asserted_attributes to opts" do
UserWithName = Struct.new(:email, :first_name, :last_name)
principal = UserWithName.new('foo@example.com', 'George', 'Washington')
builder = described_class.new(
reference_id,
issuer_uri,
principal,
audience_uri,
saml_request_id,
saml_acs_url,
algorithm,
authn_context_classref,
expiry,
encryption_opts,
session_expiry,
name_id_formats_opt,
asserted_attributes_opt
)
Timecop.travel(Time.zone.local(2010, 6, 1, 13, 0, 0)) do
expect(builder.raw).to eq("<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"_abc\" IssueInstant=\"2010-06-01T13:00:00Z\" Version=\"2.0\"><Issuer>http://sportngin.com</Issuer><Subject><NameID Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\">foo@example.com</NameID><SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><SubjectConfirmationData InResponseTo=\"123\" NotOnOrAfter=\"2010-06-01T13:03:00Z\" Recipient=\"http://saml.acs.url\"></SubjectConfirmationData></SubjectConfirmation></Subject><Conditions NotBefore=\"2010-06-01T12:59:55Z\" NotOnOrAfter=\"2010-06-01T16:00:00Z\"><AudienceRestriction><Audience>http://example.com</Audience></AudienceRestriction></Conditions><AuthnStatement AuthnInstant=\"2010-06-01T13:00:00Z\" SessionIndex=\"_abc\"><AuthnContext><AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef></AuthnContext></AuthnStatement><AttributeStatement><Attribute Name=\"GivenName\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:uri\" FriendlyName=\"GivenName\"><AttributeValue>George</AttributeValue></Attribute><Attribute Name=\"SurName\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:uri\" FriendlyName=\"SurName\"><AttributeValue>Washington</AttributeValue></Attribute></AttributeStatement></Assertion>")
end
end
end

describe "with custom session_expiry configuration" do
let(:config) { SamlIdp::Configurator.new }
before do
Expand Down