checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7cfa88e79b566cff87e814f8af69380dc2473fa7ca9e66c443fe6cde1d30e824
4
+ data.tar.gz: 11be01521bd7d5f5db73db1ad702bf198527c479d6a7e3bcde57139dc50763e8
5
+ SHA512:
6
+ metadata.gz: 9fe457d52d7509efbb253bbd76e89948502a3cdae387d870e491a2d4fe42f72626d590de9e69bac3aa298209dfd05de07efb34e8b07db48d26498c4dcb441769
7
+ data.tar.gz: 96f098e039d385bceebacca23dbf8a87ba9e05351e973df11919cfc15c21dcc40d8bf39da4e96c2e4151d5df78dd64f26b776d47b6086d648ab245ff8b39dbf1
data/.gitignore ADDED
@@ -0,0 +1,50 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ # .env
15
+
16
+ ## Specific to RubyMotion:
17
+ .dat*
18
+ .repl_history
19
+ build/
20
+ *.bridgesupport
21
+ build-iPhoneOS/
22
+ build-iPhoneSimulator/
23
+
24
+ ## Specific to RubyMotion (use of CocoaPods):
25
+ #
26
+ # We recommend against adding the Pods directory to your .gitignore. However
27
+ # you should judge for yourself, the pros and cons are mentioned at:
28
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
29
+ #
30
+ # vendor/Pods/
31
+
32
+ ## Documentation cache and generated files:
33
+ /.yardoc/
34
+ /_yardoc/
35
+ /doc/
36
+ /rdoc/
37
+
38
+ ## Environment normalization:
39
+ /.bundle/
40
+ /vendor/bundle
41
+ /lib/bundler/man/
42
+
43
+ # for a library or gem, you might want to ignore these files since the code is
44
+ # intended to run in multiple environments; otherwise, check them in:
45
+ # Gemfile.lock
46
+ # .ruby-version
47
+ # .ruby-gemset
48
+
49
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
50
+ .rvmrc
data/.rubocop.yml ADDED
@@ -0,0 +1,209 @@
1
+ # from https://github.com/rails/rails/blob/master/.rubocop.yml
2
+
3
+ AllCops:
4
+ TargetRubyVersion: 2.5
5
+ # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop
6
+ # to ignore them, so only the ones explicitly set in this file are enabled.
7
+ DisabledByDefault: true
8
+
9
+ # Prefer &&/|| over and/or.
10
+ Style/AndOr:
11
+ Enabled: true
12
+
13
+ # Do not use braces for hash literals when they are the last argument of a
14
+ # method call.
15
+ Style/BracesAroundHashParameters:
16
+ Enabled: true
17
+ EnforcedStyle: context_dependent
18
+
19
+ # Align `when` with `case`.
20
+ Layout/CaseIndentation:
21
+ Enabled: true
22
+
23
+ # Align comments with method definitions.
24
+ Layout/CommentIndentation:
25
+ Enabled: true
26
+
27
+ Layout/ElseAlignment:
28
+ Enabled: true
29
+
30
+ # Align `end` with the matching keyword or starting expression except for
31
+ # assignments, where it should be aligned with the LHS.
32
+ Layout/EndAlignment:
33
+ Enabled: true
34
+ EnforcedStyleAlignWith: variable
35
+ AutoCorrect: true
36
+
37
+ Layout/EmptyLineAfterMagicComment:
38
+ Enabled: true
39
+
40
+ Layout/EmptyLinesAroundBlockBody:
41
+ Enabled: true
42
+
43
+ # In a regular class definition, no empty lines around the body.
44
+ Layout/EmptyLinesAroundClassBody:
45
+ Enabled: true
46
+
47
+ # In a regular method definition, no empty lines around the body.
48
+ Layout/EmptyLinesAroundMethodBody:
49
+ Enabled: true
50
+
51
+ # In a regular module definition, no empty lines around the body.
52
+ Layout/EmptyLinesAroundModuleBody:
53
+ Enabled: true
54
+
55
+ Layout/IndentFirstArgument:
56
+ Enabled: true
57
+
58
+ # Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }.
59
+ Style/HashSyntax:
60
+ Enabled: true
61
+
62
+ # Method definitions after `private` or `protected` isolated calls need one
63
+ # extra level of indentation.
64
+ Layout/IndentationConsistency:
65
+ Enabled: true
66
+ EnforcedStyle: indented_internal_methods
67
+
68
+ # Two spaces, no tabs (for indentation).
69
+ Layout/IndentationWidth:
70
+ Enabled: true
71
+
72
+ Layout/LeadingCommentSpace:
73
+ Enabled: true
74
+
75
+ Layout/SpaceAfterColon:
76
+ Enabled: true
77
+
78
+ Layout/SpaceAfterComma:
79
+ Enabled: true
80
+
81
+ Layout/SpaceAfterSemicolon:
82
+ Enabled: true
83
+
84
+ Layout/SpaceAroundEqualsInParameterDefault:
85
+ Enabled: true
86
+
87
+ Layout/SpaceAroundKeyword:
88
+ Enabled: true
89
+
90
+ Layout/SpaceAroundOperators:
91
+ Enabled: true
92
+
93
+ Layout/SpaceBeforeComma:
94
+ Enabled: true
95
+
96
+ Layout/SpaceBeforeComment:
97
+ Enabled: true
98
+
99
+ Layout/SpaceBeforeFirstArg:
100
+ Enabled: true
101
+
102
+ Style/DefWithParentheses:
103
+ Enabled: true
104
+
105
+ # Defining a method with parameters needs parentheses.
106
+ Style/MethodDefParentheses:
107
+ Enabled: true
108
+
109
+ Style/FrozenStringLiteralComment:
110
+ Enabled: true
111
+ EnforcedStyle: always
112
+ Exclude:
113
+ - 'actionview/test/**/*.builder'
114
+ - 'actionview/test/**/*.ruby'
115
+ - 'actionpack/test/**/*.builder'
116
+ - 'actionpack/test/**/*.ruby'
117
+ - 'activestorage/db/migrate/**/*.rb'
118
+ - 'activestorage/db/update_migrate/**/*.rb'
119
+ - 'actionmailbox/db/migrate/**/*.rb'
120
+ - 'actiontext/db/migrate/**/*.rb'
121
+
122
+ Style/RedundantFreeze:
123
+ Enabled: true
124
+
125
+ # Use `foo {}` not `foo{}`.
126
+ Layout/SpaceBeforeBlockBraces:
127
+ Enabled: true
128
+
129
+ # Use `foo { bar }` not `foo {bar}`.
130
+ Layout/SpaceInsideBlockBraces:
131
+ Enabled: true
132
+ EnforcedStyleForEmptyBraces: space
133
+
134
+ # Use `{ a: 1 }` not `{a:1}`.
135
+ Layout/SpaceInsideHashLiteralBraces:
136
+ Enabled: true
137
+
138
+ Layout/SpaceInsideParens:
139
+ Enabled: true
140
+
141
+ # Check quotes usage according to lint rule below.
142
+ Style/StringLiterals:
143
+ Enabled: true
144
+ EnforcedStyle: double_quotes
145
+
146
+ # Detect hard tabs, no hard tabs.
147
+ Layout/Tab:
148
+ Enabled: true
149
+
150
+ # Blank lines should not have any spaces.
151
+ Layout/TrailingBlankLines:
152
+ Enabled: true
153
+
154
+ # No trailing whitespace.
155
+ Layout/TrailingWhitespace:
156
+ Enabled: true
157
+
158
+ # Use quotes for string literals when they are enough.
159
+ Style/UnneededPercentQ:
160
+ Enabled: true
161
+
162
+ Lint/AmbiguousOperator:
163
+ Enabled: true
164
+
165
+ Lint/AmbiguousRegexpLiteral:
166
+ Enabled: true
167
+
168
+ Lint/ErbNewArguments:
169
+ Enabled: true
170
+
171
+ # Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
172
+ Lint/RequireParentheses:
173
+ Enabled: true
174
+
175
+ Lint/ShadowingOuterLocalVariable:
176
+ Enabled: true
177
+
178
+ Lint/StringConversionInInterpolation:
179
+ Enabled: true
180
+
181
+ Lint/UriEscapeUnescape:
182
+ Enabled: true
183
+
184
+ Lint/UselessAssignment:
185
+ Enabled: true
186
+
187
+ Lint/DeprecatedClassMethods:
188
+ Enabled: true
189
+
190
+ Style/ParenthesesAroundCondition:
191
+ Enabled: true
192
+
193
+ Style/RedundantBegin:
194
+ Enabled: true
195
+
196
+ Style/RedundantReturn:
197
+ Enabled: true
198
+ AllowMultipleReturnValues: true
199
+
200
+ Style/Semicolon:
201
+ Enabled: true
202
+ AllowAsExpressionSeparator: true
203
+
204
+ # Prefer Foo.method over Foo::method
205
+ Style/ColonMethodCall:
206
+ Enabled: true
207
+
208
+ Style/TrivialAccessors:
209
+ Enabled: true
data/.travis.yml ADDED
@@ -0,0 +1,21 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.4.1
4
+ addons:
5
+ code_climate:
6
+ repo_token: "$CODECLIMATE_REPO_TOKEN"
7
+ install:
8
+ - travis_retry bundle install
9
+ script:
10
+ - bundle exec rspec
11
+ - bundle exec codeclimate-test-reporter
12
+ deploy:
13
+ provider: rubygems
14
+ api_key:
15
+ secure: lEUHxqrw71LBPWKy8PS9EriAFwbeNDsopIkEX1ssU2FbZSMCiE4p4AYC+C+KjmoJ7mNjz+0jtqEIfVgKT93CpP6AqRGG3uxHBXhslyuEFe/ej+QrMb+gzIDZIA3leW0IgPhi7Rtk24RssPmtFAs0wAMM3iOS8lOPpqO5kWGmWMExezcpiVxPld2KIZksLUvwflyOs9aG1+n0Zt5FNyrXKI3W9rK3BytwnPZn2f4ahc8IP5u5jjhnM0tvG697SNQm/GKGkOcQ3iJgZ7RpakosHvocrTGUH+q1FZBpqG2FDKn0HygpQCs30puEIqAlnCBxDoI53G/xjP6CaNr7JCLBYjUMJZ91nJvwHaMfA3nnDEEI3p+MrsV3lwAyvpXQxJeWt3hM1bHcH2pIql2hGMlx6f5XnPCvFaUQwN1Fi534xVCBOSEXCr2G9oStOpW5SA/U7jvohUiCZhIK7eSR5i4w1EbCsU9nQWZYjvX0EprYoVlBqgLGZEKK02JCIwGXPLVYfJwzNBv0uBtiNBkwO8jHVaAEVrJL4Ka8TBy/V3B86mZ+W5EvP6kn6lJCh8Sh7bwpj0NXKllRw2sSz9ukybayLan/OcSgj98LgRKJ09iydxwsTFIVD4kzuGidF+qtT6ImLgD4qWMr4mgCSlsJp83fOzpdzDVLlz+T3tSVB42tNjE=
16
+ gem: omniauth-globus
17
+ on:
18
+ tags: true
19
+ repo: datacite/omniauth-globus
20
+ notifications:
21
+ slack: datacite:Wt8En0ALoTA6Kjc5EOKNDWxN
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in omniauth-github.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,98 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ omniauth-globus (0.8.7)
5
+ jwt (~> 2.0)
6
+ omniauth (~> 1.9)
7
+ omniauth-oauth2 (~> 1.6)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ addressable (2.7.0)
13
+ public_suffix (>= 2.0.2, < 5.0)
14
+ ast (2.4.0)
15
+ codeclimate-test-reporter (1.0.9)
16
+ simplecov (<= 0.13)
17
+ crack (0.4.3)
18
+ safe_yaml (~> 1.0.0)
19
+ diff-lcs (1.3)
20
+ docile (1.1.5)
21
+ faraday (0.15.4)
22
+ multipart-post (>= 1.2, < 3)
23
+ hashdiff (1.0.0)
24
+ hashie (3.6.0)
25
+ jaro_winkler (1.5.3)
26
+ json (2.2.0)
27
+ jwt (2.2.1)
28
+ multi_json (1.13.1)
29
+ multi_xml (0.6.0)
30
+ multipart-post (2.1.1)
31
+ oauth2 (1.4.1)
32
+ faraday (>= 0.8, < 0.16.0)
33
+ jwt (>= 1.0, < 3.0)
34
+ multi_json (~> 1.3)
35
+ multi_xml (~> 0.5)
36
+ rack (>= 1.2, < 3)
37
+ omniauth (1.9.0)
38
+ hashie (>= 3.4.6, < 3.7.0)
39
+ rack (>= 1.6.2, < 3)
40
+ omniauth-oauth2 (1.6.0)
41
+ oauth2 (~> 1.1)
42
+ omniauth (~> 1.9)
43
+ parallel (1.17.0)
44
+ parser (2.6.4.0)
45
+ ast (~> 2.4.0)
46
+ public_suffix (4.0.1)
47
+ rack (2.0.7)
48
+ rack-test (0.6.3)
49
+ rack (>= 1.0)
50
+ rainbow (3.0.0)
51
+ rspec (3.8.0)
52
+ rspec-core (~> 3.8.0)
53
+ rspec-expectations (~> 3.8.0)
54
+ rspec-mocks (~> 3.8.0)
55
+ rspec-core (3.8.2)
56
+ rspec-support (~> 3.8.0)
57
+ rspec-expectations (3.8.4)
58
+ diff-lcs (>= 1.2.0, < 2.0)
59
+ rspec-support (~> 3.8.0)
60
+ rspec-mocks (3.8.1)
61
+ diff-lcs (>= 1.2.0, < 2.0)
62
+ rspec-support (~> 3.8.0)
63
+ rspec-support (3.8.2)
64
+ rubocop (0.74.0)
65
+ jaro_winkler (~> 1.5.1)
66
+ parallel (~> 1.10)
67
+ parser (>= 2.6)
68
+ rainbow (>= 2.2.2, < 4.0)
69
+ ruby-progressbar (~> 1.7)
70
+ unicode-display_width (>= 1.4.0, < 1.7)
71
+ ruby-progressbar (1.10.1)
72
+ safe_yaml (1.0.5)
73
+ simplecov (0.13.0)
74
+ docile (~> 1.1.0)
75
+ json (>= 1.8, < 3)
76
+ simplecov-html (~> 0.10.0)
77
+ simplecov-html (0.10.2)
78
+ unicode-display_width (1.6.0)
79
+ webmock (3.7.2)
80
+ addressable (>= 2.3.6)
81
+ crack (>= 0.3.2)
82
+ hashdiff (>= 0.4.0, < 2.0.0)
83
+
84
+ PLATFORMS
85
+ ruby
86
+
87
+ DEPENDENCIES
88
+ bundler (~> 1.0)
89
+ codeclimate-test-reporter (~> 1.0, >= 1.0.9)
90
+ omniauth-globus!
91
+ rack-test (~> 0.6.3)
92
+ rspec (~> 3.4)
93
+ rubocop (~> 0.68)
94
+ simplecov (~> 0.13.0)
95
+ webmock (~> 3.0, >= 3.0.1)
96
+
97
+ BUNDLED WITH
98
+ 1.17.3
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 DataCite
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # omniauth-globus
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/omniauth-globus.svg)](https://badge.fury.io/rb/omniauth-globus)
4
+ [![Build Status](https://travis-ci.com/datacite/omniauth-globus.svg?branch=master)](https://travis-ci.com/datacite/omniauth-globus)
5
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/13f9467872e9a688e9cb/test_coverage)](https://codeclimate.com/github/datacite/omniauth-globus/test_coverage)
6
+ [![Maintainability](https://api.codeclimate.com/v1/badges/13f9467872e9a688e9cb/maintainability)](https://codeclimate.com/github/datacite/omniauth-globus/maintainability)
7
+
8
+ Globus OAuth 2.0 Strategy for the [OmniAuth Ruby authentication framework](http://www.omniauth.org), with support for OpenID Connect.
9
+
10
+ Provides basic support for authenticating a client application via the [Globus service](http://globus.org).
11
+
12
+ ## Installation
13
+
14
+ The usual way with Bundler: add the following to your `Gemfile` to install the current version of the gem:
15
+
16
+ ```ruby
17
+ gem 'omniauth-globus'
18
+ ```
19
+
20
+ Then run `bundle install` to install into your environment.
21
+
22
+ You can also install the gem system-wide in the usual way:
23
+
24
+ ```bash
25
+ gem install omniauth-globus
26
+ ```
27
+
28
+ ## Getting started
29
+
30
+ Like other OmniAuth strategies, `OmniAuth::Strategies::Globus` is a piece of Rack middleware. Please read the OmniAuth documentation for detailed instructions: https://github.com/intridea/omniauth.
31
+
32
+ Register a client application with Globus [here](https://developers.globus.org/).
33
+
34
+ You can then configure your client application using Omniauth or [Devise](https://github.com/plataformatec/devise) and the credentials obtained from Globus:
35
+
36
+ ```ruby
37
+ use OmniAuth::Builder do
38
+ provider :globus, ENV['GLOBUS_CLIENT_ID'], ENV['GLOBUS_CLIENT_SECRET']
39
+ end
40
+ ```
41
+
42
+ ```ruby
43
+ # in config/initializers/devise.rb
44
+ config.omniauth :globus, ENV["GLOBUS_CLIENT_ID"],
45
+ ENV["GLOBUS_CLIENT_SECRET"]
46
+ ```
47
+
48
+ ## Development
49
+
50
+ We use rspec for unit testing:
51
+
52
+ ```
53
+ bundle exec rspec
54
+ ```
55
+
56
+ Follow along via [Github Issues](https://github.com/datacite/omniauth-globus/issues).
57
+
58
+ ### Note on Patches/Pull Requests
59
+
60
+ * Fork the project
61
+ * Write tests for your new feature or a test that reproduces a bug
62
+ * Implement your feature or make a bug fix
63
+ * Do not mess with Rakefile, version or history
64
+ * Commit, push and make a pull request. Bonus points for topical branches.
65
+
66
+ ## License
67
+ **omniauth-globus** is released under the [MIT License](https://github.com/datacite/omniauth-orcid/blob/master/LICENSE.md).
data/lib/omniauth-globus.rb ADDED
@@ -0,0 +1 @@
1
+ require 'omniauth/globus'
data/lib/omniauth/globus.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'omniauth/strategies/globus'
data/lib/omniauth/globus/version.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAuth
4
+ module Globus
5
+ VERSION = "0.8.7"
6
+ end
7
+ end
data/lib/omniauth/strategies/globus.rb ADDED
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jwt"
4
+ require "omniauth/strategies/oauth2"
5
+ require "uri"
6
+
7
+ module OmniAuth
8
+ module Strategies
9
+ class Globus < OmniAuth::Strategies::OAuth2
10
+ option :name, "globus"
11
+ option :issuer, "https://auth.globus.org"
12
+ option :scope, "openid profile email"
13
+ option :authorize_options, %i[access_type login_hint prompt request_visible_actions scope state redirect_uri include_granted_scopes openid_realm device_id device_name]
14
+
15
+ option(:client_options, site: 'https://auth.globus.org',
16
+ authorize_url: 'https://auth.globus.org/v2/oauth2/authorize',
17
+ token_url: 'https://auth.globus.org/v2/oauth2/token',
18
+ discovery_endpoint: "https://auth.globus.org/.well-known/openid-configuration",
19
+ authorization_endpoint: "https://auth.globus.org/v2/oauth2/authorize",
20
+ token_endpoint: "https://auth.globus.org/v2/oauth2/token",
21
+ userinfo_endpoint: "https://auth.globus.org/v2/oauth2/userinfo",
22
+ jwks_uri: "https://auth.globus.org/jwk.json",
23
+ end_session_endpoint: "https://auth.globus.org/v2/oauth2/token/revoke")
24
+
25
+ def authorize_params
26
+ super.tap do |params|
27
+ options[:authorize_options].each do |k|
28
+ params[k] = request.params[k.to_s] unless [nil, ''].include?(request.params[k.to_s])
29
+ end
30
+
31
+ params[:scope] = get_scope(params)
32
+ params[:access_type] = 'offline' if params[:access_type].nil?
33
+ params['openid.realm'] = params.delete(:openid_realm) unless params[:openid_realm].nil?
34
+
35
+ session['omniauth.state'] = params[:state] if params[:state]
36
+ end
37
+ end
38
+
39
+ uid { raw_info['sub'] }
40
+
41
+ info do
42
+ prune!(
43
+ name: raw_info['name'],
44
+ first_name: raw_info['given_name'],
45
+ last_name: raw_info['family_name'],
46
+ email: raw_info['email']
47
+ )
48
+ end
49
+
50
+ extra do
51
+ hash = {}
52
+ hash[:id_token] = access_token['id_token']
53
+ if !access_token['id_token'].nil?
54
+ decoded = ::JWT.decode(access_token['id_token'], nil, false).first
55
+
56
+ # We have to manually verify the claims because the third parameter to
57
+ # JWT.decode is false since no verification key is provided.
58
+ ::JWT::Verify.verify_claims(decoded,
59
+ verify_iss: true,
60
+ iss: options.issuer,
61
+ verify_expiration: true)
62
+
63
+ hash[:id_info] = decoded
64
+ end
65
+ hash[:raw_info] = raw_info unless skip_info?
66
+ prune! hash
67
+ end
68
+
69
+ def raw_info
70
+ @raw_info ||= access_token.get(options.client_options.userinfo_endpoint).parsed
71
+ end
72
+
73
+ def custom_build_access_token
74
+ get_access_token(request)
75
+ end
76
+
77
+ alias build_access_token custom_build_access_token
78
+
79
+ private
80
+
81
+ def callback_url
82
+ options[:redirect_uri] || (full_host + script_name + callback_path)
83
+ end
84
+
85
+ def get_access_token(request)
86
+ verifier = request.params['code']
87
+ redirect_uri = request.params['redirect_uri']
88
+ if verifier && request.xhr?
89
+ client_get_token(verifier, redirect_uri || 'postmessage')
90
+ elsif verifier
91
+ client_get_token(verifier, redirect_uri || callback_url)
92
+ elsif verify_token(request.params['access_token'])
93
+ ::OAuth2::AccessToken.from_hash(client, request.params.dup)
94
+ elsif request.content_type =~ /json/i
95
+ begin
96
+ body = JSON.parse(request.body.read)
97
+ request.body.rewind # rewind request body for downstream middlewares
98
+ verifier = body && body['code']
99
+ client_get_token(verifier, 'postmessage') if verifier
100
+ rescue JSON::ParserError => e
101
+ warn "[omniauth globus] JSON parse error=#{e}"
102
+ end
103
+ end
104
+ end
105
+
106
+ def client_get_token(verifier, redirect_uri)
107
+ client.auth_code.get_token(verifier, get_token_options(redirect_uri), get_token_params)
108
+ end
109
+
110
+ def get_token_params
111
+ deep_symbolize(options.auth_token_params || {})
112
+ end
113
+
114
+ def get_scope(params)
115
+ raw_scope = params[:scope] || options.scope
116
+ scope_list = raw_scope.split(" ").map { |item| item.split(",") }.flatten
117
+ scope_list.join(" ")
118
+ end
119
+
120
+ def get_token_options(redirect_uri = "")
121
+ { redirect_uri: redirect_uri }.merge(token_params.to_hash(symbolize_keys: true))
122
+ end
123
+
124
+ def prune!(hash)
125
+ hash.delete_if do |_, v|
126
+ prune!(v) if v.is_a?(Hash)
127
+ v.nil? || (v.respond_to?(:empty?) && v.empty?)
128
+ end
129
+ end
130
+
131
+ def strip_unnecessary_query_parameters(query_parameters)
132
+ # strip `sz` parameter (defaults to sz=50) which overrides `image_size` options
133
+ return nil if query_parameters.nil?
134
+
135
+ params = CGI.parse(query_parameters)
136
+ stripped_params = params.delete_if { |key| key == 'sz' }
137
+
138
+ # don't return an empty Hash since that would result
139
+ # in URLs with a trailing ? character: http://image.url?
140
+ return nil if stripped_params.empty?
141
+
142
+ URI.encode_www_form(stripped_params)
143
+ end
144
+
145
+ def verify_token(access_token)
146
+ return false unless access_token
147
+
148
+ raw_response = client.request(:get, options.client_options.userinfo_endpoint,
149
+ params: { access_token: access_token }).parsed
150
+ raw_response["aud"] == options.client_id
151
+ end
152
+ end
153
+ end
154
+ end
data/omniauth-globus.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require File.expand_path("../lib/omniauth/globus/version", __FILE__)
5
+
6
+ Gem::Specification.new do |s|
7
+ s.authors = ["Martin Fenner"]
8
+ s.email = ["mfenner@datacite.org"]
9
+ s.name = "omniauth-globus"
10
+ s.homepage = "https://github.com/datacite/omniauth-globus"
11
+ s.summary = "Globus OpenId connect Strategy for OmniAuth 1.0"
12
+ s.date = Date.today
13
+ s.description = "Enables third-party client apps to authenticate with the Globus service via OpenID Connect"
14
+ s.require_paths = ["lib"]
15
+ s.version = OmniAuth::Globus::VERSION
16
+ s.extra_rdoc_files = ["README.md"]
17
+ s.license = "MIT"
18
+
19
+ s.files = `git ls-files`.split("\n")
20
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
+
22
+ s.required_ruby_version = ">= 2.3"
23
+
24
+ # Declary dependencies here, rather than in the Gemfile
25
+ s.add_dependency "jwt", "~> 2.0"
26
+ s.add_dependency "omniauth", "~> 1.9"
27
+ s.add_dependency "omniauth-oauth2", "~> 1.6"
28
+ s.add_development_dependency "bundler", "~> 1.0"
29
+ s.add_development_dependency "codeclimate-test-reporter", "~> 1.0", ">= 1.0.9"
30
+ s.add_development_dependency "rack-test", "~> 0.6.3"
31
+ s.add_development_dependency "rspec", "~> 3.4"
32
+ s.add_development_dependency "rubocop", "~> 0.68"
33
+ s.add_development_dependency "simplecov", "~> 0.13.0"
34
+ s.add_development_dependency "webmock", "~> 3.0", ">= 3.0.1"
35
+ end
data/spec/fixtures/access_token.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "Martin Fenner",
3
+ "access_token": "123",
4
+ "expires_in": 631138518,
5
+ "token_type": "bearer",
6
+ "orcid": "0000-0001-6528-2027",
7
+ "scope": "/read-limited",
8
+ "refresh_token": "456"
9
+ }
data/spec/fixtures/request_info.json ADDED
@@ -0,0 +1,213 @@
1
+ {
2
+ "name": {
3
+ "credit-name": {
4
+ "value": "Martin Fenner"
5
+ },
6
+ "family-name": {
7
+ "value": "Fenner"
8
+ },
9
+ "last-modified-date": {
10
+ "value": 1500190904445
11
+ },
12
+ "created-date": {
13
+ "value": 1460669194723
14
+ },
15
+ "visibility": "PUBLIC",
16
+ "source": null,
17
+ "given-names": {
18
+ "value": "Martin"
19
+ },
20
+ "path": "0000-0001-6528-2027"
21
+ },
22
+ "last-modified-date": {
23
+ "value": 1500192622273
24
+ },
25
+ "researcher-urls": {
26
+ "researcher-url": [
27
+ {
28
+ "put-code": 42818,
29
+ "last-modified-date": {
30
+ "value": 1500192382862
31
+ },
32
+ "url": {
33
+ "value": "http://blog.martinfenner.org"
34
+ },
35
+ "created-date": {
36
+ "value": 1500192382862
37
+ },
38
+ "visibility": "PUBLIC",
39
+ "source": {
40
+ "source-orcid": {
41
+ "path": "0000-0001-6528-2027",
42
+ "host": "sandbox.orcid.org",
43
+ "uri": "http://sandbox.orcid.org/0000-0001-6528-2027"
44
+ },
45
+ "source-name": {
46
+ "value": "Martin Fenner"
47
+ },
48
+ "source-client-id": null
49
+ },
50
+ "display-index": 1,
51
+ "path": "/0000-0001-6528-2027/researcher-urls/42818",
52
+ "url-name": "Blog"
53
+ }
54
+ ],
55
+ "path": "/0000-0001-6528-2027/researcher-urls",
56
+ "last-modified-date": {
57
+ "value": 1500192382862
58
+ }
59
+ },
60
+ "other-names": {
61
+ "other-name": [
62
+ {
63
+ "put-code": 16403,
64
+ "last-modified-date": {
65
+ "value": 1500191301536
66
+ },
67
+ "created-date": {
68
+ "value": 1500191301534
69
+ },
70
+ "visibility": "PUBLIC",
71
+ "content": "Martin Hellmut Fenner",
72
+ "source": {
73
+ "source-orcid": {
74
+ "path": "0000-0001-6528-2027",
75
+ "host": "sandbox.orcid.org",
76
+ "uri": "http://sandbox.orcid.org/0000-0001-6528-2027"
77
+ },
78
+ "source-name": {
79
+ "value": "Martin Fenner"
80
+ },
81
+ "source-client-id": null
82
+ },
83
+ "display-index": 1,
84
+ "path": "/0000-0001-6528-2027/other-names/16403"
85
+ }
86
+ ],
87
+ "path": "/0000-0001-6528-2027/other-names",
88
+ "last-modified-date": {
89
+ "value": 1500191301536
90
+ }
91
+ },
92
+ "keywords": {
93
+ "path": "/0000-0001-6528-2027/keywords",
94
+ "keyword": [],
95
+ "last-modified-date": null
96
+ },
97
+ "path": "/0000-0001-6528-2027/person",
98
+ "external-identifiers": {
99
+ "path": "/0000-0001-6528-2027/external-identifiers",
100
+ "external-identifier": [
101
+ {
102
+ "external-id-value": "mfenner",
103
+ "put-code": 3882,
104
+ "last-modified-date": {
105
+ "value": 1499800792190
106
+ },
107
+ "external-id-relationship": "SELF",
108
+ "created-date": {
109
+ "value": 1499800792189
110
+ },
111
+ "external-id-type": "GitHub",
112
+ "source": {
113
+ "source-orcid": null,
114
+ "source-name": {
115
+ "value": "DataCite/ORCID Claim Tool"
116
+ },
117
+ "source-client-id": {
118
+ "path": "APP-127VAQ5PMV9OKM9F",
119
+ "host": "sandbox.orcid.org",
120
+ "uri": "http://sandbox.orcid.org/client/APP-127VAQ5PMV9OKM9F"
121
+ }
122
+ },
123
+ "visibility": "PUBLIC",
124
+ "display-index": 0,
125
+ "path": "/0000-0001-6528-2027/external-identifiers/3882",
126
+ "external-id-url": {
127
+ "value": "https://github.com/mfenner"
128
+ }
129
+ }
130
+ ],
131
+ "last-modified-date": {
132
+ "value": 1499800792190
133
+ }
134
+ },
135
+ "emails": {
136
+ "path": "/0000-0001-6528-2027/email",
137
+ "email": [
138
+ {
139
+ "put-code": null,
140
+ "verified": true,
141
+ "last-modified-date": {
142
+ "value": 1438058214424
143
+ },
144
+ "created-date": {
145
+ "value": 1438058214424
146
+ },
147
+ "visibility": "LIMITED",
148
+ "primary": true,
149
+ "source": {
150
+ "source-orcid": {
151
+ "path": "0000-0001-6528-2027",
152
+ "host": "sandbox.orcid.org",
153
+ "uri": "http://sandbox.orcid.org/0000-0001-6528-2027"
154
+ },
155
+ "source-name": {
156
+ "value": "Martin Fenner"
157
+ },
158
+ "source-client-id": null
159
+ },
160
+ "path": null,
161
+ "email": "martin.fenner@datacite.org"
162
+ }
163
+ ],
164
+ "last-modified-date": {
165
+ "value": 1438058214424
166
+ }
167
+ },
168
+ "biography": {
169
+ "content": "Martin Fenner is the DataCite Technical Director and manages the technical architecture for Datacite as well as DataCite\u2019s technical contributions for the EU-funded THOR project. From 2012 to 2015 he was the technical lead for the PLOS Article-Level Metrics project. Martin has a medical degree from the Free University of Berlin and is a Board-certified medical oncologist.",
170
+ "path": "/0000-0001-6528-2027/biography",
171
+ "created-date": {
172
+ "value": 1460669194724
173
+ },
174
+ "visibility": "PUBLIC",
175
+ "last-modified-date": {
176
+ "value": 1500206408067
177
+ }
178
+ },
179
+ "addresses": {
180
+ "path": "/0000-0001-6528-2027/address",
181
+ "address": [
182
+ {
183
+ "put-code": 4809,
184
+ "last-modified-date": {
185
+ "value": 1500192622273
186
+ },
187
+ "country": {
188
+ "value": "DE"
189
+ },
190
+ "created-date": {
191
+ "value": 1500192622273
192
+ },
193
+ "visibility": "PUBLIC",
194
+ "source": {
195
+ "source-orcid": {
196
+ "path": "0000-0001-6528-2027",
197
+ "host": "sandbox.orcid.org",
198
+ "uri": "http://sandbox.orcid.org/0000-0001-6528-2027"
199
+ },
200
+ "source-name": {
201
+ "value": "Martin Fenner"
202
+ },
203
+ "source-client-id": null
204
+ },
205
+ "display-index": 1,
206
+ "path": "/0000-0001-6528-2027/address/4809"
207
+ }
208
+ ],
209
+ "last-modified-date": {
210
+ "value": 1500192622273
211
+ }
212
+ }
213
+ }
data/spec/omniauth/strategies/globus_spec.rb ADDED
@@ -0,0 +1,514 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'json'
5
+ require 'omniauth-globus'
6
+ require 'stringio'
7
+
8
+ describe OmniAuth::Strategies::Globus do
9
+ let(:request) { double('Request', params: {}, cookies: {}, env: {}) }
10
+ let(:app) do
11
+ lambda do
12
+ [200, {}, ['Hello.']]
13
+ end
14
+ end
15
+
16
+ subject do
17
+ OmniAuth::Strategies::Globus.new(app, 'appid', 'secret', @options || {}).tap do |strategy|
18
+ allow(strategy).to receive(:request) do
19
+ request
20
+ end
21
+ end
22
+ end
23
+
24
+ before do
25
+ OmniAuth.config.test_mode = true
26
+ end
27
+
28
+ after do
29
+ OmniAuth.config.test_mode = false
30
+ end
31
+
32
+ describe '#client_options' do
33
+ it 'has correct site' do
34
+ expect(subject.client.site).to eq('https://auth.globus.org')
35
+ end
36
+
37
+ it 'has correct authorize_url' do
38
+ expect(subject.client.options[:authorize_url]).to eq('https://auth.globus.org/v2/oauth2/authorize')
39
+ end
40
+
41
+ it 'has correct token_url' do
42
+ expect(subject.client.options[:token_url]).to eq('https://auth.globus.org/v2/oauth2/token')
43
+ end
44
+
45
+ describe 'overrides' do
46
+ context 'as strings' do
47
+ it 'should allow overriding the site' do
48
+ @options = { client_options: { 'site' => 'https://example.com' } }
49
+ expect(subject.client.site).to eq('https://example.com')
50
+ end
51
+
52
+ it 'should allow overriding the authorization_endpoint' do
53
+ @options = { client_options: { 'authorization_endpoint' => 'https://example.com' } }
54
+ expect(subject.client.options[:authorization_endpoint]).to eq('https://example.com')
55
+ end
56
+
57
+ it 'should allow overriding the token_endpoint' do
58
+ @options = { client_options: { 'token_endpoint' => 'https://example.com' } }
59
+ expect(subject.client.options[:token_endpoint]).to eq('https://example.com')
60
+ end
61
+ end
62
+
63
+ context 'as symbols' do
64
+ it 'should allow overriding the site' do
65
+ @options = { client_options: { site: 'https://example.com' } }
66
+ expect(subject.client.site).to eq('https://example.com')
67
+ end
68
+
69
+ it 'should allow overriding the authorization_endpoint' do
70
+ @options = { client_options: { authorization_endpoint: 'https://example.com' } }
71
+ expect(subject.client.options[:authorization_endpoint]).to eq('https://example.com')
72
+ end
73
+
74
+ it 'should allow overriding the token_endpoint' do
75
+ @options = { client_options: { token_endpoint: 'https://example.com' } }
76
+ expect(subject.client.options[:token_endpoint]).to eq('https://example.com')
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ describe '#authorize_options' do
83
+ %i[access_type login_hint prompt scope state device_id device_name].each do |k|
84
+ it "should support #{k}" do
85
+ @options = { k => 'http://someval' }
86
+ expect(subject.authorize_params[k.to_s]).to eq('http://someval')
87
+ end
88
+ end
89
+
90
+ describe 'redirect_uri' do
91
+ it 'should default to nil' do
92
+ @options = {}
93
+ expect(subject.authorize_params['redirect_uri']).to eq(nil)
94
+ end
95
+
96
+ it 'should set the redirect_uri parameter if present' do
97
+ @options = { redirect_uri: 'https://example.com' }
98
+ expect(subject.authorize_params['redirect_uri']).to eq('https://example.com')
99
+ end
100
+ end
101
+
102
+ describe 'access_type' do
103
+ it 'should default to "offline"' do
104
+ @options = {}
105
+ expect(subject.authorize_params['access_type']).to eq('offline')
106
+ end
107
+
108
+ it 'should set the access_type parameter if present' do
109
+ @options = { access_type: 'online' }
110
+ expect(subject.authorize_params['access_type']).to eq('online')
111
+ end
112
+ end
113
+
114
+ describe 'login_hint' do
115
+ it 'should default to nil' do
116
+ expect(subject.authorize_params['login_hint']).to eq(nil)
117
+ end
118
+
119
+ it 'should set the login_hint parameter if present' do
120
+ @options = { login_hint: 'john@example.com' }
121
+ expect(subject.authorize_params['login_hint']).to eq('john@example.com')
122
+ end
123
+ end
124
+
125
+ describe 'prompt' do
126
+ it 'should default to nil' do
127
+ expect(subject.authorize_params['prompt']).to eq(nil)
128
+ end
129
+
130
+ it 'should set the prompt parameter if present' do
131
+ @options = { prompt: 'consent select_account' }
132
+ expect(subject.authorize_params['prompt']).to eq('consent select_account')
133
+ end
134
+ end
135
+
136
+ describe 'request_visible_actions' do
137
+ it 'should default to nil' do
138
+ expect(subject.authorize_params['request_visible_actions']).to eq(nil)
139
+ end
140
+
141
+ it 'should set the request_visible_actions parameter if present' do
142
+ @options = { request_visible_actions: 'something' }
143
+ expect(subject.authorize_params['request_visible_actions']).to eq('something')
144
+ end
145
+ end
146
+
147
+ describe 'include_granted_scopes' do
148
+ it 'should default to nil' do
149
+ expect(subject.authorize_params['include_granted_scopes']).to eq(nil)
150
+ end
151
+
152
+ it 'should set the include_granted_scopes parameter if present' do
153
+ @options = { include_granted_scopes: 'true' }
154
+ expect(subject.authorize_params['include_granted_scopes']).to eq('true')
155
+ end
156
+ end
157
+
158
+ describe 'scope' do
159
+ it 'should leave base scopes as is' do
160
+ @options = { scope: 'profile' }
161
+ expect(subject.authorize_params['scope']).to eq('profile')
162
+ end
163
+
164
+ it 'should join scopes' do
165
+ @options = { scope: 'profile,email' }
166
+ expect(subject.authorize_params['scope']).to eq('profile email')
167
+ end
168
+
169
+ it 'should deal with whitespace when joining scopes' do
170
+ @options = { scope: 'profile, email' }
171
+ expect(subject.authorize_params['scope']).to eq('profile email')
172
+ end
173
+
174
+ it 'should set default scope to openid, profile, email' do
175
+ expect(subject.authorize_params['scope']).to eq("openid profile email")
176
+ end
177
+
178
+ it 'should support space delimited scopes' do
179
+ @options = { scope: 'profile email' }
180
+ expect(subject.authorize_params['scope']).to eq('profile email')
181
+ end
182
+
183
+ it 'should support extremely badly formed scopes' do
184
+ @options = { scope: 'profile email,foo,steve yeah' }
185
+ expect(subject.authorize_params['scope']).to eq('profile email foo steve yeah')
186
+ end
187
+ end
188
+
189
+ describe 'state' do
190
+ it 'should set the state parameter' do
191
+ @options = { state: 'some_state' }
192
+ expect(subject.authorize_params['state']).to eq('some_state')
193
+ expect(subject.authorize_params[:state]).to eq('some_state')
194
+ expect(subject.session['omniauth.state']).to eq('some_state')
195
+ end
196
+
197
+ it 'should set the omniauth.state dynamically' do
198
+ allow(subject).to receive(:request) { double('Request', params: { 'state' => 'some_state' }, env: {}) }
199
+ expect(subject.authorize_params['state']).to eq('some_state')
200
+ expect(subject.authorize_params[:state]).to eq('some_state')
201
+ expect(subject.session['omniauth.state']).to eq('some_state')
202
+ end
203
+ end
204
+
205
+ describe 'overrides' do
206
+ it 'should include top-level options that are marked as :authorize_options' do
207
+ @options = { authorize_options: %i[scope foo request_visible_actions], scope: 'http://bar', foo: 'baz', request_visible_actions: 'something' }
208
+ expect(subject.authorize_params['scope']).to eq('http://bar')
209
+ expect(subject.authorize_params['foo']).to eq('baz')
210
+ expect(subject.authorize_params['request_visible_actions']).to eq('something')
211
+ end
212
+
213
+ describe 'request overrides' do
214
+ %i[access_type login_hint prompt scope state].each do |k|
215
+ context "authorize option #{k}" do
216
+ let(:request) { double('Request', params: { k.to_s => 'http://example.com' }, cookies: {}, env: {}) }
217
+
218
+ it "should set the #{k} authorize option dynamically in the request" do
219
+ @options = { k: '' }
220
+ expect(subject.authorize_params[k.to_s]).to eq('http://example.com')
221
+ end
222
+ end
223
+ end
224
+
225
+ describe 'custom authorize_options' do
226
+ let(:request) { double('Request', params: { 'foo' => 'something' }, cookies: {}, env: {}) }
227
+
228
+ it 'should support request overrides from custom authorize_options' do
229
+ @options = { authorize_options: [:foo], foo: '' }
230
+ expect(subject.authorize_params['foo']).to eq('something')
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
236
+
237
+ describe '#authorize_params' do
238
+ it 'should include any authorize params passed in the :authorize_params option' do
239
+ @options = { authorize_params: { request_visible_actions: 'something', foo: 'bar', baz: 'zip' }, bad: 'not_included' }
240
+ expect(subject.authorize_params['request_visible_actions']).to eq('something')
241
+ expect(subject.authorize_params['foo']).to eq('bar')
242
+ expect(subject.authorize_params['baz']).to eq('zip')
243
+ expect(subject.authorize_params['bad']).to eq(nil)
244
+ end
245
+ end
246
+
247
+ describe '#token_params' do
248
+ it 'should include any token params passed in the :token_params option' do
249
+ @options = { token_params: { foo: 'bar', baz: 'zip' } }
250
+ expect(subject.token_params['foo']).to eq('bar')
251
+ expect(subject.token_params['baz']).to eq('zip')
252
+ end
253
+ end
254
+
255
+ describe '#token_options' do
256
+ it 'should include top-level options that are marked as :token_options' do
257
+ @options = { token_options: %i[scope foo], scope: 'bar', foo: 'baz', bad: 'not_included' }
258
+ expect(subject.token_params['scope']).to eq('bar')
259
+ expect(subject.token_params['foo']).to eq('baz')
260
+ expect(subject.token_params['bad']).to eq(nil)
261
+ end
262
+ end
263
+
264
+ describe '#callback_path' do
265
+ it 'has the correct default callback path' do
266
+ expect(subject.callback_path).to eq("/auth/globus/callback")
267
+ end
268
+
269
+ it 'should set the callback_path parameter if present' do
270
+ @options = { callback_path: '/auth/foo/callback' }
271
+ expect(subject.callback_path).to eq('/auth/foo/callback')
272
+ end
273
+ end
274
+
275
+ describe '#info' do
276
+ let(:client) do
277
+ OAuth2::Client.new('abc', 'def') do |builder|
278
+ builder.request :url_encoded
279
+ builder.adapter :test do |stub|
280
+ stub.get("/v2/oauth2/userinfo") { [200, { "content-type" => "application/json" }, response_hash.to_json] }
281
+ end
282
+ end
283
+ end
284
+ let(:access_token) { OAuth2::AccessToken.from_hash(client, {}) }
285
+ before { allow(subject).to receive(:access_token).and_return(access_token) }
286
+
287
+ context 'with email' do
288
+ let(:response_hash) do
289
+ { email: 'something@domain.invalid' }
290
+ end
291
+
292
+ it 'should return email' do
293
+ expect(subject.info[:email]).to eq('something@domain.invalid')
294
+ end
295
+ end
296
+ end
297
+
298
+ describe '#extra' do
299
+ let(:client) do
300
+ OAuth2::Client.new('abc', 'def') do |builder|
301
+ builder.request :url_encoded
302
+ builder.adapter :test do |stub|
303
+ stub.get('/v2/oauth2/userinfo') { [200, { 'content-type' => 'application/json' }, '{"sub": "12345"}'] }
304
+ end
305
+ end
306
+ end
307
+ let(:access_token) { OAuth2::AccessToken.from_hash(client, {}) }
308
+
309
+ before { allow(subject).to receive(:access_token).and_return(access_token) }
310
+
311
+ describe 'id_token' do
312
+ shared_examples 'id_token issued by valid issuer' do |issuer| # rubocop:disable Metrics/BlockLength
313
+ context 'when the id_token is passed into the access token' do
314
+ let(:token_info) do
315
+ {
316
+ identity_provider_display_name: "ORCID",
317
+ sub: "abc",
318
+ preferred_username: "0000-0003-1419-2405@orcid.org",
319
+ identity_provider: "def",
320
+ organization: "DataCite",
321
+ email: "mfenner@datacite.org",
322
+ name: "Martin Fenner",
323
+ exp: Time.now.to_i + 3600,
324
+ iss: "https://auth.globus.org"
325
+ }
326
+ end
327
+ let(:id_token) { JWT.encode(token_info, 'secret') }
328
+ let(:access_token) { OAuth2::AccessToken.from_hash(client, 'id_token' => id_token) }
329
+
330
+ it 'should include id_token when set on the access_token' do
331
+ expect(subject.extra).to include(id_token: id_token)
332
+ end
333
+ end
334
+ end
335
+
336
+ it_behaves_like 'id_token issued by valid issuer', 'https://auth.globus.org'
337
+
338
+ context 'when the id_token is issued by an invalid issuer' do
339
+ let(:token_info) do
340
+ {
341
+ identity_provider_display_name: "ORCID",
342
+ sub: "abc",
343
+ preferred_username: "0000-0003-1419-2405@orcid.org",
344
+ identity_provider: "def",
345
+ organization: "DataCite",
346
+ email: "mfenner@datacite.org",
347
+ name: "Martin Fenner",
348
+ exp: Time.now.to_i + 3600,
349
+ iss: "https://fake.globus.org"
350
+ }
351
+ end
352
+ let(:id_token) { JWT.encode(token_info, 'secret') }
353
+ let(:access_token) { OAuth2::AccessToken.from_hash(client, 'id_token' => id_token) }
354
+
355
+ it 'raises JWT::InvalidIssuerError' do
356
+ expect { subject.extra }.to raise_error(JWT::InvalidIssuerError)
357
+ end
358
+ end
359
+
360
+ context 'when the id_token is missing' do
361
+ it 'should not include id_token' do
362
+ expect(subject.extra).not_to have_key(:id_token)
363
+ end
364
+
365
+ it 'should not include id_info' do
366
+ expect(subject.extra).not_to have_key(:id_info)
367
+ end
368
+ end
369
+ end
370
+
371
+ describe 'raw_info' do
372
+ context 'when skip_info is true' do
373
+ before { subject.options[:skip_info] = true }
374
+
375
+ it 'should not include raw_info' do
376
+ expect(subject.extra).not_to have_key(:raw_info)
377
+ end
378
+ end
379
+
380
+ context 'when skip_info is false' do
381
+ before { subject.options[:skip_info] = false }
382
+
383
+ it 'should include raw_info' do
384
+ expect(subject.extra[:raw_info]).to eq('sub' => '12345')
385
+ end
386
+ end
387
+ end
388
+ end
389
+
390
+ describe 'build_access_token' do
391
+ it 'should use a hybrid authorization request_uri if this is an AJAX request with a code parameter' do
392
+ allow(request).to receive(:xhr?).and_return(true)
393
+ allow(request).to receive(:params).and_return('code' => 'valid_code')
394
+
395
+ client = double(:client)
396
+ auth_code = double(:auth_code)
397
+ allow(client).to receive(:auth_code).and_return(auth_code)
398
+ expect(subject).to receive(:client).and_return(client)
399
+ expect(auth_code).to receive(:get_token).with('valid_code', { redirect_uri: 'postmessage' }, {})
400
+
401
+ expect(subject).not_to receive(:orig_build_access_token)
402
+ subject.build_access_token
403
+ end
404
+
405
+ it 'should use a hybrid authorization request_uri if this is an AJAX request (mobile) with a code parameter' do
406
+ allow(request).to receive(:xhr?).and_return(true)
407
+ allow(request).to receive(:params).and_return('code' => 'valid_code', 'redirect_uri' => '')
408
+
409
+ client = double(:client)
410
+ auth_code = double(:auth_code)
411
+ allow(client).to receive(:auth_code).and_return(auth_code)
412
+ expect(subject).to receive(:client).and_return(client)
413
+ expect(auth_code).to receive(:get_token).with('valid_code', { redirect_uri: '' }, {})
414
+
415
+ expect(subject).not_to receive(:orig_build_access_token)
416
+ subject.build_access_token
417
+ end
418
+
419
+ it 'should use the request_uri from params if this not an AJAX request (request from installed app) with a code parameter' do
420
+ allow(request).to receive(:xhr?).and_return(false)
421
+ allow(request).to receive(:params).and_return('code' => 'valid_code', 'redirect_uri' => 'redirect_uri')
422
+
423
+ client = double(:client)
424
+ auth_code = double(:auth_code)
425
+ allow(client).to receive(:auth_code).and_return(auth_code)
426
+ expect(subject).to receive(:client).and_return(client)
427
+ expect(auth_code).to receive(:get_token).with('valid_code', { redirect_uri: 'redirect_uri' }, {})
428
+
429
+ expect(subject).not_to receive(:orig_build_access_token)
430
+ subject.build_access_token
431
+ end
432
+
433
+ it 'should read access_token from hash if this is not an AJAX request with a code parameter' do
434
+ allow(request).to receive(:xhr?).and_return(false)
435
+ allow(request).to receive(:params).and_return('access_token' => 'valid_access_token')
436
+ expect(subject).to receive(:verify_token).with('valid_access_token').and_return true
437
+ expect(subject).to receive(:client).and_return(:client)
438
+
439
+ token = subject.build_access_token
440
+ expect(token).to be_instance_of(::OAuth2::AccessToken)
441
+ expect(token.token).to eq('valid_access_token')
442
+ expect(token.client).to eq(:client)
443
+ end
444
+
445
+ it 'reads the code from a json request body' do
446
+ body = StringIO.new(%({"code":"json_access_token"}))
447
+ client = double(:client)
448
+ auth_code = double(:auth_code)
449
+
450
+ allow(request).to receive(:xhr?).and_return(false)
451
+ allow(request).to receive(:content_type).and_return('application/json')
452
+ allow(request).to receive(:body).and_return(body)
453
+ allow(client).to receive(:auth_code).and_return(auth_code)
454
+ expect(subject).to receive(:client).and_return(client)
455
+
456
+ expect(auth_code).to receive(:get_token).with('json_access_token', { redirect_uri: 'postmessage' }, {})
457
+
458
+ subject.build_access_token
459
+ end
460
+
461
+ it 'should use callback_url without query_string if this is not an AJAX request' do
462
+ allow(request).to receive(:xhr?).and_return(false)
463
+ allow(request).to receive(:params).and_return('code' => 'valid_code')
464
+ allow(request).to receive(:content_type).and_return('application/x-www-form-urlencoded')
465
+
466
+ client = double(:client)
467
+ auth_code = double(:auth_code)
468
+ allow(client).to receive(:auth_code).and_return(auth_code)
469
+ allow(subject).to receive(:callback_url).and_return('redirect_uri_without_query_string')
470
+
471
+ expect(subject).to receive(:client).and_return(client)
472
+ expect(auth_code).to receive(:get_token).with('valid_code', { redirect_uri: 'redirect_uri_without_query_string' }, {})
473
+ subject.build_access_token
474
+ end
475
+ end
476
+
477
+ describe 'verify_token' do
478
+ before(:each) do
479
+ subject.options.client_options[:connection_build] = proc do |builder|
480
+ builder.request :url_encoded
481
+ builder.adapter :test do |stub|
482
+ stub.get('/v2/oauth2/userinfo?access_token=valid_access_token') do
483
+ [200, { 'Content-Type' => 'application/json; charset=UTF-8' }, JSON.dump(
484
+ {
485
+ identity_provider_display_name: "ORCID",
486
+ sub: "abc",
487
+ preferred_username: "0000-0003-1419-2405@orcid.org",
488
+ identity_provider: "def",
489
+ organization: "DataCite",
490
+ email: "mfenner@datacite.org",
491
+ name: "Martin Fenner",
492
+ exp: 1568009648,
493
+ iss: "https://auth.globus.org"
494
+ }
495
+ )]
496
+ end
497
+ stub.get('/v2/oauth2/userinfo?access_token=invalid_access_token') do
498
+ [400, { 'Content-Type' => 'application/json; charset=UTF-8' }, JSON.dump(error_description: 'Invalid Value')]
499
+ end
500
+ end
501
+ end
502
+ end
503
+
504
+ # it 'should verify token if access_token is valid' do
505
+ # expect(subject.send(:verify_token, 'valid_access_token')).to eq(true)
506
+ # end
507
+
508
+ it 'should raise error if access_token is invalid' do
509
+ expect do
510
+ subject.send(:verify_token, 'invalid_access_token')
511
+ end.to raise_error(OAuth2::Error)
512
+ end
513
+ end
514
+ end
data/spec/rubocop_spec.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'spec_helper'
4
+
5
+ describe 'Rubocop' do
6
+ it 'should pass with no offenses detected' do
7
+ expect(`rubocop`).to include('no offenses detected')
8
+ end
9
+ end
data/spec/spec_helper.rb ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ Bundler.setup
5
+
6
+ require "simplecov"
7
+ SimpleCov.start
8
+
9
+ require "rspec"
10
+ require "rack/test"
11
+ require "webmock/rspec"
12
+ require "omniauth-globus"
13
+
14
+ def fixture_path
15
+ File.expand_path("fixtures", __dir__) + "/"
16
+ end
17
+
18
+ RSpec.configure do |config|
19
+ config.include WebMock::API
20
+ config.include Rack::Test::Methods
21
+ config.extend OmniAuth::Test::StrategyMacros, type: :strategy
22
+ config.expect_with :rspec do |c|
23
+ c.syntax = :expect
24
+ end
25
+ end
metadata ADDED
@@ -0,0 +1,219 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: omniauth-globus
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.7
5
+ platform: ruby
6
+ authors:
7
+ - Martin Fenner
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-09-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: jwt
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: omniauth
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.9'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.9'
41
+ - !ruby/object:Gem::Dependency
42
+ name: omniauth-oauth2
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.6'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.6'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: codeclimate-test-reporter
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.0'
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: 1.0.9
79
+ type: :development
80
+ prerelease: false
81
+ version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - "~>"
84
+ - !ruby/object:Gem::Version
85
+ version: '1.0'
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 1.0.9
89
+ - !ruby/object:Gem::Dependency
90
+ name: rack-test
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: 0.6.3
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: 0.6.3
103
+ - !ruby/object:Gem::Dependency
104
+ name: rspec
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '3.4'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '3.4'
117
+ - !ruby/object:Gem::Dependency
118
+ name: rubocop
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '0.68'
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '0.68'
131
+ - !ruby/object:Gem::Dependency
132
+ name: simplecov
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: 0.13.0
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: 0.13.0
145
+ - !ruby/object:Gem::Dependency
146
+ name: webmock
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: '3.0'
152
+ - - ">="
153
+ - !ruby/object:Gem::Version
154
+ version: 3.0.1
155
+ type: :development
156
+ prerelease: false
157
+ version_requirements: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - "~>"
160
+ - !ruby/object:Gem::Version
161
+ version: '3.0'
162
+ - - ">="
163
+ - !ruby/object:Gem::Version
164
+ version: 3.0.1
165
+ description: Enables third-party client apps to authenticate with the Globus service
166
+ via OpenID Connect
167
+ email:
168
+ - mfenner@datacite.org
169
+ executables: []
170
+ extensions: []
171
+ extra_rdoc_files:
172
+ - README.md
173
+ files:
174
+ - ".gitignore"
175
+ - ".rubocop.yml"
176
+ - ".travis.yml"
177
+ - Gemfile
178
+ - Gemfile.lock
179
+ - LICENSE
180
+ - README.md
181
+ - lib/omniauth-globus.rb
182
+ - lib/omniauth/globus.rb
183
+ - lib/omniauth/globus/version.rb
184
+ - lib/omniauth/strategies/globus.rb
185
+ - omniauth-globus.gemspec
186
+ - spec/fixtures/access_token.json
187
+ - spec/fixtures/request_info.json
188
+ - spec/omniauth/strategies/globus_spec.rb
189
+ - spec/rubocop_spec.rb
190
+ - spec/spec_helper.rb
191
+ homepage: https://github.com/datacite/omniauth-globus
192
+ licenses:
193
+ - MIT
194
+ metadata: {}
195
+ post_install_message:
196
+ rdoc_options: []
197
+ require_paths:
198
+ - lib
199
+ required_ruby_version: !ruby/object:Gem::Requirement
200
+ requirements:
201
+ - - ">="
202
+ - !ruby/object:Gem::Version
203
+ version: '2.3'
204
+ required_rubygems_version: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ requirements: []
210
+ rubygems_version: 3.0.1
211
+ signing_key:
212
+ specification_version: 4
213
+ summary: Globus OpenId connect Strategy for OmniAuth 1.0
214
+ test_files:
215
+ - spec/fixtures/access_token.json
216
+ - spec/fixtures/request_info.json
217
+ - spec/omniauth/strategies/globus_spec.rb
218
+ - spec/rubocop_spec.rb
219
+ - spec/spec_helper.rb