googleauth 0.1.0 → 0.16.2

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.
Files changed (78) hide show
  1. checksums.yaml +5 -5
  2. data/.github/CODEOWNERS +7 -0
  3. data/.github/CONTRIBUTING.md +74 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +36 -0
  5. data/.github/ISSUE_TEMPLATE/feature_request.md +21 -0
  6. data/.github/ISSUE_TEMPLATE/support_request.md +7 -0
  7. data/.github/renovate.json +6 -0
  8. data/.github/sync-repo-settings.yaml +18 -0
  9. data/.github/workflows/ci.yml +55 -0
  10. data/.github/workflows/release-please.yml +39 -0
  11. data/.gitignore +3 -0
  12. data/.kokoro/populate-secrets.sh +76 -0
  13. data/.kokoro/release.cfg +52 -0
  14. data/.kokoro/release.sh +18 -0
  15. data/.kokoro/trampoline_v2.sh +489 -0
  16. data/.repo-metadata.json +5 -0
  17. data/.rubocop.yml +17 -0
  18. data/.toys/.toys.rb +45 -0
  19. data/.toys/ci.rb +43 -0
  20. data/.toys/kokoro/.toys.rb +66 -0
  21. data/.toys/kokoro/publish-docs.rb +67 -0
  22. data/.toys/kokoro/publish-gem.rb +53 -0
  23. data/.toys/linkinator.rb +43 -0
  24. data/.trampolinerc +48 -0
  25. data/CHANGELOG.md +199 -0
  26. data/CODE_OF_CONDUCT.md +43 -0
  27. data/Gemfile +22 -1
  28. data/{COPYING → LICENSE} +0 -0
  29. data/README.md +140 -17
  30. data/googleauth.gemspec +28 -28
  31. data/integration/helper.rb +31 -0
  32. data/integration/id_tokens/key_source_test.rb +74 -0
  33. data/lib/googleauth.rb +7 -37
  34. data/lib/googleauth/application_default.rb +81 -0
  35. data/lib/googleauth/client_id.rb +104 -0
  36. data/lib/googleauth/compute_engine.rb +73 -26
  37. data/lib/googleauth/credentials.rb +561 -0
  38. data/lib/googleauth/credentials_loader.rb +207 -0
  39. data/lib/googleauth/default_credentials.rb +93 -0
  40. data/lib/googleauth/iam.rb +75 -0
  41. data/lib/googleauth/id_tokens.rb +233 -0
  42. data/lib/googleauth/id_tokens/errors.rb +71 -0
  43. data/lib/googleauth/id_tokens/key_sources.rb +396 -0
  44. data/lib/googleauth/id_tokens/verifier.rb +142 -0
  45. data/lib/googleauth/json_key_reader.rb +50 -0
  46. data/lib/googleauth/scope_util.rb +61 -0
  47. data/lib/googleauth/service_account.rb +177 -67
  48. data/lib/googleauth/signet.rb +69 -8
  49. data/lib/googleauth/stores/file_token_store.rb +65 -0
  50. data/lib/googleauth/stores/redis_token_store.rb +96 -0
  51. data/lib/googleauth/token_store.rb +69 -0
  52. data/lib/googleauth/user_authorizer.rb +285 -0
  53. data/lib/googleauth/user_refresh.rb +129 -0
  54. data/lib/googleauth/version.rb +1 -1
  55. data/lib/googleauth/web_user_authorizer.rb +295 -0
  56. data/spec/googleauth/apply_auth_examples.rb +96 -94
  57. data/spec/googleauth/client_id_spec.rb +160 -0
  58. data/spec/googleauth/compute_engine_spec.rb +125 -55
  59. data/spec/googleauth/credentials_spec.rb +600 -0
  60. data/spec/googleauth/get_application_default_spec.rb +232 -80
  61. data/spec/googleauth/iam_spec.rb +80 -0
  62. data/spec/googleauth/scope_util_spec.rb +77 -0
  63. data/spec/googleauth/service_account_spec.rb +422 -68
  64. data/spec/googleauth/signet_spec.rb +101 -25
  65. data/spec/googleauth/stores/file_token_store_spec.rb +57 -0
  66. data/spec/googleauth/stores/redis_token_store_spec.rb +50 -0
  67. data/spec/googleauth/stores/store_examples.rb +58 -0
  68. data/spec/googleauth/user_authorizer_spec.rb +343 -0
  69. data/spec/googleauth/user_refresh_spec.rb +359 -0
  70. data/spec/googleauth/web_user_authorizer_spec.rb +172 -0
  71. data/spec/spec_helper.rb +51 -10
  72. data/test/helper.rb +33 -0
  73. data/test/id_tokens/key_sources_test.rb +240 -0
  74. data/test/id_tokens/verifier_test.rb +269 -0
  75. metadata +114 -75
  76. data/.travis.yml +0 -18
  77. data/CONTRIBUTING.md +0 -32
  78. data/Rakefile +0 -15
@@ -27,40 +27,116 @@
27
27
  # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
28
  # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
29
 
30
- spec_dir = File.expand_path(File.join(File.dirname(__FILE__)))
31
- $LOAD_PATH.unshift(spec_dir)
30
+ spec_dir = File.expand_path File.join(File.dirname(__FILE__))
31
+ $LOAD_PATH.unshift spec_dir
32
32
  $LOAD_PATH.uniq!
33
33
 
34
- require 'apply_auth_examples'
35
- require 'googleauth/signet'
36
- require 'jwt'
37
- require 'openssl'
38
- require 'spec_helper'
34
+ require "apply_auth_examples"
35
+ require "googleauth/signet"
36
+ require "jwt"
37
+ require "openssl"
38
+ require "spec_helper"
39
39
 
40
40
  describe Signet::OAuth2::Client do
41
- before(:example) do
42
- @key = OpenSSL::PKey::RSA.new(2048)
41
+ before :example do
42
+ @key = OpenSSL::PKey::RSA.new 2048
43
43
  @client = Signet::OAuth2::Client.new(
44
- token_credential_uri: 'https://accounts.google.com/o/oauth2/token',
45
- scope: 'https://www.googleapis.com/auth/userinfo.profile',
46
- issuer: 'app@example.com',
47
- audience: 'https://accounts.google.com/o/oauth2/token',
48
- signing_key: @key
49
- )
44
+ token_credential_uri: "https://oauth2.googleapis.com/token",
45
+ scope: "https://www.googleapis.com/auth/userinfo.profile",
46
+ issuer: "app@example.com",
47
+ audience: "https://oauth2.googleapis.com/token",
48
+ signing_key: @key
49
+ )
50
+ @id_client = Signet::OAuth2::Client.new(
51
+ token_credential_uri: "https://oauth2.googleapis.com/token",
52
+ target_audience: "https://pubsub.googleapis.com/",
53
+ issuer: "app@example.com",
54
+ audience: "https://oauth2.googleapis.com/token",
55
+ signing_key: @key
56
+ )
50
57
  end
51
58
 
52
- def make_auth_stubs(access_token: '')
53
- Faraday::Adapter::Test::Stubs.new do |stub|
54
- stub.post('/o/oauth2/token') do |env|
55
- params = Addressable::URI.form_unencode(env[:body])
56
- _claim, _header = JWT.decode(params.assoc('assertion').last,
57
- @key.public_key)
58
- want = ['grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer']
59
- expect(params.assoc('grant_type')).to eq(want)
60
- build_access_token_json(access_token)
59
+ def make_auth_stubs opts
60
+ body_fields = { "token_type" => "Bearer", "expires_in" => 3600 }
61
+ body_fields["access_token"] = opts[:access_token] if opts[:access_token]
62
+ body_fields["id_token"] = opts[:id_token] if opts[:id_token]
63
+ body = MultiJson.dump body_fields
64
+ blk = proc do |request|
65
+ params = Addressable::URI.form_unencode request.body
66
+ claim, _header = JWT.decode(params.assoc("assertion").last,
67
+ @key.public_key, true,
68
+ algorithm: "RS256")
69
+ !opts[:id_token] || claim["target_audience"] == "https://pubsub.googleapis.com/"
70
+ end
71
+ with_params = { body: hash_including(
72
+ "grant_type" => "urn:ietf:params:oauth:grant-type:jwt-bearer"
73
+ ) }
74
+ with_params[:headers] = { "User-Agent" => opts[:user_agent] } if opts[:user_agent]
75
+ stub_request(:post, "https://oauth2.googleapis.com/token")
76
+ .with(with_params, &blk)
77
+ .to_return(body: body,
78
+ status: 200,
79
+ headers: { "Content-Type" => "application/json" })
80
+ end
81
+
82
+ it_behaves_like "apply/apply! are OK"
83
+
84
+ describe "#configure_connection" do
85
+ it "honors default_connection" do
86
+ token = "1/abcdef1234567890"
87
+ stub = make_auth_stubs access_token: token, user_agent: "RubyRocks/1.0"
88
+ conn = Faraday.new headers: { "User-Agent" => "RubyRocks/1.0" }
89
+ @client.configure_connection default_connection: conn
90
+ md = { foo: "bar" }
91
+ @client.apply! md
92
+ want = { foo: "bar", authorization: "Bearer #{token}" }
93
+ expect(md).to eq(want)
94
+ expect(stub).to have_been_requested
95
+ end
96
+
97
+ it "honors connection_builder" do
98
+ token = "1/abcdef1234567890"
99
+ stub = make_auth_stubs access_token: token, user_agent: "RubyRocks/2.0"
100
+ connection_builder = proc do
101
+ Faraday.new headers: { "User-Agent" => "RubyRocks/2.0" }
61
102
  end
103
+ @client.configure_connection connection_builder: connection_builder
104
+ md = { foo: "bar" }
105
+ @client.apply! md
106
+ want = { foo: "bar", authorization: "Bearer #{token}" }
107
+ expect(md).to eq(want)
108
+ expect(stub).to have_been_requested
62
109
  end
63
110
  end
64
111
 
65
- it_behaves_like 'apply/apply! are OK'
112
+ describe "#fetch_access_token!" do
113
+ it "retries when orig_fetch_access_token! raises Signet::RemoteServerError" do
114
+ mocked_responses = [:raise, :raise, "success"]
115
+ allow(@client).to receive(:orig_fetch_access_token!).exactly(3).times do
116
+ response = mocked_responses.shift
117
+ response == :raise ? raise(Signet::RemoteServerError) : response
118
+ end
119
+ expect(@client.fetch_access_token!).to eq("success")
120
+ end
121
+
122
+ it "raises when the max retry count is exceeded" do
123
+ mocked_responses = [:raise, :raise, :raise, :raise, :raise, :raise, "success"]
124
+ allow(@client).to receive(:orig_fetch_access_token!).exactly(6).times do
125
+ response = mocked_responses.shift
126
+ response == :raise ? raise(Signet::RemoteServerError) : response
127
+ end
128
+ expect { @client.fetch_access_token! }.to raise_error Signet::AuthorizationError
129
+ end
130
+
131
+ it "does not retry and raises right away if it encounters a Signet::AuthorizationError" do
132
+ allow(@client).to receive(:orig_fetch_access_token!).at_most(:once)
133
+ .and_raise(Signet::AuthorizationError.new("Some Message"))
134
+ expect { @client.fetch_access_token! }.to raise_error Signet::AuthorizationError
135
+ end
136
+
137
+ it "does not retry and raises right away if it encounters a Signet::ParseError" do
138
+ allow(@client).to receive(:orig_fetch_access_token!).at_most(:once).and_raise(Signet::ParseError)
139
+ expect { @client.fetch_access_token! }.to raise_error Signet::ParseError
140
+ end
141
+ end
66
142
  end
@@ -0,0 +1,57 @@
1
+ # Copyright 2015, Google Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are
6
+ # met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright
9
+ # notice, this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above
11
+ # copyright notice, this list of conditions and the following disclaimer
12
+ # in the documentation and/or other materials provided with the
13
+ # distribution.
14
+ # * Neither the name of Google Inc. nor the names of its
15
+ # contributors may be used to endorse or promote products derived from
16
+ # this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+
30
+ spec_dir = File.expand_path File.join(File.dirname(__FILE__))
31
+ $LOAD_PATH.unshift spec_dir
32
+ $LOAD_PATH.uniq!
33
+
34
+ require "googleauth"
35
+ require "googleauth/stores/file_token_store"
36
+ require "spec_helper"
37
+ require "fakefs/safe"
38
+ require "fakefs/spec_helpers"
39
+ require "googleauth/stores/store_examples"
40
+
41
+ module FakeFS
42
+ class File
43
+ # FakeFS doesn't implement. And since we don't need to actually lock,
44
+ # just stub out...
45
+ def flock *; end
46
+ end
47
+ end
48
+
49
+ describe Google::Auth::Stores::FileTokenStore do
50
+ include FakeFS::SpecHelpers
51
+
52
+ let :store do
53
+ Google::Auth::Stores::FileTokenStore.new file: "/tokens.yaml"
54
+ end
55
+
56
+ it_behaves_like "token store"
57
+ end
@@ -0,0 +1,50 @@
1
+ # Copyright 2015, Google Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are
6
+ # met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright
9
+ # notice, this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above
11
+ # copyright notice, this list of conditions and the following disclaimer
12
+ # in the documentation and/or other materials provided with the
13
+ # distribution.
14
+ # * Neither the name of Google Inc. nor the names of its
15
+ # contributors may be used to endorse or promote products derived from
16
+ # this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+
30
+ spec_dir = File.expand_path File.join(File.dirname(__FILE__))
31
+ $LOAD_PATH.unshift spec_dir
32
+ $LOAD_PATH.uniq!
33
+
34
+ require "googleauth"
35
+ require "googleauth/stores/redis_token_store"
36
+ require "spec_helper"
37
+ require "fakeredis/rspec"
38
+ require "googleauth/stores/store_examples"
39
+
40
+ describe Google::Auth::Stores::RedisTokenStore do
41
+ let :redis do
42
+ Redis.new
43
+ end
44
+
45
+ let :store do
46
+ Google::Auth::Stores::RedisTokenStore.new redis: redis
47
+ end
48
+
49
+ it_behaves_like "token store"
50
+ end
@@ -0,0 +1,58 @@
1
+ # Copyright 2015, Google Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are
6
+ # met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright
9
+ # notice, this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above
11
+ # copyright notice, this list of conditions and the following disclaimer
12
+ # in the documentation and/or other materials provided with the
13
+ # distribution.
14
+ # * Neither the name of Google Inc. nor the names of its
15
+ # contributors may be used to endorse or promote products derived from
16
+ # this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+
30
+ spec_dir = File.expand_path File.join(File.dirname(__FILE__))
31
+ $LOAD_PATH.unshift spec_dir
32
+ $LOAD_PATH.uniq!
33
+
34
+ require "spec_helper"
35
+
36
+ shared_examples "token store" do
37
+ before :each do
38
+ store.store "default", "test"
39
+ end
40
+
41
+ it "should return a stored value" do
42
+ expect(store.load("default")).to eq "test"
43
+ end
44
+
45
+ it "should return nil for missing tokens" do
46
+ expect(store.load("notavalidkey")).to be_nil
47
+ end
48
+
49
+ it "should return nil for deleted tokens" do
50
+ store.delete "default"
51
+ expect(store.load("default")).to be_nil
52
+ end
53
+
54
+ it "should save overwrite values on store" do
55
+ store.store "default", "test2"
56
+ expect(store.load("default")).to eq "test2"
57
+ end
58
+ end
@@ -0,0 +1,343 @@
1
+ # Copyright 2015, Google Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are
6
+ # met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright
9
+ # notice, this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above
11
+ # copyright notice, this list of conditions and the following disclaimer
12
+ # in the documentation and/or other materials provided with the
13
+ # distribution.
14
+ # * Neither the name of Google Inc. nor the names of its
15
+ # contributors may be used to endorse or promote products derived from
16
+ # this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+
30
+ spec_dir = File.expand_path File.join(File.dirname(__FILE__))
31
+ $LOAD_PATH.unshift spec_dir
32
+ $LOAD_PATH.uniq!
33
+
34
+ require "googleauth"
35
+ require "googleauth/user_authorizer"
36
+ require "uri"
37
+ require "multi_json"
38
+ require "spec_helper"
39
+
40
+ describe Google::Auth::UserAuthorizer do
41
+ include TestHelpers
42
+
43
+ let(:client_id) { Google::Auth::ClientId.new "testclient", "notasecret" }
44
+ let(:scope) { %w[email profile] }
45
+ let(:token_store) { DummyTokenStore.new }
46
+ let(:callback_uri) { "https://www.example.com/oauth/callback" }
47
+ let :authorizer do
48
+ Google::Auth::UserAuthorizer.new(client_id,
49
+ scope,
50
+ token_store,
51
+ callback_uri)
52
+ end
53
+
54
+ shared_examples "valid authorization url" do
55
+ it "should have a valid base URI" do
56
+ expect(uri).to match %r{https://accounts.google.com/o/oauth2/auth}
57
+ end
58
+
59
+ it "should request offline access" do
60
+ expect(URI(uri).query).to match(/access_type=offline/)
61
+ end
62
+
63
+ it "should request response type code" do
64
+ expect(URI(uri).query).to match(/response_type=code/)
65
+ end
66
+
67
+ it "should force approval" do
68
+ expect(URI(uri).query).to match(/approval_prompt=force/)
69
+ end
70
+
71
+ it "should include granted scopes" do
72
+ expect(URI(uri).query).to match(/include_granted_scopes=true/)
73
+ end
74
+
75
+ it "should include the correct client id" do
76
+ expect(URI(uri).query).to match(/client_id=testclient/)
77
+ end
78
+
79
+ it "should not include a client secret" do
80
+ expect(URI(uri).query).to_not match(/client_secret/)
81
+ end
82
+
83
+ it "should include the redirect_uri" do
84
+ expect(URI(uri).query).to match(
85
+ %r{redirect_uri=https://www.example.com/oauth/callback}
86
+ )
87
+ end
88
+
89
+ it "should include the scope" do
90
+ expect(URI(uri).query).to match(/scope=email%20profile/)
91
+ end
92
+ end
93
+
94
+ context "when generating authorization URLs and callback_uri is 'postmessage'" do
95
+ let(:callback_uri) { "postmessage" }
96
+ let :authorizer do
97
+ Google::Auth::UserAuthorizer.new(client_id,
98
+ scope,
99
+ token_store,
100
+ callback_uri)
101
+ end
102
+ let :uri do
103
+ authorizer.get_authorization_url login_hint: "user1", state: "mystate"
104
+ end
105
+
106
+ it "should include the redirect_uri 'postmessage'" do
107
+ expect(URI(uri).query).to match(
108
+ %r{redirect_uri=postmessage}
109
+ )
110
+ end
111
+ end
112
+
113
+ context "when generating authorization URLs with user ID & state" do
114
+ let :uri do
115
+ authorizer.get_authorization_url login_hint: "user1", state: "mystate"
116
+ end
117
+
118
+ it_behaves_like "valid authorization url"
119
+
120
+ it "includes a login hint" do
121
+ expect(URI(uri).query).to match(/login_hint=user1/)
122
+ end
123
+
124
+ it "includes the app state" do
125
+ expect(URI(uri).query).to match(/state=mystate/)
126
+ end
127
+ end
128
+
129
+ context "when generating authorization URLs with user ID and no state" do
130
+ let(:uri) { authorizer.get_authorization_url login_hint: "user1" }
131
+
132
+ it_behaves_like "valid authorization url"
133
+
134
+ it "includes a login hint" do
135
+ expect(URI(uri).query).to match(/login_hint=user1/)
136
+ end
137
+
138
+ it "does not include the state parameter" do
139
+ expect(URI(uri).query).to_not match(/state/)
140
+ end
141
+ end
142
+
143
+ context "when generating authorization URLs with no user ID and no state" do
144
+ let(:uri) { authorizer.get_authorization_url }
145
+
146
+ it_behaves_like "valid authorization url"
147
+
148
+ it "does not include the login hint parameter" do
149
+ expect(URI(uri).query).to_not match(/login_hint/)
150
+ end
151
+
152
+ it "does not include the state parameter" do
153
+ expect(URI(uri).query).to_not match(/state/)
154
+ end
155
+ end
156
+
157
+ context "when retrieving tokens" do
158
+ let :token_json do
159
+ MultiJson.dump(
160
+ access_token: "accesstoken",
161
+ refresh_token: "refreshtoken",
162
+ expiration_time_millis: 1_441_234_742_000
163
+ )
164
+ end
165
+
166
+ context "with a valid user id" do
167
+ let :credentials do
168
+ token_store.store "user1", token_json
169
+ authorizer.get_credentials "user1"
170
+ end
171
+
172
+ it "should return an instance of UserRefreshCredentials" do
173
+ expect(credentials).to be_instance_of(
174
+ Google::Auth::UserRefreshCredentials
175
+ )
176
+ end
177
+
178
+ it "should return credentials with a valid refresh token" do
179
+ expect(credentials.refresh_token).to eq "refreshtoken"
180
+ end
181
+
182
+ it "should return credentials with a valid access token" do
183
+ expect(credentials.access_token).to eq "accesstoken"
184
+ end
185
+
186
+ it "should return credentials with a valid client ID" do
187
+ expect(credentials.client_id).to eq "testclient"
188
+ end
189
+
190
+ it "should return credentials with a valid client secret" do
191
+ expect(credentials.client_secret).to eq "notasecret"
192
+ end
193
+
194
+ it "should return credentials with a valid scope" do
195
+ expect(credentials.scope).to eq %w[email profile]
196
+ end
197
+
198
+ it "should return credentials with a valid expiration time" do
199
+ expect(credentials.expires_at).to eq Time.at(1_441_234_742)
200
+ end
201
+ end
202
+
203
+ context "with an invalid user id" do
204
+ it "should return nil" do
205
+ expect(authorizer.get_credentials("notauser")).to be_nil
206
+ end
207
+ end
208
+ end
209
+
210
+ context "when saving tokens" do
211
+ let(:expiry) { Time.now.to_i }
212
+ let :credentials do
213
+ Google::Auth::UserRefreshCredentials.new(
214
+ client_id: client_id.id,
215
+ client_secret: client_id.secret,
216
+ scope: scope,
217
+ refresh_token: "refreshtoken",
218
+ access_token: "accesstoken",
219
+ expires_at: expiry
220
+ )
221
+ end
222
+
223
+ let :token_json do
224
+ authorizer.store_credentials "user1", credentials
225
+ token_store.load "user1"
226
+ end
227
+
228
+ it "should persist in the token store" do
229
+ expect(token_json).to_not be_nil
230
+ end
231
+
232
+ it "should persist the refresh token" do
233
+ expect(MultiJson.load(token_json)["refresh_token"]).to eq "refreshtoken"
234
+ end
235
+
236
+ it "should persist the access token" do
237
+ expect(MultiJson.load(token_json)["access_token"]).to eq "accesstoken"
238
+ end
239
+
240
+ it "should persist the client id" do
241
+ expect(MultiJson.load(token_json)["client_id"]).to eq "testclient"
242
+ end
243
+
244
+ it "should persist the scope" do
245
+ expect(MultiJson.load(token_json)["scope"]).to include("email", "profile")
246
+ end
247
+
248
+ it "should persist the expiry as milliseconds" do
249
+ expected_expiry = expiry * 1000
250
+ expect(MultiJson.load(token_json)["expiration_time_millis"]).to eql(
251
+ expected_expiry
252
+ )
253
+ end
254
+ end
255
+
256
+ context "with valid authorization code" do
257
+ let :token_json do
258
+ MultiJson.dump("access_token" => "1/abc123",
259
+ "token_type" => "Bearer",
260
+ "expires_in" => 3600)
261
+ end
262
+
263
+ before :example do
264
+ stub_request(:post, "https://oauth2.googleapis.com/token")
265
+ .to_return(body: token_json, status: 200, headers: {
266
+ "Content-Type" => "application/json"
267
+ })
268
+ end
269
+
270
+ it "should exchange a code for credentials" do
271
+ credentials = authorizer.get_credentials_from_code(
272
+ user_id: "user1", code: "code"
273
+ )
274
+ expect(credentials.access_token).to eq "1/abc123"
275
+ expect(credentials.redirect_uri.to_s).to eq "https://www.example.com/oauth/callback"
276
+ end
277
+
278
+ it "should not store credentials when get only requested" do
279
+ authorizer.get_credentials_from_code user_id: "user1", code: "code"
280
+ expect(token_store.load("user1")).to be_nil
281
+ end
282
+
283
+ it "should store credentials when requested" do
284
+ authorizer.get_and_store_credentials_from_code(
285
+ user_id: "user1", code: "code"
286
+ )
287
+ expect(token_store.load("user1")).to_not be_nil
288
+ end
289
+ end
290
+
291
+ context "with invalid authorization code" do
292
+ before :example do
293
+ stub_request(:post, "https://oauth2.googleapis.com/token")
294
+ .to_return(status: 400)
295
+ end
296
+
297
+ it "should raise an authorization error" do
298
+ expect do
299
+ authorizer.get_credentials_from_code user_id: "user1", code: "badcode"
300
+ end.to raise_error Signet::AuthorizationError
301
+ end
302
+
303
+ it "should not store credentials when exchange fails" do
304
+ expect do
305
+ authorizer.get_credentials_from_code user_id: "user1", code: "badcode"
306
+ end.to raise_error Signet::AuthorizationError
307
+ expect(token_store.load("user1")).to be_nil
308
+ end
309
+ end
310
+
311
+ context "when reovking authorization" do
312
+ let :token_json do
313
+ MultiJson.dump(
314
+ access_token: "accesstoken",
315
+ refresh_token: "refreshtoken",
316
+ expiration_time_millis: 1_441_234_742_000
317
+ )
318
+ end
319
+
320
+ before :example do
321
+ token_store.store "user1", token_json
322
+ stub_request(:post, "https://oauth2.googleapis.com/revoke")
323
+ .with(body: hash_including("token" => "refreshtoken"))
324
+ .to_return(status: 200)
325
+ end
326
+
327
+ it "should revoke the grant" do
328
+ authorizer.revoke_authorization "user1"
329
+ expect(a_request(
330
+ :post, "https://oauth2.googleapis.com/revoke"
331
+ ).with(body: hash_including("token" => "refreshtoken")))
332
+ .to have_been_made
333
+ end
334
+
335
+ it "should remove the token from storage" do
336
+ authorizer.revoke_authorization "user1"
337
+ expect(token_store.load("user1")).to be_nil
338
+ end
339
+ end
340
+
341
+ # TODO: - Test that tokens are monitored
342
+ # TODO - Test scope enforcement (auth if upgrade required)
343
+ end