omniauth-openid-connect 0.1.0 → 0.2.1
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.
- checksums.yaml +4 -4
- data/.gitignore +0 -1
- data/.travis.yml +1 -1
- data/README.md +11 -0
- data/lib/omniauth/openid_connect.rb +1 -0
- data/lib/omniauth/openid_connect/errors.rb +6 -0
- data/lib/omniauth/openid_connect/version.rb +1 -1
- data/lib/omniauth/strategies/openid_connect.rb +170 -21
- data/omniauth-openid-connect.gemspec +1 -1
- data/test/fixtures/id_token.txt +1 -0
- data/test/fixtures/jwks.json +8 -0
- data/test/fixtures/test.crt +19 -0
- data/test/lib/omniauth/strategies/openid_connect_test.rb +285 -11
- data/test/test_helper.rb +3 -1
- metadata +14 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6684f874a6e6cf6c99e102d128363c74aea21591
|
4
|
+
data.tar.gz: 4eaaf78f7d3831ec4556bd6a11bbf48228619248
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e5cb9fc2011ceccc3eaa9b724c08177bca44c1c6f90824b89e439ec403d8dc1d133cca57626785767424707ac0667c634029dbb4420dd36ae885276c25d4e413
|
7
|
+
data.tar.gz: d4cd21adc087562e0251137894288deda422c48eb5a1ecb2706d9702f8bb1c84ee476afdb8edfe52b3424e557febf55494a0d7178e319524081d56921b44c40e
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
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,7 +1,9 @@
|
|
1
1
|
require 'addressable/uri'
|
2
|
-
require
|
2
|
+
require 'timeout'
|
3
|
+
require 'net/http'
|
4
|
+
require 'open-uri'
|
3
5
|
require 'omniauth'
|
4
|
-
require
|
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
|
-
{
|
61
|
+
{raw_info: user_info.raw_attributes}
|
51
62
|
end
|
52
63
|
|
53
64
|
credentials do
|
54
|
-
{
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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 ||=
|
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
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
100
|
-
|
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', '
|
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
|
5
|
+
assert_equal 'https', strategy.options.client_options.scheme
|
6
6
|
assert_equal 443, strategy.options.client_options.port
|
7
|
-
assert_equal
|
8
|
-
assert_equal
|
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.
|
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
|
-
|
25
|
-
|
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
|
-
|
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!({
|
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
|
-
|
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
|
data/test/test_helper.rb
CHANGED
@@ -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
|
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
|
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:
|
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.
|
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.
|
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.
|
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
|