googleauth 0.8.0 → 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.kokoro/build.sh +2 -34
- data/.kokoro/continuous/common.cfg +5 -0
- data/.kokoro/continuous/linux.cfg +1 -1
- data/.kokoro/osx.sh +2 -33
- data/.kokoro/presubmit/common.cfg +5 -0
- data/.kokoro/presubmit/linux.cfg +1 -1
- data/.kokoro/release.cfg +53 -0
- data/.kokoro/trampoline.sh +3 -23
- data/.kokoro/windows.sh +2 -30
- data/.rubocop.yml +7 -24
- data/CHANGELOG.md +24 -39
- data/Gemfile +14 -14
- data/README.md +21 -1
- data/Rakefile +84 -10
- data/googleauth.gemspec +23 -23
- data/lib/googleauth.rb +6 -6
- data/lib/googleauth/application_default.rb +11 -11
- data/lib/googleauth/client_id.rb +16 -16
- data/lib/googleauth/compute_engine.rb +27 -27
- data/lib/googleauth/credentials.rb +35 -37
- data/lib/googleauth/credentials_loader.rb +64 -67
- data/lib/googleauth/default_credentials.rb +18 -18
- data/lib/googleauth/iam.rb +9 -9
- data/lib/googleauth/json_key_reader.rb +6 -6
- data/lib/googleauth/scope_util.rb +11 -11
- data/lib/googleauth/service_account.rb +42 -42
- data/lib/googleauth/signet.rb +15 -17
- data/lib/googleauth/stores/file_token_store.rb +8 -8
- data/lib/googleauth/stores/redis_token_store.rb +17 -17
- data/lib/googleauth/token_store.rb +6 -6
- data/lib/googleauth/user_authorizer.rb +55 -59
- data/lib/googleauth/user_refresh.rb +27 -27
- data/lib/googleauth/version.rb +1 -1
- data/lib/googleauth/web_user_authorizer.rb +55 -56
- data/spec/googleauth/apply_auth_examples.rb +46 -46
- data/spec/googleauth/client_id_spec.rb +54 -54
- data/spec/googleauth/compute_engine_spec.rb +41 -41
- data/spec/googleauth/credentials_spec.rb +97 -97
- data/spec/googleauth/get_application_default_spec.rb +114 -114
- data/spec/googleauth/iam_spec.rb +25 -25
- data/spec/googleauth/scope_util_spec.rb +24 -24
- data/spec/googleauth/service_account_spec.rb +204 -194
- data/spec/googleauth/signet_spec.rb +37 -38
- data/spec/googleauth/stores/file_token_store_spec.rb +12 -12
- data/spec/googleauth/stores/redis_token_store_spec.rb +11 -11
- data/spec/googleauth/stores/store_examples.rb +16 -16
- data/spec/googleauth/user_authorizer_spec.rb +120 -121
- data/spec/googleauth/user_refresh_spec.rb +151 -146
- data/spec/googleauth/web_user_authorizer_spec.rb +66 -66
- data/spec/spec_helper.rb +19 -19
- metadata +4 -6
- data/.kokoro/common.cfg +0 -22
- data/.travis.yml +0 -40
data/lib/googleauth/signet.rb
CHANGED
@@ -27,7 +27,7 @@
|
|
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
|
-
require
|
30
|
+
require "signet/oauth_2/client"
|
31
31
|
|
32
32
|
module Signet
|
33
33
|
# OAuth2 supports OAuth2 authentication.
|
@@ -38,24 +38,24 @@ module Signet
|
|
38
38
|
# This reopens Client to add #apply and #apply! methods which update a
|
39
39
|
# hash with the fetched authentication token.
|
40
40
|
class Client
|
41
|
-
def configure_connection
|
41
|
+
def configure_connection options
|
42
42
|
@connection_info =
|
43
43
|
options[:connection_builder] || options[:default_connection]
|
44
44
|
self
|
45
45
|
end
|
46
46
|
|
47
47
|
# Updates a_hash updated with the authentication token
|
48
|
-
def apply!
|
48
|
+
def apply! a_hash, opts = {}
|
49
49
|
# fetch the access token there is currently not one, or if the client
|
50
50
|
# has expired
|
51
|
-
fetch_access_token!
|
51
|
+
fetch_access_token! opts if access_token.nil? || expires_within?(60)
|
52
52
|
a_hash[AUTH_METADATA_KEY] = "Bearer #{access_token}"
|
53
53
|
end
|
54
54
|
|
55
55
|
# Returns a clone of a_hash updated with the authentication token
|
56
|
-
def apply
|
56
|
+
def apply a_hash, opts = {}
|
57
57
|
a_copy = a_hash.clone
|
58
|
-
apply!
|
58
|
+
apply! a_copy, opts
|
59
59
|
a_copy
|
60
60
|
end
|
61
61
|
|
@@ -65,18 +65,18 @@ module Signet
|
|
65
65
|
lambda(&method(:apply))
|
66
66
|
end
|
67
67
|
|
68
|
-
def on_refresh
|
68
|
+
def on_refresh &block
|
69
69
|
@refresh_listeners ||= []
|
70
70
|
@refresh_listeners << block
|
71
71
|
end
|
72
72
|
|
73
73
|
alias orig_fetch_access_token! fetch_access_token!
|
74
|
-
def fetch_access_token!
|
74
|
+
def fetch_access_token! options = {}
|
75
75
|
unless options[:connection]
|
76
76
|
connection = build_default_connection
|
77
|
-
options = options.merge
|
77
|
+
options = options.merge connection: connection if connection
|
78
78
|
end
|
79
|
-
info = orig_fetch_access_token!
|
79
|
+
info = orig_fetch_access_token! options
|
80
80
|
notify_refresh_listeners
|
81
81
|
info
|
82
82
|
end
|
@@ -84,7 +84,7 @@ module Signet
|
|
84
84
|
def notify_refresh_listeners
|
85
85
|
listeners = @refresh_listeners || []
|
86
86
|
listeners.each do |block|
|
87
|
-
block.call
|
87
|
+
block.call self
|
88
88
|
end
|
89
89
|
end
|
90
90
|
|
@@ -98,15 +98,13 @@ module Signet
|
|
98
98
|
end
|
99
99
|
end
|
100
100
|
|
101
|
-
def retry_with_error
|
101
|
+
def retry_with_error max_retry_count = 5
|
102
102
|
retry_count = 0
|
103
103
|
|
104
104
|
begin
|
105
105
|
yield
|
106
|
-
rescue => e
|
107
|
-
if e.is_a?(Signet::AuthorizationError) || e.is_a?(Signet::ParseError)
|
108
|
-
raise e
|
109
|
-
end
|
106
|
+
rescue StandardError => e
|
107
|
+
raise e if e.is_a?(Signet::AuthorizationError) || e.is_a?(Signet::ParseError)
|
110
108
|
|
111
109
|
if retry_count < max_retry_count
|
112
110
|
retry_count += 1
|
@@ -114,7 +112,7 @@ module Signet
|
|
114
112
|
retry
|
115
113
|
else
|
116
114
|
msg = "Unexpected error: #{e.inspect}"
|
117
|
-
raise
|
115
|
+
raise Signet::AuthorizationError, msg
|
118
116
|
end
|
119
117
|
end
|
120
118
|
end
|
@@ -27,8 +27,8 @@
|
|
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
|
-
require
|
31
|
-
require
|
30
|
+
require "yaml/store"
|
31
|
+
require "googleauth/token_store"
|
32
32
|
|
33
33
|
module Google
|
34
34
|
module Auth
|
@@ -39,24 +39,24 @@ module Google
|
|
39
39
|
#
|
40
40
|
# @param [String, File] file
|
41
41
|
# Path to storage file
|
42
|
-
def initialize
|
42
|
+
def initialize options = {}
|
43
43
|
path = options[:file]
|
44
|
-
@store = YAML::Store.new
|
44
|
+
@store = YAML::Store.new path
|
45
45
|
end
|
46
46
|
|
47
47
|
# (see Google::Auth::Stores::TokenStore#load)
|
48
|
-
def load
|
48
|
+
def load id
|
49
49
|
@store.transaction { @store[id] }
|
50
50
|
end
|
51
51
|
|
52
52
|
# (see Google::Auth::Stores::TokenStore#store)
|
53
|
-
def store
|
53
|
+
def store id, token
|
54
54
|
@store.transaction { @store[id] = token }
|
55
55
|
end
|
56
56
|
|
57
57
|
# (see Google::Auth::Stores::TokenStore#delete)
|
58
|
-
def delete
|
59
|
-
@store.transaction { @store.delete
|
58
|
+
def delete id
|
59
|
+
@store.transaction { @store.delete id }
|
60
60
|
end
|
61
61
|
end
|
62
62
|
end
|
@@ -27,8 +27,8 @@
|
|
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
|
-
require
|
31
|
-
require
|
30
|
+
require "redis"
|
31
|
+
require "googleauth/token_store"
|
32
32
|
|
33
33
|
module Google
|
34
34
|
module Auth
|
@@ -37,7 +37,7 @@ module Google
|
|
37
37
|
# are stored as JSON using the supplied key, prefixed with
|
38
38
|
# `g-user-token:`
|
39
39
|
class RedisTokenStore < Google::Auth::TokenStore
|
40
|
-
DEFAULT_KEY_PREFIX =
|
40
|
+
DEFAULT_KEY_PREFIX = "g-user-token:".freeze
|
41
41
|
|
42
42
|
# Create a new store with the supplied redis client.
|
43
43
|
#
|
@@ -48,34 +48,34 @@ module Google
|
|
48
48
|
# @note If no redis instance is provided, a new one is created and
|
49
49
|
# the options passed through. You may include any other keys accepted
|
50
50
|
# by `Redis.new`
|
51
|
-
def initialize
|
52
|
-
redis = options.delete
|
53
|
-
prefix = options.delete
|
51
|
+
def initialize options = {}
|
52
|
+
redis = options.delete :redis
|
53
|
+
prefix = options.delete :prefix
|
54
54
|
@redis = case redis
|
55
55
|
when Redis
|
56
56
|
redis
|
57
57
|
else
|
58
|
-
Redis.new
|
58
|
+
Redis.new options
|
59
59
|
end
|
60
60
|
@prefix = prefix || DEFAULT_KEY_PREFIX
|
61
61
|
end
|
62
62
|
|
63
63
|
# (see Google::Auth::Stores::TokenStore#load)
|
64
|
-
def load
|
65
|
-
key = key_for
|
66
|
-
@redis.get
|
64
|
+
def load id
|
65
|
+
key = key_for id
|
66
|
+
@redis.get key
|
67
67
|
end
|
68
68
|
|
69
69
|
# (see Google::Auth::Stores::TokenStore#store)
|
70
|
-
def store
|
71
|
-
key = key_for
|
72
|
-
@redis.set
|
70
|
+
def store id, token
|
71
|
+
key = key_for id
|
72
|
+
@redis.set key, token
|
73
73
|
end
|
74
74
|
|
75
75
|
# (see Google::Auth::Stores::TokenStore#delete)
|
76
|
-
def delete
|
77
|
-
key = key_for
|
78
|
-
@redis.del
|
76
|
+
def delete id
|
77
|
+
key = key_for id
|
78
|
+
@redis.del key
|
79
79
|
end
|
80
80
|
|
81
81
|
private
|
@@ -86,7 +86,7 @@ module Google
|
|
86
86
|
# ID of the token
|
87
87
|
# @return [String]
|
88
88
|
# Redis key
|
89
|
-
def key_for
|
89
|
+
def key_for id
|
90
90
|
@prefix + id
|
91
91
|
end
|
92
92
|
end
|
@@ -43,8 +43,8 @@ module Google
|
|
43
43
|
# ID of token data to load.
|
44
44
|
# @return [String]
|
45
45
|
# The loaded token data.
|
46
|
-
def load
|
47
|
-
raise
|
46
|
+
def load _id
|
47
|
+
raise "Not implemented"
|
48
48
|
end
|
49
49
|
|
50
50
|
# Put the token data into storage for the given ID.
|
@@ -53,16 +53,16 @@ module Google
|
|
53
53
|
# ID of token data to store.
|
54
54
|
# @param [String] token
|
55
55
|
# The token data to store.
|
56
|
-
def store
|
57
|
-
raise
|
56
|
+
def store _id, _token
|
57
|
+
raise "Not implemented"
|
58
58
|
end
|
59
59
|
|
60
60
|
# Remove the token data from storage for the given ID.
|
61
61
|
#
|
62
62
|
# @param [String] id
|
63
63
|
# ID of the token data to delete
|
64
|
-
def delete
|
65
|
-
raise
|
64
|
+
def delete _id
|
65
|
+
raise "Not implemented"
|
66
66
|
end
|
67
67
|
end
|
68
68
|
end
|
@@ -27,10 +27,10 @@
|
|
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
|
-
require
|
31
|
-
require
|
32
|
-
require
|
33
|
-
require
|
30
|
+
require "uri"
|
31
|
+
require "multi_json"
|
32
|
+
require "googleauth/signet"
|
33
|
+
require "googleauth/user_refresh"
|
34
34
|
|
35
35
|
module Google
|
36
36
|
module Auth
|
@@ -53,11 +53,11 @@ module Google
|
|
53
53
|
# ...
|
54
54
|
class UserAuthorizer
|
55
55
|
MISMATCHED_CLIENT_ID_ERROR =
|
56
|
-
|
57
|
-
NIL_CLIENT_ID_ERROR =
|
58
|
-
NIL_SCOPE_ERROR =
|
59
|
-
NIL_USER_ID_ERROR =
|
60
|
-
NIL_TOKEN_STORE_ERROR =
|
56
|
+
"Token client ID of %s does not match configured client id %s".freeze
|
57
|
+
NIL_CLIENT_ID_ERROR = "Client id can not be nil.".freeze
|
58
|
+
NIL_SCOPE_ERROR = "Scope can not be nil.".freeze
|
59
|
+
NIL_USER_ID_ERROR = "User ID can not be nil.".freeze
|
60
|
+
NIL_TOKEN_STORE_ERROR = "Can not call method if token store is nil".freeze
|
61
61
|
MISSING_ABSOLUTE_URL_ERROR =
|
62
62
|
'Absolute base url required for relative callback url "%s"'.freeze
|
63
63
|
|
@@ -72,14 +72,14 @@ module Google
|
|
72
72
|
# @param [String] callback_uri
|
73
73
|
# URL (either absolute or relative) of the auth callback.
|
74
74
|
# Defaults to '/oauth2callback'
|
75
|
-
def initialize
|
75
|
+
def initialize client_id, scope, token_store, callback_uri = nil
|
76
76
|
raise NIL_CLIENT_ID_ERROR if client_id.nil?
|
77
77
|
raise NIL_SCOPE_ERROR if scope.nil?
|
78
78
|
|
79
79
|
@client_id = client_id
|
80
80
|
@scope = Array(scope)
|
81
81
|
@token_store = token_store
|
82
|
-
@callback_uri = callback_uri ||
|
82
|
+
@callback_uri = callback_uri || "/oauth2callback"
|
83
83
|
end
|
84
84
|
|
85
85
|
# Build the URL for requesting authorization.
|
@@ -97,20 +97,20 @@ module Google
|
|
97
97
|
# nil.
|
98
98
|
# @return [String]
|
99
99
|
# Authorization url
|
100
|
-
def get_authorization_url
|
100
|
+
def get_authorization_url options = {}
|
101
101
|
scope = options[:scope] || @scope
|
102
102
|
credentials = UserRefreshCredentials.new(
|
103
|
-
client_id:
|
103
|
+
client_id: @client_id.id,
|
104
104
|
client_secret: @client_id.secret,
|
105
|
-
scope:
|
105
|
+
scope: scope
|
106
106
|
)
|
107
|
-
redirect_uri = redirect_uri_for
|
108
|
-
url = credentials.authorization_uri(access_type:
|
109
|
-
redirect_uri:
|
110
|
-
approval_prompt:
|
111
|
-
state:
|
107
|
+
redirect_uri = redirect_uri_for options[:base_url]
|
108
|
+
url = credentials.authorization_uri(access_type: "offline",
|
109
|
+
redirect_uri: redirect_uri,
|
110
|
+
approval_prompt: "force",
|
111
|
+
state: options[:state],
|
112
112
|
include_granted_scopes: true,
|
113
|
-
login_hint:
|
113
|
+
login_hint: options[:login_hint])
|
114
114
|
url.to_s
|
115
115
|
end
|
116
116
|
|
@@ -123,28 +123,26 @@ module Google
|
|
123
123
|
# the requested scopes
|
124
124
|
# @return [Google::Auth::UserRefreshCredentials]
|
125
125
|
# Stored credentials, nil if none present
|
126
|
-
def get_credentials
|
127
|
-
saved_token = stored_token
|
126
|
+
def get_credentials user_id, scope = nil
|
127
|
+
saved_token = stored_token user_id
|
128
128
|
return nil if saved_token.nil?
|
129
|
-
data = MultiJson.load
|
129
|
+
data = MultiJson.load saved_token
|
130
130
|
|
131
|
-
if data.fetch(
|
132
|
-
raise
|
133
|
-
|
131
|
+
if data.fetch("client_id", @client_id.id) != @client_id.id
|
132
|
+
raise format(MISMATCHED_CLIENT_ID_ERROR,
|
133
|
+
data["client_id"], @client_id.id)
|
134
134
|
end
|
135
135
|
|
136
136
|
credentials = UserRefreshCredentials.new(
|
137
|
-
client_id:
|
137
|
+
client_id: @client_id.id,
|
138
138
|
client_secret: @client_id.secret,
|
139
|
-
scope:
|
140
|
-
access_token:
|
141
|
-
refresh_token: data[
|
142
|
-
expires_at:
|
139
|
+
scope: data["scope"] || @scope,
|
140
|
+
access_token: data["access_token"],
|
141
|
+
refresh_token: data["refresh_token"],
|
142
|
+
expires_at: data.fetch("expiration_time_millis", 0) / 1000
|
143
143
|
)
|
144
144
|
scope ||= @scope
|
145
|
-
if credentials.includes_scope?
|
146
|
-
return monitor_credentials(user_id, credentials)
|
147
|
-
end
|
145
|
+
return monitor_credentials user_id, credentials if credentials.includes_scope? scope
|
148
146
|
nil
|
149
147
|
end
|
150
148
|
|
@@ -163,20 +161,20 @@ module Google
|
|
163
161
|
# callback uri is a relative.
|
164
162
|
# @return [Google::Auth::UserRefreshCredentials]
|
165
163
|
# Credentials if exchange is successful
|
166
|
-
def get_credentials_from_code
|
164
|
+
def get_credentials_from_code options = {}
|
167
165
|
user_id = options[:user_id]
|
168
166
|
code = options[:code]
|
169
167
|
scope = options[:scope] || @scope
|
170
168
|
base_url = options[:base_url]
|
171
169
|
credentials = UserRefreshCredentials.new(
|
172
|
-
client_id:
|
170
|
+
client_id: @client_id.id,
|
173
171
|
client_secret: @client_id.secret,
|
174
|
-
redirect_uri:
|
175
|
-
scope:
|
172
|
+
redirect_uri: redirect_uri_for(base_url),
|
173
|
+
scope: scope
|
176
174
|
)
|
177
175
|
credentials.code = code
|
178
176
|
credentials.fetch_access_token!({})
|
179
|
-
monitor_credentials
|
177
|
+
monitor_credentials user_id, credentials
|
180
178
|
end
|
181
179
|
|
182
180
|
# Exchanges an authorization code returned in the oauth callback.
|
@@ -196,9 +194,9 @@ module Google
|
|
196
194
|
# callback uri is a relative.
|
197
195
|
# @return [Google::Auth::UserRefreshCredentials]
|
198
196
|
# Credentials if exchange is successful
|
199
|
-
def get_and_store_credentials_from_code
|
200
|
-
credentials = get_credentials_from_code
|
201
|
-
store_credentials
|
197
|
+
def get_and_store_credentials_from_code options = {}
|
198
|
+
credentials = get_credentials_from_code options
|
199
|
+
store_credentials options[:user_id], credentials
|
202
200
|
end
|
203
201
|
|
204
202
|
# Revokes a user's credentials. This both revokes the actual
|
@@ -206,11 +204,11 @@ module Google
|
|
206
204
|
#
|
207
205
|
# @param [String] user_id
|
208
206
|
# Unique ID of the user for loading/storing credentials.
|
209
|
-
def revoke_authorization
|
210
|
-
credentials = get_credentials
|
207
|
+
def revoke_authorization user_id
|
208
|
+
credentials = get_credentials user_id
|
211
209
|
if credentials
|
212
210
|
begin
|
213
|
-
@token_store.delete
|
211
|
+
@token_store.delete user_id
|
214
212
|
ensure
|
215
213
|
credentials.revoke!
|
216
214
|
end
|
@@ -226,15 +224,15 @@ module Google
|
|
226
224
|
# Unique ID of the user for loading/storing credentials.
|
227
225
|
# @param [Google::Auth::UserRefreshCredentials] credentials
|
228
226
|
# Credentials to store.
|
229
|
-
def store_credentials
|
227
|
+
def store_credentials user_id, credentials
|
230
228
|
json = MultiJson.dump(
|
231
|
-
client_id:
|
232
|
-
access_token:
|
233
|
-
refresh_token:
|
234
|
-
scope:
|
229
|
+
client_id: credentials.client_id,
|
230
|
+
access_token: credentials.access_token,
|
231
|
+
refresh_token: credentials.refresh_token,
|
232
|
+
scope: credentials.scope,
|
235
233
|
expiration_time_millis: credentials.expires_at.to_i * 1000
|
236
234
|
)
|
237
|
-
@token_store.store
|
235
|
+
@token_store.store user_id, json
|
238
236
|
credentials
|
239
237
|
end
|
240
238
|
|
@@ -245,11 +243,11 @@ module Google
|
|
245
243
|
# @param [String] user_id
|
246
244
|
# Unique ID of the user for loading/storing credentials.
|
247
245
|
# @return [String] The saved token from @token_store
|
248
|
-
def stored_token
|
246
|
+
def stored_token user_id
|
249
247
|
raise NIL_USER_ID_ERROR if user_id.nil?
|
250
248
|
raise NIL_TOKEN_STORE_ERROR if @token_store.nil?
|
251
249
|
|
252
|
-
@token_store.load
|
250
|
+
@token_store.load user_id
|
253
251
|
end
|
254
252
|
|
255
253
|
# Begin watching a credential for refreshes so the access token can be
|
@@ -259,9 +257,9 @@ module Google
|
|
259
257
|
# Unique ID of the user for loading/storing credentials.
|
260
258
|
# @param [Google::Auth::UserRefreshCredentials] credentials
|
261
259
|
# Credentials to store.
|
262
|
-
def monitor_credentials
|
260
|
+
def monitor_credentials user_id, credentials
|
263
261
|
credentials.on_refresh do |cred|
|
264
|
-
store_credentials
|
262
|
+
store_credentials user_id, cred
|
265
263
|
end
|
266
264
|
credentials
|
267
265
|
end
|
@@ -272,11 +270,9 @@ module Google
|
|
272
270
|
# Absolute URL to resolve the callback against if necessary.
|
273
271
|
# @return [String]
|
274
272
|
# Redirect URI
|
275
|
-
def redirect_uri_for
|
273
|
+
def redirect_uri_for base_url
|
276
274
|
return @callback_uri unless URI(@callback_uri).scheme.nil?
|
277
|
-
if base_url.nil? || URI(base_url).scheme.nil?
|
278
|
-
raise sprintf(MISSING_ABSOLUTE_URL_ERROR, @callback_uri)
|
279
|
-
end
|
275
|
+
raise format(MISSING_ABSOLUTE_URL_ERROR, @callback_uri) if base_url.nil? || URI(base_url).scheme.nil?
|
280
276
|
URI.join(base_url, @callback_uri).to_s
|
281
277
|
end
|
282
278
|
end
|