commit
1e9244689e
|
@ -1,15 +1,31 @@
|
||||||
# This configuration was generated by `rubocop --auto-gen-config`
|
# This configuration was generated by
|
||||||
# on 2015-05-18 09:38:28 -0700 using RuboCop version 0.31.0.
|
# `rubocop --auto-gen-config`
|
||||||
|
# on 2015-10-14 13:50:41 -0700 using RuboCop version 0.34.2.
|
||||||
# The point is for the user to remove these configuration records
|
# The point is for the user to remove these configuration records
|
||||||
# one by one as the offenses are removed from the code base.
|
# one by one as the offenses are removed from the code base.
|
||||||
# Note that changes in the inspected code, or installation of new
|
# Note that changes in the inspected code, or installation of new
|
||||||
# versions of RuboCop, may require this file to be generated again.
|
# versions of RuboCop, may require this file to be generated again.
|
||||||
|
|
||||||
# Offense count: 3
|
# Offense count: 4
|
||||||
Metrics/AbcSize:
|
Metrics/AbcSize:
|
||||||
Max: 24
|
Max: 27
|
||||||
|
|
||||||
# Offense count: 10
|
# Offense count: 1
|
||||||
|
# Configuration parameters: CountComments.
|
||||||
|
Metrics/ClassLength:
|
||||||
|
Max: 109
|
||||||
|
|
||||||
|
# Offense count: 1
|
||||||
|
Metrics/CyclomaticComplexity:
|
||||||
|
Max: 7
|
||||||
|
|
||||||
|
# Offense count: 16
|
||||||
# Configuration parameters: CountComments.
|
# Configuration parameters: CountComments.
|
||||||
Metrics/MethodLength:
|
Metrics/MethodLength:
|
||||||
Max: 13
|
Max: 22
|
||||||
|
|
||||||
|
# Offense count: 2
|
||||||
|
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||||
|
Style/FormatString:
|
||||||
|
Exclude:
|
||||||
|
- 'lib/googleauth/user_authorizer.rb'
|
||||||
|
|
|
@ -7,6 +7,9 @@ rvm:
|
||||||
- 1.9.3
|
- 1.9.3
|
||||||
- rbx-2
|
- rbx-2
|
||||||
- jruby
|
- jruby
|
||||||
|
matrix:
|
||||||
|
allow_failures:
|
||||||
|
- rbx-2 # See rubinius/rubinius#3485 - rubocop segfaults
|
||||||
script: "bundle exec rake"
|
script: "bundle exec rake"
|
||||||
addons:
|
addons:
|
||||||
apt:
|
apt:
|
||||||
|
|
18
Gemfile
18
Gemfile
|
@ -2,3 +2,21 @@ source 'https://rubygems.org'
|
||||||
|
|
||||||
# Specify your gem's dependencies in googleauth.gemspec
|
# Specify your gem's dependencies in googleauth.gemspec
|
||||||
gemspec
|
gemspec
|
||||||
|
|
||||||
|
group :development do
|
||||||
|
gem 'bundler', '~> 1.9'
|
||||||
|
gem 'simplecov', '~> 0.9'
|
||||||
|
gem 'coveralls', '~> 0.7'
|
||||||
|
gem 'fakefs', '~> 0.6'
|
||||||
|
gem 'rake', '~> 10.0'
|
||||||
|
gem 'rubocop', '~> 0.30'
|
||||||
|
gem 'rspec', '~> 3.0'
|
||||||
|
gem 'redis', '~> 3.2'
|
||||||
|
gem 'fakeredis', '~> 0.5'
|
||||||
|
gem 'webmock', '~> 1.21'
|
||||||
|
end
|
||||||
|
|
||||||
|
platforms :jruby do
|
||||||
|
group :development do
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -31,12 +31,4 @@ Gem::Specification.new do |s|
|
||||||
s.add_dependency 'memoist', '~> 0.12'
|
s.add_dependency 'memoist', '~> 0.12'
|
||||||
s.add_dependency 'multi_json', '~> 1.11'
|
s.add_dependency 'multi_json', '~> 1.11'
|
||||||
s.add_dependency 'signet', '~> 0.6'
|
s.add_dependency 'signet', '~> 0.6'
|
||||||
|
|
||||||
s.add_development_dependency 'bundler', '~> 1.9'
|
|
||||||
s.add_development_dependency 'simplecov', '~> 0.9'
|
|
||||||
s.add_development_dependency 'coveralls', '~> 0.7'
|
|
||||||
s.add_development_dependency 'fakefs', '~> 0.6'
|
|
||||||
s.add_development_dependency 'rake', '~> 10.0'
|
|
||||||
s.add_development_dependency 'rubocop', '~> 0.30'
|
|
||||||
s.add_development_dependency 'rspec', '~> 3.0'
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -34,6 +34,8 @@ require 'googleauth/credentials_loader'
|
||||||
require 'googleauth/compute_engine'
|
require 'googleauth/compute_engine'
|
||||||
require 'googleauth/service_account'
|
require 'googleauth/service_account'
|
||||||
require 'googleauth/user_refresh'
|
require 'googleauth/user_refresh'
|
||||||
|
require 'googleauth/client_id'
|
||||||
|
require 'googleauth/user_authorizer'
|
||||||
|
|
||||||
module Google
|
module Google
|
||||||
# Module Auth provides classes that provide Google-specific authorization
|
# Module Auth provides classes that provide Google-specific authorization
|
||||||
|
@ -56,11 +58,11 @@ END
|
||||||
json_key_io, scope = options.values_at(:json_key_io, :scope)
|
json_key_io, scope = options.values_at(:json_key_io, :scope)
|
||||||
if json_key_io
|
if json_key_io
|
||||||
json_key, clz = determine_creds_class(json_key_io)
|
json_key, clz = determine_creds_class(json_key_io)
|
||||||
clz.new(json_key_io: StringIO.new(MultiJson.dump(json_key)),
|
clz.make_creds(json_key_io: StringIO.new(MultiJson.dump(json_key)),
|
||||||
scope: scope)
|
scope: scope)
|
||||||
else
|
else
|
||||||
clz = read_creds
|
clz = read_creds
|
||||||
clz.new(scope: scope)
|
clz.make_creds(scope: scope)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
# Copyright 2014, Google Inc.
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are
|
||||||
|
# met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above
|
||||||
|
# copyright notice, this list of conditions and the following disclaimer
|
||||||
|
# in the documentation and/or other materials provided with the
|
||||||
|
# distribution.
|
||||||
|
# * Neither the name of Google Inc. nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from
|
||||||
|
# this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
require 'multi_json'
|
||||||
|
|
||||||
|
module Google
|
||||||
|
module Auth
|
||||||
|
# Representation of an application's identity for user authorization
|
||||||
|
# flows.
|
||||||
|
class ClientId
|
||||||
|
INSTALLED_APP = 'installed'
|
||||||
|
WEB_APP = 'web'
|
||||||
|
CLIENT_ID = 'client_id'
|
||||||
|
CLIENT_SECRET = 'client_secret'
|
||||||
|
MISSING_TOP_LEVEL_ELEMENT_ERROR =
|
||||||
|
"Expected top level property 'installed' or 'web' to be present."
|
||||||
|
|
||||||
|
# Text identifier of the client ID
|
||||||
|
# @return [String]
|
||||||
|
attr_reader :id
|
||||||
|
|
||||||
|
# Secret associated with the client ID
|
||||||
|
# @return [String]
|
||||||
|
attr_reader :secret
|
||||||
|
|
||||||
|
class << self
|
||||||
|
attr_accessor :default
|
||||||
|
end
|
||||||
|
|
||||||
|
# Initialize the Client ID
|
||||||
|
#
|
||||||
|
# @param [String] id
|
||||||
|
# Text identifier of the client ID
|
||||||
|
# @param [String] secret
|
||||||
|
# Secret associated with the client ID
|
||||||
|
# @note Direction instantion is discouraged to avoid embedding IDs
|
||||||
|
# & secrets in source. See {#from_file} to load from
|
||||||
|
# `client_secrets.json` files.
|
||||||
|
def initialize(id, secret)
|
||||||
|
fail 'Client id can not be nil' if id.nil?
|
||||||
|
fail 'Client secret can not be nil' if secret.nil?
|
||||||
|
@id = id
|
||||||
|
@secret = secret
|
||||||
|
end
|
||||||
|
|
||||||
|
# Constructs a Client ID from a JSON file downloaed from the
|
||||||
|
# Google Developers Console.
|
||||||
|
#
|
||||||
|
# @param [String, File] file
|
||||||
|
# Path of file to read from
|
||||||
|
# @return [Google::Auth::ClientID]
|
||||||
|
def self.from_file(file)
|
||||||
|
fail 'File can not be nil.' if file.nil?
|
||||||
|
File.open(file.to_s) do |f|
|
||||||
|
json = f.read
|
||||||
|
config = MultiJson.load(json)
|
||||||
|
from_hash(config)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Constructs a Client ID from a previously loaded JSON file. The hash
|
||||||
|
# structure should
|
||||||
|
# match the expected JSON format.
|
||||||
|
#
|
||||||
|
# @param [hash] config
|
||||||
|
# Parsed contents of the JSON file
|
||||||
|
# @return [Google::Auth::ClientID]
|
||||||
|
def self.from_hash(config)
|
||||||
|
fail 'Hash can not be nil.' if config.nil?
|
||||||
|
raw_detail = config[INSTALLED_APP] || config[WEB_APP]
|
||||||
|
fail MISSING_TOP_LEVEL_ELEMENT_ERROR if raw_detail.nil?
|
||||||
|
ClientId.new(raw_detail[CLIENT_ID], raw_detail[CLIENT_SECRET])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,61 @@
|
||||||
|
# Copyright 2015, Google Inc.
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are
|
||||||
|
# met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above
|
||||||
|
# copyright notice, this list of conditions and the following disclaimer
|
||||||
|
# in the documentation and/or other materials provided with the
|
||||||
|
# distribution.
|
||||||
|
# * Neither the name of Google Inc. nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from
|
||||||
|
# this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
require 'googleauth/signet'
|
||||||
|
require 'googleauth/credentials_loader'
|
||||||
|
require 'multi_json'
|
||||||
|
|
||||||
|
module Google
|
||||||
|
module Auth
|
||||||
|
# Small utility for normalizing scopes into canonical form
|
||||||
|
module ScopeUtil
|
||||||
|
ALIASES = {
|
||||||
|
'email' => 'https://www.googleapis.com/auth/userinfo.email',
|
||||||
|
'profile' => 'https://www.googleapis.com/auth/userinfo.profile',
|
||||||
|
'openid' => 'https://www.googleapis.com/auth/plus.me'
|
||||||
|
}
|
||||||
|
|
||||||
|
def self.normalize(scope)
|
||||||
|
list = as_array(scope)
|
||||||
|
list.map { |item| ALIASES[item] || item }
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.as_array(scope)
|
||||||
|
case scope
|
||||||
|
when Array
|
||||||
|
scope
|
||||||
|
when String
|
||||||
|
scope.split(' ')
|
||||||
|
else
|
||||||
|
fail 'Invalid scope value. Must be string or array'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -49,6 +49,26 @@ module Google
|
||||||
TOKEN_CRED_URI = 'https://www.googleapis.com/oauth2/v3/token'
|
TOKEN_CRED_URI = 'https://www.googleapis.com/oauth2/v3/token'
|
||||||
extend CredentialsLoader
|
extend CredentialsLoader
|
||||||
|
|
||||||
|
# Creates a ServiceAccountCredentials.
|
||||||
|
#
|
||||||
|
# @param json_key_io [IO] an IO from which the JSON key can be read
|
||||||
|
# @param scope [string|array|nil] the scope(s) to access
|
||||||
|
def self.make_creds(options = {})
|
||||||
|
json_key_io, scope = options.values_at(:json_key_io, :scope)
|
||||||
|
if json_key_io
|
||||||
|
private_key, client_email = read_json_key(json_key_io)
|
||||||
|
else
|
||||||
|
private_key = ENV[CredentialsLoader::PRIVATE_KEY_VAR]
|
||||||
|
client_email = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
|
||||||
|
end
|
||||||
|
|
||||||
|
new(token_credential_uri: TOKEN_CRED_URI,
|
||||||
|
audience: TOKEN_CRED_URI,
|
||||||
|
scope: scope,
|
||||||
|
issuer: client_email,
|
||||||
|
signing_key: OpenSSL::PKey::RSA.new(private_key))
|
||||||
|
end
|
||||||
|
|
||||||
# Reads the private key and client email fields from the service account
|
# Reads the private key and client email fields from the service account
|
||||||
# JSON key.
|
# JSON key.
|
||||||
def self.read_json_key(json_key_io)
|
def self.read_json_key(json_key_io)
|
||||||
|
@ -58,24 +78,8 @@ module Google
|
||||||
[json_key['private_key'], json_key['client_email']]
|
[json_key['private_key'], json_key['client_email']]
|
||||||
end
|
end
|
||||||
|
|
||||||
# Initializes a ServiceAccountCredentials.
|
|
||||||
#
|
|
||||||
# @param json_key_io [IO] an IO from which the JSON key can be read
|
|
||||||
# @param scope [string|array|nil] the scope(s) to access
|
|
||||||
def initialize(options = {})
|
def initialize(options = {})
|
||||||
json_key_io, scope = options.values_at(:json_key_io, :scope)
|
super(options)
|
||||||
if json_key_io
|
|
||||||
private_key, client_email = self.class.read_json_key(json_key_io)
|
|
||||||
else
|
|
||||||
private_key = ENV[CredentialsLoader::PRIVATE_KEY_VAR]
|
|
||||||
client_email = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
|
|
||||||
end
|
|
||||||
|
|
||||||
super(token_credential_uri: TOKEN_CRED_URI,
|
|
||||||
audience: TOKEN_CRED_URI,
|
|
||||||
scope: scope,
|
|
||||||
issuer: client_email,
|
|
||||||
signing_key: OpenSSL::PKey::RSA.new(private_key))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Extends the base class.
|
# Extends the base class.
|
||||||
|
@ -97,7 +101,8 @@ module Google
|
||||||
client_email: @issuer
|
client_email: @issuer
|
||||||
}
|
}
|
||||||
alt_clz = ServiceAccountJwtHeaderCredentials
|
alt_clz = ServiceAccountJwtHeaderCredentials
|
||||||
alt = alt_clz.new(json_key_io: StringIO.new(MultiJson.dump(cred_json)))
|
key_io = StringIO.new(MultiJson.dump(cred_json))
|
||||||
|
alt = alt_clz.make_creds(json_key_io: key_io)
|
||||||
alt.apply!(a_hash)
|
alt.apply!(a_hash)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -58,6 +58,25 @@ module Signet
|
||||||
def updater_proc
|
def updater_proc
|
||||||
lambda(&method(:apply))
|
lambda(&method(:apply))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def on_refresh(&block)
|
||||||
|
@refresh_listeners ||= []
|
||||||
|
@refresh_listeners << block
|
||||||
|
end
|
||||||
|
|
||||||
|
alias_method :orig_fetch_access_token!, :fetch_access_token!
|
||||||
|
def fetch_access_token!(options)
|
||||||
|
info = orig_fetch_access_token!(options)
|
||||||
|
notify_refresh_listeners
|
||||||
|
info
|
||||||
|
end
|
||||||
|
|
||||||
|
def notify_refresh_listeners
|
||||||
|
listeners = @refresh_listeners || []
|
||||||
|
listeners.each do |block|
|
||||||
|
block.call(self)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
# Copyright 2014, Google Inc.
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are
|
||||||
|
# met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above
|
||||||
|
# copyright notice, this list of conditions and the following disclaimer
|
||||||
|
# in the documentation and/or other materials provided with the
|
||||||
|
# distribution.
|
||||||
|
# * Neither the name of Google Inc. nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from
|
||||||
|
# this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
require 'yaml/store'
|
||||||
|
require 'googleauth/token_store'
|
||||||
|
|
||||||
|
module Google
|
||||||
|
module Auth
|
||||||
|
module Stores
|
||||||
|
# Implementation of user token storage backed by a local YAML file
|
||||||
|
class FileTokenStore < Google::Auth::TokenStore
|
||||||
|
# Create a new store with the supplied file.
|
||||||
|
#
|
||||||
|
# @param [String, File] file
|
||||||
|
# Path to storage file
|
||||||
|
def initialize(options = {})
|
||||||
|
path = options[:file]
|
||||||
|
@store = YAML::Store.new(path)
|
||||||
|
end
|
||||||
|
|
||||||
|
# (see Google::Auth::Stores::TokenStore#load)
|
||||||
|
def load(id)
|
||||||
|
@store.transaction { @store[id] }
|
||||||
|
end
|
||||||
|
|
||||||
|
# (see Google::Auth::Stores::TokenStore#store)
|
||||||
|
def store(id, token)
|
||||||
|
@store.transaction { @store[id] = token }
|
||||||
|
end
|
||||||
|
|
||||||
|
# (see Google::Auth::Stores::TokenStore#delete)
|
||||||
|
def delete(id)
|
||||||
|
@store.transaction { @store.delete(id) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,95 @@
|
||||||
|
# Copyright 2014, Google Inc.
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are
|
||||||
|
# met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above
|
||||||
|
# copyright notice, this list of conditions and the following disclaimer
|
||||||
|
# in the documentation and/or other materials provided with the
|
||||||
|
# distribution.
|
||||||
|
# * Neither the name of Google Inc. nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from
|
||||||
|
# this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
require 'redis'
|
||||||
|
require 'googleauth/token_store'
|
||||||
|
|
||||||
|
module Google
|
||||||
|
module Auth
|
||||||
|
module Stores
|
||||||
|
# Implementation of user token storage backed by Redis. Tokens
|
||||||
|
# are stored as JSON using the supplied key, prefixed with
|
||||||
|
# `g-user-token:`
|
||||||
|
class RedisTokenStore < Google::Auth::TokenStore
|
||||||
|
DEFAULT_KEY_PREFIX = 'g-user-token:'
|
||||||
|
|
||||||
|
# Create a new store with the supplied redis client.
|
||||||
|
#
|
||||||
|
# @param [::Redis, String] redis
|
||||||
|
# Initialized redis client to connect to.
|
||||||
|
# @param [String] prefix
|
||||||
|
# Prefix for keys in redis. Defaults to 'g-user-token:'
|
||||||
|
# @note If no redis instance is provided, a new one is created and
|
||||||
|
# the options passed through. You may include any other keys accepted
|
||||||
|
# by `Redis.new`
|
||||||
|
def initialize(options = {})
|
||||||
|
redis = options.delete(:redis)
|
||||||
|
prefix = options.delete(:prefix)
|
||||||
|
case redis
|
||||||
|
when Redis
|
||||||
|
@redis = redis
|
||||||
|
else
|
||||||
|
@redis = Redis.new(options)
|
||||||
|
end
|
||||||
|
@prefix = prefix || DEFAULT_KEY_PREFIX
|
||||||
|
end
|
||||||
|
|
||||||
|
# (see Google::Auth::Stores::TokenStore#load)
|
||||||
|
def load(id)
|
||||||
|
key = key_for(id)
|
||||||
|
@redis.get(key)
|
||||||
|
end
|
||||||
|
|
||||||
|
# (see Google::Auth::Stores::TokenStore#store)
|
||||||
|
def store(id, token)
|
||||||
|
key = key_for(id)
|
||||||
|
@redis.set(key, token)
|
||||||
|
end
|
||||||
|
|
||||||
|
# (see Google::Auth::Stores::TokenStore#delete)
|
||||||
|
def delete(id)
|
||||||
|
key = key_for(id)
|
||||||
|
@redis.del(key)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Generate a redis key from a token ID
|
||||||
|
#
|
||||||
|
# @param [String] id
|
||||||
|
# ID of the token
|
||||||
|
# @return [String]
|
||||||
|
# Redis key
|
||||||
|
def key_for(id)
|
||||||
|
@prefix + id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,69 @@
|
||||||
|
# Copyright 2014, Google Inc.
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are
|
||||||
|
# met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above
|
||||||
|
# copyright notice, this list of conditions and the following disclaimer
|
||||||
|
# in the documentation and/or other materials provided with the
|
||||||
|
# distribution.
|
||||||
|
# * Neither the name of Google Inc. nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from
|
||||||
|
# this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
module Google
|
||||||
|
module Auth
|
||||||
|
# Interface definition for token stores. It is not required that
|
||||||
|
# implementations inherit from this class. It is provided for documentation
|
||||||
|
# purposes to illustrate the API contract.
|
||||||
|
class TokenStore
|
||||||
|
class << self
|
||||||
|
attr_accessor :default
|
||||||
|
end
|
||||||
|
|
||||||
|
# Load the token data from storage for the given ID.
|
||||||
|
#
|
||||||
|
# @param [String] id
|
||||||
|
# ID of token data to load.
|
||||||
|
# @return [String]
|
||||||
|
# The loaded token data.
|
||||||
|
def load(_id)
|
||||||
|
fail 'Not implemented'
|
||||||
|
end
|
||||||
|
|
||||||
|
# Put the token data into storage for the given ID.
|
||||||
|
#
|
||||||
|
# @param [String] id
|
||||||
|
# ID of token data to store.
|
||||||
|
# @param [String] token
|
||||||
|
# The token data to store.
|
||||||
|
def store(_id, _token)
|
||||||
|
fail 'Not implemented'
|
||||||
|
end
|
||||||
|
|
||||||
|
# Remove the token data from storage for the given ID.
|
||||||
|
#
|
||||||
|
# @param [String] id
|
||||||
|
# ID of the token data to delete
|
||||||
|
def delete(_id)
|
||||||
|
fail 'Not implemented'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,274 @@
|
||||||
|
# Copyright 2014, Google Inc.
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are
|
||||||
|
# met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above
|
||||||
|
# copyright notice, this list of conditions and the following disclaimer
|
||||||
|
# in the documentation and/or other materials provided with the
|
||||||
|
# distribution.
|
||||||
|
# * Neither the name of Google Inc. nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from
|
||||||
|
# this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
require 'uri'
|
||||||
|
require 'multi_json'
|
||||||
|
require 'googleauth/signet'
|
||||||
|
require 'googleauth/user_refresh'
|
||||||
|
|
||||||
|
module Google
|
||||||
|
module Auth
|
||||||
|
# Handles an interactive 3-Legged-OAuth2 (3LO) user consent authorization.
|
||||||
|
#
|
||||||
|
# Example usage for a simple command line app:
|
||||||
|
#
|
||||||
|
# credentials = authorizer.get_credentials(user_id)
|
||||||
|
# if credentials.nil?
|
||||||
|
# url = authorizer.get_redirect_uri(user_id,
|
||||||
|
# nil,
|
||||||
|
# 'urn:ietf:wg:oauth:2.0:oob')
|
||||||
|
# puts "Open the following URL in the browser and enter the " +
|
||||||
|
# "resulting code after authorization"
|
||||||
|
# puts url
|
||||||
|
# code = gets
|
||||||
|
# creds = authorizer.get_and_store_credentials_from_code(user_id,
|
||||||
|
# code)
|
||||||
|
# end
|
||||||
|
# # Credentials ready to use, call APIs
|
||||||
|
# ...
|
||||||
|
class UserAuthorizer
|
||||||
|
MISMATCHED_CLIENT_ID_ERROR =
|
||||||
|
'Token client ID of %s does not match configured client id %s'
|
||||||
|
NIL_CLIENT_ID_ERROR = 'Client id can not be nil.'
|
||||||
|
NIL_SCOPE_ERROR = 'Scope can not be nil.'
|
||||||
|
NIL_USER_ID_ERROR = 'User ID can not be nil.'
|
||||||
|
NIL_TOKEN_STORE_ERROR = 'Can not call method if token store is nil'
|
||||||
|
MISSING_ABSOLUTE_URL_ERROR =
|
||||||
|
'Absolute base url required for relative callback url "%s"'
|
||||||
|
|
||||||
|
# Initialize the authorizer
|
||||||
|
#
|
||||||
|
# @param [Google::Auth::ClientID] client_id
|
||||||
|
# Configured ID & secret for this application
|
||||||
|
# @param [String, Array<String>] scope
|
||||||
|
# Authorization scope to request
|
||||||
|
# @param [Google::Auth::Stores::TokenStore] token_store
|
||||||
|
# Backing storage for persisting user credentials
|
||||||
|
# @param [String] callback_uri
|
||||||
|
# URL (either absolute or relative) of the auth callback.
|
||||||
|
# Defaults to '/oauth2callback'
|
||||||
|
def initialize(client_id, scope, token_store, callback_uri = nil)
|
||||||
|
fail NIL_CLIENT_ID_ERROR if client_id.nil?
|
||||||
|
fail NIL_SCOPE_ERROR if scope.nil?
|
||||||
|
|
||||||
|
@client_id = client_id
|
||||||
|
@scope = Array(scope)
|
||||||
|
@token_store = token_store
|
||||||
|
@callback_uri = callback_uri || '/oauth2callback'
|
||||||
|
end
|
||||||
|
|
||||||
|
# Build the URL for requesting authorization.
|
||||||
|
#
|
||||||
|
# @param [String] login_hint
|
||||||
|
# Login hint if need to authorize a specific account. Should be a
|
||||||
|
# user's email address or unique profile ID.
|
||||||
|
# @param [String] state
|
||||||
|
# Opaque state value to be returned to the oauth callback.
|
||||||
|
# @param [String] base_url
|
||||||
|
# Absolute URL to resolve the configured callback uri against. Required
|
||||||
|
# if the configured callback uri is a relative.
|
||||||
|
# @param [String, Array<String>] scope
|
||||||
|
# Authorization scope to request. Overrides the instance scopes if not
|
||||||
|
# nil.
|
||||||
|
# @return [String]
|
||||||
|
# Authorization url
|
||||||
|
def get_authorization_url(options = {})
|
||||||
|
scope = options[:scope] || @scope
|
||||||
|
credentials = UserRefreshCredentials.new(
|
||||||
|
client_id: @client_id.id,
|
||||||
|
client_secret: @client_id.secret,
|
||||||
|
scope: scope)
|
||||||
|
redirect_uri = redirect_uri_for(options[:base_url])
|
||||||
|
url = credentials.authorization_uri(access_type: 'offline',
|
||||||
|
redirect_uri: redirect_uri,
|
||||||
|
approval_prompt: 'force',
|
||||||
|
state: options[:state],
|
||||||
|
include_granted_scopes: true,
|
||||||
|
login_hint: options[:login_hint])
|
||||||
|
url.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
# Fetch stored credentials for the user.
|
||||||
|
#
|
||||||
|
# @param [String] user_id
|
||||||
|
# Unique ID of the user for loading/storing credentials.
|
||||||
|
# @param [Array<String>, String] scope
|
||||||
|
# If specified, only returns credentials that have all
|
||||||
|
# the requested scopes
|
||||||
|
# @return [Google::Auth::UserRefreshCredentials]
|
||||||
|
# Stored credentials, nil if none present
|
||||||
|
def get_credentials(user_id, scope = nil)
|
||||||
|
fail NIL_USER_ID_ERROR if user_id.nil?
|
||||||
|
fail NIL_TOKEN_STORE_ERROR if @token_store.nil?
|
||||||
|
|
||||||
|
scope ||= @scope
|
||||||
|
saved_token = @token_store.load(user_id)
|
||||||
|
return nil if saved_token.nil?
|
||||||
|
data = MultiJson.load(saved_token)
|
||||||
|
|
||||||
|
if data.fetch('client_id', @client_id.id) != @client_id.id
|
||||||
|
fail sprintf(MISMATCHED_CLIENT_ID_ERROR,
|
||||||
|
data['client_id'], @client_id.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
credentials = UserRefreshCredentials.new(
|
||||||
|
client_id: @client_id.id,
|
||||||
|
client_secret: @client_id.secret,
|
||||||
|
scope: data['scope'] || @scope,
|
||||||
|
access_token: data['access_token'],
|
||||||
|
refresh_token: data['refresh_token'],
|
||||||
|
expires_at: data.fetch('expiration_time_millis', 0) / 1000)
|
||||||
|
if credentials.includes_scope?(scope)
|
||||||
|
monitor_credentials(user_id, credentials)
|
||||||
|
return credentials
|
||||||
|
end
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# Exchanges an authorization code returned in the oauth callback
|
||||||
|
#
|
||||||
|
# @param [String] user_id
|
||||||
|
# Unique ID of the user for loading/storing credentials.
|
||||||
|
# @param [String] code
|
||||||
|
# The authorization code from the OAuth callback
|
||||||
|
# @param [String, Array<String>] scope
|
||||||
|
# Authorization scope requested. Overrides the instance
|
||||||
|
# scopes if not nil.
|
||||||
|
# @param [String] base_url
|
||||||
|
# Absolute URL to resolve the configured callback uri against.
|
||||||
|
# Required if the configured
|
||||||
|
# callback uri is a relative.
|
||||||
|
# @return [Google::Auth::UserRefreshCredentials]
|
||||||
|
# Credentials if exchange is successful
|
||||||
|
def get_credentials_from_code(options = {})
|
||||||
|
user_id = options[:user_id]
|
||||||
|
code = options[:code]
|
||||||
|
scope = options[:scope] || @scope
|
||||||
|
base_url = options[:base_url]
|
||||||
|
credentials = UserRefreshCredentials.new(
|
||||||
|
client_id: @client_id.id,
|
||||||
|
client_secret: @client_id.secret,
|
||||||
|
redirect_uri: redirect_uri_for(base_url),
|
||||||
|
scope: scope)
|
||||||
|
credentials.code = code
|
||||||
|
credentials.fetch_access_token!({})
|
||||||
|
monitor_credentials(user_id, credentials)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Exchanges an authorization code returned in the oauth callback.
|
||||||
|
# Additionally, stores the resulting credentials in the token store if
|
||||||
|
# the exchange is successful.
|
||||||
|
#
|
||||||
|
# @param [String] user_id
|
||||||
|
# Unique ID of the user for loading/storing credentials.
|
||||||
|
# @param [String] code
|
||||||
|
# The authorization code from the OAuth callback
|
||||||
|
# @param [String, Array<String>] scope
|
||||||
|
# Authorization scope requested. Overrides the instance
|
||||||
|
# scopes if not nil.
|
||||||
|
# @param [String] base_url
|
||||||
|
# Absolute URL to resolve the configured callback uri against.
|
||||||
|
# Required if the configured
|
||||||
|
# callback uri is a relative.
|
||||||
|
# @return [Google::Auth::UserRefreshCredentials]
|
||||||
|
# Credentials if exchange is successful
|
||||||
|
def get_and_store_credentials_from_code(options = {})
|
||||||
|
credentials = get_credentials_from_code(options)
|
||||||
|
monitor_credentials(options[:user_id], credentials)
|
||||||
|
store_credentials(options[:user_id], credentials)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Revokes a user's credentials. This both revokes the actual
|
||||||
|
# grant as well as removes the token from the token store.
|
||||||
|
#
|
||||||
|
# @param [String] user_id
|
||||||
|
# Unique ID of the user for loading/storing credentials.
|
||||||
|
def revoke_authorization(user_id)
|
||||||
|
credentials = get_credentials(user_id)
|
||||||
|
if credentials
|
||||||
|
begin
|
||||||
|
@token_store.delete(user_id)
|
||||||
|
ensure
|
||||||
|
credentials.revoke!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# Store credentials for a user. Generally not required to be
|
||||||
|
# called directly, but may be used to migrate tokens from one
|
||||||
|
# store to another.
|
||||||
|
#
|
||||||
|
# @param [String] user_id
|
||||||
|
# Unique ID of the user for loading/storing credentials.
|
||||||
|
# @param [Google::Auth::UserRefreshCredentials] credentials
|
||||||
|
# Credentials to store.
|
||||||
|
def store_credentials(user_id, credentials)
|
||||||
|
json = MultiJson.dump(
|
||||||
|
client_id: credentials.client_id,
|
||||||
|
access_token: credentials.access_token,
|
||||||
|
refresh_token: credentials.refresh_token,
|
||||||
|
scope: credentials.scope,
|
||||||
|
expiration_time_millis: (credentials.expires_at.to_i) * 1000)
|
||||||
|
@token_store.store(user_id, json)
|
||||||
|
credentials
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Begin watching a credential for refreshes so the access token can be
|
||||||
|
# saved.
|
||||||
|
#
|
||||||
|
# @param [String] user_id
|
||||||
|
# Unique ID of the user for loading/storing credentials.
|
||||||
|
# @param [Google::Auth::UserRefreshCredentials] credentials
|
||||||
|
# Credentials to store.
|
||||||
|
def monitor_credentials(user_id, credentials)
|
||||||
|
credentials.on_refresh do |cred|
|
||||||
|
store_credentials(user_id, cred)
|
||||||
|
end
|
||||||
|
credentials
|
||||||
|
end
|
||||||
|
|
||||||
|
# Resolve the redirect uri against a base.
|
||||||
|
#
|
||||||
|
# @param [String] base_url
|
||||||
|
# Absolute URL to resolve the callback against if necessary.
|
||||||
|
# @return [String]
|
||||||
|
# Redirect URI
|
||||||
|
def redirect_uri_for(base_url)
|
||||||
|
return @callback_uri unless URI(@callback_uri).scheme.nil?
|
||||||
|
fail sprintf(
|
||||||
|
MISSING_ABSOLUTE_URL_ERROR,
|
||||||
|
@callback_uri) if base_url.nil? || URI(base_url).scheme.nil?
|
||||||
|
URI.join(base_url, @callback_uri).to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -29,6 +29,7 @@
|
||||||
|
|
||||||
require 'googleauth/signet'
|
require 'googleauth/signet'
|
||||||
require 'googleauth/credentials_loader'
|
require 'googleauth/credentials_loader'
|
||||||
|
require 'googleauth/scope_util'
|
||||||
require 'multi_json'
|
require 'multi_json'
|
||||||
|
|
||||||
module Google
|
module Google
|
||||||
|
@ -46,8 +47,30 @@ module Google
|
||||||
# cf [Application Default Credentials](http://goo.gl/mkAHpZ)
|
# cf [Application Default Credentials](http://goo.gl/mkAHpZ)
|
||||||
class UserRefreshCredentials < Signet::OAuth2::Client
|
class UserRefreshCredentials < Signet::OAuth2::Client
|
||||||
TOKEN_CRED_URI = 'https://www.googleapis.com/oauth2/v3/token'
|
TOKEN_CRED_URI = 'https://www.googleapis.com/oauth2/v3/token'
|
||||||
|
AUTHORIZATION_URI = 'https://accounts.google.com/o/oauth2/auth'
|
||||||
|
REVOKE_TOKEN_URI = 'https://accounts.google.com/o/oauth2/revoke'
|
||||||
extend CredentialsLoader
|
extend CredentialsLoader
|
||||||
|
|
||||||
|
# Create a UserRefreshCredentials.
|
||||||
|
#
|
||||||
|
# @param json_key_io [IO] an IO from which the JSON key can be read
|
||||||
|
# @param scope [string|array|nil] the scope(s) to access
|
||||||
|
def self.make_creds(options = {})
|
||||||
|
json_key_io, scope = options.values_at(:json_key_io, :scope)
|
||||||
|
user_creds = read_json_key(json_key_io) if json_key_io
|
||||||
|
user_creds ||= {
|
||||||
|
'client_id' => ENV[CredentialsLoader::CLIENT_ID_VAR],
|
||||||
|
'client_secret' => ENV[CredentialsLoader::CLIENT_SECRET_VAR],
|
||||||
|
'refresh_token' => ENV[CredentialsLoader::REFRESH_TOKEN_VAR]
|
||||||
|
}
|
||||||
|
|
||||||
|
new(token_credential_uri: TOKEN_CRED_URI,
|
||||||
|
client_id: user_creds['client_id'],
|
||||||
|
client_secret: user_creds['client_secret'],
|
||||||
|
refresh_token: user_creds['refresh_token'],
|
||||||
|
scope: scope)
|
||||||
|
end
|
||||||
|
|
||||||
# Reads the client_id, client_secret and refresh_token fields from the
|
# Reads the client_id, client_secret and refresh_token fields from the
|
||||||
# JSON key.
|
# JSON key.
|
||||||
def self.read_json_key(json_key_io)
|
def self.read_json_key(json_key_io)
|
||||||
|
@ -59,24 +82,38 @@ module Google
|
||||||
json_key
|
json_key
|
||||||
end
|
end
|
||||||
|
|
||||||
# Initializes a UserRefreshCredentials.
|
|
||||||
#
|
|
||||||
# @param json_key_io [IO] an IO from which the JSON key can be read
|
|
||||||
# @param scope [string|array|nil] the scope(s) to access
|
|
||||||
def initialize(options = {})
|
def initialize(options = {})
|
||||||
json_key_io, scope = options.values_at(:json_key_io, :scope)
|
options ||= {}
|
||||||
user_creds = self.class.read_json_key(json_key_io) if json_key_io
|
options[:token_credential_uri] ||= TOKEN_CRED_URI
|
||||||
user_creds ||= {
|
options[:authorization_uri] ||= AUTHORIZATION_URI
|
||||||
'client_id' => ENV[CredentialsLoader::CLIENT_ID_VAR],
|
super(options)
|
||||||
'client_secret' => ENV[CredentialsLoader::CLIENT_SECRET_VAR],
|
end
|
||||||
'refresh_token' => ENV[CredentialsLoader::REFRESH_TOKEN_VAR]
|
|
||||||
}
|
|
||||||
|
|
||||||
super(token_credential_uri: TOKEN_CRED_URI,
|
# Revokes the credential
|
||||||
client_id: user_creds['client_id'],
|
def revoke!(options = {})
|
||||||
client_secret: user_creds['client_secret'],
|
c = options[:connection] || Faraday.default_connection
|
||||||
refresh_token: user_creds['refresh_token'],
|
resp = c.get(REVOKE_TOKEN_URI, token: refresh_token || access_token)
|
||||||
scope: scope)
|
case resp.status
|
||||||
|
when 200
|
||||||
|
self.access_token = nil
|
||||||
|
self.refresh_token = nil
|
||||||
|
self.expires_at = 0
|
||||||
|
else
|
||||||
|
fail(Signet::AuthorizationError,
|
||||||
|
"Unexpected error code #{resp.status}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Verifies that a credential grants the requested scope
|
||||||
|
#
|
||||||
|
# @param [Array<String>, String] required_scope
|
||||||
|
# Scope to verify
|
||||||
|
# @return [Boolean]
|
||||||
|
# True if scope is granted
|
||||||
|
def includes_scope?(required_scope)
|
||||||
|
missing_scope = Google::Auth::ScopeUtil.normalize(required_scope) -
|
||||||
|
Google::Auth::ScopeUtil.normalize(scope)
|
||||||
|
missing_scope.empty?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -56,17 +56,30 @@ shared_examples 'apply/apply! are OK' do
|
||||||
# @make_auth_stubs, which should stub out the expected http behaviour of the
|
# @make_auth_stubs, which should stub out the expected http behaviour of the
|
||||||
# auth client
|
# auth client
|
||||||
describe '#fetch_access_token' do
|
describe '#fetch_access_token' do
|
||||||
it 'should set access_token to the fetched value' do
|
let(:token) { '1/abcdef1234567890' }
|
||||||
token = '1/abcdef1234567890'
|
let(:stubs) do
|
||||||
stubs = make_auth_stubs access_token: token
|
make_auth_stubs access_token: token
|
||||||
c = Faraday.new do |b|
|
end
|
||||||
|
let(:connection) do
|
||||||
|
Faraday.new do |b|
|
||||||
b.adapter(:test, stubs)
|
b.adapter(:test, stubs)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@client.fetch_access_token!(connection: c)
|
it 'should set access_token to the fetched value' do
|
||||||
|
@client.fetch_access_token!(connection: connection)
|
||||||
expect(@client.access_token).to eq(token)
|
expect(@client.access_token).to eq(token)
|
||||||
stubs.verify_stubbed_calls
|
stubs.verify_stubbed_calls
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'should notify refresh listeners after updating' do
|
||||||
|
expect do |b|
|
||||||
|
@client.on_refresh(&b)
|
||||||
|
@client.fetch_access_token!(connection: connection)
|
||||||
|
end.to yield_with_args(have_attributes(
|
||||||
|
access_token: '1/abcdef1234567890'))
|
||||||
|
stubs.verify_stubbed_calls
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#apply!' do
|
describe '#apply!' do
|
||||||
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
# Copyright 2015, Google Inc.
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are
|
||||||
|
# met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above
|
||||||
|
# copyright notice, this list of conditions and the following disclaimer
|
||||||
|
# in the documentation and/or other materials provided with the
|
||||||
|
# distribution.
|
||||||
|
# * Neither the name of Google Inc. nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from
|
||||||
|
# this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
spec_dir = File.expand_path(File.join(File.dirname(__FILE__)))
|
||||||
|
$LOAD_PATH.unshift(spec_dir)
|
||||||
|
$LOAD_PATH.uniq!
|
||||||
|
|
||||||
|
require 'fakefs/safe'
|
||||||
|
require 'googleauth'
|
||||||
|
|
||||||
|
describe Google::Auth::ClientId do
|
||||||
|
shared_examples 'it has a valid config' do
|
||||||
|
it 'should include a valid id' do
|
||||||
|
expect(client_id.id).to eql 'abc@example.com'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should include a valid secret' do
|
||||||
|
expect(client_id.secret).to eql 'notasecret'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'it can successfully load client_id' do
|
||||||
|
context 'loaded from hash' do
|
||||||
|
let(:client_id) { Google::Auth::ClientId.from_hash(config) }
|
||||||
|
|
||||||
|
it_behaves_like 'it has a valid config'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'loaded from file' do
|
||||||
|
file_path = '/client_secrets.json'
|
||||||
|
|
||||||
|
let(:client_id) do
|
||||||
|
FakeFS do
|
||||||
|
content = MultiJson.dump(config)
|
||||||
|
File.write(file_path, content)
|
||||||
|
Google::Auth::ClientId.from_file(file_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'it has a valid config'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'with web config' do
|
||||||
|
let(:config) do
|
||||||
|
{
|
||||||
|
'web' => {
|
||||||
|
'client_id' => 'abc@example.com',
|
||||||
|
'client_secret' => 'notasecret'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
it_behaves_like 'it can successfully load client_id'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'with installed app config' do
|
||||||
|
let(:config) do
|
||||||
|
{
|
||||||
|
'installed' => {
|
||||||
|
'client_id' => 'abc@example.com',
|
||||||
|
'client_secret' => 'notasecret'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
it_behaves_like 'it can successfully load client_id'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with missing top level property' do
|
||||||
|
let(:config) do
|
||||||
|
{
|
||||||
|
'notvalid' => {
|
||||||
|
'client_id' => 'abc@example.com',
|
||||||
|
'client_secret' => 'notasecret'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should raise error' do
|
||||||
|
expect { Google::Auth::ClientId.from_hash(config) }.to raise_error(
|
||||||
|
/Expected top level property/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with missing client id' do
|
||||||
|
let(:config) do
|
||||||
|
{
|
||||||
|
'web' => {
|
||||||
|
'client_secret' => 'notasecret'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should raise error' do
|
||||||
|
expect { Google::Auth::ClientId.from_hash(config) }.to raise_error(
|
||||||
|
/Client id can not be nil/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with missing client secret' do
|
||||||
|
let(:config) do
|
||||||
|
{
|
||||||
|
'web' => {
|
||||||
|
'client_id' => 'abc@example.com'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should raise error' do
|
||||||
|
expect { Google::Auth::ClientId.from_hash(config) }.to raise_error(
|
||||||
|
/Client secret can not be nil/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,75 @@
|
||||||
|
# Copyright 2015, Google Inc.
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are
|
||||||
|
# met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above
|
||||||
|
# copyright notice, this list of conditions and the following disclaimer
|
||||||
|
# in the documentation and/or other materials provided with the
|
||||||
|
# distribution.
|
||||||
|
# * Neither the name of Google Inc. nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from
|
||||||
|
# this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
spec_dir = File.expand_path(File.join(File.dirname(__FILE__)))
|
||||||
|
$LOAD_PATH.unshift(spec_dir)
|
||||||
|
$LOAD_PATH.uniq!
|
||||||
|
|
||||||
|
require 'googleauth/scope_util'
|
||||||
|
|
||||||
|
describe Google::Auth::ScopeUtil do
|
||||||
|
shared_examples 'normalizes scopes' do
|
||||||
|
let(:normalized) { Google::Auth::ScopeUtil.normalize(source) }
|
||||||
|
|
||||||
|
it 'normalizes the email scope' do
|
||||||
|
expect(normalized).to include(
|
||||||
|
'https://www.googleapis.com/auth/userinfo.email')
|
||||||
|
expect(normalized).to_not include 'email'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'normalizes the profile scope' do
|
||||||
|
expect(normalized).to include(
|
||||||
|
'https://www.googleapis.com/auth/userinfo.profile')
|
||||||
|
expect(normalized).to_not include 'profile'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'normalizes the openid scope' do
|
||||||
|
expect(normalized).to include 'https://www.googleapis.com/auth/plus.me'
|
||||||
|
expect(normalized).to_not include 'openid'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'leaves other other scopes as-is' do
|
||||||
|
expect(normalized).to include 'https://www.googleapis.com/auth/drive'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with scope as string' do
|
||||||
|
let(:source) do
|
||||||
|
'email profile openid https://www.googleapis.com/auth/drive'
|
||||||
|
end
|
||||||
|
it_behaves_like 'normalizes scopes'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with scope as Array' do
|
||||||
|
let(:source) do
|
||||||
|
%w(email profile openid https://www.googleapis.com/auth/drive)
|
||||||
|
end
|
||||||
|
it_behaves_like 'normalizes scopes'
|
||||||
|
end
|
||||||
|
end
|
|
@ -121,7 +121,7 @@ describe Google::Auth::ServiceAccountCredentials do
|
||||||
|
|
||||||
before(:example) do
|
before(:example) do
|
||||||
@key = OpenSSL::PKey::RSA.new(2048)
|
@key = OpenSSL::PKey::RSA.new(2048)
|
||||||
@client = ServiceAccountCredentials.new(
|
@client = ServiceAccountCredentials.make_creds(
|
||||||
json_key_io: StringIO.new(cred_json_text),
|
json_key_io: StringIO.new(cred_json_text),
|
||||||
scope: 'https://www.googleapis.com/auth/userinfo.profile'
|
scope: 'https://www.googleapis.com/auth/userinfo.profile'
|
||||||
)
|
)
|
||||||
|
@ -276,7 +276,7 @@ describe Google::Auth::ServiceAccountJwtHeaderCredentials do
|
||||||
|
|
||||||
before(:example) do
|
before(:example) do
|
||||||
@key = OpenSSL::PKey::RSA.new(2048)
|
@key = OpenSSL::PKey::RSA.new(2048)
|
||||||
@client = clz.new(json_key_io: StringIO.new(cred_json_text))
|
@client = clz.make_creds(json_key_io: StringIO.new(cred_json_text))
|
||||||
end
|
end
|
||||||
|
|
||||||
def cred_json_text
|
def cred_json_text
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
# Copyright 2015, Google Inc.
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are
|
||||||
|
# met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above
|
||||||
|
# copyright notice, this list of conditions and the following disclaimer
|
||||||
|
# in the documentation and/or other materials provided with the
|
||||||
|
# distribution.
|
||||||
|
# * Neither the name of Google Inc. nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from
|
||||||
|
# this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
spec_dir = File.expand_path(File.join(File.dirname(__FILE__)))
|
||||||
|
$LOAD_PATH.unshift(spec_dir)
|
||||||
|
$LOAD_PATH.uniq!
|
||||||
|
|
||||||
|
require 'googleauth'
|
||||||
|
require 'googleauth/stores/file_token_store'
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'fakefs/safe'
|
||||||
|
require 'fakefs/spec_helpers'
|
||||||
|
require 'googleauth/stores/store_examples'
|
||||||
|
|
||||||
|
module FakeFS
|
||||||
|
class File
|
||||||
|
# FakeFS doesn't implement. And since we don't need to actually lock,
|
||||||
|
# just stub out...
|
||||||
|
def flock(*)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Google::Auth::Stores::FileTokenStore do
|
||||||
|
include FakeFS::SpecHelpers
|
||||||
|
|
||||||
|
let(:store) do
|
||||||
|
Google::Auth::Stores::FileTokenStore.new(file: '/tokens.yaml')
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'token store'
|
||||||
|
end
|
|
@ -0,0 +1,50 @@
|
||||||
|
# Copyright 2015, Google Inc.
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are
|
||||||
|
# met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above
|
||||||
|
# copyright notice, this list of conditions and the following disclaimer
|
||||||
|
# in the documentation and/or other materials provided with the
|
||||||
|
# distribution.
|
||||||
|
# * Neither the name of Google Inc. nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from
|
||||||
|
# this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
spec_dir = File.expand_path(File.join(File.dirname(__FILE__)))
|
||||||
|
$LOAD_PATH.unshift(spec_dir)
|
||||||
|
$LOAD_PATH.uniq!
|
||||||
|
|
||||||
|
require 'googleauth'
|
||||||
|
require 'googleauth/stores/redis_token_store'
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'fakeredis/rspec'
|
||||||
|
require 'googleauth/stores/store_examples'
|
||||||
|
|
||||||
|
describe Google::Auth::Stores::RedisTokenStore do
|
||||||
|
let(:redis) do
|
||||||
|
Redis.new
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:store) do
|
||||||
|
Google::Auth::Stores::RedisTokenStore.new(redis: redis)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'token store'
|
||||||
|
end
|
|
@ -0,0 +1,58 @@
|
||||||
|
# Copyright 2015, Google Inc.
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are
|
||||||
|
# met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above
|
||||||
|
# copyright notice, this list of conditions and the following disclaimer
|
||||||
|
# in the documentation and/or other materials provided with the
|
||||||
|
# distribution.
|
||||||
|
# * Neither the name of Google Inc. nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from
|
||||||
|
# this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
spec_dir = File.expand_path(File.join(File.dirname(__FILE__)))
|
||||||
|
$LOAD_PATH.unshift(spec_dir)
|
||||||
|
$LOAD_PATH.uniq!
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
shared_examples 'token store' do
|
||||||
|
before(:each) do
|
||||||
|
store.store('default', 'test')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should return a stored value' do
|
||||||
|
expect(store.load('default')).to eq 'test'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should return nil for missing tokens' do
|
||||||
|
expect(store.load('notavalidkey')).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should return nil for deleted tokens' do
|
||||||
|
store.delete('default')
|
||||||
|
expect(store.load('default')).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should save overwrite values on store' do
|
||||||
|
store.store('default', 'test2')
|
||||||
|
expect(store.load('default')).to eq 'test2'
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,314 @@
|
||||||
|
# Copyright 2015, Google Inc.
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are
|
||||||
|
# met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above
|
||||||
|
# copyright notice, this list of conditions and the following disclaimer
|
||||||
|
# in the documentation and/or other materials provided with the
|
||||||
|
# distribution.
|
||||||
|
# * Neither the name of Google Inc. nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from
|
||||||
|
# this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
spec_dir = File.expand_path(File.join(File.dirname(__FILE__)))
|
||||||
|
$LOAD_PATH.unshift(spec_dir)
|
||||||
|
$LOAD_PATH.uniq!
|
||||||
|
|
||||||
|
require 'googleauth'
|
||||||
|
require 'googleauth/user_authorizer'
|
||||||
|
require 'uri'
|
||||||
|
require 'multi_json'
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Google::Auth::UserAuthorizer do
|
||||||
|
include TestHelpers
|
||||||
|
|
||||||
|
let(:client_id) { Google::Auth::ClientId.new('testclient', 'notasecret') }
|
||||||
|
let(:scope) { %w(email profile) }
|
||||||
|
let(:token_store) { DummyTokenStore.new }
|
||||||
|
let(:callback_uri) { 'https://www.example.com/oauth/callback' }
|
||||||
|
let(:authorizer) do
|
||||||
|
Google::Auth::UserAuthorizer.new(client_id,
|
||||||
|
scope,
|
||||||
|
token_store,
|
||||||
|
callback_uri)
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'valid authorization url' do
|
||||||
|
it 'should have a valid base URI' do
|
||||||
|
expect(uri).to match %r{https://accounts.google.com/o/oauth2/auth}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should request offline access' do
|
||||||
|
expect(URI(uri).query).to match(/access_type=offline/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should request response type code' do
|
||||||
|
expect(URI(uri).query).to match(/response_type=code/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should force approval' do
|
||||||
|
expect(URI(uri).query).to match(/approval_prompt=force/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should include granted scopes' do
|
||||||
|
expect(URI(uri).query).to match(/include_granted_scopes=true/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should include the correct client id' do
|
||||||
|
expect(URI(uri).query).to match(/client_id=testclient/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not include a client secret' do
|
||||||
|
expect(URI(uri).query).to_not match(/client_secret/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should include the callback uri' do
|
||||||
|
expect(URI(uri).query).to match(
|
||||||
|
%r{redirect_uri=https://www.example.com/oauth/callback})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should include the scope' do
|
||||||
|
expect(URI(uri).query).to match(/scope=email%20profile/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when generating authorization URLs with user ID & state' do
|
||||||
|
let(:uri) do
|
||||||
|
authorizer.get_authorization_url(login_hint: 'user1', state: 'mystate')
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'valid authorization url'
|
||||||
|
|
||||||
|
it 'includes a login hint' do
|
||||||
|
expect(URI(uri).query).to match(/login_hint=user1/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes the app state' do
|
||||||
|
expect(URI(uri).query).to match(/state=mystate/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when generating authorization URLs with user ID and no state' do
|
||||||
|
let(:uri) { authorizer.get_authorization_url(login_hint: 'user1') }
|
||||||
|
|
||||||
|
it_behaves_like 'valid authorization url'
|
||||||
|
|
||||||
|
it 'includes a login hint' do
|
||||||
|
expect(URI(uri).query).to match(/login_hint=user1/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not include the state parameter' do
|
||||||
|
expect(URI(uri).query).to_not match(/state/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when generating authorization URLs with no user ID and no state' do
|
||||||
|
let(:uri) { authorizer.get_authorization_url }
|
||||||
|
|
||||||
|
it_behaves_like 'valid authorization url'
|
||||||
|
|
||||||
|
it 'does not include the login hint parameter' do
|
||||||
|
expect(URI(uri).query).to_not match(/login_hint/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not include the state parameter' do
|
||||||
|
expect(URI(uri).query).to_not match(/state/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when retrieving tokens' do
|
||||||
|
let(:token_json) do
|
||||||
|
MultiJson.dump(
|
||||||
|
access_token: 'accesstoken',
|
||||||
|
refresh_token: 'refreshtoken',
|
||||||
|
expiration_time_millis: 1_441_234_742_000)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a valid user id' do
|
||||||
|
let(:credentials) do
|
||||||
|
token_store.store('user1', token_json)
|
||||||
|
authorizer.get_credentials('user1')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should return an instance of UserRefreshCredentials' do
|
||||||
|
expect(credentials).to be_instance_of(
|
||||||
|
Google::Auth::UserRefreshCredentials)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should return credentials with a valid refresh token' do
|
||||||
|
expect(credentials.refresh_token).to eq 'refreshtoken'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should return credentials with a valid access token' do
|
||||||
|
expect(credentials.access_token).to eq 'accesstoken'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should return credentials with a valid client ID' do
|
||||||
|
expect(credentials.client_id).to eq 'testclient'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should return credentials with a valid client secret' do
|
||||||
|
expect(credentials.client_secret).to eq 'notasecret'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should return credentials with a valid scope' do
|
||||||
|
expect(credentials.scope).to eq %w(email profile)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should return credentials with a valid expiration time' do
|
||||||
|
expect(credentials.expires_at).to eq Time.at(1_441_234_742)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an invalid user id' do
|
||||||
|
it 'should return nil' do
|
||||||
|
expect(authorizer.get_credentials('notauser')).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when saving tokens' do
|
||||||
|
let(:expiry) { Time.now.to_i }
|
||||||
|
let(:credentials) do
|
||||||
|
Google::Auth::UserRefreshCredentials.new(
|
||||||
|
client_id: client_id.id,
|
||||||
|
client_secret: client_id.secret,
|
||||||
|
scope: scope,
|
||||||
|
refresh_token: 'refreshtoken',
|
||||||
|
access_token: 'accesstoken',
|
||||||
|
expires_at: expiry
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:token_json) do
|
||||||
|
authorizer.store_credentials('user1', credentials)
|
||||||
|
token_store.load('user1')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should persist in the token store' do
|
||||||
|
expect(token_json).to_not be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should persist the refresh token' do
|
||||||
|
expect(MultiJson.load(token_json)['refresh_token']).to eq 'refreshtoken'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should persist the access token' do
|
||||||
|
expect(MultiJson.load(token_json)['access_token']).to eq 'accesstoken'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should persist the client id' do
|
||||||
|
expect(MultiJson.load(token_json)['client_id']).to eq 'testclient'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should persist the scope' do
|
||||||
|
expect(MultiJson.load(token_json)['scope']).to include('email', 'profile')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should persist the expiry as milliseconds' do
|
||||||
|
expected_expiry = expiry * 1000
|
||||||
|
expect(MultiJson.load(token_json)['expiration_time_millis']).to eql(
|
||||||
|
expected_expiry)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with valid authorization code' do
|
||||||
|
let(:token_json) do
|
||||||
|
MultiJson.dump('access_token' => '1/abc123',
|
||||||
|
'token_type' => 'Bearer',
|
||||||
|
'expires_in' => 3600)
|
||||||
|
end
|
||||||
|
|
||||||
|
before(:example) do
|
||||||
|
stub_request(:post, 'https://www.googleapis.com/oauth2/v3/token')
|
||||||
|
.to_return(body: token_json, status: 200, headers: {
|
||||||
|
'Content-Type' => 'application/json' })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should exchange a code for credentials' do
|
||||||
|
credentials = authorizer.get_credentials_from_code(
|
||||||
|
user_id: 'user1', code: 'code')
|
||||||
|
expect(credentials.access_token).to eq '1/abc123'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not store credentials when get only requested' do
|
||||||
|
authorizer.get_credentials_from_code(user_id: 'user1', code: 'code')
|
||||||
|
expect(token_store.load('user1')).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should store credentials when requested' do
|
||||||
|
authorizer.get_and_store_credentials_from_code(
|
||||||
|
user_id: 'user1', code: 'code')
|
||||||
|
expect(token_store.load('user1')).to_not be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid authorization code' do
|
||||||
|
before(:example) do
|
||||||
|
stub_request(:post, 'https://www.googleapis.com/oauth2/v3/token')
|
||||||
|
.to_return(status: 400)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should raise an authorization error' do
|
||||||
|
expect do
|
||||||
|
authorizer.get_credentials_from_code(user_id: 'user1', code: 'badcode')
|
||||||
|
end.to raise_error Signet::AuthorizationError
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not store credentials when exchange fails' do
|
||||||
|
expect do
|
||||||
|
authorizer.get_credentials_from_code(user_id: 'user1', code: 'badcode')
|
||||||
|
end.to raise_error Signet::AuthorizationError
|
||||||
|
expect(token_store.load('user1')).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when reovking authorization' do
|
||||||
|
let(:token_json) do
|
||||||
|
MultiJson.dump(
|
||||||
|
access_token: 'accesstoken',
|
||||||
|
refresh_token: 'refreshtoken',
|
||||||
|
expiration_time_millis: 1_441_234_742_000)
|
||||||
|
end
|
||||||
|
|
||||||
|
before(:example) do
|
||||||
|
token_store.store('user1', token_json)
|
||||||
|
stub_request(
|
||||||
|
:get, 'https://accounts.google.com/o/oauth2/revoke?token=refreshtoken')
|
||||||
|
.to_return(status: 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should revoke the grant' do
|
||||||
|
authorizer.revoke_authorization('user1')
|
||||||
|
expect(a_request(
|
||||||
|
:get, 'https://accounts.google.com/o/oauth2/revoke?token=refreshtoken'))
|
||||||
|
.to have_been_made
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should remove the token from storage' do
|
||||||
|
authorizer.revoke_authorization('user1')
|
||||||
|
expect(token_store.load('user1')).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: - Test that tokens are monitored
|
||||||
|
# TODO - Test scope enforcement (auth if upgrade required)
|
||||||
|
end
|
|
@ -57,7 +57,7 @@ describe Google::Auth::UserRefreshCredentials do
|
||||||
|
|
||||||
before(:example) do
|
before(:example) do
|
||||||
@key = OpenSSL::PKey::RSA.new(2048)
|
@key = OpenSSL::PKey::RSA.new(2048)
|
||||||
@client = UserRefreshCredentials.new(
|
@client = UserRefreshCredentials.make_creds(
|
||||||
json_key_io: StringIO.new(cred_json_text),
|
json_key_io: StringIO.new(cred_json_text),
|
||||||
scope: 'https://www.googleapis.com/auth/userinfo.profile'
|
scope: 'https://www.googleapis.com/auth/userinfo.profile'
|
||||||
)
|
)
|
||||||
|
@ -142,10 +142,11 @@ describe Google::Auth::UserRefreshCredentials do
|
||||||
ENV[CLIENT_SECRET_VAR] = cred_json[:client_secret]
|
ENV[CLIENT_SECRET_VAR] = cred_json[:client_secret]
|
||||||
ENV[REFRESH_TOKEN_VAR] = cred_json[:refresh_token]
|
ENV[REFRESH_TOKEN_VAR] = cred_json[:refresh_token]
|
||||||
ENV[ACCOUNT_TYPE_VAR] = cred_json[:type]
|
ENV[ACCOUNT_TYPE_VAR] = cred_json[:type]
|
||||||
expect(@clz.from_env(@scope)).to_not be_nil
|
creds = @clz.from_env(@scope)
|
||||||
expect(subject.client_id).to eq(cred_json[:client_id])
|
expect(creds).to_not be_nil
|
||||||
expect(subject.client_secret).to eq(cred_json[:client_secret])
|
expect(creds.client_id).to eq(cred_json[:client_id])
|
||||||
expect(subject.refresh_token).to eq(cred_json[:refresh_token])
|
expect(creds.client_secret).to eq(cred_json[:client_secret])
|
||||||
|
expect(creds.refresh_token).to eq(cred_json[:refresh_token])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -227,4 +228,87 @@ describe Google::Auth::UserRefreshCredentials do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
shared_examples 'revoked token' do
|
||||||
|
it 'should nil the refresh token' do
|
||||||
|
expect(@client.refresh_token).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should nil the access token' do
|
||||||
|
expect(@client.access_token).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should mark the token as expired' do
|
||||||
|
expect(@client.expired?).to be_truthy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'when revoking a refresh token' do
|
||||||
|
let(:stubs) do
|
||||||
|
Faraday::Adapter::Test::Stubs.new do |stub|
|
||||||
|
stub.get('/o/oauth2/revoke') do |env|
|
||||||
|
expect(env.params['token']).to eql 'refreshtoken'
|
||||||
|
[200]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:connection) do
|
||||||
|
Faraday.new do |c|
|
||||||
|
c.adapter(:test, stubs)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
before(:example) do
|
||||||
|
@client.revoke!(connection: connection)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'revoked token'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'when revoking an access token' do
|
||||||
|
let(:stubs) do
|
||||||
|
Faraday::Adapter::Test::Stubs.new do |stub|
|
||||||
|
stub.get('/o/oauth2/revoke') do |env|
|
||||||
|
expect(env.params['token']).to eql 'accesstoken'
|
||||||
|
[200]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:connection) do
|
||||||
|
Faraday.new do |c|
|
||||||
|
c.adapter(:test, stubs)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
before(:example) do
|
||||||
|
@client.refresh_token = nil
|
||||||
|
@client.access_token = 'accesstoken'
|
||||||
|
@client.revoke!(connection: connection)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'revoked token'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'when revoking an invalid token' do
|
||||||
|
let(:stubs) do
|
||||||
|
Faraday::Adapter::Test::Stubs.new do |stub|
|
||||||
|
stub.get('/o/oauth2/revoke') do |_env|
|
||||||
|
[400]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:connection) do
|
||||||
|
Faraday.new do |c|
|
||||||
|
c.adapter(:test, stubs)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises an authorization error' do
|
||||||
|
expect { @client.revoke!(connection: connection) }.to raise_error(
|
||||||
|
Signet::AuthorizationError)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -46,6 +46,7 @@ require 'faraday'
|
||||||
require 'rspec'
|
require 'rspec'
|
||||||
require 'logging'
|
require 'logging'
|
||||||
require 'rspec/logging_helper'
|
require 'rspec/logging_helper'
|
||||||
|
require 'webmock/rspec'
|
||||||
|
|
||||||
# Allow Faraday to support test stubs
|
# Allow Faraday to support test stubs
|
||||||
Faraday::Adapter.load_middleware(:test)
|
Faraday::Adapter.load_middleware(:test)
|
||||||
|
@ -55,4 +56,28 @@ Faraday::Adapter.load_middleware(:test)
|
||||||
RSpec.configure do |config|
|
RSpec.configure do |config|
|
||||||
include RSpec::LoggingHelper
|
include RSpec::LoggingHelper
|
||||||
config.capture_log_messages
|
config.capture_log_messages
|
||||||
|
config.include WebMock::API
|
||||||
|
end
|
||||||
|
|
||||||
|
module TestHelpers
|
||||||
|
include WebMock::API
|
||||||
|
include WebMock::Matchers
|
||||||
|
end
|
||||||
|
|
||||||
|
class DummyTokenStore
|
||||||
|
def initialize
|
||||||
|
@tokens = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def load(id)
|
||||||
|
@tokens[id]
|
||||||
|
end
|
||||||
|
|
||||||
|
def store(id, token)
|
||||||
|
@tokens[id] = token
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete(id)
|
||||||
|
@tokens.delete(id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue