omniauth-saml 0.9.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of omniauth-saml might be problematic. Click here for more details.

data/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # OmniAuth SAML Version History
2
+
3
+ A generic SAML strategy for OmniAuth.
4
+
5
+ https://github.com/PracticallyGreen/omniauth-saml
6
+
7
+ ## 1.0.0 (2012-11-12)
8
+
9
+ * remove SAML code and port to ruby-saml gem
10
+ * fix incompatibility with OmniAuth 1.1
11
+
12
+ ## 0.9.2 (2012-03-30)
13
+
14
+ * validate the SAML response
15
+ * 100% test coverage
16
+ * now requires ruby 1.9.2+
17
+
18
+ ## 0.9.1 (2012-02-23)
19
+
20
+ * return first and last name in the info hash
21
+ * no longer use LDAP OIDs for name and email selection
22
+ * return SAML attributes as the omniauth raw_info hash
23
+
24
+ ## 0.9.0 (2012-02-14)
25
+
26
+ * initial release
27
+ * extracts commits from omniauth 0-3-stable branch
28
+ * port to omniauth 1.0 strategy format
29
+ * update README with more documentation and license
30
+ * package as the `omniauth-saml` gem
data/README.md CHANGED
@@ -6,7 +6,7 @@ https://github.com/PracticallyGreen/omniauth-saml
6
6
 
7
7
  ## Requirements
8
8
 
9
- * [OmniAuth](http://www.omniauth.org/) 1.0+
9
+ * [OmniAuth](http://www.omniauth.org/) 1.1+
10
10
  * Ruby 1.9.2
11
11
 
12
12
  ## Usage
@@ -46,6 +46,8 @@ Rails.application.config.middleware.use OmniAuth::Builder do
46
46
  end
47
47
  ```
48
48
 
49
+ For IdP-initiated SSO, users should directly access the IdP SSO target URL. Set the `href` of your application's login link to the value of `idp_sso_target_url`. For SP-initiated SSO, link to `/auth/saml`.
50
+
49
51
  ## Options
50
52
 
51
53
  * `:assertion_consumer_service_url` - The URL at which the SAML assertion should be
@@ -70,7 +72,9 @@ end
70
72
  application. If you need the email address, use "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress".
71
73
  See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf section 8.3 for
72
74
  other options. Note that the identity provider might not support all options.
73
- Optional.
75
+ Used during SP-initiated SSO. Optional.
76
+
77
+ * See the `Onelogin::Saml::Settings` class in the [Ruby SAML gem](https://github.com/onelogin/ruby-saml) for additional supported options.
74
78
 
75
79
  ## Authors
76
80
 
@@ -80,7 +84,7 @@ Maintained by [Rajiv Aaron Manglani](http://www.rajivmanglani.com/).
80
84
 
81
85
  ## License
82
86
 
83
- Copyright (c) 2011-2012 [Practically Green, Inc.](http://www.practicallygreen.com/).
87
+ Copyright (c) 2011-2012 [Practically Green, Inc.](http://www.practicallygreen.com/).
84
88
  All rights reserved. Released under the MIT license.
85
89
 
86
90
  Portions Copyright (c) 2007 Sun Microsystems Inc.
data/lib/omniauth-saml.rb CHANGED
@@ -1 +1,2 @@
1
1
  require 'omniauth/strategies/saml'
2
+ require 'omniauth/strategies/saml/validation_error'
@@ -1,5 +1,5 @@
1
1
  module OmniAuth
2
2
  module SAML
3
- VERSION = "0.9.2"
3
+ VERSION = "1.0.0"
4
4
  end
5
5
  end
@@ -1,34 +1,42 @@
1
1
  require 'omniauth'
2
+ require 'ruby-saml'
2
3
 
3
4
  module OmniAuth
4
5
  module Strategies
5
6
  class SAML
6
7
  include OmniAuth::Strategy
7
- autoload :AuthRequest, 'omniauth/strategies/saml/auth_request'
8
- autoload :AuthResponse, 'omniauth/strategies/saml/auth_response'
9
- autoload :ValidationError, 'omniauth/strategies/saml/validation_error'
10
- autoload :XMLSecurity, 'omniauth/strategies/saml/xml_security'
11
8
 
12
9
  option :name_identifier_format, "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
13
10
 
14
11
  def request_phase
15
- request = OmniAuth::Strategies::SAML::AuthRequest.new
16
- redirect(request.create(options))
12
+ request = Onelogin::Saml::Authrequest.new
13
+ settings = Onelogin::Saml::Settings.new(options)
14
+
15
+ redirect(request.create(settings))
17
16
  end
18
17
 
19
18
  def callback_phase
20
- begin
21
- response = OmniAuth::Strategies::SAML::AuthResponse.new(request.params['SAMLResponse'])
22
- response.settings = options
19
+ unless request.params['SAMLResponse']
20
+ raise OmniAuth::Strategies::SAML::ValidationError.new("SAML response missing")
21
+ end
22
+
23
+ response = Onelogin::Saml::Response.new(request.params['SAMLResponse'])
24
+ response.settings = Onelogin::Saml::Settings.new(options)
23
25
 
24
- @name_id = response.name_id
25
- @attributes = response.attributes
26
+ @name_id = response.name_id
27
+ @attributes = response.attributes
26
28
 
27
- return fail!(:invalid_ticket, 'Invalid SAML Ticket') if @name_id.nil? || @name_id.empty? || !response.valid?
28
- super
29
- rescue ArgumentError => e
30
- fail!(:invalid_ticket, 'Invalid SAML Response')
29
+ if @name_id.nil? || @name_id.empty?
30
+ raise OmniAuth::Strategies::SAML::ValidationError.new("SAML response missing 'name_id'")
31
31
  end
32
+
33
+ response.validate!
34
+
35
+ super
36
+ rescue OmniAuth::Strategies::SAML::ValidationError
37
+ fail!(:invalid_ticket, $!)
38
+ rescue Onelogin::Saml::ValidationError
39
+ fail!(:invalid_ticket, $!)
32
40
  end
33
41
 
34
42
  uid { @name_id }
@@ -43,7 +51,6 @@ module OmniAuth
43
51
  end
44
52
 
45
53
  extra { { :raw_info => @attributes } }
46
-
47
54
  end
48
55
  end
49
56
  end
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  RSpec::Matchers.define :fail_with do |message|
4
4
  match do |actual|
5
- actual.redirect? && actual.location == "/auth/failure?message=#{message}"
5
+ actual.redirect? && /\?.*message=#{message}/ === actual.location
6
6
  end
7
7
  end
8
8
 
@@ -19,7 +19,7 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
19
19
  :assertion_consumer_service_url => "http://localhost:3000/auth/saml/callback",
20
20
  :issuer => "https://saml.issuer.url/issuers/29490",
21
21
  :idp_sso_target_url => "https://idp.sso.target_url/signon/29490",
22
- :idp_cert_fingerprint => "E6:87:89:FB:F2:5F:CD:B0:31:32:7E:05:44:84:53:B1:EC:4E:3F:FA",
22
+ :idp_cert_fingerprint => "C1:59:74:2B:E8:0C:6C:A9:41:0F:6E:83:F6:D1:52:25:45:58:89:FB",
23
23
  :name_identifier_format => "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
24
24
  }
25
25
  end
@@ -41,7 +41,7 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
41
41
  let(:xml) { :example_response }
42
42
 
43
43
  before :each do
44
- Time.stub(:now).and_return(Time.new(2012, 3, 8, 16, 25, 00, 0))
44
+ Time.stub(:now).and_return(Time.new(2012, 11, 8, 20, 40, 00, 0))
45
45
  end
46
46
 
47
47
  context "when the response is valid" do
@@ -50,23 +50,15 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
50
50
  end
51
51
 
52
52
  it "should set the uid to the nameID in the SAML response" do
53
- auth_hash['uid'].should == 'THISISANAMEID'
53
+ auth_hash['uid'].should == '_1f6fcf6be5e13b08b1e3610e7ff59f205fbd814f23'
54
54
  end
55
55
 
56
56
  it "should set the raw info to all attributes" do
57
57
  auth_hash['extra']['raw_info'].to_hash.should == {
58
- 'forename' => 'Steven',
59
- 'surname' => 'Anderson',
60
- 'address_1' => '24 Made Up Drive',
61
- 'address_2' => nil,
62
- 'companyName' => 'Test Company Ltd',
63
- 'postcode' => 'XX2 4XX',
64
- 'city' => 'Newcastle',
65
- 'country' => 'United Kingdom',
66
- 'userEmailID' => 'steve@example.com',
67
- 'county' => 'TYNESIDE',
68
- 'versionID' => '1',
69
- 'bundleID' => '1'
58
+ 'first_name' => 'Rajiv',
59
+ 'last_name' => 'Manglani',
60
+ 'email' => 'user@example.com',
61
+ 'company_name' => 'Example Company'
70
62
  }
71
63
  end
72
64
  end
@@ -89,7 +81,7 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
89
81
 
90
82
  context "when the fingerprint is invalid" do
91
83
  before :each do
92
- saml_options[:idp_cert_fingerprint] = "E6:87:89:FB:F2:5F:CD:B0:31:32:7E:05:44:84:53:B1:EC:4E:3F:FB"
84
+ saml_options[:idp_cert_fingerprint] = "00:00:00:00:00:0C:6C:A9:41:0F:6E:83:F6:D1:52:25:45:58:89:FB"
93
85
  post_xml
94
86
  end
95
87
 
@@ -111,23 +103,5 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
111
103
 
112
104
  it { should fail_with(:invalid_ticket) }
113
105
  end
114
-
115
- context "when the time is before the NotBefore date" do
116
- before :each do
117
- Time.stub(:now).and_return(Time.new(2000, 3, 8, 16, 25, 00, 0))
118
- post_xml
119
- end
120
-
121
- it { should fail_with(:invalid_ticket) }
122
- end
123
-
124
- context "when the time is after the NotOnOrAfter date" do
125
- before :each do
126
- Time.stub(:now).and_return(Time.new(3000, 3, 8, 16, 25, 00, 0))
127
- post_xml
128
- end
129
-
130
- it { should fail_with(:invalid_ticket) }
131
- end
132
106
  end
133
107
  end
data/spec/spec_helper.rb CHANGED
@@ -6,7 +6,6 @@ require 'rack/test'
6
6
  require 'rexml/document'
7
7
  require 'rexml/xpath'
8
8
  require 'base64'
9
- require File.expand_path('../shared/validating_method.rb', __FILE__)
10
9
 
11
10
  RSpec.configure do |config|
12
11
  config.include Rack::Test::Methods
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omniauth-saml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.2
4
+ version: 1.0.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -12,96 +12,120 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2012-03-30 00:00:00.000000000Z
15
+ date: 2012-11-13 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: omniauth
19
- requirement: &70159259960020 !ruby/object:Gem::Requirement
19
+ requirement: !ruby/object:Gem::Requirement
20
20
  none: false
21
21
  requirements:
22
22
  - - ~>
23
23
  - !ruby/object:Gem::Version
24
- version: '1.0'
24
+ version: '1.1'
25
25
  type: :runtime
26
26
  prerelease: false
27
- version_requirements: *70159259960020
28
- - !ruby/object:Gem::Dependency
29
- name: xmlcanonicalizer
30
- requirement: &70159259959000 !ruby/object:Gem::Requirement
27
+ version_requirements: !ruby/object:Gem::Requirement
31
28
  none: false
32
29
  requirements:
33
- - - =
30
+ - - ~>
34
31
  - !ruby/object:Gem::Version
35
- version: 0.1.1
36
- type: :runtime
37
- prerelease: false
38
- version_requirements: *70159259959000
32
+ version: '1.1'
39
33
  - !ruby/object:Gem::Dependency
40
- name: uuid
41
- requirement: &70159259958260 !ruby/object:Gem::Requirement
34
+ name: ruby-saml
35
+ requirement: !ruby/object:Gem::Requirement
42
36
  none: false
43
37
  requirements:
44
38
  - - ~>
45
39
  - !ruby/object:Gem::Version
46
- version: '2.3'
40
+ version: '0.6'
47
41
  type: :runtime
48
42
  prerelease: false
49
- version_requirements: *70159259958260
43
+ version_requirements: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ~>
47
+ - !ruby/object:Gem::Version
48
+ version: '0.6'
50
49
  - !ruby/object:Gem::Dependency
51
50
  name: guard
52
- requirement: &70159259957340 !ruby/object:Gem::Requirement
51
+ requirement: !ruby/object:Gem::Requirement
53
52
  none: false
54
53
  requirements:
55
- - - =
54
+ - - ~>
56
55
  - !ruby/object:Gem::Version
57
- version: 1.0.1
56
+ version: '1.0'
58
57
  type: :development
59
58
  prerelease: false
60
- version_requirements: *70159259957340
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ~>
63
+ - !ruby/object:Gem::Version
64
+ version: '1.0'
61
65
  - !ruby/object:Gem::Dependency
62
66
  name: guard-rspec
63
- requirement: &70159259956340 !ruby/object:Gem::Requirement
67
+ requirement: !ruby/object:Gem::Requirement
64
68
  none: false
65
69
  requirements:
66
- - - =
70
+ - - ~>
67
71
  - !ruby/object:Gem::Version
68
- version: 0.6.0
72
+ version: '2.1'
69
73
  type: :development
70
74
  prerelease: false
71
- version_requirements: *70159259956340
75
+ version_requirements: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ~>
79
+ - !ruby/object:Gem::Version
80
+ version: '2.1'
72
81
  - !ruby/object:Gem::Dependency
73
82
  name: rspec
74
- requirement: &70159259955720 !ruby/object:Gem::Requirement
83
+ requirement: !ruby/object:Gem::Requirement
75
84
  none: false
76
85
  requirements:
77
- - - =
86
+ - - ~>
78
87
  - !ruby/object:Gem::Version
79
88
  version: '2.8'
80
89
  type: :development
81
90
  prerelease: false
82
- version_requirements: *70159259955720
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ none: false
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: '2.8'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: simplecov
85
- requirement: &70159259954920 !ruby/object:Gem::Requirement
99
+ requirement: !ruby/object:Gem::Requirement
86
100
  none: false
87
101
  requirements:
88
- - - =
102
+ - - ~>
89
103
  - !ruby/object:Gem::Version
90
- version: 0.6.1
104
+ version: '0.6'
91
105
  type: :development
92
106
  prerelease: false
93
- version_requirements: *70159259954920
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ none: false
109
+ requirements:
110
+ - - ~>
111
+ - !ruby/object:Gem::Version
112
+ version: '0.6'
94
113
  - !ruby/object:Gem::Dependency
95
114
  name: rack-test
96
- requirement: &70159259954240 !ruby/object:Gem::Requirement
115
+ requirement: !ruby/object:Gem::Requirement
97
116
  none: false
98
117
  requirements:
99
- - - =
118
+ - - ~>
100
119
  - !ruby/object:Gem::Version
101
- version: 0.6.1
120
+ version: '0.6'
102
121
  type: :development
103
122
  prerelease: false
104
- version_requirements: *70159259954240
123
+ version_requirements: !ruby/object:Gem::Requirement
124
+ none: false
125
+ requirements:
126
+ - - ~>
127
+ - !ruby/object:Gem::Version
128
+ version: '0.6'
105
129
  description: A generic SAML strategy for OmniAuth.
106
130
  email: rajiv@alum.mit.edu
107
131
  executables: []
@@ -109,18 +133,12 @@ extensions: []
109
133
  extra_rdoc_files: []
110
134
  files:
111
135
  - README.md
112
- - lib/omniauth/strategies/saml/auth_request.rb
113
- - lib/omniauth/strategies/saml/auth_response.rb
136
+ - CHANGELOG.md
114
137
  - lib/omniauth/strategies/saml/validation_error.rb
115
- - lib/omniauth/strategies/saml/xml_security.rb
116
138
  - lib/omniauth/strategies/saml.rb
117
139
  - lib/omniauth-saml/version.rb
118
140
  - lib/omniauth-saml.rb
119
- - spec/omniauth/strategies/saml/auth_request_spec.rb
120
- - spec/omniauth/strategies/saml/auth_response_spec.rb
121
- - spec/omniauth/strategies/saml/validation_error_spec.rb
122
141
  - spec/omniauth/strategies/saml_spec.rb
123
- - spec/shared/validating_method.rb
124
142
  - spec/spec_helper.rb
125
143
  homepage: https://github.com/PracticallyGreen/omniauth-saml
126
144
  licenses: []
@@ -142,14 +160,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
142
160
  version: '0'
143
161
  requirements: []
144
162
  rubyforge_project:
145
- rubygems_version: 1.8.10
163
+ rubygems_version: 1.8.23
146
164
  signing_key:
147
165
  specification_version: 3
148
166
  summary: A generic SAML strategy for OmniAuth.
149
167
  test_files:
150
- - spec/omniauth/strategies/saml/auth_request_spec.rb
151
- - spec/omniauth/strategies/saml/auth_response_spec.rb
152
- - spec/omniauth/strategies/saml/validation_error_spec.rb
153
168
  - spec/omniauth/strategies/saml_spec.rb
154
- - spec/shared/validating_method.rb
155
169
  - spec/spec_helper.rb
@@ -1,38 +0,0 @@
1
- require "base64"
2
- require "uuid"
3
- require "zlib"
4
- require "cgi"
5
-
6
- module OmniAuth
7
- module Strategies
8
- class SAML
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
@@ -1,148 +0,0 @@
1
- require "time"
2
-
3
- module OmniAuth
4
- module Strategies
5
- class SAML
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, :document, :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.document = OmniAuth::Strategies::SAML::XMLSecurity::SignedDocument.new(Base64.decode64(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
- @name_id ||= begin
32
- node = xpath("/p:Response/a:Assertion[@ID='#{signed_element_id}']/a:Subject/a:NameID")
33
- node ||= xpath("/p:Response[@ID='#{signed_element_id}']/a:Assertion/a:Subject/a:NameID")
34
- node.nil? ? nil : strip(node.text)
35
- end
36
- end
37
-
38
- # A hash of all the attributes with the response. Assuming there is only one value for each key
39
- def attributes
40
- @attr_statements ||= begin
41
- stmt_element = xpath("/p:Response/a:Assertion/a:AttributeStatement")
42
- return {} if stmt_element.nil?
43
-
44
- {}.tap do |result|
45
- stmt_element.elements.each do |attr_element|
46
- name = attr_element.attributes["Name"]
47
- value = strip(attr_element.elements.first.text)
48
-
49
- result[name] = result[name.to_sym] = value
50
- end
51
- end
52
- end
53
- end
54
-
55
- # When this user session should expire at latest
56
- def session_expires_at
57
- @expires_at ||= begin
58
- node = xpath("/p:Response/a:Assertion/a:AuthnStatement")
59
- parse_time(node, "SessionNotOnOrAfter")
60
- end
61
- end
62
-
63
- # Conditions (if any) for the assertion to run
64
- def conditions
65
- @conditions ||= begin
66
- xpath("/p:Response/a:Assertion[@ID='#{signed_element_id}']/a:Conditions")
67
- end
68
- end
69
-
70
- private
71
-
72
- def validation_error(message)
73
- raise OmniAuth::Strategies::SAML::ValidationError.new(message)
74
- end
75
-
76
- def validate(soft = true)
77
- validate_response_state(soft) &&
78
- validate_conditions(soft) &&
79
- document.validate(get_fingerprint, soft)
80
- end
81
-
82
- def validate_response_state(soft = true)
83
- if response.empty?
84
- return soft ? false : validation_error("Blank response")
85
- end
86
-
87
- if settings.nil?
88
- return soft ? false : validation_error("No settings on response")
89
- end
90
-
91
- if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
92
- return soft ? false : validation_error("No fingerprint or certificate on settings")
93
- end
94
-
95
- true
96
- end
97
-
98
- def get_fingerprint
99
- if settings.idp_cert
100
- cert = OpenSSL::X509::Certificate.new(settings.idp_cert.gsub(/^ +/, ''))
101
- Digest::SHA1.hexdigest(cert.to_der).upcase.scan(/../).join(":")
102
- else
103
- settings.idp_cert_fingerprint
104
- end
105
- end
106
-
107
- def validate_conditions(soft = true)
108
- return true if conditions.nil?
109
- return true if options[:skip_conditions]
110
-
111
- if not_before = parse_time(conditions, "NotBefore")
112
- if Time.now.utc < not_before
113
- return soft ? false : validation_error("Current time is earlier than NotBefore condition")
114
- end
115
- end
116
-
117
- if not_on_or_after = parse_time(conditions, "NotOnOrAfter")
118
- if Time.now.utc >= not_on_or_after
119
- return soft ? false : validation_error("Current time is on or after NotOnOrAfter condition")
120
- end
121
- end
122
-
123
- true
124
- end
125
-
126
- def parse_time(node, attribute)
127
- if node && node.attributes[attribute]
128
- Time.parse(node.attributes[attribute])
129
- end
130
- end
131
-
132
- def strip(string)
133
- return string unless string
134
- string.gsub(/^\s+/, '').gsub(/\s+$/, '')
135
- end
136
-
137
- def xpath(path)
138
- REXML::XPath.first(document, path, { "p" => PROTOCOL, "a" => ASSERTION })
139
- end
140
-
141
- def signed_element_id
142
- doc_id = document.signed_element_id
143
- doc_id[1, doc_id.size]
144
- end
145
- end
146
- end
147
- end
148
- end
@@ -1,126 +0,0 @@
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 Reserved
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
-
32
- module OmniAuth
33
- module Strategies
34
- class SAML
35
-
36
- module XMLSecurity
37
-
38
- class SignedDocument < REXML::Document
39
- DSIG = "http://www.w3.org/2000/09/xmldsig#"
40
-
41
- attr_accessor :signed_element_id
42
-
43
- def initialize(response)
44
- super(response)
45
- extract_signed_element_id
46
- end
47
-
48
- def validate(idp_cert_fingerprint, soft = true)
49
- # get cert from response
50
- base64_cert = self.elements["//ds:X509Certificate"].text
51
- cert_text = Base64.decode64(base64_cert)
52
- cert = OpenSSL::X509::Certificate.new(cert_text)
53
-
54
- # check cert matches registered idp cert
55
- fingerprint = Digest::SHA1.hexdigest(cert.to_der)
56
-
57
- if fingerprint != idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
58
- return soft ? false : (raise OmniAuth::Strategies::SAML::ValidationError.new("Fingerprint mismatch"))
59
- end
60
-
61
- validate_doc(base64_cert, soft)
62
- end
63
-
64
- def validate_doc(base64_cert, soft = true)
65
- # validate references
66
-
67
- # check for inclusive namespaces
68
-
69
- inclusive_namespaces = []
70
- inclusive_namespace_element = REXML::XPath.first(self, "//ec:InclusiveNamespaces")
71
-
72
- if inclusive_namespace_element
73
- prefix_list = inclusive_namespace_element.attributes.get_attribute('PrefixList').value
74
- inclusive_namespaces = prefix_list.split(" ")
75
- end
76
-
77
- # remove signature node
78
- sig_element = REXML::XPath.first(self, "//ds:Signature", {"ds"=>"http://www.w3.org/2000/09/xmldsig#"})
79
- sig_element.remove
80
-
81
- # check digests
82
- REXML::XPath.each(sig_element, "//ds:Reference", {"ds"=>"http://www.w3.org/2000/09/xmldsig#"}) do |ref|
83
- uri = ref.attributes.get_attribute("URI").value
84
- hashed_element = REXML::XPath.first(self, "//[@ID='#{uri[1,uri.size]}']")
85
- canoner = XML::Util::XmlCanonicalizer.new(false, true)
86
- canoner.inclusive_namespaces = inclusive_namespaces if canoner.respond_to?(:inclusive_namespaces) && !inclusive_namespaces.empty?
87
- canon_hashed_element = canoner.canonicalize(hashed_element)
88
- hash = Base64.encode64(Digest::SHA1.digest(canon_hashed_element)).chomp
89
- digest_value = REXML::XPath.first(ref, "//ds:DigestValue", {"ds"=>"http://www.w3.org/2000/09/xmldsig#"}).text
90
-
91
- if hash != digest_value
92
- return soft ? false : (raise OmniAuth::Strategies::SAML::ValidationError.new("Digest mismatch"))
93
- end
94
- end
95
-
96
- # verify signature
97
- canoner = XML::Util::XmlCanonicalizer.new(false, true)
98
- signed_info_element = REXML::XPath.first(sig_element, "//ds:SignedInfo", {"ds"=>"http://www.w3.org/2000/09/xmldsig#"})
99
- canon_string = canoner.canonicalize(signed_info_element)
100
-
101
- base64_signature = REXML::XPath.first(sig_element, "//ds:SignatureValue", {"ds"=>"http://www.w3.org/2000/09/xmldsig#"}).text
102
- signature = Base64.decode64(base64_signature)
103
-
104
- # get certificate object
105
- cert_text = Base64.decode64(base64_cert)
106
- cert = OpenSSL::X509::Certificate.new(cert_text)
107
-
108
- if !cert.public_key.verify(OpenSSL::Digest::SHA1.new, signature, canon_string)
109
- return soft ? false : (raise OmniAuth::Strategies::SAML::ValidationError.new("Key validation error"))
110
- end
111
-
112
- return true
113
- end
114
-
115
- private
116
-
117
- def extract_signed_element_id
118
- reference_element = REXML::XPath.first(self, "//ds:Signature/ds:SignedInfo/ds:Reference", {"ds"=>DSIG})
119
- self.signed_element_id = reference_element.attribute("URI").value unless reference_element.nil?
120
- end
121
- end
122
- end
123
-
124
- end
125
- end
126
- end
@@ -1,75 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe OmniAuth::Strategies::SAML::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/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/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
@@ -1,90 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe OmniAuth::Strategies::SAML::AuthResponse do
4
- let(:xml) { :example_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
-
15
- describe :name_id do
16
- it "should load the name id from the assertion" do
17
- subject.name_id.should == 'THISISANAMEID'
18
- end
19
-
20
- context "when the response contains the signed_element_id" do
21
- let(:xml) { :response_contains_signed_element }
22
-
23
- it "should load the name id from the assertion" do
24
- subject.name_id.should == 'THISISANAMEID'
25
- end
26
- end
27
- end
28
-
29
- describe :attributes do
30
- it "should return all of the attributes as a hash" do
31
- subject.attributes.should == {
32
- :forename => 'Steven',
33
- :surname => 'Anderson',
34
- :address_1 => '24 Made Up Drive',
35
- :address_2 => nil,
36
- :companyName => 'Test Company Ltd',
37
- :postcode => 'XX2 4XX',
38
- :city => 'Newcastle',
39
- :country => 'United Kingdom',
40
- :userEmailID => 'steve@example.com',
41
- :county => 'TYNESIDE',
42
- :versionID => '1',
43
- :bundleID => '1',
44
-
45
- 'forename' => 'Steven',
46
- 'surname' => 'Anderson',
47
- 'address_1' => '24 Made Up Drive',
48
- 'address_2' => nil,
49
- 'companyName' => 'Test Company Ltd',
50
- 'postcode' => 'XX2 4XX',
51
- 'city' => 'Newcastle',
52
- 'country' => 'United Kingdom',
53
- 'userEmailID' => 'steve@example.com',
54
- 'county' => 'TYNESIDE',
55
- 'versionID' => '1',
56
- 'bundleID' => '1'
57
- }
58
- end
59
-
60
- context "when no attributes exist in the XML" do
61
- let(:xml) { :no_attributes }
62
-
63
- it "should return an empty hash" do
64
- subject.attributes.should == {}
65
- end
66
- end
67
- end
68
-
69
- describe :session_expires_at do
70
- it "should return the SessionNotOnOrAfter as a Ruby date" do
71
- subject.session_expires_at.to_i.should == Time.new(2012, 04, 8, 12, 0, 24, 0).to_i
72
- end
73
- end
74
-
75
- describe :conditions do
76
- it "should return the conditions element from the XML" do
77
- subject.conditions.attributes['NotOnOrAfter'].should == '2012-03-08T16:30:01.336Z'
78
- subject.conditions.attributes['NotBefore'].should == '2012-03-08T16:20:01.336Z'
79
- REXML::XPath.first(subject.conditions, '//saml:Audience').text.should include 'AUDIENCE'
80
- end
81
- end
82
-
83
- describe :valid? do
84
- it_should_behave_like 'a validating method', true
85
- end
86
-
87
- describe :validate! do
88
- it_should_behave_like 'a validating method', false
89
- end
90
- end
@@ -1,5 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe OmniAuth::Strategies::SAML::ValidationError do
4
- it { should be_a Exception }
5
- end
@@ -1,129 +0,0 @@
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.document.stub(:validate).and_return(false)
24
- else
25
- subject.document.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.document.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) { :no_conditions }
59
-
60
- assert_is_valid(soft)
61
- end
62
-
63
- context "when the current time is before the NotBefore time" do
64
- before :each do
65
- Time.stub(:now).and_return(Time.new(2000, 01, 01, 10, 00, 00, 0))
66
- end
67
-
68
- assert_is_not_valid(soft)
69
- end
70
-
71
- context "when the current time is after the NotOnOrAfter time" do
72
- before :each do
73
- # We're assuming here that this code will be out of use in 1000 years...
74
- Time.stub(:now).and_return(Time.new(3012, 01, 01, 10, 00, 00, 0))
75
- end
76
-
77
- assert_is_not_valid(soft)
78
- end
79
-
80
- context "when the current time is between the NotBefore and NotOnOrAfter times" do
81
- before :each do
82
- Time.stub(:now).and_return(Time.new(2012, 3, 8, 16, 25, 00, 0))
83
- end
84
-
85
- assert_is_valid(soft)
86
- end
87
-
88
- context "when skip_conditions option is given" do
89
- before :each do
90
- subject.options[:skip_conditions] = true
91
- end
92
-
93
- assert_is_valid(soft)
94
- end
95
-
96
- context "when the SAML document is valid" do
97
- before :each do
98
- subject.document.should_receive(:validate).with('FINGERPRINT', soft).and_return(true)
99
- subject.options[:skip_conditions] = true
100
- end
101
-
102
- assert_is_valid(soft)
103
- end
104
-
105
- context "when the SAML document is valid and the idp_cert is given" do
106
- let(:cert) do
107
- filename = File.expand_path(File.join('..', '..', 'support', "example_cert.pem"), __FILE__)
108
- IO.read(filename)
109
- end
110
- let(:expected) { 'E6:87:89:FB:F2:5F:CD:B0:31:32:7E:05:44:84:53:B1:EC:4E:3F:FA' }
111
-
112
- before :each do
113
- subject.settings.stub(:idp_cert).and_return(cert)
114
- subject.document.should_receive(:validate).with(expected, soft).and_return(true)
115
- subject.options[:skip_conditions] = true
116
- end
117
-
118
- assert_is_valid(soft)
119
- end
120
-
121
- context "when the SAML document is invalid" do
122
- before :each do
123
- stub_validate_to_fail(soft)
124
- subject.options[:skip_conditions] = true
125
- end
126
-
127
- assert_is_not_valid(soft)
128
- end
129
- end