How to migrate from OOB OAuth? - python

I am running a python script on the device with no screen/no browser (ssh access only). The script used offline Google's OAuth:
def get_authenticated_service( self , args):
flow = flow_from_clientsecrets(os.path.dirname(os.path.abspath(__file__)) + "/" + CLIENT_SECRET_FILE,
scope='https://www.googleapis.com/auth/photoslibrary',
message='client_secrets files is missing')
credentials = None
storage = Storage(os.path.dirname(os.path.abspath(__file__)) + "/" + "token-oauth2.json")
credentials = storage.get()
if credentials is None or credentials.invalid:
credentials = run_flow(flow, storage, args)
self.token = credentials.access_token
return build('photoslibrary', 'v1',
http=credentials.authorize(httplib2.Http()))
If token is expired, it says:
Go to the following link in your browser:
https://accounts.google.com/o/oauth2/auth?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fphotoslibrary&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&response_type=code&client_id=12345678-abcdefg.apps.googleusercontent.com&access_type=offline
But now Google says that redirect_uri=ietf:wg:oauth:2.0:oob is invalid.
I tried to manually replace it with 'http://localhost' (taken from CLIENT_SECRET_FILE), but when I take the code from the URL:
http://localhost/?code=4/12345_67899-t_vgPpDmLMGwD75F_w&scope=https://www.googleapis.com/auth/photoslibrary
the script says Authentication has failed: redirect_uri_mismatchBad Request.
I use google api client library 1.6.2.
How should I fix it?

I am running a python script on the device with no screen/no browser
(ssh access only).
The issue you are going to have is that the code needs to be authorized at least once. Run it on your local machine. in order to create the token the first time. Then copy it over to your server. with the file created by storage . there should be a refresh token in there.
Your refresh token should not be expiring. However if it expiring after a week then make sure that you have set your project to production in Google cloud console this way your refresh token will stop expiring.
There is no way around showing that consent screen to the user someone must authorize the code. That is done by showing the consent screen in a web browser.
oob issue
open your credentials.json file and make sure it says this
"redirect_uris":["http://localhost"]
If the oob end point is still there remove it.
The reason for your error is that the old credeintils.json file contained the following
"redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}
When the python client library ran it would always pick up the first one. That being "urn:ietf:wg:oauth:2.0:oob" which no longer works.
Now when you run your code it will probalby display a 404 error ignore this for now and look at the top bar url bar the code should be there in the response.
404
if you are getting a 404 and want to remove that read Loopback IP address (macOS, Linux, Windows desktop)
Google states.
To receive the authorization code using this URL, your application must be listening on the local web server. That is possible on many, but not all, platforms. However, if your platform supports it, this is the recommended mechanism for obtaining the authorization code.
So you need to figure out how to set up a local web app for an installed application in order to catch the response.
Google has not at this time told us how this is possible with all languages.
Sample
The following example works. It stores the user credentials in tokenPhotos.json. It will reload them as needed. Im not seeing any issues with OOB in this.
# To install the Google client library for Python, run the following command:
# pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib
from __future__ import print_function
import os.path
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
# If modifying these scopes, delete the file token.json.
SCOPES = ['https://www.googleapis.com/auth/photoslibrary']
CREDS = 'C:\YouTube\dev\credentials.json';
def main():
"""Shows basic usage of the Photos v1 API.
Prints the names and ids of the first 20 albums the user has access to.
"""
creds = None
# The file token.json stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first
# time.
if os.path.exists('tokenPhotos.json'):
creds = Credentials.from_authorized_user_file('tokenPhotos.json', SCOPES)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
CREDS, SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open('tokenPhotos.json', 'w') as token:
token.write(creds.to_json())
try:
service = build('photoslibrary', 'v1', credentials=creds, static_discovery=False)
# Call the Photos v1 API
results = service.albums().list(
pageSize=10, fields="albums(id,title),nextPageToken").execute()
albums = results.get('albums', [])
if not albums:
print('No albums found.')
return
print('albums:')
for album in albums:
print(u'{0} ({1})'.format(album['title'], album['id']))
except HttpError as error:
# TODO(developer) - Handle errors from Photos API.
print(f'An error occurred: {error}')
if __name__ == '__main__':
main()

Related

Is there a workaround to prevent Gmail API for python from asking for a new token each time I run my python script?

I have a python script that sends emails with attachments using GMAIL's API. Each time(mostly after a day) I run the script, I get an error that the token's invalid.
The only solution I have identified so far is to download the json file each time I run the script but I was expecting this to be done only once as I intend to convert the script to a desktop application.
Google sends you an authToken and a RefreshToken, who need to be stored to refresh your token when he is no longer valid.
Check that :
https://developers.google.com/identity/protocols/oauth2
There are two types of tokens access tokens and refresh tokens.
Access tokens are only good for an hour. Refresh tokens are long lived and should work until the access has been removed. Or if your application is still in the testing phase then the token will only work for seven days. There is one other thing, if you are creating your tokens from google oauth2 playground I bleave they are only good for three hours give or take.
The best solution for all of the above is to ensure that your app is first off set to prodctuion, and second that you are properly storing your token after you have created it.
In the sample below the token is stored in the token.json for later use.
def Authorize(credentials_file_path, token_file_path):
"""Shows basic usage of authorization"""
try:
credentials = None
# The file token.json stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first
# time.
if os.path.exists(token_file_path):
try:
credentials = Credentials.from_authorized_user_file(token_file_path, SCOPES)
credentials.refresh(Request())
except google.auth.exceptions.RefreshError as error:
# if refresh token fails, reset creds to none.
credentials = None
print(f'An refresh authorization error occurred: {error}')
# If there are no (valid) credentials available, let the user log in.
if not credentials or not credentials.valid:
if credentials and credentials.expired and credentials.refresh_token:
credentials.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
credentials_file_path, SCOPES)
credentials = flow.run_local_server(port=0)
# Save the credentials for the next run
with open(token_file_path, 'w') as token:
token.write(credentials.to_json())
except HttpError as error:
# Todo handle error
print(f'An authorization error occurred: {error}')
return credentials
if __name__ == '__main__':
creds = Authorize('C:\\YouTube\\dev\\credentials.json', "token.json")

Using Google Speadsheet API

[Updated]
I am trying to use Google Spreadsheet API to automate our reporting process (which is being handled manually). I have created Service Account and downloaded a json file.
from __future__ import print_function
import google.auth
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
def create(title):
"""
Creates the Sheet the user has access to.
Load pre-authorized user credentials from the environment.
TODO(developer) - See https://developers.google.com/identity
for guides on implementing OAuth2 for the application.
"""
creds, _ = google.auth.default()
# pylint: disable=maybe-no-member
try:
service = build('sheets', 'v4', credentials=creds)
spreadsheet = {
'properties': {
'title': title
}
}
spreadsheet = service.spreadsheets().create(body=spreadsheet,
fields='spreadsheetId') \
.execute()
print(f"Spreadsheet ID: {(spreadsheet.get('spreadsheetId'))}")
return spreadsheet.get('spreadsheetId')
except HttpError as error:
print(f"An error occurred: {error}")
return error
if __name__ == '__main__':
# Pass: title
create("mysheet1")
Here is the result:
TransportError: ("Failed to retrieve http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/?recursive=true from the Google Compute Enginemetadata service. Status: 404 Response:\nb''", <google_auth_httplib2._Response object at 0x7f262a34fd90>)
It's strange that there is no code lines to receive access to Google Work Space (like connect to API using keys?)
AFTER SETTING UP OAUTH CLIENTS ID
Now I am facing new problem. After running the code, It asked me to "Please visit this URL to authorize this application: with a link", I clicked and got this message: "This site can’t be reachedlocalhost refused to connect".(I am running the code on Colab)
Have you enabled the spreadsheet API ?
https://support.google.com/googleapi/answer/6158841?hl=en
I suspect it's because you have not given it the scope:
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
SCOPES = ['https://www.googleapis.com/auth/spreadsheets.readonly']
def main():
flow = InstalledAppFlow.from_client_secrets_file(
'credentials.json', SCOPES)
creds = flow.run_local_server(port=0)
service = build('sheets', 'v4', credentials=creds)
Where credentials.json is your file
Scopes: https://developers.google.com/identity/protocols/oauth2/scopes#sheets
You will also need to download the gcloud cli:
If you ever try to run it locally, on your windows/linux/mac machine.
default credentials are the credentials stored by either the default service account in google cloud or your local machine
https://cloud.google.com/sdk/gcloud
And run
gcloud auth login

Google App Script - "message": "Requested entity was not found." with devMode = false

I am trying to connect my Python script with my project in Google App Script. I have followed all the insctructions in this guide.
I have of course deployed it as an executable API and have tested it with access to only myself, my organization and anyone options.
When I pass the request with devMode as true, it all works fine. I understand that in this case, it is running the latest saved version. However when I set it to false then I get back the error "message": "Requested entity was not found." which as I understand is trying to run the latest deployed version.
I have also tried going through these questions 1 and 2 but apparently, the problem they had was the opposite where the script wouldn't run with devMode set to true.
Everything else seems to be executing correctly but I cannot find the reason why it wouldn't run the script without being on devMode
This is my script:
"""
Shows basic usage of the Apps Script API.
Call the Apps Script API to create a new script project, upload a file to the
project, and log the script's URL to the user.
"""
from __future__ import print_function
import pickle
import os.path
from googleapiclient import errors
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
# If modifying these scopes, delete the file token.pickle.
SCOPES = ['https://www.googleapis.com/auth/script.projects',
'https://www.googleapis.com/auth/spreadsheets.currentonly'
]
SCRIPT_ID = 'someid'
def main():
"""Calls the Apps Script API.
"""
creds = None
# The file token.pickle stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first
# time.
if os.path.exists('token.pickle'):
with open('token.pickle', 'rb') as token:
creds = pickle.load(token)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
'credentials.json', SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open('token.pickle', 'wb') as token:
pickle.dump(creds, token)
service = build('script', 'v1', credentials=creds)
# Call the Apps Script API
try:
# Create a new project
request = {'function': 'setData'}
response = service.scripts().run(body=request,
scriptId=SCRIPT_ID).execute()
print(response)
except errors.HttpError as error:
# The API encountered a problem.
print(error.content)
if __name__ == '__main__':
main()
I have been able to face the same issue and I have found out that changing the script Id to the Deployment Id worked.
The deployment Id can be found on the Apps Script script:
Open the script
Go to Deploy -> Manage Deployments
Get the Deployment ID from the active deployment
Once you have the deployment, go to the python script and modify the SCRIPT ID with the Deployment Id

Safari can't open the page localhost:random_port

I followed the instructions on Google API quickstart Python so that I can get started quickly. I downloaded the JSON file set with the default name and as a Desktop App. After making sure the each Python library was up to date, I pasted and ran the code on the page.
What happens after many tries is the following:
A browser tab will open
I will be asked to choose an account
I then get the screen where I am asked to allow access
View your Google Spreadsheets
I click Allow and wait
I then get a screen that tells me
Safari can't open the page localhost:random_port ...
&scope=https://www.googleapis.com/auth/spreadsheets.readonly
To make sure that there are no issues, I added the client ID to the list of allowed connections on admin.google.com, but that didn't seem to help.
Does anyone know what could be causing this issue?
Is there something else I need to do on admin.google.com?
I am using VS Code on Mac. My library versions are
google-api-core 1.23.0
google-api-python-client 1.12.4
google-auth 1.22.1
google-auth-httplib2 0.0.4
google-auth-oauthlib 0.4.1
googleapis-common-protos 1.52.0
For reference, here's the code from the top link that I am trying to use.
from __future__ import print_function
import pickle
import os.path
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
# If modifying these scopes, delete the file token.pickle.
SCOPES = ['https://www.googleapis.com/auth/spreadsheets.readonly']
# The ID and range of a sample spreadsheet.
SAMPLE_SPREADSHEET_ID = '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms'
SAMPLE_RANGE_NAME = 'Class Data!A2:E'
def main():
"""Shows basic usage of the Sheets API.
Prints values from a sample spreadsheet.
"""
creds = None
# The file token.pickle stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first
# time.
if os.path.exists('token.pickle'):
with open('token.pickle', 'rb') as token:
creds = pickle.load(token)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
'credentials.json', SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open('token.pickle', 'wb') as token:
pickle.dump(creds, token)
service = build('sheets', 'v4', credentials=creds)
# Call the Sheets API
sheet = service.spreadsheets()
result = sheet.values().get(spreadsheetId=SAMPLE_SPREADSHEET_ID,
range=SAMPLE_RANGE_NAME).execute()
values = result.get('values', [])
if not values:
print('No data found.')
else:
print('Name, Major:')
for row in values:
# Print columns A and E, which correspond to indices 0 and 4.
print('%s, %s' % (row[0], row[4]))
if __name__ == '__main__':
main()
Just Ran into the same question.
If you use Safari, it will try to access to https://localhost:xxxx/xxxx instead.
Pasting the url to another browser such as Chrome works fine.
to stop Safari keeps forcing HTTPS on localhost, go to (Safari > Preferences > Privacy > Manage Website Data...)
Search for localhost and press Remove.
Try the script again to see if it works.
The port you use in the following statement in the code:
flow.run_local_server(port=0)
should be exactly the same as the one you allow in the consent screen for that project.
Make sure the JS redirect URL points to the same port you will be serving from locally.
See google_auth_oauthlib.flow for more information about the module.
For anyone else having this issue. The issue is related to the OS/software on OS. I had no issues running this on a fresh install of Mac OS.
In my MacOS settings I changed my default browser to Google Chrome, and this same authentication flow began working.

Getting a Error 400: redirect_uri_mismatch when trying to use OAuth2 with Google Sheets from a Django view

I am trying to connect to Google Sheets' API from a Django view. The bulk of the code I have taken from this link:
https://developers.google.com/sheets/api/quickstart/python
Anyway, here are the codes:
sheets.py (Copy pasted from the link above, function renamed)
from __future__ import print_function
import pickle
import os.path
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
# If modifying these scopes, delete the file token.pickle.
SCOPES = ['https://www.googleapis.com/auth/spreadsheets.readonly']
# The ID and range of a sample spreadsheet.
SAMPLE_SPREADSHEET_ID = '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms'
SAMPLE_RANGE_NAME = 'Class Data!A2:E'
def test():
"""Shows basic usage of the Sheets API.
Prints values from a sample spreadsheet.
"""
creds = None
# The file token.pickle stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first
# time.
if os.path.exists('token.pickle'):
with open('token.pickle', 'rb') as token:
creds = pickle.load(token)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
'credentials.json', SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open('token.pickle', 'wb') as token:
pickle.dump(creds, token)
service = build('sheets', 'v4', credentials=creds)
# Call the Sheets API
sheet = service.spreadsheets()
result = sheet.values().get(spreadsheetId=SAMPLE_SPREADSHEET_ID,
range=SAMPLE_RANGE_NAME).execute()
values = result.get('values', [])
if not values:
print('No data found.')
else:
print('Name, Major:')
for row in values:
# Print columns A and E, which correspond to indices 0 and 4.
print('%s, %s' % (row[0], row[4]))
urls.py
urlpatterns = [
path('', views.index, name='index')
]
views.py
from django.http import HttpResponse
from django.shortcuts import render
from .sheets import test
# Views
def index(request):
test()
return HttpResponse('Hello world')
All the view function does is just call the test() method from the sheets.py module. Anyway, when I run my server and go the URL, another tab opens up for the Google oAuth2, which means that the credentials file is detected and everything. However, in this tab, the following error message is displayed from Google:
Error 400: redirect_uri_mismatch The redirect URI in the request, http://localhost:65262/, does not match the ones authorized for the OAuth client.
In my API console, I have the callback URL set exactly to 127.0.0.1:8000 to match my Django's view URL. I don't even know where the http://localhost:65262/ URL comes from. Any help in fixing this? And can someone explain to me why this is happening? Thanks in advance.
EDIT
I tried to remove the port=0 in the flow method as mentioned in the comment, then the URL mismatch occurs with http://localhost:8080/, which is again pretty weird because my Django app is running in the 8000 port.
You shouldn't be using Flow.run_local_server() unless you don't have the intention of deploying the code. This is because run_local_server launches a browser on the server to complete the flow.
This works just fine if you're developing the project locally for yourself.
If you're intent on using the local server to negotiate the OAuth flow. The Redirect URI configured in your secrets must match that, the local server default for the host is localhost and port is 8080.
If you're looking to deploy the code, you must perform the flow via an exchange between the user's browser, your server and Google.
Since you have a Django server already running, you can use that to negotiate the flow.
For example,
Say there is a tweets app in a Django project with urls.py module as follows.
from django.urls import path, include
from . import views
urlpatterns = [
path('google_oauth', views.google_oath, name='google_oauth'),
path('hello', views.say_hello, name='hello'),
]
urls = include(urlpatterns)
You could implement a guard for views that require credentials as follow.
import functools
import json
import urllib
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from django.shortcuts import redirect
from django.http import HttpResponse
SCOPES = ['https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile', 'openid']
def provides_credentials(func):
#functools.wraps(func)
def wraps(request):
# If OAuth redirect response, get credentials
flow = InstalledAppFlow.from_client_secrets_file(
'credentials.json', SCOPES,
redirect_uri="http://localhost:8000/tweet/hello")
existing_state = request.GET.get('state', None)
current_path = request.path
if existing_state:
secure_uri = request.build_absolute_uri(
).replace('http', 'https')
location_path = urllib.parse.urlparse(existing_state).path
flow.fetch_token(
authorization_response=secure_uri,
state=existing_state
)
request.session['credentials'] = flow.credentials.to_json()
if location_path == current_path:
return func(request, flow.credentials)
# Head back to location stored in state when
# it is different from the configured redirect uri
return redirect(existing_state)
# Otherwise, retrieve credential from request session.
stored_credentials = request.session.get('credentials', None)
if not stored_credentials:
# It's strongly recommended to encrypt state.
# location is needed in state to remember it.
location = request.build_absolute_uri()
# Commence OAuth dance.
auth_url, _ = flow.authorization_url(state=location)
return redirect(auth_url)
# Hydrate stored credentials.
credentials = Credentials(**json.loads(stored_credentials))
# If credential is expired, refresh it.
if credentials.expired and creds.refresh_token:
creds.refresh(Request())
# Store JSON representation of credentials in session.
request.session['credentials'] = credentials.to_json()
return func(request, credentials=credentials)
return wraps
#provides_credentials
def google_oauth(request, credentials):
return HttpResponse('Google OAUTH Say Hello')
#provides_credentials
def say_hello(request, credentials):
# Use credentials for whatever
return HttpResponse('Hello')
Note that this is only an example. If you decide to go this route, I recommend looking into extracting the OAuth flow to its very own Django App.
I had the same problem with the redirect_uri error and it turned out (as implied above) that I created my credentials in the google console as type "Web server" instead of "desktop app". I created new creds as "desktop app", downloaded the JSON and it worked.
Ultimately, I want to use the GMAIL API for a web server, but that is a different flow than the sample.
The redirect URI tells Google the location you would like the authorization to be returned to. This must be set up properly in google developer console to avoid anyone hijacking your client. It must match exactly.
To to Google developer console. Edit the client you are currently using and add the following as a redirect uri
http://localhost:65262/
Tip click the little pencil icon to edit a client :)
TBH while in development its easier to just add the port that google says you are calling from then fiddle with the settings in your application.

Categories

Resources