Google API Server-to-Server Communication not working (Ruby implementation) - ruby

I have followed all the steps to set up a Domain-Wide Delegation of Authority (https://developers.google.com/admin-sdk/reports/v1/guides/delegation) and followed this link too (https://developers.google.com/identity/protocols/OAuth2ServiceAccount#delegatingauthority) and I ended up creating this class in Ruby.
class Gsuite::ServiceAccount
def initialize(person: nil)
end
def authorized_client
Google::Auth::ServiceAccountCredentials.make_creds(
authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
json_key_io: StringIO.new(File.read AppConfig[:gsuite_service_account]
[:credentials_file]),
scope: AppConfig[:gsuite_service_account][:scope])
client = authorizer.fetch_access_token!
end
end
This class returns me this hash
{"access_token"=>"a_long_token_string_here", "expires_in"=>3600, "token_type"=>"Bearer"}
Then, I've created this method (within my Admin class) to connect to Gmail Service
def gsuite_client_access
#client ||= Gsuite::ServiceAccount.new(person: self.email.to_s).authorized_client
authorized_client = Google::Apis::GmailV1::GmailService.new
authorized_client.authorization = #client
authorized_client
end
So, when I try to list my Gmail Messages with this line in another part of the code
inbox = current_admin.gsuite_client_access.list_user_messages('me', max_results: 10)
I get the following error message =>
Sending HTTP get https://www.googleapis.com/gmail/v1/users/me/messages?maxResults=10
401
#<Hurley::Response GET https://www.googleapis.com/gmail/v1/users/me/messages?maxResults=10 == 401 (238 bytes) 645ms>
Caught error Unauthorized
Error - #<Google::Apis::AuthorizationError: Unauthorized>
Retrying after authentication failure
Google::Apis::AuthorizationError: Unauthorized
Any ideas what's missing here?

Finally, I got it working. Turns out, you need to use this line to use the sub method to the "impersonated user" to be able to connect.
authorizer.sub = #person
And, for your delight, here is the updated test code for reading Gmail messages so you can follow in case you want to use it. Just remember to save the credentials.json file in your project folder to make it work and use the same scope you added in the GSuite Dashboard.
class Gsuite::ServiceAccount
def initialize(person: nil)
#person = person
end
def read_messages
client = service_account_access
inbox = client.list_user_messages(#person, max_results: 5, label_ids: "INBOX" )
if inbox.result_size_estimate.nonzero?
inbox.messages.each do |message|
response = client.get_user_message(#person, message.id)
end
end
end
private
def service_account_access
token = authorized_client
client = Signet::OAuth2::Client.new(access_token: token['access_token'])
client.expires_in = Time.current + token["expires_in"]
auth_client = Google::Apis::GmailV1::GmailService.new
auth_client.authorization = client
auth_client
end
def authorized_client
authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
json_key_io: StringIO.new(File.read AppConfig[:credentials_file]),
scope: AppConfig[:gsuite_scope]).dup
authorizer.sub = #person
authorizer.fetch_access_token!
end
end

Related

How do I authorize a service account for Google Calendar API in Ruby?

How do I authorize a service account for Google Calendar API in Ruby? I tried the quick start guide, but it crashed.
https://developers.google.com/calendar/quickstart/ruby#step_3_set_up_the_sample
quickstart.rb
require 'google/apis/calendar_v3'
require 'googleauth'
require 'googleauth/stores/file_token_store'
require 'fileutils'
OOB_URI = 'urn:ietf:wg:oauth:2.0:oob'.freeze
APPLICATION_NAME = 'Google Calendar API Ruby Quickstart'.freeze
CLIENT_SECRETS_PATH = 'client_secrets.json'.freeze
CREDENTIALS_PATH = 'token.yaml'.freeze
SCOPE = Google::Apis::CalendarV3::AUTH_CALENDAR_READONLY
##
# Ensure valid credentials, either by restoring from the saved credentials
# files or intitiating an OAuth2 authorization. If authorization is required,
# the user's default browser will be launched to approve the request.
#
# #return [Google::Auth::UserRefreshCredentials] OAuth2 credentials
def authorize
client_id = Google::Auth::ClientId.from_file(CLIENT_SECRETS_PATH) ### LINE 19
token_store = Google::Auth::Stores::FileTokenStore.new(file: CREDENTIALS_PATH)
authorizer = Google::Auth::UserAuthorizer.new(client_id, SCOPE, token_store)
user_id = 'default'
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:\n' + url
code = gets
credentials = authorizer.get_and_store_credentials_from_code(
user_id: user_id, code: code, base_url: OOB_URI
)
end
credentials
end
# Initialize the API
service = Google::Apis::CalendarV3::CalendarService.new
service.client_options.application_name = APPLICATION_NAME
service.authorization = authorize
# Fetch the next 10 events for the user
calendar_id = 'primary'
response = service.list_events(calendar_id,
max_results: 10,
single_events: true,
order_by: 'startTime',
time_min: Time.now.iso8601)
puts 'Upcoming events:'
puts 'No upcoming events found' if response.items.empty?
response.items.each do |event|
start = event.start.date || event.start.date_time
puts "- #{event.summary} (#{start})"
end
Console
>ruby quickstart.rb
C:/ruby23/lib/ruby/gems/2.3.0/gems/googleauth-0.6.2/lib/googleauth/client_id.rb:97:in `from_hash': Expected top level property 'installed' or 'web' to be present. (RuntimeError)
from C:/ruby23/lib/ruby/gems/2.3.0/gems/googleauth-0.6.2/lib/googleauth/client_id.rb:83:in `block in from_file'
from C:/ruby23/lib/ruby/gems/2.3.0/gems/googleauth-0.6.2/lib/googleauth/client_id.rb:80:in `open'
from C:/ruby23/lib/ruby/gems/2.3.0/gems/googleauth-0.6.2/lib/googleauth/client_id.rb:80:in `from_file'
from quickstart.rb:19:in `authorize'
from quickstart.rb:39:in `<main>'
The (outdated) documentation for authorizing service accounts only has examples for Java and Python.
https://developers.google.com/identity/protocols/OAuth2ServiceAccount#authorizingrequests
Old similar question with 0 answers: Google Calendar API in Ruby using Service Account
For anyone still looking this is what worked for me:
require 'google/apis/calendar_v3'
require 'googleauth'
scope = 'https://www.googleapis.com/auth/calendar'
authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
json_key_io: File.open('/path/to/creds.json'),
scope: scope)
authorizer.fetch_access_token!
service = Google::Apis::CalendarV3::CalendarService.new
service.authorization = authorizer
calendar_id = 'primary'
response = service.list_events(calendar_id,
max_results: 10,
single_events: true,
order_by: 'startTime',
time_min: Time.now.iso8601)
puts 'Upcoming events:'
puts 'No upcoming events found' if response.items.empty?
response.items.each do |event|
start = event.start.date || event.start.date_time
puts "- #{event.summary} (#{start})"
end
The trick was finding the docs for the google-auth-library-ruby
https://github.com/googleapis/google-auth-library-ruby
OK I found a way.
https://developers.google.com/api-client-library/ruby/auth/service-accounts
require 'google/apis/calendar_v3'
require 'googleauth'
# Get the environment configured authorization
scopes = ['https://www.googleapis.com/auth/calendar']
authorization = Google::Auth.get_application_default(scopes)
# Clone and set the subject
auth_client = authorization.dup
auth_client.sub = 'myemail#mydomain.com'
auth_client.fetch_access_token!
# Initialize the API
service = Google::Apis::CalendarV3::CalendarService.new
service.authorization = auth_client
# Fetch the next 10 events for the user
calendar_id = 'primary'
response = service.list_events(calendar_id,
max_results: 10,
single_events: true,
order_by: 'startTime',
time_min: Time.now.iso8601)
puts 'Upcoming events:'
puts 'No upcoming events found' if response.items.empty?
response.items.each do |event|
start = event.start.date || event.start.date_time
puts "- #{event.summary} (#{start})"
end
And
>set GOOGLE_APPLICATION_CREDENTIALS=client_secrets.json
C:\Users\Chloe\workspace>ruby quickstart.rb
Upcoming events:
- Test (2018-05-17)
- SSL Certificate for CMS (2019-02-13)
But I wonder where it saves the refresh token and access token? All I have to do now is make it work for ephemeral file systems like Heroku and store the tokens in the database.

User Session Authentication in Rails 5

I am trying to implement notification using the action cable in Rails 5.
After reading the tutorial for the Action cable, which work on the session & Cookies based Authentication to receive the notification for the current logged in user.
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
logger.add_tags 'ActionCable', current_user.username
end
protected
def find_verified_user
if verified_user = env['warden'].session(:user)
verified_user
else
reject_unauthorized_connection
end
end
end
In the find_verified_user , I am not able to get the user object from the session.
Can anyone help me, to authenticate the user in action cable.
If your session_store (config/initializers/session_store.rb) uses :cookie_store, then you can read the session from encrypted cookies:
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
if user = User.find_by(id: session[:user_id])
self.current_user = user
else
reject_unauthorized_connection
end
end
private
def session
key = Rails.application.config.session_options.fetch(:key)
cookies.encrypted[key]&.symbolize_keys || {}
end
end
end
Because you have not access to session, you must use cookie:
1) Go to your_app/config/initializers/session_store.rb and paste this code:
Rails.application.config.session_store :cookie_store, key: 'your_super_secret_code'
2) After anywhere you can get user id:
key = Rails.application.config.session_options.fetch(:key)
cookies.encrypted[key]&.symbolize_keys || {}
User.find(cookies["warden.user.user.key"][0][0])
Example for session: How can I find a devise user by it's session id?

testing padrino post methods are stopped by csrf

I have a padrino controller with a single post method and a single get method. I can use rack-test to test the get method but not the post method. When I am testing the request returns 403. I think this is because of padrino's built in csrf protection because when I comment out the line with set :protect_from_csrf, true I can test the post route. Obviously I don't want to comment out this line as csrf is useful. How can I get temporary access to test these routes for the purpose of testing?
Controller
SailPowerCourses::Admin.controllers :owners do
get :index do
puts 'hello'
end
post :index do
puts params
end
end
Test
class OwnersControllerTest < MiniTest::Test
def setup
app SailPowerCourses::Admin
end
def test_creates_an_owner
email = 'test#example.com'
assert_empty Owner
post '/owners', owner: {email: email}
puts last_response.status
refute_empty Owner
end
def test_other
email = 'test#example.com'
get '/owners', owner: {email: email}
end
end
```
When setting up an app in minitest you can use a block to access and change settings. such as csrf protection. I found the best solution to be the following. in test_config.rb I set up a version of the app with csrf protection off.
class OwnersControllerTest < MiniTest::Test
def setup
app SailPowerCourses::Admin do
set :protect_from_csrf, false
end
end
def test_creates_an_owner
email = 'test#example.com'
assert_empty Owner
post '/owners', owner: {email: email}
puts last_response.status
refute_empty Owner
end
def test_other
email = 'test#example.com'
get '/owners', owner: {email: email}
end
end

Trying to log in with a specific client from my app

So I`m using the google-api-ruby-client to make a google analytics app, and I wanted to log in every time with a specific user instead of having to be redirected to oauth everytime.
My question is: is there any way to insert the login/password of that client into the code so when I use the app I don't have to authorize anything?
Here is the code that makes the autentication:
class TokenPair
attr_accessor :id
attr_accessor :refresh_token
attr_accessor :access_token
attr_accessor :issued_at
def initialize
##id ||= 1
self.id = ##id
##id += 1
end
def self.get(id)
##els ||= {}
tp = ##els.fetch(id, TokenPair.new)
##els[tp.id] = tp
end
def update_token!(object)
self.refresh_token = object.refresh_token
self.access_token = object.access_token
#self.expires_in = object.expires_in
self.issued_at = object.issued_at
end
def to_hash
{
refresh_token: refresh_token,
access_token: access_token,
# expires_in: expires_in,
issued_at: issued_at ? Time.at(issued_at) : ''
}
end
end
def logout
reset_session
redirect_to root_url
end
def logged_in?
if session["token_id"]
redirect_to profile_path
end
end
def login
logged_in?
end
def self.params
##params
end
def update_token
#client = Google::APIClient.new
#client.authorization.client_id = '209273986197.apps.googleusercontent.com'
#client.authorization.client_secret = '6sCG5ynCiz9Ej07pwIm653TU'
#client.authorization.scope = 'https://www.googleapis.com/auth/analytics.readonly'
#client.authorization.redirect_uri = callback_url
#client.authorization.code = params[:code] if params[:code]
logger.debug session.inspect
if session[:token_id]
# Load the access token here if it's available
token_pair = TokenPair.get(session[:token_id])
#client.authorization.update_token!(token_pair.to_hash)
end
if #client.authorization.refresh_token && #client.authorization.expired?
#client.authorization.fetch_access_token!
end
#analytics = #client.discovered_api('analytics', 'v3')
unless #client.authorization.access_token || request.path_info =~ /^\/oauth2/
redirect_to authorize_path
end
end
def authorize
redirect_to #client.authorization.authorization_uri.to_s, :status => 303
end
def callback
begin
#client.authorization.fetch_access_token!
# Persist the token here
token_pair = TokenPair.get(session[:token_id])
token_pair.update_token!(#client.authorization)
session[:token_id] = token_pair.id
redirect_to profile_url
rescue ArgumentError
redirect_to root_url
end
end
def get_web_properties
result = #client.execute(
api_method: #analytics.management.profiles.list,
parameters: {accountId: "~all", webPropertyId: "~all"}
#parameters: {accountId: "582717"}
)
result.data
end
Even if your app is always acting as the same user, OAuth is still the preferred mechanism for various reasons -- easier to revoke access, limited access in case of compromise, client login auth mechanism is deprecated, etc.
By default the client will request offline access, which allows you to keep refreshing the access token indefinitely without having to go through the full oauth flow each time. You can simply authorize the app once, save the refresh token, and when it expires, just call fetch_access_token! again. If you're using the latest version of the library, the client will automatically attempt refreshing the token if it expired, so all you need to take care of is the initial authorization & storage of the refresh token.

OAuth2 gem: implementation for third party - access other accounts data in seek.com

I'm connecting to an API (seek.com.au) which uses OAuth2 for authentication. I struggled with OAuth2 gem for a while and I ended up writing the plain requests as will follow. Although this is working, I still would like to understand what was wrong with my initial OAuth2 implementation.
Here is my current working code, **the third party* relates to the fact that I'm accessing the API with an account that have access to other accounts. This logic is mainly implemented in the scope method (at the bottom of this snippet).
The following includes some extra logic, but the get_grant and post_for_token methods should include everything.
module Seek::Base
CONFIG = YAML.load_file "#{Rails.root}/config/seek.yml"
HOST = 'http://test.api.seek.com.au/v1/'
REQUEST_URIS = {
get_grant: HOST + 'OAuth/auth',
post_for_token: HOST + 'OAuth/token',
get_applications: HOST + 'advertiser/applications'
}
def uri_for(request, params = {})
uri = REQUEST_URIS[request]
uri += '?' + params.to_param if params.any?
URI.parse uri
end
end
class Seek::OAuth2 # TODO? is instance needed?
include Seek::Base
# by account_id
##tokens = {}
def initialize(account_id)
#account_id = account_id
end
def self.authenticate!(account_id)
new(account_id).authenticate!
end
# eg: when a request responded that the token is expired
def self.expire_token(account_id)
##tokens.delete account_id
end
###########################################################################
############################### begin #####################################
# authentication
# see: http://developer.seek.com.au/docs/partner-api/api-methods/oauth-2.0
def authenticate!
##tokens[#account_id] ||= begin
grant = get_grant
raise Exception.new(#error) if #error
Rails.logger.info "Retrive token for #{#account_id}"
post_for_token
end
end
private
# part of t he authentication process
# as we have one account for many entities, we use third party variation
# see: http://developer.seek.com.au/docs/partner-api/api-methods/oauth2/auth
def get_grant
uri = uri_for :get_grant, {response_type: :code, client_id: username, scope: scope}
response = Net::HTTP.get_response uri
params = response['location'].split('?').second
#error = params.split('error=').second
#grant_code = params.split('code=').second
end
# part of the authentication process
# see: http://developer.seek.com.au/docs/partner-api/api-methods/oauth2/token
def post_for_token
uri = uri_for :post_for_token
request = Net::HTTP::Post.new uri.path, {'Content-Type' => 'application/x-www-form-urlencoded;charset=UTF-8'}
request.set_form grant_type: :authorization_code, code: #grant_code, redirect_uri: ''
request.basic_auth username, password
response = Net::HTTP.new(uri.host, uri.port).request request
JSON(response.body)['access_token']
end
########################## end ############################################
###########################################################################
def username
CONFIG['credentials']['username']
end
def password
CONFIG['credentials']['password']
end
############## the scope method
############## I think I need to insert this in the OAuth request
def scope
"urn:seek:thirdparty:username:#{username},urn:seek:advertiser:identity:#{#account_id}"
end
end
And here are the few lines (to replace the authenticate! method) that were meant to do the same, but sadly, OAuth returns invalid_client.
client = OAuth2::Client.new(username, password, :site => 'http://test.api.seek.com.au/v1')
client.auth_code.authorize_url redirect_uri: ''
token = client.auth_code.get_token 'authorization_code_value',
headers: {'Authorization' => %^Basic #{Base64.encode64 "#{username}:#{password}"}^ }
I think the problem relies in the scope method created by OAuth (see bottom of the first snippet), but I'm not sure and anyways I couldn't find a way to amend it.
I also opened an issue in GitHub, but I think this is covered, just it's not documented (or I can't find it).
Ruby (Rails) implementation
This implementation is not using any wrapper, I tried the gem OAuth2 but I was not able to get the grant code,
I presume because the third party implementation require a customization of the scope which I was not able to set with the gem.
module Api::Base
CONFIG = YAML.load_file "#{Rails.root}/config/api.yml"
HOST = 'https://api.com.au/v1/'
REQUEST_URIS = {
get_grant: HOST + 'OAuth/auth',
post_for_token: HOST + 'OAuth/token',
get_applications: HOST + 'advertiser/applications'
}
def uri_for(request, params = {})
uri = REQUEST_URIS[request]
uri += '?' + params.to_param if params.any?
URI.parse uri
end
end
class Api::OAuth2
include Api::Base
# by account_id
##tokens = {}
def initialize(account_id)
#account_id = account_id
end
def self.authenticate!(account_id)
new(account_id).authenticate!
end
# eg: when a request responded that the token is expired
def self.expire_token(account_id)
##tokens.delete account_id
end
# authentication
def authenticate!
##tokens[#account_id] ||= begin
grant = get_grant
raise StandardError.new(#error) if #error
puts "Retrive token for #{#account_id}"
post_for_token
end
end
private
# part of t he authentication process
# as we have one account for many entities, we use third party variation
def get_grant
uri = uri_for :get_grant, {response_type: :code, client_id: username, scope: scope}
http = Net::HTTP.new uri.host, uri.port
http.use_ssl = uri.port == 443
puts "SSL not set for uri #{uri}" unless http.use_ssl?
response = http.get uri.to_s
raise Exception.new(response.message) unless response.is_a? Net::HTTPFound
params = response['location'].split('?').second
#error = params.split('error=').second
#grant_code = params.split('code=').second
end
# part of the authentication process
def post_for_token
uri = uri_for :post_for_token
request = Net::HTTP::Post.new uri.path, {'Content-Type' => 'application/x-www-form-urlencoded;charset=UTF-8'}
request.set_form grant_type: 'authorization_code', code: #grant_code, redirect_uri: ''
request.basic_auth username, password
http = Net::HTTP.new uri.host, uri.port
http.use_ssl = uri.port == 443
response = http.start {|http| http.request request}
JSON(response.body)['access_token']
end
end
def username
CONFIG['credentials']['username']
end
def password
CONFIG['credentials']['password']
end
def scope
"urn:api:thirdparty:username:#{username},urn:api:advertiser:identity:#{#account_id}"
end
end
I'm still planning to use OAuth 2, I'll post my updates here if any

Resources