omniauth-openid-connect 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3b7008499c533fd017b461f9d06c1c4edf013bdb
4
- data.tar.gz: 931b2338aa8c6b3db58f064f7a3be258b796b8c9
3
+ metadata.gz: 6684f874a6e6cf6c99e102d128363c74aea21591
4
+ data.tar.gz: 4eaaf78f7d3831ec4556bd6a11bbf48228619248
5
5
  SHA512:
6
- metadata.gz: 97d00ff91a14014f3eaaa7718b6ee17dc9f9cd6c6cd7ce7f4d89740150e5ca9fd94eff3345b8b862177e7c32ca85e13b314e54435e76c92fd76c7c60ad3d688c
7
- data.tar.gz: 4bc327762fe0e2ad553fa195e5861ce3f71ee840a7684c8c5356c08782a69665d02f984e5fca2f4e5654c489a7552bebc232cd484ecdaf3d13019006f62170bc
6
+ metadata.gz: e5cb9fc2011ceccc3eaa9b724c08177bca44c1c6f90824b89e439ec403d8dc1d133cca57626785767424707ac0667c634029dbb4420dd36ae885276c25d4e413
7
+ data.tar.gz: d4cd21adc087562e0251137894288deda422c48eb5a1ecb2706d9702f8bb1c84ee476afdb8edfe52b3424e557febf55494a0d7178e319524081d56921b44c40e
data/.gitignore CHANGED
@@ -3,7 +3,6 @@
3
3
  .bundle
4
4
  .config
5
5
  .yardoc
6
- Gemfile.lock
7
6
  InstalledFiles
8
7
  _yardoc
9
8
  coverage
@@ -2,5 +2,5 @@ rvm:
2
2
  - 1.9.3
3
3
  - 2.0.0
4
4
  - 2.1.0
5
- - jruby
5
+ - 2.2.0
6
6
  - rbx
data/README.md CHANGED
@@ -47,6 +47,17 @@ Configuration details:
47
47
  is valid. There are plans to bring in implicit flow and hybrid flow at some
48
48
  point, but it hasn't come up yet for me. Those flows aren't best practive for
49
49
  server side web apps anyway and are designed more for native/mobile apps.
50
+ * If you want to pass `state` paramete by yourself. You can set Proc Object.
51
+ e.g. `state: Proc.new{ SecureRandom.hex(32) }`
52
+ * `nonce` is optional. If don't want to pass "nonce" parameter to provider, You should specify
53
+ `false` to `send_nonce` option. (default true)
54
+ * Support for other client authentication methods. If don't specified
55
+ `:client_auth_method` option, automatically set `:basic`.
56
+ * Use "OpenID Connect Discovery", You should specify `true` to `discovery` option. (default false)
57
+ * In "OpenID Connect Discovery", generally provider should have Webfinger endpoint.
58
+ If provider does not have Webfinger endpoint, You can specify "Issuer" to option.
59
+ e.g. `issuer: "myprovider.com"`
60
+ It means to get configuration from "https://myprovider.com/.well-known/openid-configuration".
50
61
 
51
62
  For the full low down on OpenID Connect, please check out
52
63
  [the spec](http://openid.net/specs/openid-connect-core-1_0.html).
@@ -1,2 +1,3 @@
1
+ require "omniauth/openid_connect/errors"
1
2
  require "omniauth/openid_connect/version"
2
3
  require "omniauth/strategies/openid_connect"
@@ -0,0 +1,6 @@
1
+ module OmniAuth
2
+ module OpenIDConnect
3
+ class Error < RuntimeError; end
4
+ class MissingCodeError < Error; end
5
+ end
6
+ end
@@ -1,5 +1,5 @@
1
1
  module OmniAuth
2
2
  module OpenIDConnect
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.1"
4
4
  end
5
5
  end
@@ -1,7 +1,9 @@
1
1
  require 'addressable/uri'
2
- require "net/http"
2
+ require 'timeout'
3
+ require 'net/http'
4
+ require 'open-uri'
3
5
  require 'omniauth'
4
- require "openid_connect"
6
+ require 'openid_connect'
5
7
 
6
8
  module OmniAuth
7
9
  module Strategies
@@ -17,19 +19,27 @@ module OmniAuth
17
19
  port: 443,
18
20
  authorization_endpoint: "/authorize",
19
21
  token_endpoint: "/token",
20
- userinfo_endpoint: "/userinfo"
22
+ userinfo_endpoint: "/userinfo",
23
+ jwks_uri: '/jwk'
21
24
  }
25
+ option :issuer
26
+ option :discovery, false
27
+ option :client_signing_alg
28
+ option :client_jwk_signing_key
29
+ option :client_x509_signing_key
22
30
  option :scope, [:openid]
23
31
  option :response_type, "code"
24
32
  option :state
25
33
  option :response_mode
26
- option :display, nil#, [:page, :popup, :touch, :wap]
27
- option :prompt, nil#, [:none, :login, :consent, :select_account]
34
+ option :display, nil #, [:page, :popup, :touch, :wap]
35
+ option :prompt, nil #, [:none, :login, :consent, :select_account]
28
36
  option :max_age
29
37
  option :ui_locales
30
38
  option :id_token_hint
31
39
  option :login_hint
32
40
  option :acr_values
41
+ option :send_nonce, true
42
+ option :client_auth_method
33
43
 
34
44
  uid { user_info.sub }
35
45
 
@@ -40,6 +50,7 @@ module OmniAuth
40
50
  nickname: user_info.preferred_username,
41
51
  first_name: user_info.given_name,
42
52
  last_name: user_info.family_name,
53
+ gender: user_info.gender,
43
54
  image: user_info.picture,
44
55
  phone: user_info.phone_number,
45
56
  urls: { website: user_info.website }
@@ -47,62 +58,200 @@ module OmniAuth
47
58
  end
48
59
 
49
60
  extra do
50
- { raw_info: user_info.as_json } # UserInfo#as_json actually returns a hash
61
+ {raw_info: user_info.raw_attributes}
51
62
  end
52
63
 
53
64
  credentials do
54
- { token: access_token.access_token }
65
+ {
66
+ id_token: access_token.id_token,
67
+ token: access_token.access_token,
68
+ refresh_token: access_token.refresh_token,
69
+ expires_in: access_token.expires_in,
70
+ scope: access_token.scope
71
+ }
55
72
  end
56
73
 
57
74
  def client
58
75
  @client ||= ::OpenIDConnect::Client.new(client_options)
59
76
  end
60
77
 
78
+ def config
79
+ @config ||= ::OpenIDConnect::Discovery::Provider::Config.discover!(options.issuer)
80
+ end
81
+
61
82
  def request_phase
83
+ options.issuer = issuer if options.issuer.blank?
84
+ discover! if options.discovery
62
85
  redirect authorize_uri
63
86
  end
64
87
 
65
88
  def callback_phase
66
- client.redirect_uri = client_options.redirect_uri
67
- client.authorization_code = authorization_code
68
- access_token
69
- super
89
+ error = request.params['error_reason'] || request.params['error']
90
+ if error
91
+ raise CallbackError.new(request.params['error'], request.params['error_description'] || request.params['error_reason'], request.params['error_uri'])
92
+ elsif request.params['state'].to_s.empty? || request.params['state'] != stored_state
93
+ return Rack::Response.new(['401 Unauthorized'], 401).finish
94
+ elsif !request.params["code"]
95
+ return fail!(:missing_code, OmniAuth::OpenIDConnect::MissingCodeError.new(request.params["error"]))
96
+ else
97
+ options.issuer = issuer if options.issuer.blank?
98
+ discover! if options.discovery
99
+ client.redirect_uri = client_options.redirect_uri
100
+ client.authorization_code = authorization_code
101
+ access_token
102
+ super
103
+ end
104
+ rescue CallbackError => e
105
+ fail!(:invalid_credentials, e)
106
+ rescue ::Timeout::Error, ::Errno::ETIMEDOUT => e
107
+ fail!(:timeout, e)
108
+ rescue ::SocketError => e
109
+ fail!(:failed_to_connect, e)
70
110
  end
71
111
 
112
+
72
113
  def authorization_code
73
114
  request.params["code"]
74
115
  end
75
116
 
117
+ def authorize_uri
118
+ client.redirect_uri = client_options.redirect_uri
119
+ opts = {
120
+ response_type: options.response_type,
121
+ scope: options.scope,
122
+ state: new_state,
123
+ nonce: (new_nonce if options.send_nonce),
124
+ }
125
+ client.authorization_uri(opts.reject{|k,v| v.nil?})
126
+ end
127
+
128
+ def public_key
129
+ if options.discovery
130
+ config.jwks
131
+ else
132
+ key_or_secret
133
+ end
134
+ end
135
+
76
136
  private
77
137
 
138
+ def issuer
139
+ resource = "#{client_options.scheme}://#{client_options.host}" + ((client_options.port) ? ":#{client_options.port.to_s}" : '')
140
+ ::OpenIDConnect::Discovery::Provider.discover!(resource).issuer
141
+ end
142
+
143
+ def discover!
144
+ client_options.authorization_endpoint = config.authorization_endpoint
145
+ client_options.token_endpoint = config.token_endpoint
146
+ client_options.userinfo_endpoint = config.userinfo_endpoint
147
+ client_options.jwks_uri = config.jwks_uri
148
+ end
149
+
78
150
  def user_info
79
151
  @user_info ||= access_token.userinfo!
80
152
  end
81
153
 
82
154
  def access_token
83
- @access_token ||= client.access_token!
155
+ @access_token ||= lambda {
156
+ _access_token = client.access_token!(
157
+ scope: options.scope,
158
+ client_auth_method: options.client_auth_method
159
+ )
160
+ _id_token = decode_id_token _access_token.id_token
161
+ _id_token.verify!(
162
+ issuer: options.issuer,
163
+ client_id: client_options.identifier,
164
+ nonce: stored_nonce
165
+ )
166
+ _access_token
167
+ }.call()
84
168
  end
85
169
 
86
- def authorize_uri
87
- client.redirect_uri = client_options.redirect_uri
88
- client.authorization_uri(
89
- response_type: options.response_type,
90
- scope: options.scope,
91
- nonce: nonce,
92
- )
170
+ def decode_id_token(id_token)
171
+ header = JSON.parse(UrlSafeBase64.decode64(id_token.split('.').first))
172
+ if header.has_key?('kid')
173
+ keys = public_key.inject({}) do |keys, jwk|
174
+ key = JSON::JWK.new(jwk)
175
+ keys.merge! jwk['kid'] => key.to_key.public_key
176
+ end
177
+ ::OpenIDConnect::ResponseObject::IdToken.decode(id_token, keys[header[:kid]])
178
+ else
179
+ case public_key.class
180
+ when JSON::JWK::Set
181
+ jwk = JSON::JWK.new(public_key.first)
182
+ ::OpenIDConnect::ResponseObject::IdToken.decode(id_token, jwk.to_key)
183
+ else
184
+ ::OpenIDConnect::ResponseObject::IdToken.decode(id_token, public_key)
185
+ end
186
+ end
93
187
  end
94
188
 
189
+
95
190
  def client_options
96
191
  options.client_options
97
192
  end
98
193
 
99
- def nonce
100
- session[:nonce] = SecureRandom.hex(16)
194
+ def new_state
195
+ state = options.state.call if options.state.respond_to? :call
196
+ session['omniauth.state'] = state || SecureRandom.hex(16)
197
+ end
198
+
199
+ def stored_state
200
+ session.delete('omniauth.state')
201
+ end
202
+
203
+ def new_nonce
204
+ session['omniauth.nonce'] = SecureRandom.hex(16)
205
+ end
206
+
207
+ def stored_nonce
208
+ session.delete('omniauth.nonce')
101
209
  end
102
210
 
103
211
  def session
104
212
  @env.nil? ? {} : super
105
213
  end
214
+
215
+ def key_or_secret
216
+ case options.client_signing_alg
217
+ when :HS256, :HS384, :HS512
218
+ return client_options.secret
219
+ when :RS256, :RS384, :RS512
220
+ if options.client_jwk_signing_key
221
+ return parse_jwk_key(options.client_jwk_signing_key)
222
+ elsif options.client_x509_signing_key
223
+ return parse_x509_key(options.client_x509_signing_key)
224
+ end
225
+ else
226
+ end
227
+ end
228
+
229
+ def parse_x509_key(key)
230
+ OpenSSL::X509::Certificate.new(key).public_key
231
+ end
232
+
233
+ def parse_jwk_key(key)
234
+ json = JSON.parse(key)
235
+ JSON::JWK::Set.new json['keys']
236
+ end
237
+
238
+ def decode(str)
239
+ UrlSafeBase64.decode64(str).unpack('B*').first.to_i(2).to_s
240
+ end
241
+
242
+ class CallbackError < StandardError
243
+ attr_accessor :error, :error_reason, :error_uri
244
+
245
+ def initialize(error, error_reason=nil, error_uri=nil)
246
+ self.error = error
247
+ self.error_reason = error_reason
248
+ self.error_uri = error_uri
249
+ end
250
+
251
+ def message
252
+ [error, error_reason, error_uri].compact.join(' | ')
253
+ end
254
+ end
106
255
  end
107
256
  end
108
257
  end
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
 
21
21
  spec.add_dependency 'omniauth', '~> 1.1'
22
- spec.add_dependency 'openid_connect', '= 0.7.3'
22
+ spec.add_dependency 'openid_connect', '~> 0.9.2'
23
23
  spec.add_dependency 'addressable', '~> 2.3'
24
24
  spec.add_development_dependency "bundler", "~> 1.5"
25
25
  spec.add_development_dependency "minitest"
@@ -0,0 +1 @@
1
+ eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAKfQ.ggW8hZ1EuVLuxNuuIJKX_V8a_OMXzR0EHR9R6jgdqrOOF4daGU96Sr_P6qJp6IcmD3HP99Obi1PRs-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CJNqeGpe-gccMg4vfKjkM8FcGvnzZUN4_KSP0aAp1tOJ1zZwgjxqGByKHiOtX7TpdQyHE5lcMiKPXfEIQILVq0pc_E2DzL7emopWoaoZTF_m0_N0YzFC6g6EJbOEoRoSK5hoDalrcvRYLSrQAZZKflyuVCyixEoV9GfNQC3_osjzw2PAithfubEEBLuVVk4XUVrWOLrLl0nx7RkKU8NXNHq-rvKMzqg
@@ -0,0 +1,8 @@
1
+ {"keys": [{
2
+ "kty": "RSA",
3
+ "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
4
+ "e": "AQAB",
5
+ "alg": "RS256",
6
+ "kid": "1e9gdk7"
7
+ }]
8
+ }
@@ -0,0 +1,19 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDJDCCAgwCCQC57Ob2JfXb+DANBgkqhkiG9w0BAQUFADBUMQswCQYDVQQGEwJK
3
+ UDEOMAwGA1UECBMFVG9reW8xITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5
4
+ IEx0ZDESMBAGA1UEAxMJbG9jYWxob3N0MB4XDTE0MDgwMTA4NTAxM1oXDTE1MDgw
5
+ MTA4NTAxM1owVDELMAkGA1UEBhMCSlAxDjAMBgNVBAgTBVRva3lvMSEwHwYDVQQK
6
+ ExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCWxvY2FsaG9zdDCC
7
+ ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN+7czSGHN2087T+oX2kBCY/
8
+ XN6UOS/mdU2Gn//omZlyxsQXIqvgBLNWeCVt4QdlFUbgPLggfXUelECV/RUOCIIi
9
+ F2Th4t3x1LviN2XkUiva0DZBnOycqEaJdkyreEuGL1CLVZgZjKmSzNqLl0Yci3D0
10
+ zgVsXFZSadQebietm4CCmfJYREt9NJxXcrLxVDgat/Xm/KJBsohs3f+cbBT8EXer
11
+ 7+2oZjZoVUgw1hu0alaOvAfE4mxsVwjn3g2mjDqRJLbbuWqgDobjMHah+d4zwJvN
12
+ ePK8E0hfaz/XBLsJ4e6bQA3M3bANEgSvsicup/qb/0th4gUdc/kj4aJGj0RP7oEC
13
+ AwEAATANBgkqhkiG9w0BAQUFAAOCAQEADuVec/8u2qJiq6K2W/gSLGYCBZq64OrA
14
+ s7L2+S82m9/3gAb62wGcDNZjIGFDQubXmO6RhHv7JUT5YZqv9/kRGTJcHDUrwwoN
15
+ IE99CIPizp7VfnrZ6GsYeszSsw3m+mKTETm+6ELmaSDbYAsrCg4IpGwUF0L88ATv
16
+ CJ8QzW4X7b9dYVc7UAYyCie2N65GXfesBbRlSwFLuVqIzZfMdNpNijTIUwUqGSME
17
+ b8IjLYzvekP53CO4wEBRrAVIPNXgftorxIE30OLWua2Qw3y6Pn+Qp5fLe47025S7
18
+ Lcec18/FbHG0Vbq0qO9cKQw80XyK31N6z556wr2GN2WyixkzVRddXA==
19
+ -----END CERTIFICATE-----
@@ -2,35 +2,189 @@ require_relative '../../../test_helper'
2
2
 
3
3
  class OmniAuth::Strategies::OpenIDConnectTest < StrategyTestCase
4
4
  def test_client_options_defaults
5
- assert_equal "https", strategy.options.client_options.scheme
5
+ assert_equal 'https', strategy.options.client_options.scheme
6
6
  assert_equal 443, strategy.options.client_options.port
7
- assert_equal "/authorize", strategy.options.client_options.authorization_endpoint
8
- assert_equal "/token", strategy.options.client_options.token_endpoint
7
+ assert_equal '/authorize', strategy.options.client_options.authorization_endpoint
8
+ assert_equal '/token', strategy.options.client_options.token_endpoint
9
9
  end
10
10
 
11
11
  def test_request_phase
12
- expected_redirect = /^https:\/\/example\.com\/authorize\?client_id=1234&nonce=[\w\d]{32}&response_type=code&scope=openid$/
13
- strategy.options.client_options.host = "example.com"
12
+ expected_redirect = /^https:\/\/example\.com\/authorize\?client_id=1234&nonce=[\w\d]{32}&response_type=code&scope=openid&state=[\w\d]{32}$/
13
+ strategy.options.issuer = 'example.com'
14
+ strategy.options.client_options.host = 'example.com'
15
+ strategy.expects(:redirect).with(regexp_matches(expected_redirect))
16
+ strategy.request_phase
17
+ end
18
+
19
+ def test_request_phase_with_discovery
20
+ expected_redirect = /^https:\/\/example\.com\/authorization\?client_id=1234&nonce=[\w\d]{32}&response_type=code&scope=openid&state=[\w\d]{32}$/
21
+ strategy.options.client_options.host = 'example.com'
22
+ strategy.options.discovery = true
23
+
24
+ issuer = stub('OpenIDConnect::Discovery::Issuer')
25
+ issuer.stubs(:issuer).returns('https://example.com/')
26
+ ::OpenIDConnect::Discovery::Provider.stubs(:discover!).returns(issuer)
27
+
28
+ config = stub('OpenIDConnect::Discovery::Provder::Config')
29
+ config.stubs(:authorization_endpoint).returns('https://example.com/authorization')
30
+ config.stubs(:token_endpoint).returns('https://example.com/token')
31
+ config.stubs(:userinfo_endpoint).returns('https://example.com/userinfo')
32
+ config.stubs(:jwks_uri).returns('https://example.com/jwks')
33
+ ::OpenIDConnect::Discovery::Provider::Config.stubs(:discover!).with('https://example.com/').returns(config)
34
+
14
35
  strategy.expects(:redirect).with(regexp_matches(expected_redirect))
15
36
  strategy.request_phase
37
+
38
+ assert_equal strategy.options.issuer, 'https://example.com/'
39
+ assert_equal strategy.options.client_options.authorization_endpoint, 'https://example.com/authorization'
40
+ assert_equal strategy.options.client_options.token_endpoint, 'https://example.com/token'
41
+ assert_equal strategy.options.client_options.userinfo_endpoint, 'https://example.com/userinfo'
42
+ assert_equal strategy.options.client_options.jwks_uri, 'https://example.com/jwks'
16
43
  end
17
44
 
18
45
  def test_uid
19
46
  assert_equal user_info.sub, strategy.uid
20
47
  end
21
48
 
22
- def test_callback_phase
49
+ def test_callback_phase(session = {}, params = {})
23
50
  code = SecureRandom.hex(16)
24
- request.stubs(:params).returns({"code" => code})
25
- request.stubs(:path_info).returns("")
51
+ state = SecureRandom.hex(16)
52
+ nonce = SecureRandom.hex(16)
53
+ request.stubs(:params).returns({'code' => code,'state' => state})
54
+ request.stubs(:path_info).returns('')
55
+
56
+ strategy.options.issuer = 'example.com'
57
+ strategy.options.client_signing_alg = :RS256
58
+ strategy.options.client_jwk_signing_key = File.read('test/fixtures/jwks.json')
59
+
60
+ id_token = stub('OpenIDConnect::ResponseObject::IdToken')
61
+ id_token.stubs(:verify!).with({:issuer => strategy.options.issuer, :client_id => @identifier, :nonce => nonce}).returns(true)
62
+ ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token)
26
63
 
27
64
  strategy.unstub(:user_info)
28
65
  access_token = stub('OpenIDConnect::AccessToken')
29
66
  access_token.stubs(:access_token)
30
- client.expects(:access_token!).returns(access_token)
67
+ access_token.stubs(:refresh_token)
68
+ access_token.stubs(:expires_in)
69
+ access_token.stubs(:scope)
70
+ access_token.stubs(:id_token).returns(File.read('test/fixtures/id_token.txt'))
71
+ client.expects(:access_token!).at_least_once.returns(access_token)
72
+ access_token.expects(:userinfo!).returns(user_info)
73
+
74
+ strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}})
75
+ strategy.callback_phase
76
+ end
77
+
78
+ def test_callback_phase_with_discovery
79
+ code = SecureRandom.hex(16)
80
+ state = SecureRandom.hex(16)
81
+ nonce = SecureRandom.hex(16)
82
+ jwks = JSON::JWK::Set.new(JSON.parse(File.read('test/fixtures/jwks.json'))['keys'])
83
+
84
+ request.stubs(:params).returns({'code' => code,'state' => state})
85
+ request.stubs(:path_info).returns('')
86
+
87
+ strategy.options.client_options.host = 'example.com'
88
+ strategy.options.discovery = true
89
+
90
+ issuer = stub('OpenIDConnect::Discovery::Issuer')
91
+ issuer.stubs(:issuer).returns('https://example.com/')
92
+ ::OpenIDConnect::Discovery::Provider.stubs(:discover!).returns(issuer)
93
+
94
+ config = stub('OpenIDConnect::Discovery::Provder::Config')
95
+ config.stubs(:authorization_endpoint).returns('https://example.com/authorization')
96
+ config.stubs(:token_endpoint).returns('https://example.com/token')
97
+ config.stubs(:userinfo_endpoint).returns('https://example.com/userinfo')
98
+ config.stubs(:jwks_uri).returns('https://example.com/jwks')
99
+ config.stubs(:jwks).returns(jwks)
100
+
101
+ ::OpenIDConnect::Discovery::Provider::Config.stubs(:discover!).with('https://example.com/').returns(config)
102
+
103
+ id_token = stub('OpenIDConnect::ResponseObject::IdToken')
104
+ id_token.stubs(:verify!).with({:issuer => 'https://example.com/', :client_id => @identifier, :nonce => nonce}).returns(true)
105
+ ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token)
106
+
107
+ strategy.unstub(:user_info)
108
+ access_token = stub('OpenIDConnect::AccessToken')
109
+ access_token.stubs(:access_token)
110
+ access_token.stubs(:refresh_token)
111
+ access_token.stubs(:expires_in)
112
+ access_token.stubs(:scope)
113
+ access_token.stubs(:id_token).returns(File.read('test/fixtures/id_token.txt'))
114
+ client.expects(:access_token!).at_least_once.returns(access_token)
31
115
  access_token.expects(:userinfo!).returns(user_info)
32
116
 
33
- strategy.call!({"rack.session" => {}})
117
+ strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}})
118
+ strategy.callback_phase
119
+ end
120
+
121
+ def test_callback_phase_with_error
122
+ state = SecureRandom.hex(16)
123
+ nonce = SecureRandom.hex(16)
124
+ request.stubs(:params).returns({'error' => 'invalid_request'})
125
+ request.stubs(:path_info).returns('')
126
+
127
+ strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}})
128
+ strategy.expects(:fail!)
129
+ strategy.callback_phase
130
+ end
131
+
132
+ def test_callback_phase_with_invalid_state
133
+ code = SecureRandom.hex(16)
134
+ state = SecureRandom.hex(16)
135
+ nonce = SecureRandom.hex(16)
136
+ request.stubs(:params).returns({'code' => code,'state' => 'foobar'})
137
+ request.stubs(:path_info).returns('')
138
+
139
+ strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}})
140
+ result = strategy.callback_phase
141
+
142
+ assert result.kind_of?(Array)
143
+ assert result.first == 401, "Expecting unauthorized"
144
+ end
145
+
146
+ def test_callback_phase_with_timeout
147
+ code = SecureRandom.hex(16)
148
+ state = SecureRandom.hex(16)
149
+ nonce = SecureRandom.hex(16)
150
+ request.stubs(:params).returns({'code' => code,'state' => state})
151
+ request.stubs(:path_info).returns('')
152
+
153
+ strategy.options.issuer = 'example.com'
154
+
155
+ strategy.stubs(:access_token).raises(::Timeout::Error.new('error'))
156
+ strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}})
157
+ strategy.expects(:fail!)
158
+ strategy.callback_phase
159
+ end
160
+
161
+ def test_callback_phase_with_etimeout
162
+ code = SecureRandom.hex(16)
163
+ state = SecureRandom.hex(16)
164
+ nonce = SecureRandom.hex(16)
165
+ request.stubs(:params).returns({'code' => code,'state' => state})
166
+ request.stubs(:path_info).returns('')
167
+
168
+ strategy.options.issuer = 'example.com'
169
+
170
+ strategy.stubs(:access_token).raises(::Errno::ETIMEDOUT.new('error'))
171
+ strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}})
172
+ strategy.expects(:fail!)
173
+ strategy.callback_phase
174
+ end
175
+
176
+ def test_callback_phase_with_socket_error
177
+ code = SecureRandom.hex(16)
178
+ state = SecureRandom.hex(16)
179
+ nonce = SecureRandom.hex(16)
180
+ request.stubs(:params).returns({'code' => code,'state' => state})
181
+ request.stubs(:path_info).returns('')
182
+
183
+ strategy.options.issuer = 'example.com'
184
+
185
+ strategy.stubs(:access_token).raises(::SocketError.new('error'))
186
+ strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}})
187
+ strategy.expects(:fail!)
34
188
  strategy.callback_phase
35
189
  end
36
190
 
@@ -41,6 +195,7 @@ class OmniAuth::Strategies::OpenIDConnectTest < StrategyTestCase
41
195
  assert_equal user_info.preferred_username, info[:nickname]
42
196
  assert_equal user_info.given_name, info[:first_name]
43
197
  assert_equal user_info.family_name, info[:last_name]
198
+ assert_equal user_info.gender, info[:gender]
44
199
  assert_equal user_info.picture, info[:image]
45
200
  assert_equal user_info.phone_number, info[:phone]
46
201
  assert_equal({ website: user_info.website }, info[:urls])
@@ -51,10 +206,129 @@ class OmniAuth::Strategies::OpenIDConnectTest < StrategyTestCase
51
206
  end
52
207
 
53
208
  def test_credentials
209
+ strategy.options.issuer = 'example.com'
210
+ strategy.options.client_signing_alg = :RS256
211
+ strategy.options.client_jwk_signing_key = File.read('test/fixtures/jwks.json')
212
+
213
+ id_token = stub('OpenIDConnect::ResponseObject::IdToken')
214
+ id_token.stubs(:verify!).returns(true)
215
+ ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token)
216
+
54
217
  access_token = stub('OpenIDConnect::AccessToken')
55
218
  access_token.stubs(:access_token).returns(SecureRandom.hex(16))
219
+ access_token.stubs(:refresh_token).returns(SecureRandom.hex(16))
220
+ access_token.stubs(:expires_in).returns(Time.now)
221
+ access_token.stubs(:scope).returns('openidconnect')
222
+ access_token.stubs(:id_token).returns(File.read('test/fixtures/id_token.txt'))
223
+
56
224
  client.expects(:access_token!).returns(access_token)
225
+ access_token.expects(:refresh_token).returns(access_token.refresh_token)
226
+ access_token.expects(:expires_in).returns(access_token.expires_in)
227
+
228
+ assert_equal({ id_token: access_token.id_token,
229
+ token: access_token.access_token,
230
+ refresh_token: access_token.refresh_token,
231
+ expires_in: access_token.expires_in,
232
+ scope: access_token.scope
233
+ }, strategy.credentials)
234
+ end
235
+
236
+ def test_option_send_nonce
237
+ strategy.options.client_options[:host] = "foobar.com"
238
+
239
+ assert(strategy.authorize_uri =~ /nonce=/, "URI must contain nonce")
240
+
241
+ strategy.options.send_nonce = false
242
+ assert(!(strategy.authorize_uri =~ /nonce=/), "URI must not contain nonce")
243
+ end
244
+
245
+ def test_failure_endpoint_redirect
246
+ OmniAuth.config.stubs(:failure_raise_out_environments).returns([])
247
+ strategy.stubs(:env).returns({})
248
+ request.stubs(:params).returns({"error" => "access denied"})
249
+
250
+ result = strategy.callback_phase
251
+
252
+ assert(result.is_a? Array)
253
+ assert(result[0] == 302, "Redirect")
254
+ assert(result[1]["Location"] =~ /\/auth\/failure/)
255
+ end
256
+
257
+ def test_state
258
+ strategy.options.state = lambda { 42 }
259
+ session = { "state" => 42 }
260
+
261
+ expected_redirect = /&state=/
262
+ strategy.options.issuer = 'example.com'
263
+ strategy.options.client_options.host = "example.com"
264
+ strategy.expects(:redirect).with(regexp_matches(expected_redirect))
265
+ strategy.request_phase
266
+
267
+ # this should succeed as the correct state is passed with the request
268
+ test_callback_phase(session, { "state" => 42 })
269
+
270
+ # the following should fail because the wrong state is passed to the callback
271
+ code = SecureRandom.hex(16)
272
+ request.stubs(:params).returns({"code" => code, "state" => 43})
273
+ request.stubs(:path_info).returns("")
274
+ strategy.call!({"rack.session" => session})
275
+
276
+ result = strategy.callback_phase
277
+
278
+ assert result.kind_of?(Array)
279
+ assert result.first == 401, "Expecting unauthorized"
280
+ end
281
+
282
+ def test_option_client_auth_method
283
+ code = SecureRandom.hex(16)
284
+ state = SecureRandom.hex(16)
285
+ nonce = SecureRandom.hex(16)
286
+
287
+ opts = strategy.options.client_options
288
+ opts[:host] = "foobar.com"
289
+ strategy.options.issuer = "foobar.com"
290
+ strategy.options.client_auth_method = :not_basic
291
+ strategy.options.client_signing_alg = :RS256
292
+ strategy.options.client_jwk_signing_key = File.read('test/fixtures/jwks.json')
293
+
294
+ json_response = {access_token: 'test_access_token',
295
+ id_token: File.read('test/fixtures/id_token.txt'),
296
+ token_type: 'Bearer',
297
+ }.to_json
298
+ success = Struct.new(:status, :body).new(200, json_response)
299
+
300
+ request.stubs(:path_info).returns('')
301
+ strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}})
302
+
303
+ id_token = stub('OpenIDConnect::ResponseObject::IdToken')
304
+ id_token.stubs(:verify!).with({:issuer => strategy.options.issuer, :client_id => @identifier, :nonce => nonce}).returns(true)
305
+ ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token)
306
+
307
+ HTTPClient.any_instance.stubs(:post).with(
308
+ "#{opts.scheme}://#{opts.host}:#{opts.port}#{opts.token_endpoint}",
309
+ {scope: 'openid', :grant_type => :client_credentials, :client_id => @identifier, :client_secret => @secret},
310
+ {}
311
+ ).returns(success)
312
+
313
+ assert(strategy.send :access_token)
314
+ end
315
+
316
+ def test_public_key_with_jwk
317
+ strategy.options.client_signing_alg = :RS256
318
+ strategy.options.client_jwk_signing_key = File.read('./test/fixtures/jwks.json')
319
+
320
+ assert_equal JSON::JWK::Set, strategy.public_key.class
321
+ end
322
+
323
+ def test_public_key_with_x509
324
+ strategy.options.client_signing_alg = :RS256
325
+ strategy.options.client_x509_signing_key = File.read('./test/fixtures/test.crt')
326
+ assert_equal OpenSSL::PKey::RSA, strategy.public_key.class
327
+ end
57
328
 
58
- assert_equal({ token: access_token.access_token }, strategy.credentials)
329
+ def test_public_key_with_hmac
330
+ strategy.options.client_options.secret = 'secret'
331
+ strategy.options.client_signing_alg = :HS256
332
+ assert_equal strategy.options.client_options.secret, strategy.public_key
59
333
  end
60
334
  end
@@ -8,6 +8,7 @@ Coveralls.wear!
8
8
  require 'minitest/autorun'
9
9
  require 'mocha/mini_test'
10
10
  require 'faker'
11
+ require 'active_support'
11
12
  require_relative '../lib/omniauth-openid-connect'
12
13
 
13
14
  OmniAuth.config.test_mode = true
@@ -29,7 +30,7 @@ class StrategyTestCase < MiniTest::Test
29
30
  end
30
31
 
31
32
  def user_info
32
- @user_info ||= OpenIDConnect::ResponseObject::UserInfo::OpenID.new(
33
+ @user_info ||= OpenIDConnect::ResponseObject::UserInfo.new(
33
34
  sub: SecureRandom.hex(16),
34
35
  name: Faker::Name.name,
35
36
  email: Faker::Internet.email,
@@ -37,6 +38,7 @@ class StrategyTestCase < MiniTest::Test
37
38
  preferred_username: Faker::Internet.user_name,
38
39
  given_name: Faker::Name.first_name,
39
40
  family_name: Faker::Name.last_name,
41
+ gender: 'female',
40
42
  picture: Faker::Internet.url + ".png",
41
43
  phone_number: Faker::PhoneNumber.phone_number,
42
44
  website: Faker::Internet.url,
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omniauth-openid-connect
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Bohn
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-03-15 00:00:00.000000000 Z
11
+ date: 2015-11-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: omniauth
@@ -28,16 +28,16 @@ dependencies:
28
28
  name: openid_connect
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - '='
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 0.7.3
33
+ version: 0.9.2
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - '='
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 0.7.3
40
+ version: 0.9.2
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: addressable
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -222,9 +222,13 @@ files:
222
222
  - Rakefile
223
223
  - lib/omniauth-openid-connect.rb
224
224
  - lib/omniauth/openid_connect.rb
225
+ - lib/omniauth/openid_connect/errors.rb
225
226
  - lib/omniauth/openid_connect/version.rb
226
227
  - lib/omniauth/strategies/openid_connect.rb
227
228
  - omniauth-openid-connect.gemspec
229
+ - test/fixtures/id_token.txt
230
+ - test/fixtures/jwks.json
231
+ - test/fixtures/test.crt
228
232
  - test/lib/omniauth/openid_connect/version_test.rb
229
233
  - test/lib/omniauth/strategies/openid_connect_test.rb
230
234
  - test/test_helper.rb
@@ -248,11 +252,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
248
252
  version: '0'
249
253
  requirements: []
250
254
  rubyforge_project:
251
- rubygems_version: 2.2.0
255
+ rubygems_version: 2.4.5.1
252
256
  signing_key:
253
257
  specification_version: 4
254
258
  summary: OpenID Connect Strategy for OmniAuth
255
259
  test_files:
260
+ - test/fixtures/id_token.txt
261
+ - test/fixtures/jwks.json
262
+ - test/fixtures/test.crt
256
263
  - test/lib/omniauth/openid_connect/version_test.rb
257
264
  - test/lib/omniauth/strategies/openid_connect_test.rb
258
265
  - test/test_helper.rb