googleauth 0.1.0 → 0.16.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) 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/workflows/ci.yml +55 -0
  8. data/.github/workflows/release-please.yml +39 -0
  9. data/.gitignore +3 -0
  10. data/.kokoro/populate-secrets.sh +76 -0
  11. data/.kokoro/release.cfg +52 -0
  12. data/.kokoro/release.sh +18 -0
  13. data/.kokoro/trampoline_v2.sh +489 -0
  14. data/.repo-metadata.json +5 -0
  15. data/.rubocop.yml +17 -0
  16. data/.toys/.toys.rb +45 -0
  17. data/.toys/ci.rb +43 -0
  18. data/.toys/kokoro/.toys.rb +66 -0
  19. data/.toys/kokoro/publish-docs.rb +67 -0
  20. data/.toys/kokoro/publish-gem.rb +53 -0
  21. data/.toys/linkinator.rb +43 -0
  22. data/.trampolinerc +48 -0
  23. data/CHANGELOG.md +192 -0
  24. data/CODE_OF_CONDUCT.md +43 -0
  25. data/Gemfile +22 -1
  26. data/{COPYING → LICENSE} +0 -0
  27. data/README.md +140 -17
  28. data/googleauth.gemspec +28 -28
  29. data/integration/helper.rb +31 -0
  30. data/integration/id_tokens/key_source_test.rb +74 -0
  31. data/lib/googleauth.rb +7 -37
  32. data/lib/googleauth/application_default.rb +81 -0
  33. data/lib/googleauth/client_id.rb +104 -0
  34. data/lib/googleauth/compute_engine.rb +73 -26
  35. data/lib/googleauth/credentials.rb +561 -0
  36. data/lib/googleauth/credentials_loader.rb +207 -0
  37. data/lib/googleauth/default_credentials.rb +93 -0
  38. data/lib/googleauth/iam.rb +75 -0
  39. data/lib/googleauth/id_tokens.rb +233 -0
  40. data/lib/googleauth/id_tokens/errors.rb +71 -0
  41. data/lib/googleauth/id_tokens/key_sources.rb +396 -0
  42. data/lib/googleauth/id_tokens/verifier.rb +142 -0
  43. data/lib/googleauth/json_key_reader.rb +50 -0
  44. data/lib/googleauth/scope_util.rb +61 -0
  45. data/lib/googleauth/service_account.rb +175 -67
  46. data/lib/googleauth/signet.rb +69 -8
  47. data/lib/googleauth/stores/file_token_store.rb +65 -0
  48. data/lib/googleauth/stores/redis_token_store.rb +96 -0
  49. data/lib/googleauth/token_store.rb +69 -0
  50. data/lib/googleauth/user_authorizer.rb +285 -0
  51. data/lib/googleauth/user_refresh.rb +129 -0
  52. data/lib/googleauth/version.rb +1 -1
  53. data/lib/googleauth/web_user_authorizer.rb +295 -0
  54. data/spec/googleauth/apply_auth_examples.rb +96 -94
  55. data/spec/googleauth/client_id_spec.rb +160 -0
  56. data/spec/googleauth/compute_engine_spec.rb +125 -55
  57. data/spec/googleauth/credentials_spec.rb +600 -0
  58. data/spec/googleauth/get_application_default_spec.rb +232 -80
  59. data/spec/googleauth/iam_spec.rb +80 -0
  60. data/spec/googleauth/scope_util_spec.rb +77 -0
  61. data/spec/googleauth/service_account_spec.rb +422 -68
  62. data/spec/googleauth/signet_spec.rb +101 -25
  63. data/spec/googleauth/stores/file_token_store_spec.rb +57 -0
  64. data/spec/googleauth/stores/redis_token_store_spec.rb +50 -0
  65. data/spec/googleauth/stores/store_examples.rb +58 -0
  66. data/spec/googleauth/user_authorizer_spec.rb +343 -0
  67. data/spec/googleauth/user_refresh_spec.rb +359 -0
  68. data/spec/googleauth/web_user_authorizer_spec.rb +172 -0
  69. data/spec/spec_helper.rb +51 -10
  70. data/test/helper.rb +33 -0
  71. data/test/id_tokens/key_sources_test.rb +240 -0
  72. data/test/id_tokens/verifier_test.rb +269 -0
  73. metadata +112 -75
  74. data/.travis.yml +0 -18
  75. data/CONTRIBUTING.md +0 -32
  76. 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