google-auth-library-ruby/lib/googleauth/user_authorizer.rb

286 lines
11 KiB
Ruby

# 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_authorization_url(
# base_url: OOB_URI)
# puts "Open the following URL in the browser and enter the " +
# "resulting code after authorization"
# puts url
# code = gets
# credentials = authorizer.get_and_store_credentials_from_code(
# user_id: user_id, code: code, base_url: OOB_URI)
# 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'.freeze
NIL_CLIENT_ID_ERROR = 'Client id can not be nil.'.freeze
NIL_SCOPE_ERROR = 'Scope can not be nil.'.freeze
NIL_USER_ID_ERROR = 'User ID can not be nil.'.freeze
NIL_TOKEN_STORE_ERROR = 'Can not call method if token store is nil'.freeze
MISSING_ABSOLUTE_URL_ERROR =
'Absolute base url required for relative callback url "%s"'.freeze
# 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)
raise NIL_CLIENT_ID_ERROR if client_id.nil?
raise 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)
saved_token = stored_token(user_id)
return nil if saved_token.nil?
data = MultiJson.load(saved_token)
if data.fetch('client_id', @client_id.id) != @client_id.id
raise 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
)
scope ||= @scope
if credentials.includes_scope?(scope)
return monitor_credentials(user_id, 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
# @private Fetch stored token with given user_id
#
# @param [String] user_id
# Unique ID of the user for loading/storing credentials.
# @return [String] The saved token from @token_store
def stored_token(user_id)
raise NIL_USER_ID_ERROR if user_id.nil?
raise NIL_TOKEN_STORE_ERROR if @token_store.nil?
@token_store.load(user_id)
end
# 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?
if base_url.nil? || URI(base_url).scheme.nil?
raise sprintf(ISSING_ABSOLUTE_URL_ERROR, @callback_uri)
end
URI.join(base_url, @callback_uri).to_s
end
end
end
end