omniauth-saml-rstr 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,109 @@
1
+ # OmniAuth SAML-RSTR
2
+
3
+ An XML & SAML strategy for OmniAuth integration with ADFS 2.0.
4
+
5
+ https://github.com/highgroove/omniauth-saml-rstr
6
+
7
+ ## Requirements
8
+
9
+ * [OmniAuth](http://www.omniauth.org/) 1.0+
10
+ * nokogiri 1.5
11
+ * Ruby 1.9.2
12
+
13
+ ## Usage
14
+
15
+ Use the SAML strategy as a middleware in your application:
16
+
17
+ ```ruby
18
+ require 'omniauth'
19
+ use OmniAuth::Strategies::SAML_RSTR,
20
+ :assertion_consumer_service_url => "consumer_service_url",
21
+ :issuer => "issuer",
22
+ :idp_sso_target_url => "idp_sso_target_url",
23
+ :idp_cert => "-----BEGIN CERTIFICATE-----\n...-----END CERTIFICATE-----",
24
+ :idp_cert_fingerprint => "E7:91:B2:E1:...",
25
+ :name_identifier_format => "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
26
+ ```
27
+
28
+ or in your Rails application:
29
+
30
+ in `Gemfile`:
31
+
32
+ ```ruby
33
+ gem 'omniauth-saml-rstr'
34
+ ```
35
+
36
+ and in `config/initializers/omniauth.rb`:
37
+
38
+ ```ruby
39
+ Rails.application.config.middleware.use OmniAuth::Builder do
40
+ provider :saml_rstr,
41
+ :assertion_consumer_service_url => "consumer_service_url",
42
+ :issuer => "rails-application",
43
+ :idp_sso_target_url => "idp_sso_target_url",
44
+ :idp_cert => "-----BEGIN CERTIFICATE-----\n...-----END CERTIFICATE-----",
45
+ :idp_cert_fingerprint => "E7:91:B2:E1:...",
46
+ :name_identifier_format => "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
47
+ end
48
+ ```
49
+
50
+ ## Options
51
+
52
+ * `:assertion_consumer_service_url` - The URL at which the SAML assertion should be
53
+ received. With OmniAuth this is typically `http://example.com/auth/callback`.
54
+ **Required**.
55
+
56
+ * `:issuer` - The name of your application. Some identity providers might need this
57
+ to establish the identity of the service provider requesting the login. **Required**.
58
+
59
+ * `:idp_sso_target_url` - The URL to which the authentication request should be sent.
60
+ This would be on the identity provider. **Required**.
61
+
62
+ * `:idp_cert` - The identity provider's certificate in PEM format. Takes precedence
63
+ over the fingerprint option below. This option or `:idp_cert_fingerprint` must
64
+ be present.
65
+
66
+ * `:idp_cert_fingerprint` - The SHA256 fingerprint of the certificate, e.g.
67
+ "90:CC:16:F0:8D:...". This is provided from the identity provider when setting up
68
+ the relationship. This option or `:idp_cert` must be present.
69
+
70
+ * `:name_identifier_format` - Describes the format of the username required by this
71
+ application. If you need the email address, use "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress".
72
+ See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf section 8.3 for
73
+ other options. Note that the identity provider might not support all options.
74
+ Optional.
75
+
76
+ ## Authors
77
+
78
+ Authored by Josh Skeen [www.joshskeen.com].
79
+ Based on the work of Raecoo Cao, Todd W Saxton, Ryan Wilcox, Rajiv Aaron Manglani, and Steven Anderson.
80
+
81
+ <!-- Maintained by [Rajiv Aaron Manglani](http://www.rajivmanglani.com/). -->
82
+
83
+ ## License
84
+
85
+ Copyright (c) 2012 Apangea [http://www.apangea.com/]
86
+ Developed at Highgroove Studios [http://www.highgroove.com]
87
+
88
+ Copyright (c) 2011-2012 [Practically Green, Inc.](http://www.practicallygreen.com/).
89
+ All rights reserved. Released under the MIT license.
90
+
91
+ Portions Copyright (c) 2007 Sun Microsystems Inc.
92
+
93
+ Permission is hereby granted, free of charge, to any person obtaining a copy
94
+ of this software and associated documentation files (the "Software"), to deal
95
+ in the Software without restriction, including without limitation the rights
96
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
97
+ copies of the Software, and to permit persons to whom the Software is
98
+ furnished to do so, subject to the following conditions:
99
+
100
+ The above copyright notice and this permission notice shall be included in
101
+ all copies or substantial portions of the Software.
102
+
103
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
104
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
105
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
106
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
107
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
108
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
109
+ THE SOFTWARE.
@@ -0,0 +1 @@
1
+ require 'omniauth/strategies/saml-rstr'
@@ -0,0 +1,5 @@
1
+ module OmniAuth
2
+ module SAML_RSTR
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,52 @@
1
+ require 'omniauth'
2
+
3
+ module OmniAuth
4
+ module Strategies
5
+ class SAML_RSTR
6
+ include OmniAuth::Strategy
7
+
8
+ autoload :AuthRequest, 'omniauth/strategies/saml-rstr/auth_request'
9
+ autoload :AuthResponse, 'omniauth/strategies/saml-rstr/auth_response'
10
+ autoload :ValidationError, 'omniauth/strategies/saml-rstr/validation_error'
11
+ autoload :XMLSecurity, 'omniauth/strategies/saml-rstr/xml_security'
12
+
13
+ option :name_identifier_format, "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
14
+
15
+ def request_phase
16
+ request = OmniAuth::Strategies::SAML_RSTR::AuthRequest.new
17
+ req = request.create(options)
18
+ redirect(req)
19
+ end
20
+
21
+ def callback_phase
22
+
23
+ begin
24
+ response = OmniAuth::Strategies::SAML_RSTR::AuthResponse.new(request.params['wresult'])
25
+ response.settings = options
26
+
27
+ @name_id = response.name_id
28
+ @attributes = response.attributes
29
+
30
+ return fail!(:invalid_ticket, OmniAuth::Error.new('Invalid SAML_RSTR Ticket')) if @name_id.nil? || @name_id.empty? || !response.valid?
31
+ super
32
+ rescue ArgumentError => e
33
+ fail!(:invalid_ticket, OmniAuth::Error.new('Invalid SAML_RSTR Response'))
34
+ end
35
+ end
36
+
37
+ uid { @name_id }
38
+
39
+ info do
40
+ {
41
+ :name => @name_id
42
+ }
43
+ end
44
+
45
+ extra { { :raw_info => @attributes } }
46
+
47
+ end
48
+ end
49
+ end
50
+
51
+ # OmniAuth.config.add_camelization 'saml', 'SAML'
52
+ OmniAuth.config.add_camelization 'saml_rstr', 'SAML_RSTR'
@@ -0,0 +1,38 @@
1
+ require "base64"
2
+ require "uuid"
3
+ require "zlib"
4
+ require "cgi"
5
+
6
+ module OmniAuth
7
+ module Strategies
8
+ class SAML_RSTR
9
+ class AuthRequest
10
+
11
+ def create(settings, params = {})
12
+ uuid = "_" + UUID.new.generate
13
+ time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
14
+
15
+ request =
16
+ "<samlp:AuthnRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\"#{uuid}\" Version=\"2.0\" IssueInstant=\"#{time}\" ProtocolBinding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" AssertionConsumerServiceURL=\"#{settings[:assertion_consumer_service_url]}\">" +
17
+ "<saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">#{settings[:issuer]}</saml:Issuer>\n" +
18
+ "<samlp:NameIDPolicy xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" Format=\"#{settings[:name_identifier_format]}\" AllowCreate=\"true\"></samlp:NameIDPolicy>\n" +
19
+ "<samlp:RequestedAuthnContext xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" Comparison=\"exact\">" +
20
+ "<saml:AuthnContextClassRef xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></samlp:RequestedAuthnContext>\n" +
21
+ "</samlp:AuthnRequest>"
22
+
23
+ deflated_request = Zlib::Deflate.deflate(request, 9)[2..-5]
24
+ base64_request = Base64.encode64(deflated_request)
25
+ encoded_request = CGI.escape(base64_request)
26
+ request_params = "?SAMLRequest=" + encoded_request
27
+
28
+ params.each_pair do |key, value|
29
+ request_params << "&#{key}=#{CGI.escape(value.to_s)}"
30
+ end
31
+
32
+ settings[:idp_sso_target_url] + request_params
33
+ end
34
+
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,128 @@
1
+ require "time"
2
+
3
+ module OmniAuth
4
+ module Strategies
5
+ class SAML_RSTR
6
+ class AuthResponse
7
+
8
+ ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
9
+ PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
10
+ DSIG = "http://www.w3.org/2000/09/xmldsig#"
11
+
12
+ attr_accessor :options, :response, :security_token_content, :settings
13
+
14
+ def initialize(response, options = {})
15
+ raise ArgumentError.new("Response cannot be nil") if response.nil?
16
+ self.options = options
17
+ self.response = response
18
+ self.security_token_content = OmniAuth::Strategies::SAML_RSTR::XMLSecurity::SecurityTokenResponseContent.new(response)
19
+ end
20
+
21
+ def valid?
22
+ validate(soft = true)
23
+ end
24
+
25
+ def validate!
26
+ validate(soft = false)
27
+ end
28
+
29
+ # The value of the user identifier as designated by the initialization request response
30
+ def name_id
31
+ @security_token_content.name_identifier
32
+ end
33
+
34
+ # A hash of all the attributes with the response. Assuming there is only one value for each key
35
+ def attributes
36
+ { :userEmailID => @security_token_content.name_identifier}
37
+ end
38
+
39
+ # When this user session should expire at latest
40
+ def session_expires_at
41
+ @expires_at ||= begin
42
+ parse_time(security_token_content.conditions_not_on_or_after)
43
+ end
44
+ end
45
+
46
+ # Conditions (if any) for the assertion to run
47
+ def conditions
48
+ @conditions ||= begin
49
+ {
50
+ :before => security_token_content.conditions_before,
51
+ :not_on_or_after => security_token_content.conditions_not_on_or_after
52
+ }
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def validation_error(message)
59
+ raise OmniAuth::Strategies::SAML_RSTR::ValidationError.new(message)
60
+ end
61
+
62
+ def validate(soft = true)
63
+ status = validate_response_state(soft) && security_token_content.validate(get_fingerprint, soft)
64
+ return status
65
+ end
66
+
67
+ def validate_response_state(soft = true)
68
+ if response.empty?
69
+ return soft ? false : validation_error("Blank response")
70
+ end
71
+
72
+ if settings.nil?
73
+ return soft ? false : validation_error("No settings on response")
74
+ end
75
+
76
+ if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
77
+ return soft ? false : validation_error("No fingerprint or certificate on settings")
78
+ end
79
+ true
80
+ end
81
+
82
+ def get_fingerprint
83
+ if settings.idp_cert
84
+ cert = OpenSSL::X509::Certificate.new(settings.idp_cert.gsub(/^ +/, ''))
85
+ Digest::SHA1.hexdigest(cert.to_der).upcase.scan(/../).join(":")
86
+ else
87
+ settings.idp_cert_fingerprint
88
+ end
89
+ end
90
+
91
+ def validate_conditions(soft = true)
92
+ return true if conditions.nil?
93
+ return true if options[:skip_conditions]
94
+
95
+ if not_before = parse_time(security_token_content.conditions_before)
96
+ if Time.now.utc < not_before
97
+ return soft ? false : validation_error("Current time is earlier than NotBefore condition")
98
+ end
99
+ end
100
+
101
+ if not_on_or_after = parse_time(security_token_content.conditions_not_on_or_after)
102
+ if Time.now.utc >= not_on_or_after
103
+ return soft ? false : validation_error("Current time is on or after NotOnOrAfter condition")
104
+ end
105
+ end
106
+
107
+ true
108
+ end
109
+
110
+ private
111
+
112
+ def parse_time(attribute)
113
+ Time.parse(attribute)
114
+ end
115
+
116
+ def strip(string)
117
+ return string unless string
118
+ string.gsub(/^\s+/, '').gsub(/\s+$/, '')
119
+ end
120
+
121
+ def signed_element_id
122
+ doc_id = security_token_content.signed_element_id
123
+ doc_id[1, doc_id.size]
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,8 @@
1
+ module OmniAuth
2
+ module Strategies
3
+ class SAML_RSTR
4
+ class ValidationError < Exception
5
+ end
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,119 @@
1
+ # The contents of this file are subject to the terms
2
+ # of the Common Development and Distribution License
3
+ # (the License). You may not use this file except in
4
+ # compliance with the License.
5
+ #
6
+ # You can obtain a copy of the License at
7
+ # https://opensso.dev.java.net/public/CDDLv1.0.html or
8
+ # opensso/legal/CDDLv1.0.txt
9
+ # See the License for the specific language governing
10
+ # permission and limitations under the License.
11
+ #
12
+ # When distributing Covered Code, include this CDDL
13
+ # Header Notice in each file and include the License file
14
+ # at opensso/legal/CDDLv1.0.txt.
15
+ # If applicable, add the following below the CDDL Header,
16
+ # with the fields enclosed by brackets [] replaced by
17
+ # your own identifying information:
18
+ # "Portions Copyrighted [year] [name of copyright owner]"
19
+ #
20
+ # $Id: xml_sec.rb,v 1.6 2007/10/24 00:28:41 todddd Exp $
21
+ #
22
+ # Copyright 2007 Sun Microsystems Inc. All Rights responseerved
23
+ # Portions Copyrighted 2007 Todd W Saxton.
24
+
25
+ require 'rubygems'
26
+ require "rexml/document"
27
+ require "rexml/xpath"
28
+ require "openssl"
29
+ require "xmlcanonicalizer"
30
+ require "digest/sha1"
31
+ require "nokogiri"
32
+
33
+ module OmniAuth
34
+ module Strategies
35
+ class SAML_RSTR
36
+
37
+ module XMLSecurity
38
+
39
+ class SecurityTokenResponseContent
40
+
41
+ #plugging these namespaces in was required in order to get nokogiri to use them. eg @xml.at_xpath("//ds:SignatureValue", {"ds" => DSIG}).text. Any way to avoid this?
42
+ DSIG = "http://www.w3.org/2000/09/xmldsig#"
43
+ SAML = "urn:oasis:names:tc:SAML:1.0:assertion"
44
+ WSP = "http://schemas.xmlsoap.org/ws/2004/09/policy"
45
+ WSA = "http://www.w3.org/2005/08/addressing"
46
+ WSU = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
47
+ TRUST = "http://schemas.xmlsoap.org/ws/2005/02/trust"
48
+
49
+ attr_accessor :name_identifier, :xml, :xml_unnamespaced, :name_identifier_test, :x509_cert, :conditions_not_on_or_after, :conditions_before, :info_element
50
+
51
+ def initialize(response)
52
+ self.xml_unnamespaced = Nokogiri::XML::Document.parse(response).remove_namespaces!()
53
+ self.xml = Nokogiri::XML::Document.parse(response)
54
+ end
55
+
56
+ def signature
57
+ @xml.at_xpath("//ds:SignatureValue", {"ds" => DSIG}).text
58
+ end
59
+
60
+ def info_element
61
+ @xml.at_xpath("//ds:SignedInfo", {"ds" => DSIG})
62
+ end
63
+
64
+ def name_identifier
65
+ @xml_unnamespaced.css("NameIdentifier").text
66
+ end
67
+
68
+ def conditions_before
69
+ if !conditions.nil?
70
+ conditions.attribute("NotBefore").value
71
+ end
72
+ end
73
+
74
+ def conditions_not_on_or_after
75
+ if !conditions.nil?
76
+ conditions.attribute("NotOnOrAfter").value
77
+ end
78
+ end
79
+
80
+ def x509_cert
81
+ @xml_unnamespaced.css("X509Certificate").text
82
+ end
83
+
84
+ #validate the response fingerprint matches the plugin fingerprint
85
+ #validate the certificate signature matches the signature generated from signing the certificate's SignedInfo node
86
+ def validate(idp_cert_fingerprint, soft = true)
87
+
88
+ cert_text = Base64.decode64(x509_cert)
89
+
90
+ certificate = OpenSSL::X509::Certificate.new(cert_text)
91
+ fingerprint = Digest::SHA1.hexdigest(certificate.to_der)
92
+
93
+ if fingerprint != idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
94
+ raise OmniAuth::Strategies::SAML_RSTR::ValidationError.new("Fingerprint validation error")
95
+ end
96
+
97
+ canon_string = info_element.canonicalize(Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0)
98
+ sig = Base64.decode64(signature)
99
+
100
+ if !certificate.public_key.verify(OpenSSL::Digest::SHA256.new, sig, canon_string)
101
+ return soft ? false : (raise OmniAuth::Strategies::SAML_RSTR::ValidationError.new("Key validation error"))
102
+ end
103
+
104
+ return true
105
+ end
106
+
107
+ private
108
+
109
+ def conditions
110
+ @xml.at_xpath("//saml:Conditions", {"saml" => SAML})
111
+ end
112
+
113
+ end
114
+
115
+ end
116
+
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+
3
+ describe OmniAuth::Strategies::SAML_RSTR::AuthRequest do
4
+ describe :create do
5
+ let(:url) do
6
+ described_class.new.create(
7
+ {
8
+ :idp_sso_target_url => 'example.com',
9
+ :assertion_consumer_service_url => 'http://example.com/auth/saml-rstr/callback',
10
+ :issuer => 'This is an issuer',
11
+ :name_identifier_format => 'Some Policy'
12
+ },
13
+ {
14
+ :some_param => 'foo',
15
+ :some_other => 'bar'
16
+ }
17
+ )
18
+ end
19
+ let(:saml_request) { url.match(/SAMLRequest=(.*)/)[1] }
20
+
21
+ describe "the url" do
22
+ subject { url }
23
+
24
+ it "should contain a SAMLRequest query string param" do
25
+ subject.should match /^example\.com\?SAMLRequest=/
26
+ end
27
+
28
+ it "should contain any other parameters passed through" do
29
+ subject.should match /^example\.com\?SAMLRequest=(.*)&some_param=foo&some_other=bar/
30
+ end
31
+ end
32
+
33
+ describe "the saml request" do
34
+ subject { saml_request }
35
+
36
+ let(:decoded) do
37
+ cgi_unescaped = CGI.unescape(subject)
38
+ base64_decoded = Base64.decode64(cgi_unescaped)
39
+ Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(base64_decoded)
40
+ end
41
+
42
+ let(:xml) { REXML::Document.new(decoded) }
43
+ let(:root_element) { REXML::XPath.first(xml, '//samlp:AuthnRequest') }
44
+
45
+ it "should contain base64 encoded and zlib deflated xml" do
46
+ decoded.should match /^<samlp:AuthnRequest/
47
+ end
48
+
49
+ it "should contain a uuid with an underscore in front" do
50
+ UUID.any_instance.stub(:generate).and_return('MY_UUID')
51
+
52
+ root_element.attributes['ID'].should == '_MY_UUID'
53
+ end
54
+
55
+ it "should contain the current time as the IssueInstant" do
56
+ t = Time.now
57
+ Time.stub(:now).and_return(t)
58
+
59
+ root_element.attributes['IssueInstant'].should == t.utc.iso8601
60
+ end
61
+
62
+ it "should contain the callback url in the settings" do
63
+ root_element.attributes['AssertionConsumerServiceURL'].should == 'http://example.com/auth/saml-rstr/callback'
64
+ end
65
+
66
+ it "should contain the issuer" do
67
+ REXML::XPath.first(xml, '//saml:Issuer').text.should == 'This is an issuer'
68
+ end
69
+
70
+ it "should contain the name identifier format" do
71
+ REXML::XPath.first(xml, '//samlp:NameIDPolicy').attributes['Format'].should == 'Some Policy'
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ describe OmniAuth::Strategies::SAML_RSTR::AuthResponse do
4
+ let(:xml) { :rstr_response }
5
+ subject { described_class.new(load_xml(xml)) }
6
+
7
+ describe :initialize do
8
+ context "when the response is nil" do
9
+ it "should raise an exception" do
10
+ expect { described_class.new(nil) }.to raise_error ArgumentError
11
+ end
12
+ end
13
+ end
14
+ # 2012-07-25T21:16:34.271Z
15
+ describe :session_expires_at do
16
+ it "should return the SessionNotOnOrAfter as a Ruby date" do
17
+ subject.session_expires_at.to_i.should == Time.new(2012, 07, 25, 21, 16, 34, 0).to_i
18
+ end
19
+ end
20
+
21
+ describe :name_id do
22
+ it "should load the name id from the assertion" do
23
+ subject.name_id.should == 'highgroove@thinkthroughmath.com'
24
+ end
25
+ end
26
+
27
+ describe :valid? do
28
+ it_should_behave_like 'a validating method', true
29
+ end
30
+
31
+ describe :validate! do
32
+ it_should_behave_like 'a validating method', false
33
+ end
34
+
35
+ end
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+
3
+ describe OmniAuth::Strategies::SAML_RSTR::ValidationError do
4
+ it { should be_a Exception }
5
+ end
@@ -0,0 +1,122 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec::Matchers.define :fail_with do |message|
4
+ match do |actual|
5
+ actual.redirect? && actual.location.include?("/auth/failure?message")
6
+ end
7
+ end
8
+
9
+ def post_xml(xml=:rstr_response)
10
+ post "/auth/saml_rstr/callback", {'wresult' => load_xml(xml)}
11
+ end
12
+
13
+ describe OmniAuth::Strategies::SAML_RSTR, :type => :strategy do
14
+ include OmniAuth::Test::StrategyTestCase
15
+ let(:invalid_ticket){ OmniAuth::Error.new }
16
+ let(:auth_hash){ last_request.env['omniauth.auth'] }
17
+ let(:saml_options) do
18
+ {
19
+ :assertion_consumer_service_url => "http://localhost:3000/auth/saml_rstr/callback",
20
+ :issuer => "https://saml.issuer.url/issuers/29490",
21
+ :idp_sso_target_url => "https://idp.sso.target_url/signon/29490",
22
+ :idp_cert_fingerprint => "76:C5:6A:64:E0:D8:81:44:11:24:F2:9C:1B:41:56:27:6E:3B:FB:8C",
23
+ :name_identifier_format => "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
24
+ }
25
+ end
26
+ let(:strategy) { [OmniAuth::Strategies::SAML_RSTR, saml_options] }
27
+
28
+ describe 'GET /auth/saml_rstr' do
29
+ before do
30
+ get '/auth/saml_rstr'
31
+ end
32
+
33
+ it 'should get authentication page' do
34
+ last_response.should be_redirect
35
+ end
36
+ end
37
+
38
+ describe 'POST /auth/saml_rstr/callback' do
39
+ subject { last_response }
40
+ let(:xml) { :rstr_response }
41
+ before :each do
42
+ Time.stub(:now).and_return(Time.new(2012, 3, 8, 16, 25, 00, 0))
43
+ end
44
+
45
+ context "when the response is valid" do
46
+ puts "when the response is valid"
47
+ before :each do
48
+ post_xml
49
+ end
50
+
51
+ it "should set the uid to the nameID in the SAML response" do
52
+ auth_hash['uid'].should == 'highgroove@thinkthroughmath.com'
53
+ end
54
+
55
+ it "should set the raw info to all attributes" do
56
+ auth_hash['extra']['raw_info'].to_hash.should == {
57
+ 'userEmailID' => 'highgroove@thinkthroughmath.com'
58
+ }
59
+ end
60
+ end
61
+
62
+ context "when there is no wresult parameter" do
63
+ before :each do
64
+ post '/auth/saml_rstr/callback'
65
+ end
66
+ it { should fail_with(:invalid_ticket) }
67
+ end
68
+
69
+ context "when there is no name id in the XML" do
70
+ before :each do
71
+ post_xml :rstr_no_name
72
+ end
73
+
74
+ it { should fail_with(:invalid_ticket) }
75
+ end
76
+
77
+ context "when the fingerprint is invalid" do
78
+ before :each do
79
+ saml_options[:idp_cert_fingerprint] = "E6:87:89:FB:F2:5F:CD:B0:31:32:7E:05:44:84:53:B1:EC:4E:3F:gg"
80
+ post_xml
81
+ end
82
+ it { should fail_with(:invalid_ticket) }
83
+ # it {should raise_error(OmniAuth::Strategies::SAML_RSTR::ValidationError, "Fingerprint validation error")}
84
+ end
85
+
86
+ context "when the digest is invalid" do
87
+ before :each do
88
+ post_xml :digest_mismatch
89
+ end
90
+
91
+ it { should fail_with(:invalid_ticket) }
92
+ end
93
+
94
+ context "when the signature is invalid" do
95
+ before :each do
96
+ post_xml :rstr_invalid_signature
97
+ puts "invalid signature"
98
+ end
99
+ it { should fail_with(:invalid_ticket) }
100
+ end
101
+
102
+ context "when the time is before the NotBefore date" do
103
+ before :each do
104
+ Time.stub(:now).and_return(Time.new(2000, 3, 8, 16, 25, 00, 0))
105
+ post_xml
106
+ end
107
+
108
+ it { should fail_with(:invalid_ticket) }
109
+ end
110
+
111
+ context "when the time is after the NotOnOrAfter date" do
112
+ before :each do
113
+ Time.stub(:now).and_return(Time.new(3000, 3, 8, 16, 25, 00, 0))
114
+ post_xml
115
+ end
116
+
117
+ it { should fail_with(:invalid_ticket) }
118
+ end
119
+
120
+
121
+ end
122
+ end
@@ -0,0 +1,128 @@
1
+ def assert_is_valid(soft)
2
+ if soft
3
+ it { should be_valid }
4
+ else
5
+ it "should be valid" do
6
+ expect { subject.validate! }.not_to raise_error
7
+ end
8
+ end
9
+ end
10
+
11
+ def assert_is_not_valid(soft)
12
+ if soft
13
+ it { should_not be_valid }
14
+ else
15
+ it "should be invalid" do
16
+ expect { subject.validate! }.to raise_error
17
+ end
18
+ end
19
+ end
20
+
21
+ def stub_validate_to_fail(soft)
22
+ if soft
23
+ subject.security_token_content.stub(:validate).and_return(false)
24
+ else
25
+ subject.security_token_content.stub(:validate).and_raise(Exception)
26
+ end
27
+ end
28
+
29
+ shared_examples_for 'a validating method' do |soft|
30
+ before :each do
31
+ subject.settings = mock(Object, :idp_cert_fingerprint => 'FINGERPRINT', :idp_cert => nil)
32
+ subject.security_token_content.stub(:validate).and_return(true)
33
+ end
34
+
35
+ context "when the response is empty" do
36
+ subject { described_class.new('') }
37
+
38
+ assert_is_not_valid(soft)
39
+ end
40
+
41
+ context "when the settings are nil" do
42
+ before :each do
43
+ subject.settings = nil
44
+ end
45
+
46
+ assert_is_not_valid(soft)
47
+ end
48
+
49
+ context "when there is no idp_cert_fingerprint and idp_cert" do
50
+ before :each do
51
+ subject.settings = mock(Object, :idp_cert_fingerprint => nil, :idp_cert => nil)
52
+ end
53
+
54
+ assert_is_not_valid(soft)
55
+ end
56
+
57
+ context "when conditions are not given" do
58
+ let(:xml) { :rstr_response_no_conditions }
59
+ assert_is_valid(soft)
60
+ end
61
+
62
+ context "when the current time is before the NotBefore time" do
63
+ before :each do
64
+ Time.stub(:now).and_return(Time.new(2000, 01, 01, 10, 00, 00, 0))
65
+ end
66
+
67
+ assert_is_not_valid(soft)
68
+ end
69
+
70
+ context "when the current time is after the NotOnOrAfter time" do
71
+ before :each do
72
+ # We're assuming here that this code will be out of use in 1000 years...
73
+ Time.stub(:now).and_return(Time.new(3012, 01, 01, 10, 00, 00, 0))
74
+ end
75
+
76
+ assert_is_not_valid(soft)
77
+ end
78
+
79
+ context "when the current time is between the NotBefore and NotOnOrAfter times" do
80
+ before :each do
81
+ Time.stub(:now).and_return(Time.new(2012, 3, 8, 16, 25, 00, 0))
82
+ end
83
+
84
+ assert_is_valid(soft)
85
+ end
86
+
87
+ context "when skip_conditions option is given" do
88
+ before :each do
89
+ subject.options[:skip_conditions] = true
90
+ end
91
+
92
+ assert_is_valid(soft)
93
+ end
94
+
95
+ context "when the SAML document is valid" do
96
+ before :each do
97
+ subject.document.should_receive(:validate).with('FINGERPRINT', soft).and_return(true)
98
+ subject.options[:skip_conditions] = true
99
+ end
100
+
101
+ assert_is_valid(soft)
102
+ end
103
+
104
+ context "when the SAML document is valid and the idp_cert is given" do
105
+ let(:cert) do
106
+ filename = File.expand_path(File.join('..', '..', 'support', "example_cert.pem"), __FILE__)
107
+ IO.read(filename)
108
+ end
109
+ let(:expected) { 'E6:87:89:FB:F2:5F:CD:B0:31:32:7E:05:44:84:53:B1:EC:4E:3F:FA' }
110
+
111
+ before :each do
112
+ subject.settings.stub(:idp_cert).and_return(cert)
113
+ subject.document.should_receive(:validate).with(expected, soft).and_return(true)
114
+ subject.options[:skip_conditions] = true
115
+ end
116
+
117
+ assert_is_valid(soft)
118
+ end
119
+
120
+ context "when the SAML document is invalid" do
121
+ before :each do
122
+ stub_validate_to_fail(soft)
123
+ subject.options[:skip_conditions] = true
124
+ end
125
+
126
+ assert_is_not_valid(soft)
127
+ end
128
+ end
@@ -0,0 +1,17 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
3
+ require 'omniauth-saml-rstr'
4
+ require 'rack/test'
5
+ require 'rexml/document'
6
+ require 'rexml/xpath'
7
+ require 'base64'
8
+ require File.expand_path('../shared/validating_method.rb', __FILE__)
9
+
10
+ RSpec.configure do |config|
11
+ config.include Rack::Test::Methods
12
+ end
13
+
14
+ def load_xml(filename=:rstr_response)
15
+ filename = File.expand_path(File.join('..', 'support', "#{filename.to_s}.xml"), __FILE__)
16
+ result = IO.read(filename)
17
+ end
metadata ADDED
@@ -0,0 +1,208 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: omniauth-saml-rstr
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Josh Skeen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-31 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: omniauth
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: xmlcanonicalizer
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - '='
36
+ - !ruby/object:Gem::Version
37
+ version: 0.1.1
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - '='
44
+ - !ruby/object:Gem::Version
45
+ version: 0.1.1
46
+ - !ruby/object:Gem::Dependency
47
+ name: uuid
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '2.3'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '2.3'
62
+ - !ruby/object:Gem::Dependency
63
+ name: guard
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - '='
68
+ - !ruby/object:Gem::Version
69
+ version: 1.0.1
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - '='
76
+ - !ruby/object:Gem::Version
77
+ version: 1.0.1
78
+ - !ruby/object:Gem::Dependency
79
+ name: guard-rspec
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - '='
84
+ - !ruby/object:Gem::Version
85
+ version: 0.6.0
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - '='
92
+ - !ruby/object:Gem::Version
93
+ version: 0.6.0
94
+ - !ruby/object:Gem::Dependency
95
+ name: rspec
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - '='
100
+ - !ruby/object:Gem::Version
101
+ version: '2.8'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - '='
108
+ - !ruby/object:Gem::Version
109
+ version: '2.8'
110
+ - !ruby/object:Gem::Dependency
111
+ name: simplecov
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - '='
116
+ - !ruby/object:Gem::Version
117
+ version: 0.6.1
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - '='
124
+ - !ruby/object:Gem::Version
125
+ version: 0.6.1
126
+ - !ruby/object:Gem::Dependency
127
+ name: rack-test
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - '='
132
+ - !ruby/object:Gem::Version
133
+ version: 0.6.1
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - '='
140
+ - !ruby/object:Gem::Version
141
+ version: 0.6.1
142
+ - !ruby/object:Gem::Dependency
143
+ name: nokogiri
144
+ requirement: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - '='
148
+ - !ruby/object:Gem::Version
149
+ version: 1.5.5
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - '='
156
+ - !ruby/object:Gem::Version
157
+ version: 1.5.5
158
+ description: A RequestSecurityTokenResponse for ADFS strategy based on https://github.com/PracticallyGreen/omniauth-saml.
159
+ email: josh@highgroove.com
160
+ executables: []
161
+ extensions: []
162
+ extra_rdoc_files: []
163
+ files:
164
+ - README.md
165
+ - lib/omniauth/strategies/saml-rstr/auth_request.rb
166
+ - lib/omniauth/strategies/saml-rstr/auth_response.rb
167
+ - lib/omniauth/strategies/saml-rstr/validation_error.rb
168
+ - lib/omniauth/strategies/saml-rstr/xml_security.rb
169
+ - lib/omniauth/strategies/saml-rstr.rb
170
+ - lib/omniauth-saml-rstr/version.rb
171
+ - lib/omniauth-saml-rstr.rb
172
+ - spec/omniauth/strategies/saml-rstr/auth_request_spec.rb
173
+ - spec/omniauth/strategies/saml-rstr/auth_response_spec.rb
174
+ - spec/omniauth/strategies/saml-rstr/validation_error_spec.rb
175
+ - spec/omniauth/strategies/saml_rstr_spec.rb
176
+ - spec/shared/validating_method.rb
177
+ - spec/spec_helper.rb
178
+ homepage: https://github.com/mutexkid/omniauth-saml-rstr
179
+ licenses: []
180
+ post_install_message:
181
+ rdoc_options: []
182
+ require_paths:
183
+ - lib
184
+ required_ruby_version: !ruby/object:Gem::Requirement
185
+ none: false
186
+ requirements:
187
+ - - ! '>='
188
+ - !ruby/object:Gem::Version
189
+ version: '0'
190
+ required_rubygems_version: !ruby/object:Gem::Requirement
191
+ none: false
192
+ requirements:
193
+ - - ! '>='
194
+ - !ruby/object:Gem::Version
195
+ version: '0'
196
+ requirements: []
197
+ rubyforge_project:
198
+ rubygems_version: 1.8.24
199
+ signing_key:
200
+ specification_version: 3
201
+ summary: A RequestSecurityTokenResponse for ADFS strategy based on https://github.com/PracticallyGreen/omniauth-saml.
202
+ test_files:
203
+ - spec/omniauth/strategies/saml-rstr/auth_request_spec.rb
204
+ - spec/omniauth/strategies/saml-rstr/auth_response_spec.rb
205
+ - spec/omniauth/strategies/saml-rstr/validation_error_spec.rb
206
+ - spec/omniauth/strategies/saml_rstr_spec.rb
207
+ - spec/shared/validating_method.rb
208
+ - spec/spec_helper.rb