saml_idp 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2 -4
- data/app/controllers/saml_idp/idp_controller.rb +7 -1
- data/lib/saml_idp/assertion_builder.rb +11 -8
- data/lib/saml_idp/controller.rb +19 -3
- data/lib/saml_idp/incoming_metadata.rb +1 -1
- data/lib/saml_idp/logout_builder.rb +18 -10
- data/lib/saml_idp/logout_request_builder.rb +1 -8
- data/lib/saml_idp/logout_response_builder.rb +5 -12
- data/lib/saml_idp/metadata_builder.rb +6 -5
- data/lib/saml_idp/request.rb +2 -1
- data/lib/saml_idp/response_builder.rb +10 -8
- data/lib/saml_idp/version.rb +1 -1
- data/spec/acceptance/idp_controller_spec.rb +0 -2
- data/spec/lib/saml_idp/assertion_builder_spec.rb +12 -2
- data/spec/lib/saml_idp/controller_spec.rb +49 -36
- data/spec/lib/saml_idp/metadata_builder_spec.rb +37 -0
- data/spec/lib/saml_idp/request_spec.rb +1 -0
- data/spec/lib/saml_idp/response_builder_spec.rb +10 -0
- data/spec/rails_app/app/controllers/saml_idp_controller.rb +0 -2
- data/spec/spec_helper.rb +0 -1
- data/spec/support/saml_request_macros.rb +0 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 33d61d6767a79ea9158672fd6ef93c08c70bf4e6
|
4
|
+
data.tar.gz: a2160be6f4da871e04811e6568695328c2e226e9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3675637b4e2c170f3d2c14a9920e61584b95cdfbaf32d8e93492a45c59bd3df32d2c546e6b819b6712c8c039906f527facad0ecff511a7379b4167a2d28e1104
|
7
|
+
data.tar.gz: ba1f8db3b4f1e994b8b07ceb455c9bfc76de28b476ef727d6eecf59418d7df3c089a328fdac0c42dfbe94fcdeea27022a2999dad7341e25ad08cda60faf8cf88
|
data/README.md
CHANGED
@@ -42,9 +42,7 @@ match '/saml/logout' => 'saml_idp#logout', via: [:get, :post, :delete]
|
|
42
42
|
Create a controller that looks like this, customize to your own situation:
|
43
43
|
|
44
44
|
``` ruby
|
45
|
-
class SamlIdpController
|
46
|
-
include SamlIdp::IdpController
|
47
|
-
|
45
|
+
class SamlIdpController < SamlIdp::IdpController
|
48
46
|
def idp_authenticate(email, password) # not using params intentionally
|
49
47
|
user = User.by_email(email).first
|
50
48
|
user && user.valid_password?(password) ? user : nil
|
@@ -209,7 +207,7 @@ end
|
|
209
207
|
To generate the SAML Response it uses a default X.509 certificate and secret key... which isn't so secret.
|
210
208
|
You can find them in `SamlIdp::Default`. The X.509 certificate is valid until year 2032.
|
211
209
|
Obviously you shouldn't use these if you intend to use this in production environments. In that case,
|
212
|
-
within the controller set the properties `x509_certificate` and `secret_key` using a `
|
210
|
+
within the controller set the properties `x509_certificate` and `secret_key` using a `prepend_before_action`
|
213
211
|
callback within the current request context or set them globally via the `SamlIdp.config.x509_certificate`
|
214
212
|
and `SamlIdp.config.secret_key` properties.
|
215
213
|
|
@@ -1,11 +1,17 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
+
|
2
3
|
module SamlIdp
|
3
4
|
class IdpController < ActionController::Base
|
4
5
|
include SamlIdp::Controller
|
5
6
|
|
6
7
|
unloadable unless Rails::VERSION::MAJOR >= 4
|
7
8
|
protect_from_forgery
|
8
|
-
|
9
|
+
|
10
|
+
if Rails::VERSION::MAJOR >= 4
|
11
|
+
before_action :validate_saml_request, only: [:new, :create]
|
12
|
+
else
|
13
|
+
before_filter :validate_saml_request, only: [:new, :create]
|
14
|
+
end
|
9
15
|
|
10
16
|
def new
|
11
17
|
render template: "saml_idp/idp/new"
|
@@ -42,9 +42,12 @@ module SamlIdp
|
|
42
42
|
assertion.Subject do |subject|
|
43
43
|
subject.NameID name_id, Format: name_id_format[:name]
|
44
44
|
subject.SubjectConfirmation Method: Saml::XML::Namespaces::Methods::BEARER do |confirmation|
|
45
|
-
|
46
|
-
|
47
|
-
|
45
|
+
confirmation_hash = {}
|
46
|
+
confirmation_hash[:InResponseTo] = saml_request_id unless saml_request_id.nil?
|
47
|
+
confirmation_hash[:NotOnOrAfter] = not_on_or_after_subject
|
48
|
+
confirmation_hash[:Recipient] = saml_acs_url
|
49
|
+
|
50
|
+
confirmation.SubjectConfirmationData "", confirmation_hash
|
48
51
|
end
|
49
52
|
end
|
50
53
|
assertion.Conditions NotBefore: not_before, NotOnOrAfter: not_on_or_after_condition do |conditions|
|
@@ -52,6 +55,11 @@ module SamlIdp
|
|
52
55
|
restriction.Audience audience_uri
|
53
56
|
end
|
54
57
|
end
|
58
|
+
assertion.AuthnStatement AuthnInstant: now_iso, SessionIndex: reference_string do |statement|
|
59
|
+
statement.AuthnContext do |context|
|
60
|
+
context.AuthnContextClassRef authn_context_classref
|
61
|
+
end
|
62
|
+
end
|
55
63
|
if asserted_attributes
|
56
64
|
assertion.AttributeStatement do |attr_statement|
|
57
65
|
asserted_attributes.each do |friendly_name, attrs|
|
@@ -67,11 +75,6 @@ module SamlIdp
|
|
67
75
|
end
|
68
76
|
end
|
69
77
|
end
|
70
|
-
assertion.AuthnStatement AuthnInstant: now_iso, SessionIndex: reference_string do |statement|
|
71
|
-
statement.AuthnContext do |context|
|
72
|
-
context.AuthnContextClassRef authn_context_classref
|
73
|
-
end
|
74
|
-
end
|
75
78
|
end
|
76
79
|
end
|
77
80
|
alias_method :raw, :fresh
|
data/lib/saml_idp/controller.rb
CHANGED
@@ -14,17 +14,32 @@ module SamlIdp
|
|
14
14
|
end
|
15
15
|
|
16
16
|
attr_accessor :algorithm
|
17
|
-
attr_accessor :saml_request
|
18
17
|
|
19
18
|
protected
|
20
19
|
|
20
|
+
def saml_request
|
21
|
+
@saml_request ||= Struct.new(:request_id) do
|
22
|
+
def authn_request?
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
def issuer
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def acs_url
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
end.new(nil)
|
34
|
+
end
|
35
|
+
|
21
36
|
def validate_saml_request(raw_saml_request = params[:SAMLRequest])
|
22
37
|
decode_request(raw_saml_request)
|
23
38
|
render nothing: true, status: :forbidden unless valid_saml_request?
|
24
39
|
end
|
25
40
|
|
26
41
|
def decode_request(raw_saml_request)
|
27
|
-
|
42
|
+
@saml_request = Request.from_deflated_request(raw_saml_request)
|
28
43
|
end
|
29
44
|
|
30
45
|
def authn_context_classref
|
@@ -37,6 +52,7 @@ module SamlIdp
|
|
37
52
|
audience_uri = opts[:audience_uri] || saml_request.issuer || saml_acs_url[/^(.*?\/\/.*?\/)/, 1]
|
38
53
|
opt_issuer_uri = opts[:issuer_uri] || issuer_uri
|
39
54
|
my_authn_context_classref = opts[:authn_context_classref] || authn_context_classref
|
55
|
+
acs_url = opts[:acs_url] || saml_acs_url
|
40
56
|
expiry = opts[:expiry] || 60*60
|
41
57
|
encryption_opts = opts[:encryption] || nil
|
42
58
|
|
@@ -47,7 +63,7 @@ module SamlIdp
|
|
47
63
|
principal,
|
48
64
|
audience_uri,
|
49
65
|
saml_request_id,
|
50
|
-
|
66
|
+
acs_url,
|
51
67
|
(opts[:algorithm] || algorithm || default_algorithm),
|
52
68
|
my_authn_context_classref,
|
53
69
|
expiry,
|
@@ -128,7 +128,7 @@ module SamlIdp
|
|
128
128
|
end
|
129
129
|
|
130
130
|
def contact_person_document
|
131
|
-
@contact_person_document ||= xpath("//md:ContactPerson", md: metadata_namespace).first
|
131
|
+
@contact_person_document ||= (xpath("//md:ContactPerson", md: metadata_namespace).first || Saml::XML::Document.new)
|
132
132
|
end
|
133
133
|
|
134
134
|
def metadata_namespace
|
@@ -3,35 +3,43 @@ module SamlIdp
|
|
3
3
|
class LogoutBuilder
|
4
4
|
include Signable
|
5
5
|
|
6
|
+
attr_accessor :response_id
|
7
|
+
attr_accessor :issuer_uri
|
8
|
+
attr_accessor :saml_slo_url
|
9
|
+
attr_accessor :algorithm
|
10
|
+
|
11
|
+
def initialize(response_id, issuer_uri, saml_slo_url, algorithm)
|
12
|
+
self.response_id = response_id
|
13
|
+
self.issuer_uri = issuer_uri
|
14
|
+
self.saml_slo_url = saml_slo_url
|
15
|
+
self.algorithm = algorithm
|
16
|
+
end
|
17
|
+
|
6
18
|
# this is an abstract base class.
|
7
19
|
def build
|
8
20
|
raise "#{self.class} must implement build method"
|
9
21
|
end
|
10
22
|
|
11
23
|
def reference_id
|
12
|
-
|
13
|
-
end
|
14
|
-
|
15
|
-
def digest
|
16
|
-
algorithm.hexdigest raw
|
24
|
+
self.response_id
|
17
25
|
end
|
18
26
|
|
19
27
|
def encoded
|
20
28
|
@encoded ||= encode
|
21
|
-
end
|
29
|
+
end
|
22
30
|
|
23
|
-
def raw
|
31
|
+
def raw
|
24
32
|
build
|
25
|
-
end
|
33
|
+
end
|
26
34
|
|
27
35
|
def encode
|
28
36
|
Base64.strict_encode64(raw)
|
29
|
-
end
|
37
|
+
end
|
30
38
|
private :encode
|
31
39
|
|
32
40
|
def response_id_string
|
33
41
|
"_#{response_id}"
|
34
|
-
end
|
42
|
+
end
|
35
43
|
private :response_id_string
|
36
44
|
|
37
45
|
def now_iso
|
@@ -1,18 +1,11 @@
|
|
1
1
|
require 'saml_idp/logout_builder'
|
2
2
|
module SamlIdp
|
3
3
|
class LogoutRequestBuilder < LogoutBuilder
|
4
|
-
attr_accessor :response_id
|
5
|
-
attr_accessor :issuer_uri
|
6
|
-
attr_accessor :saml_slo_url
|
7
4
|
attr_accessor :name_id
|
8
|
-
attr_accessor :algorithm
|
9
5
|
|
10
6
|
def initialize(response_id, issuer_uri, saml_slo_url, name_id, algorithm)
|
11
|
-
|
12
|
-
self.issuer_uri = issuer_uri
|
13
|
-
self.saml_slo_url = saml_slo_url
|
7
|
+
super(response_id, issuer_uri, saml_slo_url, algorithm)
|
14
8
|
self.name_id = name_id
|
15
|
-
self.algorithm = algorithm
|
16
9
|
end
|
17
10
|
|
18
11
|
def build
|
@@ -1,19 +1,12 @@
|
|
1
1
|
require 'saml_idp/logout_builder'
|
2
2
|
module SamlIdp
|
3
3
|
class LogoutResponseBuilder < LogoutBuilder
|
4
|
-
attr_accessor :response_id
|
5
|
-
attr_accessor :issuer_uri
|
6
|
-
attr_accessor :saml_slo_url
|
7
4
|
attr_accessor :saml_request_id
|
8
|
-
attr_accessor :algorithm
|
9
5
|
|
10
6
|
def initialize(response_id, issuer_uri, saml_slo_url, saml_request_id, algorithm)
|
11
|
-
|
12
|
-
self.issuer_uri = issuer_uri
|
13
|
-
self.saml_slo_url = saml_slo_url
|
7
|
+
super(response_id, issuer_uri, saml_slo_url, algorithm)
|
14
8
|
self.saml_request_id = saml_request_id
|
15
|
-
|
16
|
-
end
|
9
|
+
end
|
17
10
|
|
18
11
|
def build
|
19
12
|
builder = Builder::XmlMarkup.new
|
@@ -26,9 +19,9 @@ module SamlIdp
|
|
26
19
|
response.Issuer issuer_uri, xmlns: Saml::XML::Namespaces::ASSERTION
|
27
20
|
sign response
|
28
21
|
response.Status xmlns: Saml::XML::Namespaces::PROTOCOL do |status|
|
29
|
-
status.StatusCode Value: Saml::XML::Namespaces::Statuses::SUCCESS
|
30
|
-
end
|
31
|
-
end
|
22
|
+
status.StatusCode Value: Saml::XML::Namespaces::Statuses::SUCCESS
|
23
|
+
end
|
24
|
+
end
|
32
25
|
end
|
33
26
|
private :build
|
34
27
|
end
|
@@ -24,11 +24,11 @@ module SamlIdp
|
|
24
24
|
|
25
25
|
entity.IDPSSODescriptor protocolSupportEnumeration: protocol_enumeration do |descriptor|
|
26
26
|
build_key_descriptor descriptor
|
27
|
-
build_name_id_formats descriptor
|
28
27
|
descriptor.SingleLogoutService Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
29
28
|
Location: single_logout_service_post_location
|
30
29
|
descriptor.SingleLogoutService Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
|
31
30
|
Location: single_logout_service_redirect_location
|
31
|
+
build_name_id_formats descriptor
|
32
32
|
descriptor.SingleSignOnService Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
|
33
33
|
Location: single_service_post_location
|
34
34
|
build_attribute descriptor
|
@@ -94,10 +94,11 @@ module SamlIdp
|
|
94
94
|
|
95
95
|
def build_contact(el)
|
96
96
|
el.ContactPerson contactType: "technical" do |contact|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
97
|
+
contact.Company technical_contact.company if technical_contact.company
|
98
|
+
contact.GivenName technical_contact.given_name if technical_contact.given_name
|
99
|
+
contact.SurName technical_contact.sur_name if technical_contact.sur_name
|
100
|
+
contact.EmailAddress technical_contact.mail_to_string if technical_contact.mail_to_string
|
101
|
+
contact.TelephoneNumber technical_contact.telephone if technical_contact.telephone
|
101
102
|
end
|
102
103
|
end
|
103
104
|
private :build_contact
|
data/lib/saml_idp/request.rb
CHANGED
@@ -115,10 +115,11 @@ module SamlIdp
|
|
115
115
|
end
|
116
116
|
|
117
117
|
def service_provider?
|
118
|
-
service_provider.valid?
|
118
|
+
service_provider && service_provider.valid?
|
119
119
|
end
|
120
120
|
|
121
121
|
def service_provider
|
122
|
+
return unless issuer.present?
|
122
123
|
@_service_provider ||= ServiceProvider.new((service_provider_finder[issuer] || {}).merge(identifier: issuer))
|
123
124
|
end
|
124
125
|
|
@@ -29,15 +29,17 @@ module SamlIdp
|
|
29
29
|
private :encode
|
30
30
|
|
31
31
|
def build
|
32
|
+
resp_options = {}
|
33
|
+
resp_options[:ID] = response_id_string
|
34
|
+
resp_options[:Version] = "2.0"
|
35
|
+
resp_options[:IssueInstant] = now_iso
|
36
|
+
resp_options[:Destination] = saml_acs_url
|
37
|
+
resp_options[:Consent] = Saml::XML::Namespaces::Consents::UNSPECIFIED
|
38
|
+
resp_options[:InResponseTo] = saml_request_id unless saml_request_id.nil?
|
39
|
+
resp_options["xmlns:samlp"] = Saml::XML::Namespaces::PROTOCOL
|
40
|
+
|
32
41
|
builder = Builder::XmlMarkup.new
|
33
|
-
builder.tag! "samlp:Response",
|
34
|
-
ID: response_id_string,
|
35
|
-
Version: "2.0",
|
36
|
-
IssueInstant: now_iso,
|
37
|
-
Destination: saml_acs_url,
|
38
|
-
Consent: Saml::XML::Namespaces::Consents::UNSPECIFIED,
|
39
|
-
InResponseTo: saml_request_id,
|
40
|
-
"xmlns:samlp" => Saml::XML::Namespaces::PROTOCOL do |response|
|
42
|
+
builder.tag! "samlp:Response", resp_options do |response|
|
41
43
|
response.Issuer issuer_uri, xmlns: Saml::XML::Namespaces::ASSERTION
|
42
44
|
response.tag! "samlp:Status" do |status|
|
43
45
|
status.tag! "samlp:StatusCode", Value: Saml::XML::Namespaces::Statuses::SUCCESS
|
data/lib/saml_idp/version.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/acceptance_helper')
|
2
2
|
|
3
3
|
feature 'IdpController' do
|
4
|
-
|
5
4
|
scenario 'Login via default signup page' do
|
6
5
|
saml_request = make_saml_request("http://foo.example.com/saml/consume")
|
7
6
|
visit "/saml/auth?SAMLRequest=#{CGI.escape(saml_request)}"
|
@@ -12,5 +11,4 @@ feature 'IdpController' do
|
|
12
11
|
current_url.should == 'http://foo.example.com/saml/consume'
|
13
12
|
page.should have_content "foo@example.com"
|
14
13
|
end
|
15
|
-
|
16
14
|
end
|
@@ -31,9 +31,19 @@ module SamlIdp
|
|
31
31
|
expiry
|
32
32
|
) }
|
33
33
|
|
34
|
+
context "No Request ID" do
|
35
|
+
let(:saml_request_id) { nil }
|
36
|
+
|
37
|
+
it "builds a legit raw XML file" do
|
38
|
+
Timecop.travel(Time.zone.local(2010, 6, 1, 13, 0, 0)) do
|
39
|
+
subject.raw.should == "<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 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=\"email-address\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:uri\" FriendlyName=\"emailAddress\"><AttributeValue>foo@example.com</AttributeValue></Attribute></AttributeStatement></Assertion>"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
34
44
|
it "builds a legit raw XML file" do
|
35
45
|
Timecop.travel(Time.zone.local(2010, 6, 1, 13, 0, 0)) do
|
36
|
-
subject.raw.should == "<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><AttributeStatement><Attribute Name=\"email-address\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:uri\" FriendlyName=\"emailAddress\"><AttributeValue>foo@example.com</AttributeValue></Attribute></AttributeStatement
|
46
|
+
subject.raw.should == "<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=\"email-address\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:uri\" FriendlyName=\"emailAddress\"><AttributeValue>foo@example.com</AttributeValue></Attribute></AttributeStatement></Assertion>"
|
37
47
|
end
|
38
48
|
end
|
39
49
|
|
@@ -71,7 +81,7 @@ module SamlIdp
|
|
71
81
|
expiry
|
72
82
|
)
|
73
83
|
Timecop.travel(Time.zone.local(2010, 6, 1, 13, 0, 0)) do
|
74
|
-
builder.raw.should == "<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><AttributeStatement><Attribute Name=\"emailAddress\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:uri\" FriendlyName=\"emailAddress\"><AttributeValue>foo@example.com</AttributeValue></Attribute></AttributeStatement
|
84
|
+
builder.raw.should == "<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=\"emailAddress\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:uri\" FriendlyName=\"emailAddress\"><AttributeValue>foo@example.com</AttributeValue></Attribute></AttributeStatement></Assertion>"
|
75
85
|
end
|
76
86
|
end
|
77
87
|
end
|
@@ -19,11 +19,6 @@ describe SamlIdp::Controller do
|
|
19
19
|
end
|
20
20
|
|
21
21
|
context "SAML Responses" do
|
22
|
-
before(:each) do
|
23
|
-
params[:SAMLRequest] = make_saml_request
|
24
|
-
validate_saml_request
|
25
|
-
end
|
26
|
-
|
27
22
|
let(:principal) { double email_address: "foo@example.com" }
|
28
23
|
let (:encryption_opts) do
|
29
24
|
{
|
@@ -33,28 +28,24 @@ describe SamlIdp::Controller do
|
|
33
28
|
}
|
34
29
|
end
|
35
30
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
31
|
+
context "unsolicited Response" do
|
32
|
+
it "should create a SAML Response" do
|
33
|
+
saml_response = encode_response(principal, { audience_uri: 'http://example.com/issuer', issuer_uri: 'http://example.com', acs_url: 'https://foo.example.com/saml/consume' })
|
34
|
+
response = OneLogin::RubySaml::Response.new(saml_response)
|
35
|
+
response.name_id.should == "foo@example.com"
|
36
|
+
response.issuers.first.should == "http://example.com"
|
37
|
+
response.settings = saml_settings
|
38
|
+
response.is_valid?.should be_truthy
|
39
|
+
end
|
43
40
|
end
|
44
41
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
response = OneLogin::RubySaml::Logoutresponse.new(saml_response, saml_settings)
|
51
|
-
response.validate.should == true
|
52
|
-
response.issuer.should == "http://example.com"
|
53
|
-
end
|
42
|
+
context "solicited Response" do
|
43
|
+
before(:each) do
|
44
|
+
params[:SAMLRequest] = make_saml_request
|
45
|
+
validate_saml_request
|
46
|
+
end
|
54
47
|
|
55
|
-
|
56
|
-
it "should create a SAML Response using the #{algorithm_name} algorithm" do
|
57
|
-
self.algorithm = algorithm_name
|
48
|
+
it "should create a SAML Response" do
|
58
49
|
saml_response = encode_response(principal)
|
59
50
|
response = OneLogin::RubySaml::Response.new(saml_response)
|
60
51
|
response.name_id.should == "foo@example.com"
|
@@ -63,19 +54,41 @@ describe SamlIdp::Controller do
|
|
63
54
|
response.is_valid?.should be_truthy
|
64
55
|
end
|
65
56
|
|
66
|
-
it "should
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
response = OneLogin::RubySaml::
|
72
|
-
response.
|
73
|
-
response.
|
74
|
-
|
75
|
-
|
76
|
-
|
57
|
+
it "should create a SAML Logout Response" do
|
58
|
+
params[:SAMLRequest] = make_saml_logout_request
|
59
|
+
validate_saml_request
|
60
|
+
expect(saml_request.logout_request?).to eq true
|
61
|
+
saml_response = encode_response(principal)
|
62
|
+
response = OneLogin::RubySaml::Logoutresponse.new(saml_response, saml_settings)
|
63
|
+
response.validate.should == true
|
64
|
+
response.issuer.should == "http://example.com"
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
[:sha1, :sha256, :sha384, :sha512].each do |algorithm_name|
|
69
|
+
it "should create a SAML Response using the #{algorithm_name} algorithm" do
|
70
|
+
self.algorithm = algorithm_name
|
71
|
+
saml_response = encode_response(principal)
|
72
|
+
response = OneLogin::RubySaml::Response.new(saml_response)
|
73
|
+
response.name_id.should == "foo@example.com"
|
74
|
+
response.issuers.first.should == "http://example.com"
|
75
|
+
response.settings = saml_settings
|
76
|
+
response.is_valid?.should be_truthy
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should encrypt SAML Response assertion" do
|
80
|
+
self.algorithm = algorithm_name
|
81
|
+
saml_response = encode_response(principal, encryption: encryption_opts)
|
82
|
+
resp_settings = saml_settings
|
83
|
+
resp_settings.private_key = SamlIdp::Default::SECRET_KEY
|
84
|
+
response = OneLogin::RubySaml::Response.new(saml_response, settings: resp_settings)
|
85
|
+
response.document.to_s.should_not match("foo@example.com")
|
86
|
+
response.decrypted_document.to_s.should match("foo@example.com")
|
87
|
+
response.name_id.should == "foo@example.com"
|
88
|
+
response.issuers.first.should == "http://example.com"
|
89
|
+
response.is_valid?.should be_truthy
|
90
|
+
end
|
77
91
|
end
|
78
92
|
end
|
79
93
|
end
|
80
|
-
|
81
94
|
end
|
@@ -15,6 +15,43 @@ module SamlIdp
|
|
15
15
|
'<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://example.com/saml/logout"/>'
|
16
16
|
)
|
17
17
|
end
|
18
|
+
|
19
|
+
context "technical contact" do
|
20
|
+
before do
|
21
|
+
subject.configurator.technical_contact.company = nil
|
22
|
+
subject.configurator.technical_contact.given_name = nil
|
23
|
+
subject.configurator.technical_contact.sur_name = nil
|
24
|
+
subject.configurator.technical_contact.telephone = nil
|
25
|
+
subject.configurator.technical_contact.email_address = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
it "all fields" do
|
29
|
+
subject.configurator.technical_contact.company = "ACME Corporation"
|
30
|
+
subject.configurator.technical_contact.given_name = "Road"
|
31
|
+
subject.configurator.technical_contact.sur_name = "Runner"
|
32
|
+
subject.configurator.technical_contact.telephone = "1-800-555-5555"
|
33
|
+
subject.configurator.technical_contact.email_address = "acme@example.com"
|
34
|
+
|
35
|
+
subject.fresh.should match(
|
36
|
+
'<ContactPerson contactType="technical"><Company>ACME Corporation</Company><GivenName>Road</GivenName><SurName>Runner</SurName><EmailAddress>mailto:acme@example.com</EmailAddress><TelephoneNumber>1-800-555-5555</TelephoneNumber></ContactPerson>'
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "no fields" do
|
41
|
+
subject.fresh.should match(
|
42
|
+
'<ContactPerson contactType="technical"></ContactPerson>'
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "just email" do
|
47
|
+
subject.configurator.technical_contact.email_address = "acme@example.com"
|
48
|
+
subject.fresh.should match(
|
49
|
+
'<ContactPerson contactType="technical"><EmailAddress>mailto:acme@example.com</EmailAddress></ContactPerson>'
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
18
55
|
it "includes logout element as HTTP Redirect" do
|
19
56
|
subject.configurator.single_logout_service_redirect_location = 'https://example.com/saml/logout'
|
20
57
|
subject.fresh.should match(
|
@@ -28,5 +28,15 @@ module SamlIdp
|
|
28
28
|
subject.raw.should == "<samlp:Response ID=\"_abc\" Version=\"2.0\" IssueInstant=\"2010-06-01T13:00:00Z\" Destination=\"http://sportngin.com\" Consent=\"urn:oasis:names:tc:SAML:2.0:consent:unspecified\" InResponseTo=\"134\" xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://example.com</Issuer><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/></samlp:Status><Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"_abc\" IssueInstant=\"2013-07-31T05:00:00Z\" Version=\"2.0\"><Issuer>http://sportngin.com</Issuer><signature>stuff</signature><Subject><NameID Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\">jon.phenow@sportngin.com</NameID><SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><SubjectConfirmationData InResponseTo=\"123\" NotOnOrAfter=\"2013-07-31T05:03:00Z\" Recipient=\"http://saml.acs.url\"/></SubjectConfirmation></Subject><Conditions NotBefore=\"2013-07-31T04:59:55Z\" NotOnOrAfter=\"2013-07-31T06:00:00Z\"><AudienceRestriction><Audience>http://example.com</Audience></AudienceRestriction></Conditions><AttributeStatement><Attribute Name=\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress\"><AttributeValue>jon.phenow@sportngin.com</AttributeValue></Attribute></AttributeStatement><AuthnStatment AuthnInstant=\"2013-07-31T05:00:00Z\" SessionIndex=\"_abc\"><AuthnContext><AuthnContextClassRef>urn:federation:authentication:windows</AuthnContextClassRef></AuthnContext></AuthnStatment></Assertion></samlp:Response>"
|
29
29
|
end
|
30
30
|
end
|
31
|
+
|
32
|
+
context "No request ID" do
|
33
|
+
let(:saml_request_id) { nil }
|
34
|
+
|
35
|
+
it "builds a legit raw XML file without a request ID" do
|
36
|
+
Timecop.travel(Time.zone.local(2010, 6, 1, 13, 0, 0)) do
|
37
|
+
subject.raw.should == "<samlp:Response ID=\"_abc\" Version=\"2.0\" IssueInstant=\"2010-06-01T13:00:00Z\" Destination=\"http://sportngin.com\" Consent=\"urn:oasis:names:tc:SAML:2.0:consent:unspecified\" xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://example.com</Issuer><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/></samlp:Status><Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"_abc\" IssueInstant=\"2013-07-31T05:00:00Z\" Version=\"2.0\"><Issuer>http://sportngin.com</Issuer><signature>stuff</signature><Subject><NameID Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\">jon.phenow@sportngin.com</NameID><SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><SubjectConfirmationData InResponseTo=\"123\" NotOnOrAfter=\"2013-07-31T05:03:00Z\" Recipient=\"http://saml.acs.url\"/></SubjectConfirmation></Subject><Conditions NotBefore=\"2013-07-31T04:59:55Z\" NotOnOrAfter=\"2013-07-31T06:00:00Z\"><AudienceRestriction><Audience>http://example.com</Audience></AudienceRestriction></Conditions><AttributeStatement><Attribute Name=\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress\"><AttributeValue>jon.phenow@sportngin.com</AttributeValue></Attribute></AttributeStatement><AuthnStatment AuthnInstant=\"2013-07-31T05:00:00Z\" SessionIndex=\"_abc\"><AuthnContext><AuthnContextClassRef>urn:federation:authentication:windows</AuthnContextClassRef></AuthnContext></AuthnStatment></Assertion></samlp:Response>"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
31
41
|
end
|
32
42
|
end
|
@@ -1,5 +1,4 @@
|
|
1
1
|
class SamlIdpController < SamlIdp::IdpController
|
2
|
-
|
3
2
|
def idp_authenticate(email, password)
|
4
3
|
{ :email => email }
|
5
4
|
end
|
@@ -7,5 +6,4 @@ class SamlIdpController < SamlIdp::IdpController
|
|
7
6
|
def idp_make_saml_response(user)
|
8
7
|
encode_response(user[:email])
|
9
8
|
end
|
10
|
-
|
11
9
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
require 'saml_idp/logout_request_builder'
|
2
2
|
|
3
3
|
module SamlRequestMacros
|
4
|
-
|
5
4
|
def make_saml_request(requested_saml_acs_url = "https://foo.example.com/saml/consume")
|
6
5
|
auth_request = OneLogin::RubySaml::Authrequest.new
|
7
6
|
auth_url = auth_request.create(saml_settings(requested_saml_acs_url))
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: saml_idp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jon Phenow
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-06-
|
11
|
+
date: 2017-06-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|