omniauth-saml 0.9.2 → 1.0.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.

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