omniauth-saml-rstr 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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