openid_connect_client 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6e1880db733cf2f8d0daa8a44f973113eb7094c9
4
+ data.tar.gz: da7152755f9b914d25c7e0afab78adffd30801b0
5
+ SHA512:
6
+ metadata.gz: f7506f7123ffcbf9017dba62c5eb43d002ce99d8c77d0006f97d2ed91268b26b2749525bc2cb9ca1d0888e5f7e30754c0caf528e2cba29aaf8708f2588d2865c
7
+ data.tar.gz: 2988f54c6c221b8549b2592c9e9e87f15e1d84a220b5eb1c06351b446e79324a02ab5745fa2ed2283f450f8059e68d4abd786926be387a7e168d6897bef95e92
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.3
4
+ before_install: gem install bundler -v 1.11.2
@@ -0,0 +1,49 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, and in the interest of
4
+ fostering an open and welcoming community, we pledge to respect all people who
5
+ contribute through reporting issues, posting feature requests, updating
6
+ documentation, submitting pull requests or patches, and other activities.
7
+
8
+ We are committed to making participation in this project a harassment-free
9
+ experience for everyone, regardless of level of experience, gender, gender
10
+ identity and expression, sexual orientation, disability, personal appearance,
11
+ body size, race, ethnicity, age, religion, or nationality.
12
+
13
+ Examples of unacceptable behavior by participants include:
14
+
15
+ * The use of sexualized language or imagery
16
+ * Personal attacks
17
+ * Trolling or insulting/derogatory comments
18
+ * Public or private harassment
19
+ * Publishing other's private information, such as physical or electronic
20
+ addresses, without explicit permission
21
+ * Other unethical or unprofessional conduct
22
+
23
+ Project maintainers have the right and responsibility to remove, edit, or
24
+ reject comments, commits, code, wiki edits, issues, and other contributions
25
+ that are not aligned to this Code of Conduct, or to ban temporarily or
26
+ permanently any contributor for other behaviors that they deem inappropriate,
27
+ threatening, offensive, or harmful.
28
+
29
+ By adopting this Code of Conduct, project maintainers commit themselves to
30
+ fairly and consistently applying these principles to every aspect of managing
31
+ this project. Project maintainers who do not follow or enforce the Code of
32
+ Conduct may be permanently removed from the project team.
33
+
34
+ This code of conduct applies both within project spaces and in public spaces
35
+ when an individual is representing the project or its community.
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
38
+ reported by contacting a project maintainer at zeta@widcket.com. All
39
+ complaints will be reviewed and investigated and will result in a response that
40
+ is deemed necessary and appropriate to the circumstances. Maintainers are
41
+ obligated to maintain confidentiality with regard to the reporter of an
42
+ incident.
43
+
44
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
45
+ version 1.3.0, available at
46
+ [http://contributor-covenant.org/version/1/3/0/][version]
47
+
48
+ [homepage]: http://contributor-covenant.org
49
+ [version]: http://contributor-covenant.org/version/1/3/0/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in openid_connect_client.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,25 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Rita Zerrizuela
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10
+
11
+ ------
12
+
13
+ OpenID-Connect-PHP License
14
+
15
+ Licensed under the Apache License, Version 2.0 (the "License");
16
+ you may not use this file except in compliance with the License.
17
+ You may obtain a copy of the License at
18
+
19
+ [http://www.apache.org/licenses/LICENSE-2.0]
20
+
21
+ Unless required by applicable law or agreed to in writing, software
22
+ distributed under the License is distributed on an "AS IS" BASIS,
23
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
24
+ See the License for the specific language governing permissions and
25
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # openid-connect-ruby
2
+
3
+ A literal, not so idiomatic ruby port of Michael Jett's excellent [OpenID Connect](https://github.com/jumbojett/OpenID-Connect-PHP) library for PHP.
4
+
5
+ ## Requirements
6
+ - Curb
7
+
8
+ ## Usage
9
+ See `example.rb`
10
+ ```
11
+ oidc = OpenIDConnectClient.new('https://provider.com/openid', 'CLIENT_ID', 'SECRET')
12
+ oidc.redirect_url = "http://yourweb.com/callback"
13
+ oidc.scopes = "openid email profile address phone"
14
+
15
+ oidc.authorize()
16
+
17
+ session[:state] = oidc.state
18
+ redirect_to(oidc.auth_endpoint)
19
+ ```
20
+
21
+ ### On the callback
22
+ ```
23
+ oidc = OpenIDConnectClient.new('https://provider.com/openid', 'CLIENT_ID', 'SECRET')
24
+ oidc.redirect_url = "http://yourweb.com/callback"
25
+ oidc.scopes = "openid email profile address phone"
26
+
27
+ oidc.authenticate()
28
+
29
+ email = oidc.get('email')
30
+ given_name = oidc.get('given_name')
31
+ address = oidc.get('address')
32
+ ```
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "openid_connect_client"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/example.rb ADDED
@@ -0,0 +1,63 @@
1
+ require 'nyny'
2
+ require 'erb'
3
+ require 'openid_connect_client'
4
+
5
+ TEMPLATE = DATA.read.freeze
6
+
7
+ class App < NYNY::App
8
+ use Rack::Session::Cookie, :secret => 'your_secret'
9
+
10
+ get '/' do
11
+ oidc = get_client()
12
+ oidc.authorize()
13
+
14
+ session[:state] = oidc.state
15
+ redirect_to(oidc.auth_endpoint)
16
+ end
17
+
18
+ get '/callback' do
19
+ oidc = get_client(params)
20
+ oidc.authenticate()
21
+
22
+ email = oidc.get('email')
23
+ given_name = oidc.get('given_name')
24
+ address = oidc.get('address')
25
+
26
+ ERB.new(TEMPLATE).result(binding)
27
+ end
28
+
29
+ helpers do
30
+ def get_client(params = nil)
31
+ oidc = OpenIDConnectClient::Client.new('PROVIDER_ENDPOINT', 'CLIENT_ID', 'SECRET')
32
+ oidc.redirect_url = 'http://localhost:9292/callback'
33
+ oidc.scopes = 'openid email profile address phone'
34
+
35
+ oidc.state = session[:state]
36
+ oidc.params = params if params
37
+
38
+ oidc
39
+ end
40
+ end
41
+ end
42
+
43
+ App.run!
44
+
45
+ __END__
46
+ <html>
47
+ <head>
48
+ <title>OpenID Connect Client Example</title>
49
+ <style>
50
+ body {
51
+ font-family: Helvetica, Arial, sans-serif;
52
+ }
53
+ </style>
54
+ </head>
55
+ <body>
56
+ <div>
57
+ Hi <%= given_name %><br>
58
+ Your email is <%= email %><br>
59
+ Your address is <%= address[:street_address] %><br>
60
+ Your postal code is <%= address[:postal_code] %>
61
+ </div>
62
+ </body>
63
+ </html>
@@ -0,0 +1,3 @@
1
+ module OpenIDConnectClient
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,685 @@
1
+ require "openid_connect_client/version"
2
+
3
+ module OpenIDConnectClient
4
+ class OpenIDConnectClientException < Exception
5
+ end
6
+
7
+ class Client
8
+ require 'securerandom'
9
+ require 'digest/md5'
10
+ require 'cgi'
11
+ require 'base64'
12
+ require 'openssl'
13
+ require 'xml/libxml'
14
+ require 'curb'
15
+
16
+
17
+ private #==============================================================================================================================
18
+
19
+ #
20
+ # Gets anything that we need configuration wise including endpoints, and other values
21
+ #
22
+ # @return void
23
+ # @throws OpenIDConnectClientException
24
+ #
25
+ def get_provider_config
26
+
27
+ well_known_config_response = fetch_url(@well_known_config_url).body_str
28
+
29
+ unless well_known_config_response
30
+ raise OpenIDConnectClientException, "Unable to get provider configuration data. Make sure your provider has a well known configuration available."
31
+ end
32
+
33
+ values = JSON[well_known_config_response]
34
+
35
+ values.each do |key, value|
36
+ @state[key.to_sym] = value
37
+ end
38
+ end
39
+
40
+ #
41
+ # @param param
42
+ # @throws OpenIDConnectClientException
43
+ # @return string
44
+ #
45
+ def get_provider_config_value(param)
46
+ # If the configuration value is not available, attempt to fetch it from a well known config endpoint
47
+ # This is also known as auto "discovery"
48
+ if @state[param].nil?
49
+ well_known_config_response = fetch_url(@well_known_config_url).body_str
50
+
51
+ unless well_known_config_response
52
+ raise OpenIDConnectClientException, "Unable to get provider configuration data. Make sure your provider has a well known configuration available."
53
+ end
54
+
55
+ value = JSON[well_known_config_response][param.to_s]
56
+
57
+ unless value
58
+ raise OpenIDConnectClientException, "The provider #{param} has not been set. Make sure your provider has a well known configuration available."
59
+ end
60
+
61
+ @state[param] = value
62
+ end
63
+
64
+ @state[param]
65
+ end
66
+
67
+ #
68
+ # @param array keys
69
+ # @param array header
70
+ # @throws OpenIDConnectClientException
71
+ # @return object
72
+ #
73
+ def get_key_for_header(keys, header)
74
+ keys.each do |key|
75
+ if !(key["alg"] and header["kid"]) and key["kty"] == 'RSA' or (key["alg"] == header["alg"] and key["kid"] == header["kid"])
76
+ return key
77
+ end
78
+ end
79
+
80
+ if header["kid"]
81
+ raise OpenIDConnectClientException, "Unable to find a key for (algorithm, kid): #{header["alg"]}, #{header["kid"]}."
82
+ else
83
+ raise OpenIDConnectClientException, "Unable to find a key for RSA."
84
+ end
85
+ end
86
+
87
+ #
88
+ # @param jwt string encoded JWT
89
+ # @throws OpenIDConnectClientException
90
+ # @return bool
91
+ #
92
+ def verify_JWT_signature(jwt)
93
+ parts = jwt.split(".")
94
+ signature = decode_64(parts.pop())
95
+
96
+ decoded_header = decode_64(parts[0])
97
+ header = JSON[decoded_header]
98
+
99
+ payload = parts.join(".")
100
+
101
+ fetched_jwks = fetch_url(get_provider_config_value(:jwks_uri)).body_str
102
+ jwks = JSON[fetched_jwks]
103
+
104
+ unless not jwks.nil?
105
+ raise OpenIDConnectClientException, "Error decoding JSON from jwks_uri."
106
+ end
107
+
108
+ verified = false
109
+
110
+ case header["alg"]
111
+ when 'RS256', 'RS384', 'RS512'
112
+ hashtype = "sha" + header["alg"][0,2]
113
+ verified = verify_RSA_JWT_signature(hashtype, get_key_for_header(jwks["keys"], header), payload, signature)
114
+ else
115
+ raise OpenIDConnectClientException, "No support for signature type: #{header["alg"]}."
116
+ end
117
+
118
+ verified
119
+ end
120
+
121
+ #
122
+ # @param string hashtype
123
+ # @param object key
124
+ # @throws OpenIDConnectClientException
125
+ #
126
+ def verify_RSA_JWT_signature(hashtype, key, payload, signature)
127
+ unless key["n"] and key["e"]
128
+ raise OpenIDConnectClientException, "Malformed key object."
129
+ end
130
+
131
+ public_key_xml = "<RSAKeyValue>\r\n <Modulus>#{url_safe_base64(key["n"])}</Modulus>\r\n <Exponent>#{url_safe_base64(key["e"])}</Exponent>\r\n</RSAKeyValue>"
132
+
133
+ digest = case hashtype
134
+ when 'md2' then OpenSSL::Digest::MD2.new
135
+ when 'md5' then OpenSSL::Digest::MD5.new
136
+ when 'sha1' then OpenSSL::Digest::SHA1.new
137
+ when 'sha256' then OpenSSL::Digest::SHA256.new
138
+ when 'sha384' then OpenSSL::Digest::SHA384.new
139
+ when 'sha512' then OpenSSL::Digest::SHA512.new
140
+ else OpenSSL::Digest::SHA256.new
141
+ end
142
+
143
+ key = rsa_key_from_xml(public_key_xml)
144
+ key.public_key.verify(digest, signature, payload)
145
+ end
146
+
147
+ #
148
+ # @param object claims
149
+ # @return bool
150
+ #
151
+ def verify_JWT_claims(claims)
152
+ (claims["iss"] == @provider_url and ((claims["aud"] == @client_id) or (claims["aud"].include? @client_id)) and (claims["nonce"] == @state[:openid_connect_nonce]))
153
+ end
154
+
155
+ #
156
+ # @param jwt string encoded JWT
157
+ # @param int section the section we would like to decode
158
+ # @return object
159
+ #
160
+ def decode_JWT(jwt, section = 0)
161
+ parts = jwt.split(".")
162
+ url_decoded = decode_64(parts[section])
163
+
164
+ JSON[url_decoded]
165
+ end
166
+
167
+
168
+ # Utility methods ==================================================================================================================
169
+
170
+ #
171
+ # @param string json
172
+ # @return bool
173
+ #
174
+ def is_valid_url?(url)
175
+ begin
176
+ URI.parse(url)
177
+ return url
178
+ rescue URI::InvalidURIError
179
+ return false
180
+ end
181
+ end
182
+
183
+ #
184
+ # @param string json
185
+ # @return bool
186
+ #
187
+ def is_json?(json)
188
+ begin
189
+ JSON.parse(json)
190
+ return true
191
+ rescue JSON::ParserError => e
192
+ return false
193
+ end
194
+ end
195
+
196
+ #
197
+ # @param object object
198
+ # @return string
199
+ #
200
+ def http_build_query(object)
201
+ h = hashify(object)
202
+ result = ""
203
+ separator = '&'
204
+ h.keys.sort.each do |key|
205
+ result << (CGI.escape(key) + '=' + CGI.escape(h[key]) + separator)
206
+ end
207
+
208
+ result = result.sub(/#{separator}$/, '') # Remove the trailing k-v separator
209
+ return result
210
+ end
211
+
212
+ #
213
+ # @param object object
214
+ # @param string parent_key
215
+ #
216
+ def hashify(object, parent_key = '')
217
+ unless object.is_a?(Hash) or object.is_a?(Array) or parent_key.length > 0
218
+ raise ArgumentError.new('This is made for serializing Hashes and Arrays only.')
219
+ end
220
+
221
+ result = {}
222
+
223
+ case object
224
+ when String, Symbol, Numeric
225
+ result[parent_key] = object.to_s
226
+ when Hash
227
+ # Recursively call hashify, building closure-like state by
228
+ # appending the current location in the tree as new "parent_key"
229
+ # values.
230
+ hashes = object.map do |key, value|
231
+ if parent_key =~ /^[0-9]+/ or parent_key.length == 0
232
+ new_parent_key = key.to_s
233
+ else
234
+ new_parent_key = parent_key + '[' + key.to_s + ']'
235
+ end
236
+
237
+ hashify(value, new_parent_key)
238
+ end
239
+
240
+ hash = hashes.reduce { |memo, hash| memo.merge hash }
241
+ result.merge! hash
242
+ when Enumerable
243
+ # _Very_ similar to above, but iterating with "each_with_index"
244
+ hashes = {}
245
+ object.each_with_index do |value, index|
246
+
247
+ if parent_key.length == 0
248
+ new_parent_key = index.to_s
249
+ else
250
+ new_parent_key = parent_key + '[' + index.to_s + ']'
251
+ end
252
+
253
+ hashes.merge! hashify(value, new_parent_key)
254
+ end
255
+
256
+ result.merge! hashes
257
+ else
258
+ raise Exception.new("This should only be serializing Strings, Symbols, or Numerics.")
259
+ end
260
+
261
+ return result
262
+ end
263
+
264
+ #
265
+ # @param string str
266
+ # @return string
267
+ #
268
+ def decode_64(str)
269
+ Base64.decode64(url_safe_base64(str))
270
+ end
271
+
272
+ #
273
+ # Per RFC4648, "base64 encoding with URL-safe and filename-safe alphabet". This just replaces characters 62 and 63.
274
+ # None of the reference implementations seem to restore the padding if necessary, but we'll do it anyway.
275
+ #
276
+ # @param string str
277
+ # @return string
278
+ #
279
+ def url_safe_base64(str)
280
+ # add '=' padding
281
+ str = case str.length % 4
282
+ when 2 then str + '=='
283
+ when 3 then str + '='
284
+ else str
285
+ end
286
+
287
+ str.tr('-_', '+/')
288
+ end
289
+
290
+ #
291
+ # @param string xml_string
292
+ # @return object
293
+ #
294
+ def rsa_key_from_xml(xml_string)
295
+ d = XML::Parser.string(xml_string).parse
296
+ m = Base64.decode64(d.find_first('Modulus').content).unpack('H*')
297
+ e = Base64.decode64(d.find_first('Exponent').content).unpack('H*')
298
+
299
+ pub_key = OpenSSL::PKey::RSA.new
300
+
301
+ #modules
302
+ pub_key.n = OpenSSL::BN.new(m[0].hex.to_s)
303
+
304
+ #exponent
305
+ pub_key.e = OpenSSL::BN.new(e[0].hex.to_s)
306
+
307
+ #return Public Key
308
+ pub_key
309
+ end
310
+
311
+ #
312
+ # Used for arbitrary value generation for nonces and state
313
+ #
314
+ # @return string
315
+ #
316
+ def random_string()
317
+ SecureRandom.urlsafe_base64
318
+ end
319
+
320
+ #
321
+ # @param url
322
+ # @param null post_body string If this is set the post type will be POST
323
+ # @param array headers Extra headers to be send with the request. Format as 'NameHeader: ValueHeader'
324
+ # @throws OpenIDConnectClientException
325
+ # @return mixed
326
+ #
327
+ def fetch_url(url, post_body = nil, headers = Array.new)
328
+ curb = Curl::Easy.new(url) do |curl|
329
+ headers.each do |key, value|
330
+ curl.headers[key] = value
331
+ end
332
+
333
+ if post_body
334
+ if is_json?(post_body)
335
+ content_type = "application/json"
336
+ else
337
+ content_type = "application/x-www-form-urlencoded"
338
+ end
339
+
340
+ curl.headers["Content-Type"] = content_type
341
+ curl.post_body = post_body
342
+
343
+ else
344
+ curl.http(:GET)
345
+ end
346
+
347
+ curl.timeout = 60
348
+ curl.proxy_url = @proxy_url if self.instance_variable_defined? :@proxy_url
349
+ curl.verbose = true
350
+
351
+ if self.instance_variable_defined? :@cert_path
352
+ curl.ssl_verify_peer = true
353
+ curl.ssl_verify_host = true
354
+ curl.cert = @cert_path
355
+ else
356
+ curl.ssl_verify_peer = false
357
+ end
358
+ end
359
+
360
+ curb.post_body = post_body if post_body
361
+ result = curb.perform
362
+
363
+ if result
364
+ return curb
365
+ else
366
+ return false
367
+ end
368
+ end
369
+
370
+
371
+ public #==============================================================================================================================
372
+
373
+ attr_reader :access_token, :refresh_token, :auth_endpoint
374
+ attr_writer :http_proxy, :cert_path, :params
375
+ attr_accessor :client_name, :client_id, :client_secret, :well_known_config_url, :state, :provider_config
376
+
377
+ #
378
+ # @param provider_url string optional
379
+ # @param client_id string optional
380
+ # @param client_secret string optional
381
+ #
382
+ def initialize(provider_url = nil, client_id = nil, client_secret = nil)
383
+ @scopes = Hash.new
384
+ @state = Hash.new
385
+ @state = Hash.new
386
+ @auth_params = Hash.new
387
+ @user_info = Hash.new
388
+ @params = Hash.new
389
+ @response = Hash.new
390
+
391
+ @client_id = client_id
392
+ @client_secret = client_secret
393
+ @provider_url = provider_url
394
+
395
+ substitute = "/"
396
+
397
+ if self.instance_variable_defined? :@provider_url
398
+ @well_known_config_url = provider_url.gsub(/[#{substitute}]+$/, '') + "/.well-known/openid-configuration/"
399
+ end
400
+ end
401
+
402
+ #
403
+ # Builds the user authentication url.
404
+ #
405
+ # @return void
406
+ #
407
+ def authorize()
408
+ get_provider_config()
409
+
410
+ auth_endpoint = get_provider_config_value(:authorization_endpoint)
411
+ response_type = "code"
412
+
413
+ # Generate and store a nonce in the session
414
+ # The nonce is an arbitrary value
415
+ nonce = random_string()
416
+ @state[:openid_connect_nonce] = nonce
417
+
418
+ # State essentially acts as a session key for OIDC
419
+ state = random_string()
420
+ @state[:openid_connect_state] = state
421
+
422
+ @auth_params = @auth_params.merge({
423
+ response_type: response_type,
424
+ redirect_uri: @redirect_url,
425
+ client_id: @client_id,
426
+ nonce: nonce,
427
+ state: state,
428
+ scope: 'openid'
429
+ })
430
+
431
+ # If the client has been registered with additional scopes
432
+ if @scopes.length > 0
433
+ @auth_params[:scope] = @scopes.join(' ')
434
+ auth_endpoint += '?' + http_build_query(@auth_params)
435
+ @auth_endpoint = auth_endpoint
436
+ end
437
+ end
438
+
439
+ #
440
+ # Gets the access token needed to request user info.
441
+ #
442
+ # @return bool
443
+ # @throws OpenIDConnectClientException
444
+ #
445
+ def authenticate()
446
+ # Do a preemptive check to see if the provider has raised an error from a previous redirect
447
+ unless @response[:error].nil?
448
+ raise OpenIDConnectClientException, "Error: #{@response[:error]} Description: #{@response[:error_description]}"
449
+ end
450
+
451
+ # If we have an authorization code then proceed to request a token
452
+ if not @params["code"].nil? || @params["code"].empty?
453
+ code = @params["code"]
454
+ token_endpoint = get_provider_config_value(:token_endpoint)
455
+ grant_type = "authorization_code"
456
+
457
+ token_params = {
458
+ grant_type: grant_type,
459
+ code: code,
460
+ redirect_uri: @redirect_url,
461
+ client_id: @client_id,
462
+ client_secret: @client_secret
463
+ }
464
+
465
+ # Convert token params to string format
466
+ token_params = http_build_query(token_params)
467
+
468
+ token_data = fetch_url(token_endpoint, token_params).body_str
469
+
470
+ unless token_data
471
+ raise OpenIDConnectClientException, "Unable to get token data from the provider."
472
+ end
473
+
474
+ token_json = JSON[token_data]
475
+
476
+ # Throw an error if the server returns one
477
+ if token_json["error"]
478
+ raise OpenIDConnectClientException, token_json["error_description"]
479
+ end
480
+
481
+ # Do an OpenID Connect session check
482
+ unless @params[:state] == @state[:openid_connect_state]
483
+ raise OpenIDConnectClientException, "Unable to determine state."
484
+ end
485
+
486
+ unless token_json["id_token"]
487
+ raise OpenIDConnectClientException, "User did not authorize openid scope."
488
+ end
489
+
490
+ # Verify the signature
491
+ unless verify_JWT_signature(token_json["id_token"])
492
+ raise OpenIDConnectClientException, "Unable to verify signature."
493
+ end
494
+
495
+ claims = decode_JWT(token_json["id_token"], 1)
496
+
497
+ # If this is a valid claim
498
+ unless verify_JWT_claims(claims)
499
+ raise OpenIDConnectClientException, "Unable to verify JWT claims."
500
+ end
501
+
502
+ # Save the access token
503
+ @access_token = token_json["access_token"]
504
+
505
+ # Save the refresh token, if we got one
506
+ if token_json["refresh_token"]
507
+ @refresh_token = token_json["refresh_token"]
508
+ end
509
+
510
+ # Success!
511
+ return true
512
+ end
513
+ end
514
+
515
+ #
516
+ # @param attribute
517
+ #
518
+ #
519
+ # Attribute Type Description
520
+ # user_id string REQUIRED Identifier for the End-User at the Issuer.
521
+ # name string End-User's full name in displayable form including all name parts, ordered according to End-User's locale and preferences.
522
+ # given_name string Given name or first name of the End-User.
523
+ # family_name string Surname or last name of the End-User.
524
+ # middle_name string Middle name of the End-User.
525
+ # nickname string Casual name of the End-User that may or may not be the same as the given_name. For instance, a nickname value of Mike might be returned alongside a given_name value of Michael.
526
+ # profile string URL of End-User's profile page.
527
+ # picture string URL of the End-User's profile picture.
528
+ # website string URL of End-User's web page or blog.
529
+ # email string The End-User's preferred e-mail address.
530
+ # verified boolean True if the End-User's e-mail address has been verified; otherwise false.
531
+ # gender string The End-User's gender: Values defined by this specification are female and male. Other values MAY be used when neither of the defined values are applicable.
532
+ # birthday string The End-User's birthday, represented as a date string in MM/DD/YYYY format. The year MAY be 0000, indicating that it is omitted.
533
+ # zoneinfo string String from zoneinfo [zoneinfo] time zone database. For example, Europe/Paris or America/Los_Angeles.
534
+ # locale string The End-User's locale, represented as a BCP47 [RFC5646] language tag. This is typically an ISO 639-1 Alpha-2 [ISO639‑1] language code in lowercase and an ISO 3166-1 Alpha-2 [ISO3166‑1] country code in uppercase, separated by a dash. For example, en-US or fr-CA. As a compatibility note, some implementations have used an underscore as the separator rather than a dash, for example, en_US; Implementations MAY choose to accept this locale syntax as well.
535
+ # phone_number string The End-User's preferred telephone number. E.164 [E.164] is RECOMMENDED as the format of this Claim. For example, +1 (425) 555-1212 or +56 (2) 687 2400.
536
+ # address JSON object The End-User's preferred address. The value of the address member is a JSON [RFC4627] structure containing some or all of the members defined in Section 2.4.2.1.
537
+ # updated_time string Time the End-User's information was last updated, represented as a RFC 3339 [RFC3339] datetime. For example, 2011-01-03T23:58:42+0000.
538
+ #
539
+ # @return mixed
540
+ # @throws OpenIDConnectClientException
541
+ #
542
+ def get(attribute)
543
+ if @user_info.include? attribute
544
+ return @user_info["#{attribute}"]
545
+ end
546
+
547
+ user_info_endpoint = get_provider_config_value(:userinfo_endpoint)
548
+ schema = "openid"
549
+ user_info_endpoint += "?schema=#{schema}"
550
+ headers = {"Authorization" => "Bearer #{@access_token}"}
551
+ user_data = fetch_url(user_info_endpoint, nil, headers).body_str
552
+
553
+ if user_data.nil? || user_data.empty?
554
+ raise OpenIDConnectClientException, "Unable to get #{attribute} from the provider."
555
+ end
556
+
557
+ user_json = JSON[user_data]
558
+ @user_info = user_json
559
+
560
+ if @user_info.include? attribute
561
+ return @user_info["#{attribute}"]
562
+ end
563
+
564
+ return nil
565
+ end
566
+
567
+ #
568
+ # Dynamic registration
569
+ #
570
+ # @return void
571
+ # @throws OpenIDConnectClientException
572
+ #
573
+ def register
574
+ registration_endpoint = get_provider_config_value(:registration_endpoint)
575
+
576
+ send_object = {
577
+ redirect_uris: [@redirect_url],
578
+ client_name: @client_name
579
+ }
580
+
581
+ @response = fetch_url(registration_endpoint, JSON[send_object])
582
+ json_response = JSON[response]
583
+
584
+ if not json_response
585
+ raise OpenIDConnectClientException, "Error registering: JSON response received from the server was invalid."
586
+ elsif json_response[:error_description]
587
+ raise OpenIDConnectClientException, json_response[:error_description]
588
+ end
589
+
590
+ if json_response[:client_id]
591
+ @client_secret = json_response[:client_id]
592
+ else
593
+ raise OpenIDConnectClientException, "Error registering: Please contact the OpenID Connect provider and obtain a Client ID and Secret directly from them"
594
+ end
595
+ end
596
+
597
+
598
+ # Getters/Setters ==================================================================================================================
599
+
600
+ #
601
+ # @param hash hash
602
+ # @return hash
603
+ #
604
+ def add_auth_param(hash)
605
+ @auth_params = @auth_params.merge(hash)
606
+ end
607
+
608
+ #
609
+ # @param hash hash
610
+ # @return hash
611
+ #
612
+ def add_provider_config_param(hash)
613
+ @state = @state.merge(hash)
614
+ end
615
+
616
+ #
617
+ # @param scopes - example: openid, given_name, etc...
618
+ #
619
+ def scopes=(scopes)
620
+ @scopes = scopes.split(' ') if scopes
621
+ end
622
+
623
+ #
624
+ # @param hash state
625
+ # @return hash
626
+ #
627
+ def state=(state)
628
+ @state = @state.merge(state) if state
629
+ end
630
+
631
+ #
632
+ # @return string
633
+ # @throws OpenIDConnectClientException
634
+ #
635
+ def provider_url()
636
+ # If the provider URL has been set then return it.
637
+ unless self.instance_variable_defined? :@provider_url
638
+ raise OpenIDConnectClientException, "The provider URL has not been set."
639
+ end
640
+
641
+ @provider_url
642
+ end
643
+
644
+ #
645
+ # @param provider_url
646
+ # @return string
647
+ # @throws OpenIDConnectClientException
648
+ #
649
+ def provider_url=(url)
650
+ unless is_valid_url?(url)
651
+ raise OpenIDConnectClientException, "Invalid URL."
652
+ end
653
+
654
+ @state[:issuer] = url
655
+ end
656
+
657
+ #
658
+ # Gets the URL of the current page we are on, encodes, and returns it
659
+ #
660
+ # @return string
661
+ # @throws OpenIDConnectClientException
662
+ #
663
+ def redirect_url()
664
+ # If the redirect URL has been set then return it.
665
+ unless self.instance_variable_defined? :@redirect_url
666
+ raise OpenIDConnectClientException, "The redirect URL has not been set."
667
+ end
668
+
669
+ @redirect_url
670
+ end
671
+
672
+ #
673
+ # @param url Sets redirect URL for auth flow
674
+ # @return string
675
+ # @throws OpenIDConnectClientException
676
+ #
677
+ def redirect_url=(url)
678
+ unless is_valid_url?(url)
679
+ raise OpenIDConnectClientException, "Invalid URL."
680
+ end
681
+
682
+ @redirect_url = url
683
+ end
684
+ end
685
+ end
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'openid_connect_client/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "openid_connect_client"
8
+ spec.version = OpenIDConnectClient::VERSION
9
+ spec.authors = ["Rita Zerrizuela"]
10
+ spec.email = ["zeta@widcket.com"]
11
+
12
+ spec.summary = %q{An easy to use OpenID Connect Client for Ruby.}
13
+ spec.description = %q{This one is a literal, not so idiomatic port of OpenID Connect PHP. However, the due to the different nature of Ruby and PHP, usage is not an exact match. See Readme.}
14
+ spec.homepage = "https://github.com/LabGCBA/openid-connect-ruby.git"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.11"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "rspec", "~> 3.0"
25
+
26
+ spec.add_runtime_dependency "curb", '~> 0'
27
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: openid_connect_client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Rita Zerrizuela
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-04-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.11'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.11'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: curb
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: This one is a literal, not so idiomatic port of OpenID Connect PHP. However,
70
+ the due to the different nature of Ruby and PHP, usage is not an exact match. See
71
+ Readme.
72
+ email:
73
+ - zeta@widcket.com
74
+ executables: []
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - ".gitignore"
79
+ - ".rspec"
80
+ - ".travis.yml"
81
+ - CODE_OF_CONDUCT.md
82
+ - Gemfile
83
+ - LICENSE.txt
84
+ - README.md
85
+ - Rakefile
86
+ - bin/console
87
+ - bin/setup
88
+ - example.rb
89
+ - lib/openid_connect_client.rb
90
+ - lib/openid_connect_client/version.rb
91
+ - openid_connect_client.gemspec
92
+ homepage: https://github.com/LabGCBA/openid-connect-ruby.git
93
+ licenses:
94
+ - MIT
95
+ metadata: {}
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubyforge_project:
112
+ rubygems_version: 2.4.5.1
113
+ signing_key:
114
+ specification_version: 4
115
+ summary: An easy to use OpenID Connect Client for Ruby.
116
+ test_files: []