Merge pull request #51 from sqrrrl/master
Round 2 of 3LO support - web flow
This commit is contained in:
commit
2be5bfe375
|
@ -29,3 +29,4 @@ Metrics/MethodLength:
|
||||||
Style/FormatString:
|
Style/FormatString:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'lib/googleauth/user_authorizer.rb'
|
- 'lib/googleauth/user_authorizer.rb'
|
||||||
|
- 'lib/googleauth/web_user_authorizer.rb'
|
||||||
|
|
1
Gemfile
1
Gemfile
|
@ -14,6 +14,7 @@ group :development do
|
||||||
gem 'redis', '~> 3.2'
|
gem 'redis', '~> 3.2'
|
||||||
gem 'fakeredis', '~> 0.5'
|
gem 'fakeredis', '~> 0.5'
|
||||||
gem 'webmock', '~> 1.21'
|
gem 'webmock', '~> 1.21'
|
||||||
|
gem 'rack-test', '~> 0.6'
|
||||||
end
|
end
|
||||||
|
|
||||||
platforms :jruby do
|
platforms :jruby do
|
||||||
|
|
|
@ -36,6 +36,7 @@ require 'googleauth/service_account'
|
||||||
require 'googleauth/user_refresh'
|
require 'googleauth/user_refresh'
|
||||||
require 'googleauth/client_id'
|
require 'googleauth/client_id'
|
||||||
require 'googleauth/user_authorizer'
|
require 'googleauth/user_authorizer'
|
||||||
|
require 'googleauth/web_user_authorizer'
|
||||||
|
|
||||||
module Google
|
module Google
|
||||||
# Module Auth provides classes that provide Google-specific authorization
|
# Module Auth provides classes that provide Google-specific authorization
|
||||||
|
|
|
@ -0,0 +1,288 @@
|
||||||
|
# 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'
|
||||||
|
require 'googleauth/signet'
|
||||||
|
require 'googleauth/user_authorizer'
|
||||||
|
require 'googleauth/user_refresh'
|
||||||
|
require 'securerandom'
|
||||||
|
|
||||||
|
module Google
|
||||||
|
module Auth
|
||||||
|
# Varation on {Google::Auth::UserAuthorizer} adapted for Rack based
|
||||||
|
# web applications.
|
||||||
|
#
|
||||||
|
# Example usage:
|
||||||
|
#
|
||||||
|
# get('/') do
|
||||||
|
# user_id = request.session['user_email']
|
||||||
|
# credentials = authorizer.get_credentials(user_id, request)
|
||||||
|
# if credentials.nil?
|
||||||
|
# redirect authorizer.get_redirect_uri(user_id, request)
|
||||||
|
# end
|
||||||
|
# # Credentials are valid, can call APIs
|
||||||
|
# ...
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# get('/oauth2callback') do
|
||||||
|
# user_id = request.session['user_email']
|
||||||
|
# _, return_uri = authorizer.handle_auth_callback(user_id, request)
|
||||||
|
# redirect return_uri
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# Instead of implementing the callback directly, applications are
|
||||||
|
# encouraged to use {Google::Auth::Web::AuthCallbackApp} instead.
|
||||||
|
#
|
||||||
|
# For rails apps, see {Google::Auth::ControllerHelpers}
|
||||||
|
#
|
||||||
|
# @see {Google::Auth::AuthCallbackApp}
|
||||||
|
# @see {Google::Auth::ControllerHelpers}
|
||||||
|
# @note Requires sessions are enabled
|
||||||
|
class WebUserAuthorizer < Google::Auth::UserAuthorizer
|
||||||
|
STATE_PARAM = 'state'
|
||||||
|
AUTH_CODE_KEY = 'code'
|
||||||
|
ERROR_CODE_KEY = 'error'
|
||||||
|
SESSION_ID_KEY = 'session_id'
|
||||||
|
CALLBACK_STATE_KEY = 'g-auth-callback'
|
||||||
|
CURRENT_URI_KEY = 'current_uri'
|
||||||
|
XSRF_KEY = 'g-xsrf-token'
|
||||||
|
SCOPE_KEY = 'scope'
|
||||||
|
|
||||||
|
NIL_REQUEST_ERROR = 'Request is required.'
|
||||||
|
NIL_SESSION_ERROR = 'Sessions must be enabled'
|
||||||
|
MISSING_AUTH_CODE_ERROR = 'Missing authorization code in request'
|
||||||
|
AUTHORIZATION_ERROR = 'Authorization error: %s'
|
||||||
|
INVALID_STATE_TOKEN_ERROR = 'State token does not match expected value'
|
||||||
|
|
||||||
|
class << self
|
||||||
|
attr_accessor :default
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handle the result of the oauth callback. This version defers the
|
||||||
|
# exchange of the code by temporarily stashing the results in the user's
|
||||||
|
# session. This allows apps to use the generic
|
||||||
|
# {Google::Auth::WebUserAuthorizer::CallbackApp} handler for the callback
|
||||||
|
# without any additional customization.
|
||||||
|
#
|
||||||
|
# Apps that wish to handle the callback directly should use
|
||||||
|
# {#handle_auth_callback} instead.
|
||||||
|
#
|
||||||
|
# @param [Rack::Request] request
|
||||||
|
# Current request
|
||||||
|
def self.handle_auth_callback_deferred(request)
|
||||||
|
callback_state, redirect_uri = extract_callback_state(request)
|
||||||
|
request.session[CALLBACK_STATE_KEY] = MultiJson.dump(callback_state)
|
||||||
|
redirect_uri
|
||||||
|
end
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
super(client_id, scope, token_store, callback_uri)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handle the result of the oauth callback. Exchanges the authorization
|
||||||
|
# code from the request and persists to storage.
|
||||||
|
#
|
||||||
|
# @param [String] user_id
|
||||||
|
# Unique ID of the user for loading/storing credentials.
|
||||||
|
# @param [Rack::Request] request
|
||||||
|
# Current request
|
||||||
|
# @return (Google::Auth::UserRefreshCredentials, String)
|
||||||
|
# credentials & next URL to redirect to
|
||||||
|
def handle_auth_callback(user_id, request)
|
||||||
|
callback_state, redirect_uri = WebUserAuthorizer.extract_callback_state(
|
||||||
|
request)
|
||||||
|
WebUserAuthorizer.validate_callback_state(callback_state, request)
|
||||||
|
credentials = get_and_store_credentials_from_code(
|
||||||
|
user_id: user_id,
|
||||||
|
code: callback_state[AUTH_CODE_KEY],
|
||||||
|
scope: callback_state[SCOPE_KEY],
|
||||||
|
base_url: request.url)
|
||||||
|
[credentials, redirect_uri]
|
||||||
|
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 [Rack::Request] request
|
||||||
|
# Current request
|
||||||
|
# @param [String] redirect_to
|
||||||
|
# Optional URL to proceed to after authorization complete. Defaults to
|
||||||
|
# the current URL.
|
||||||
|
# @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 = {})
|
||||||
|
options = options.dup
|
||||||
|
request = options[:request]
|
||||||
|
fail NIL_REQUEST_ERROR if request.nil?
|
||||||
|
fail NIL_SESSION_ERROR if request.session.nil?
|
||||||
|
|
||||||
|
redirect_to = options[:redirect_to] || request.url
|
||||||
|
request.session[XSRF_KEY] = SecureRandom.base64
|
||||||
|
options[:state] = MultiJson.dump(
|
||||||
|
SESSION_ID_KEY => request.session[XSRF_KEY],
|
||||||
|
CURRENT_URI_KEY => redirect_to)
|
||||||
|
options[:base_url] = request.url
|
||||||
|
super(options)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Fetch stored credentials for the user.
|
||||||
|
#
|
||||||
|
# @param [String] user_id
|
||||||
|
# Unique ID of the user for loading/storing credentials.
|
||||||
|
# @param [Rack::Request] request
|
||||||
|
# Current request
|
||||||
|
# @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
|
||||||
|
# @raise [Signet::AuthorizationError]
|
||||||
|
# May raise an error if an authorization code is present in the session
|
||||||
|
# and exchange of the code fails
|
||||||
|
def get_credentials(user_id, request, scope = nil)
|
||||||
|
if request.session.key?(CALLBACK_STATE_KEY)
|
||||||
|
# Note - in theory, no need to check required scope as this is
|
||||||
|
# expected to be called immediately after a return from authorization
|
||||||
|
state_json = request.session.delete(CALLBACK_STATE_KEY)
|
||||||
|
callback_state = MultiJson.load(state_json)
|
||||||
|
WebUserAuthorizer.validate_callback_state(callback_state, request)
|
||||||
|
get_and_store_credentials_from_code(
|
||||||
|
user_id: user_id,
|
||||||
|
code: callback_state[AUTH_CODE_KEY],
|
||||||
|
scope: callback_state[SCOPE_KEY],
|
||||||
|
base_url: request.url)
|
||||||
|
else
|
||||||
|
super(user_id, scope)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.extract_callback_state(request)
|
||||||
|
state = MultiJson.load(request[STATE_PARAM] || '{}')
|
||||||
|
redirect_uri = state[CURRENT_URI_KEY]
|
||||||
|
callback_state = {
|
||||||
|
AUTH_CODE_KEY => request[AUTH_CODE_KEY],
|
||||||
|
ERROR_CODE_KEY => request[ERROR_CODE_KEY],
|
||||||
|
SESSION_ID_KEY => state[SESSION_ID_KEY],
|
||||||
|
SCOPE_KEY => request[SCOPE_KEY]
|
||||||
|
}
|
||||||
|
[callback_state, redirect_uri]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Verifies the results of an authorization callback
|
||||||
|
#
|
||||||
|
# @param [Hash] state
|
||||||
|
# Callback state
|
||||||
|
# @option state [String] AUTH_CODE_KEY
|
||||||
|
# The authorization code
|
||||||
|
# @option state [String] ERROR_CODE_KEY
|
||||||
|
# Error message if failed
|
||||||
|
# @param [Rack::Request] request
|
||||||
|
# Current request
|
||||||
|
def self.validate_callback_state(state, request)
|
||||||
|
if state[AUTH_CODE_KEY].nil?
|
||||||
|
fail Signet::AuthorizationError, MISSING_AUTH_CODE_ERROR
|
||||||
|
elsif state[ERROR_CODE_KEY]
|
||||||
|
fail Signet::AuthorizationError,
|
||||||
|
sprintf(AUTHORIZATION_ERROR, state[ERROR_CODE_KEY])
|
||||||
|
elsif request.session[XSRF_KEY] != state[SESSION_ID_KEY]
|
||||||
|
fail Signet::AuthorizationError, INVALID_STATE_TOKEN_ERROR
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Small Rack app which acts as the default callback handler for the app.
|
||||||
|
#
|
||||||
|
# To configure in Rails, add to routes.rb:
|
||||||
|
#
|
||||||
|
# match '/oauth2callback',
|
||||||
|
# to: Google::Auth::WebUserAuthorizer::CallbackApp,
|
||||||
|
# via: :all
|
||||||
|
#
|
||||||
|
# With Rackup, add to config.ru:
|
||||||
|
#
|
||||||
|
# map '/oauth2callback' do
|
||||||
|
# run Google::Auth::WebUserAuthorizer::CallbackApp
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# Or in a classic Sinatra app:
|
||||||
|
#
|
||||||
|
# get('/oauth2callback') do
|
||||||
|
# Google::Auth::WebUserAuthorizer::CallbackApp.call(env)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# @see {Google::Auth::WebUserAuthorizer}
|
||||||
|
class CallbackApp
|
||||||
|
LOCATION_HEADER = 'Location'
|
||||||
|
REDIR_STATUS = 302
|
||||||
|
ERROR_STATUS = 500
|
||||||
|
|
||||||
|
# Handle a rack request. Simply stores the results the authorization
|
||||||
|
# in the session temporarily and redirects back to to the previously
|
||||||
|
# saved redirect URL. Credentials can be later retrieved by calling.
|
||||||
|
# {Google::Auth::Web::WebUserAuthorizer#get_credentials}
|
||||||
|
#
|
||||||
|
# See {Google::Auth::Web::WebUserAuthorizer#get_authorization_uri}
|
||||||
|
# for how to initiate authorization requests.
|
||||||
|
#
|
||||||
|
# @param [Hash] env
|
||||||
|
# Rack environment
|
||||||
|
# @return [Array]
|
||||||
|
# HTTP response
|
||||||
|
def self.call(env)
|
||||||
|
request = Rack::Request.new(env)
|
||||||
|
return_url = WebUserAuthorizer.handle_auth_callback_deferred(request)
|
||||||
|
if return_url
|
||||||
|
[REDIR_STATUS, { LOCATION_HEADER => return_url }, []]
|
||||||
|
else
|
||||||
|
[ERROR_STATUS, {}, ['No return URL is present in the request.']]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(env)
|
||||||
|
self.class.call(env)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -31,6 +31,7 @@ spec_dir = File.expand_path(File.join(File.dirname(__FILE__)))
|
||||||
$LOAD_PATH.unshift(spec_dir)
|
$LOAD_PATH.unshift(spec_dir)
|
||||||
$LOAD_PATH.uniq!
|
$LOAD_PATH.uniq!
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
require 'fakefs/safe'
|
require 'fakefs/safe'
|
||||||
require 'googleauth'
|
require 'googleauth'
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,159 @@
|
||||||
|
# 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/web_user_authorizer'
|
||||||
|
require 'uri'
|
||||||
|
require 'multi_json'
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'rack'
|
||||||
|
|
||||||
|
describe Google::Auth::WebUserAuthorizer do
|
||||||
|
include TestHelpers
|
||||||
|
|
||||||
|
let(:client_id) { Google::Auth::ClientId.new('testclient', 'notasecret') }
|
||||||
|
let(:scope) { %w(email profile) }
|
||||||
|
let(:token_store) { DummyTokenStore.new }
|
||||||
|
let(:authorizer) do
|
||||||
|
Google::Auth::WebUserAuthorizer.new(client_id, scope, token_store)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#get_authorization_url' do
|
||||||
|
let(:env) do
|
||||||
|
Rack::MockRequest.env_for(
|
||||||
|
'http://example.com:8080/test',
|
||||||
|
'REMOTE_ADDR' => '10.10.10.10')
|
||||||
|
end
|
||||||
|
let(:request) { Rack::Request.new(env) }
|
||||||
|
it 'should include current url in state' do
|
||||||
|
url = authorizer.get_authorization_url(request: request)
|
||||||
|
expect(url).to match(
|
||||||
|
%r{%22current_uri%22:%22http://example.com:8080/test%22})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should include request forgery token in state' do
|
||||||
|
expect(SecureRandom).to receive(:base64).and_return('aGVsbG8=')
|
||||||
|
url = authorizer.get_authorization_url(request: request)
|
||||||
|
expect(url).to match(/%22session_id%22:%22aGVsbG8=%22/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should include request forgery token in session' do
|
||||||
|
expect(SecureRandom).to receive(:base64).and_return('aGVsbG8=')
|
||||||
|
authorizer.get_authorization_url(request: request)
|
||||||
|
expect(request.session['g-xsrf-token']).to eq 'aGVsbG8='
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should resolve callback against base URL' do
|
||||||
|
url = authorizer.get_authorization_url(request: request)
|
||||||
|
expect(url).to match(
|
||||||
|
%r{redirect_uri=http://example.com:8080/oauth2callback})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should allow overriding the current URL' do
|
||||||
|
url = authorizer.get_authorization_url(
|
||||||
|
request: request,
|
||||||
|
redirect_to: '/foo')
|
||||||
|
expect(url).to match %r{%22current_uri%22:%22/foo%22}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should pass through login hint' do
|
||||||
|
url = authorizer.get_authorization_url(
|
||||||
|
request: request,
|
||||||
|
login_hint: 'user@example.com')
|
||||||
|
expect(url).to match(/login_hint=user@example.com/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'handles callback' 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
|
||||||
|
|
||||||
|
let(:env) do
|
||||||
|
Rack::MockRequest.env_for(
|
||||||
|
'http://example.com:8080/oauth2callback?code=authcode&'\
|
||||||
|
'state=%7B%22current_uri%22%3A%22%2Ffoo%22%2C%22'\
|
||||||
|
'session_id%22%3A%22abc%22%7D',
|
||||||
|
'REMOTE_ADDR' => '10.10.10.10')
|
||||||
|
end
|
||||||
|
let(:request) { Rack::Request.new(env) }
|
||||||
|
|
||||||
|
before(:example) do
|
||||||
|
request.session['g-xsrf-token'] = 'abc'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should return credentials when valid code present' do
|
||||||
|
expect(credentials).to be_instance_of(
|
||||||
|
Google::Auth::UserRefreshCredentials)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should return next URL to redirect to' do
|
||||||
|
expect(next_url).to eq '/foo'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should fail if xrsf token in session and does not match request' do
|
||||||
|
request.session['g-xsrf-token'] = '123'
|
||||||
|
expect { credentials }.to raise_error(Signet::AuthorizationError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#handle_auth_callback' do
|
||||||
|
let(:result) { authorizer.handle_auth_callback('user1', request) }
|
||||||
|
let(:credentials) { result[0] }
|
||||||
|
let(:next_url) { result[1] }
|
||||||
|
|
||||||
|
it_behaves_like 'handles callback'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#handle_auth_callback_deferred and #get_credentials' do
|
||||||
|
let(:next_url) do
|
||||||
|
Google::Auth::WebUserAuthorizer.handle_auth_callback_deferred(request)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:credentials) do
|
||||||
|
next_url
|
||||||
|
authorizer.get_credentials('user1', request)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'handles callback'
|
||||||
|
end
|
||||||
|
end
|
|
@ -47,6 +47,10 @@ require 'rspec'
|
||||||
require 'logging'
|
require 'logging'
|
||||||
require 'rspec/logging_helper'
|
require 'rspec/logging_helper'
|
||||||
require 'webmock/rspec'
|
require 'webmock/rspec'
|
||||||
|
require 'multi_json'
|
||||||
|
|
||||||
|
# Preload adapter to work around Rubinius error with FakeFS
|
||||||
|
MultiJson.use(:json_gem)
|
||||||
|
|
||||||
# Allow Faraday to support test stubs
|
# Allow Faraday to support test stubs
|
||||||
Faraday::Adapter.load_middleware(:test)
|
Faraday::Adapter.load_middleware(:test)
|
||||||
|
|
Loading…
Reference in New Issue