Base version of 3LO support.
This commit is contained in:
parent
5cae4f7988
commit
e903a12563
|
@ -1,15 +1,31 @@
|
|||
# This configuration was generated by `rubocop --auto-gen-config`
|
||||
# on 2015-05-18 09:38:28 -0700 using RuboCop version 0.31.0.
|
||||
# This configuration was generated by
|
||||
# `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
|
||||
# one by one as the offenses are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
# versions of RuboCop, may require this file to be generated again.
|
||||
|
||||
# Offense count: 3
|
||||
# Offense count: 4
|
||||
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.
|
||||
Metrics/MethodLength:
|
||||
Max: 13
|
||||
Max: 22
|
||||
|
||||
# Offense count: 2
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||
Style/FormatString:
|
||||
Exclude:
|
||||
- 'lib/googleauth/user_authorizer.rb'
|
||||
|
|
18
Gemfile
18
Gemfile
|
@ -2,3 +2,21 @@ source 'https://rubygems.org'
|
|||
|
||||
# Specify your gem's dependencies in googleauth.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 'multi_json', '~> 1.11'
|
||||
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
|
||||
|
|
|
@ -34,6 +34,8 @@ require 'googleauth/credentials_loader'
|
|||
require 'googleauth/compute_engine'
|
||||
require 'googleauth/service_account'
|
||||
require 'googleauth/user_refresh'
|
||||
require 'googleauth/client_id'
|
||||
require 'googleauth/user_authorizer'
|
||||
|
||||
module Google
|
||||
# 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)
|
||||
if json_key_io
|
||||
json_key, clz = determine_creds_class(json_key_io)
|
||||
clz.new(json_key_io: StringIO.new(MultiJson.dump(json_key)),
|
||||
scope: scope)
|
||||
clz.make_creds(json_key_io: StringIO.new(MultiJson.dump(json_key)),
|
||||
scope: scope)
|
||||
else
|
||||
clz = read_creds
|
||||
clz.new(scope: scope)
|
||||
clz.make_creds(scope: scope)
|
||||
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'
|
||||
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
|
||||
# JSON key.
|
||||
def self.read_json_key(json_key_io)
|
||||
|
@ -58,24 +78,8 @@ module Google
|
|||
[json_key['private_key'], json_key['client_email']]
|
||||
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 = {})
|
||||
json_key_io, scope = options.values_at(:json_key_io, :scope)
|
||||
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))
|
||||
super(options)
|
||||
end
|
||||
|
||||
# Extends the base class.
|
||||
|
@ -97,7 +101,8 @@ module Google
|
|||
client_email: @issuer
|
||||
}
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -58,6 +58,25 @@ module Signet
|
|||
def updater_proc
|
||||
lambda(&method(:apply))
|
||||
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
|
||||
|
|
|
@ -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/credentials_loader'
|
||||
require 'googleauth/scope_util'
|
||||
require 'multi_json'
|
||||
|
||||
module Google
|
||||
|
@ -46,8 +47,30 @@ module Google
|
|||
# cf [Application Default Credentials](http://goo.gl/mkAHpZ)
|
||||
class UserRefreshCredentials < Signet::OAuth2::Client
|
||||
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
|
||||
|
||||
# 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
|
||||
# JSON key.
|
||||
def self.read_json_key(json_key_io)
|
||||
|
@ -59,24 +82,38 @@ module Google
|
|||
json_key
|
||||
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 = {})
|
||||
json_key_io, scope = options.values_at(:json_key_io, :scope)
|
||||
user_creds = self.class.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]
|
||||
}
|
||||
options ||= {}
|
||||
options[:token_credential_uri] ||= TOKEN_CRED_URI
|
||||
options[:authorization_uri] ||= AUTHORIZATION_URI
|
||||
super(options)
|
||||
end
|
||||
|
||||
super(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)
|
||||
# Revokes the credential
|
||||
def revoke!(options = {})
|
||||
c = options[:connection] || Faraday.default_connection
|
||||
resp = c.get(REVOKE_TOKEN_URI, token: refresh_token || access_token)
|
||||
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
|
||||
|
|
|
@ -56,17 +56,30 @@ shared_examples 'apply/apply! are OK' do
|
|||
# @make_auth_stubs, which should stub out the expected http behaviour of the
|
||||
# auth client
|
||||
describe '#fetch_access_token' do
|
||||
it 'should set access_token to the fetched value' do
|
||||
token = '1/abcdef1234567890'
|
||||
stubs = make_auth_stubs access_token: token
|
||||
c = Faraday.new do |b|
|
||||
let(:token) { '1/abcdef1234567890' }
|
||||
let(:stubs) do
|
||||
make_auth_stubs access_token: token
|
||||
end
|
||||
let(:connection) do
|
||||
Faraday.new do |b|
|
||||
b.adapter(:test, stubs)
|
||||
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)
|
||||
stubs.verify_stubbed_calls
|
||||
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
|
||||
|
||||
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
|
||||
@key = OpenSSL::PKey::RSA.new(2048)
|
||||
@client = ServiceAccountCredentials.new(
|
||||
@client = ServiceAccountCredentials.make_creds(
|
||||
json_key_io: StringIO.new(cred_json_text),
|
||||
scope: 'https://www.googleapis.com/auth/userinfo.profile'
|
||||
)
|
||||
|
@ -276,7 +276,7 @@ describe Google::Auth::ServiceAccountJwtHeaderCredentials do
|
|||
|
||||
before(:example) do
|
||||
@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
|
||||
|
||||
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
|
||||
@key = OpenSSL::PKey::RSA.new(2048)
|
||||
@client = UserRefreshCredentials.new(
|
||||
@client = UserRefreshCredentials.make_creds(
|
||||
json_key_io: StringIO.new(cred_json_text),
|
||||
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[REFRESH_TOKEN_VAR] = cred_json[:refresh_token]
|
||||
ENV[ACCOUNT_TYPE_VAR] = cred_json[:type]
|
||||
expect(@clz.from_env(@scope)).to_not be_nil
|
||||
expect(subject.client_id).to eq(cred_json[:client_id])
|
||||
expect(subject.client_secret).to eq(cred_json[:client_secret])
|
||||
expect(subject.refresh_token).to eq(cred_json[:refresh_token])
|
||||
creds = @clz.from_env(@scope)
|
||||
expect(creds).to_not be_nil
|
||||
expect(creds.client_id).to eq(cred_json[:client_id])
|
||||
expect(creds.client_secret).to eq(cred_json[:client_secret])
|
||||
expect(creds.refresh_token).to eq(cred_json[:refresh_token])
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -227,4 +228,87 @@ describe Google::Auth::UserRefreshCredentials do
|
|||
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
|
||||
|
|
|
@ -46,6 +46,7 @@ require 'faraday'
|
|||
require 'rspec'
|
||||
require 'logging'
|
||||
require 'rspec/logging_helper'
|
||||
require 'webmock/rspec'
|
||||
|
||||
# Allow Faraday to support test stubs
|
||||
Faraday::Adapter.load_middleware(:test)
|
||||
|
@ -55,4 +56,28 @@ Faraday::Adapter.load_middleware(:test)
|
|||
RSpec.configure do |config|
|
||||
include RSpec::LoggingHelper
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue