Error: Client is unauthorized to retrieve access tokens (ServiceAccountCredentials) - google-app-engine

I recently updated the SignedJwtAssertionCredentials to ServiceAccountCredentials.
Code Snippet:
SUB = "XXXXXXXXXXXXXXXX#XXXXXXXXX.com"
scopes = ["XXXXXXXXXXXXXXXX]
json_file = os.path.join(os.path.dirname(__file__), "XXXXXXXXX.json")
credentials = ServiceAccountCredentials.from_json_keyfile_name(json_file, scopes=scopes)
delegated_credentials = credentials.create_delegated(SUB)
http = httplib2.Http()
http = delegated_credentials.authorize(http)
return build('admin', 'directory_v1', http=http)
Error:
File "/base/data/home/apps/project_name/main.py", line 867, in authorize
return build('admin', 'directory_v1', http=http)
File "/base/data/home/apps/project_name/lib/oauth2client/util.py", line 128, in positional_wrapper
return wrapped(*args, **kwargs)
File "/base/data/home/apps/project_name/lib/apiclient/discovery.py", line 193, in build
resp, content = http.request(requested_url)
File "/base/data/home/apps/project_name/lib/oauth2client/transport.py", line 159, in new_request
credentials._refresh(orig_request_method)
File "/base/data/home/apps/project_name/lib/oauth2client/client.py", line 744, in _refresh
self._do_refresh_request(http)
File "/base/data/home/apps/project_name/lib/oauth2client/client.py", line 812, in _do_refresh_request
raise HttpAccessTokenRefreshError(error_msg, status=resp.status)
HttpAccessTokenRefreshError: unauthorized_client: Client is unauthorized to retrieve access tokens using this method.
What I've done:
I delegated domain-wide authority using the Client ID (Google Cloud Platform > IAM & Admin > Service accounts > View Client Id) which I authorized in the G Suite domain's admin console following the documentation.
I thought this would fix the problem but the next day, same error message. What is the issue?

It looks like the oauth2client build is trying to refresh the access token but a service account can't refresh the access token for sub account credentials with that method.
What if you skip the two http lines and just do:
SUB = "XXXXXXXXXXXXXXXX#XXXXXXXXX.com"
scopes = ["XXXXXXXXXXXXXXXX]
json_file = os.path.join(os.path.dirname(__file__), "XXXXXXXXX.json")
credentials = ServiceAccountCredentials.from_json_keyfile_name(json_file, scopes=scopes)
delegated_credentials = credentials.create_delegated(SUB)
return build('admin', 'directory_v1', credentials=delegated_credentials)
passing the credentials instead of the authorized http object.

Related

read full message in email with gmail api

beacuse I am using in snippet I get a A short part of the message text.
I am want to change that for getting the full body of the message
how i can do it ?
def get_message_detail(service, message_id, format='raw', metadata_headers=[]):
try:
message_detail = service.users().messages().get(
userId='me',
id=message_id,
format=format,
metadataHeaders=metadata_headers
).execute()
return message_detail
except Exception as e:
print(e)
return None
if email_messages!= None:
for email_message in email_messages:
messageId = email_message['threadId']
messageSubject = '(No subject) ({0})'.format(messageId)
messsageDetail = get_message_detail(
gmail_service, email_message['id'], format='full',
metadata_headers=['parts'])
messageDetailPayload = messsageDetail.get('payload')
#print(messageDetailPayload)
for item in messageDetailPayload['headers']:
if item['name'] == 'Subject':
if item['value']:
messageSubject = '{0} ({1})'.format(item['value'],messageId)
email_data = messsageDetail['payload']['headers']
#print(email_data)
#print(messageSubject)
for values in email_data:
name = values['name']
if name == "From":
from_name = values['value']
get_detil_msg = messsageDetail['snippet']
print(get_detil_msg)
This will return the full mime message if thats what your looking for.
# 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 base64
import email
import json
import os.path
import google.auth.exceptions
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://mail.google.com/']
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
def ListMessages(credentials):
try:
# create a gmail service object
service = build('gmail', 'v1', credentials=credentials)
# Call the Gmail v1 API
results = service.users().messages().list(userId='me').execute()
messages = results.get('messages', [])
if not messages:
print('No messages where found.')
return
print('Messages:')
for message in messages:
getMessage(credentials, message['id'])
except HttpError as error:
# TODO(developer) - Handle errors from gmail API.
print(f'An error occurred: {error}')
def getMessage(credentials, message_id):
# get a message
try:
service = build('gmail', 'v1', credentials=credentials)
# Call the Gmail v1 API, retrieve message data.
message = service.users().messages().get(userId='me', id=message_id, format='raw').execute()
# Parse the raw message.
mime_msg = email.message_from_bytes(base64.urlsafe_b64decode(message['raw']))
print(mime_msg['from'])
print(mime_msg['to'])
print(mime_msg['subject'])
print("----------------------------------------------------")
# Find full message body
message_main_type = mime_msg.get_content_maintype()
if message_main_type == 'multipart':
for part in mime_msg.get_payload():
if part.get_content_maintype() == 'text':
print(part.get_payload())
elif message_main_type == 'text':
print(mime_msg.get_payload())
print("----------------------------------------------------")
# Message snippet only.
# print('Message snippet: %s' % message['snippet'])
except HttpError as error:
# TODO(developer) - Handle errors from gmail API.
print(f'A message get error occurred: {error}')
if __name__ == '__main__':
creds = Authorize('C:\\YouTube\\dev\\credentials.json', "token.json")
ListMessages(creds)
Full tutorial: How to read gmail message body with python?

Service account request to IAP-protected app results in 'Invalid GCIP ID token: JWT signature is invalid'

I am trying to programmatically access an IAP-protected App Engine Standard app via Python from outside of the GCP environment.
I have tried various methods, including the method shown in the docs here: https://cloud.google.com/iap/docs/authentication-howto#iap-make-request-python. Here is my code:
from google.auth.transport.requests import Request
from google.oauth2 import id_token
import requests
def make_iap_request(url, client_id, method='GET', **kwargs):
"""Makes a request to an application protected by Identity-Aware Proxy.
Args:
url: The Identity-Aware Proxy-protected URL to fetch.
client_id: The client ID used by Identity-Aware Proxy.
method: The request method to use
('GET', 'OPTIONS', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE')
**kwargs: Any of the parameters defined for the request function:
https://github.com/requests/requests/blob/master/requests/api.py
If no timeout is provided, it is set to 90 by default.
Returns:
The page body, or raises an exception if the page couldn't be retrieved.
"""
# Set the default timeout, if missing
if 'timeout' not in kwargs:
kwargs['timeout'] = 90
# Obtain an OpenID Connect (OIDC) token from metadata server or using service
# account.
open_id_connect_token = id_token.fetch_id_token(Request(), client_id)
print(f'{open_id_connect_token=}')
# Fetch the Identity-Aware Proxy-protected URL, including an
# Authorization header containing "Bearer " followed by a
# Google-issued OpenID Connect token for the service account.
resp = requests.request(
method, url,
headers={'Authorization': 'Bearer {}'.format(
open_id_connect_token)}, **kwargs)
print(f'{resp=}')
if resp.status_code == 403:
raise Exception('Service account does not have permission to '
'access the IAP-protected application.')
elif resp.status_code != 200:
raise Exception(
'Bad response from application: {!r} / {!r} / {!r}'.format(
resp.status_code, resp.headers, resp.text))
else:
return resp.text
if __name__ == '__main__':
res = make_iap_request(
'https://MYAPP.ue.r.appspot.com/',
'Client ID from IAP>App Engine app>Edit OAuth Client>Client ID'
)
print(res)
When I run it locally, I have the GOOGLE_APPLICATION_CREDENTIALS environment variable set to a local JSON credential file containing the keys for the service account I want to use. I have also tried running this in Cloud Functions so it would presumably use the metadata service to pick up the App Engine default service account (I think?).
In both cases, I am able to generate a token that appears valid. Using jwt.io, I see that it contains the expected data and the signature is valid. However, when I make a request to the app using the token, I always get this exception:
Bad response from application: 401 / {'X-Goog-IAP-Generated-Response': 'true', 'Date': 'Tue, 09 Feb 2021 19:25:43 GMT', 'Content-Type': 'text/html', 'Server': 'Google Frontend', 'Content-Length': '47', 'Alt-Svc': 'h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"'} / 'Invalid GCIP ID token: JWT signature is invalid'
What could I be doing wrong?
The solution to this problem is to exchange the Google Identity Token for an Identity Platform Identity Token.
The reason for the error Invalid GCIP ID token: JWT signature is invalid is caused by using a Google Identity Token which is signed by a Google RSA private key and not by a Google Identity Platform RSA private key. I overlooked GCIP in the error message, which would have told me the solution once we validated that the token was not corrupted in use.
In the question, this line of code fetches the Google Identity Token:
open_id_connect_token = id_token.fetch_id_token(Request(), client_id)
The above line of code requires that Google Cloud Application Default Credentials are setup. Example: set GOOGLE_APPLICATION_CREDENTIALS=c:\config\service-account.json
The next step is to exchange this token for an Identity Platform token:
def exchange_google_id_token_for_gcip_id_token(google_open_id_connect_token):
SIGN_IN_WITH_IDP_API = 'https://identitytoolkit.googleapis.com/v1/accounts:signInWithIdp'
API_KEY = '';
url = SIGN_IN_WITH_IDP_API + '?key=' + API_KEY;
data={
'requestUri': 'http://localhost',
'returnSecureToken': True,
'postBody':'id_token=' + google_open_id_connect_token + '&providerId=google.com'}
try:
resp = requests.post(url, data)
res = resp.json()
if 'error' in res:
print("Error: {}".format(res['error']['message']))
exit(1)
# print(res)
return res['idToken']
except Exception as ex:
print("Exception: {}".format(ex))
exit(1)
The API Key can be found in the Google Cloud Console -> Identity Platform. Top right "Application Setup Details". This will show the apiKey and authDomain.
More information can be found at this link:
Exchanging a Google token for an Identity Platform token

googleapiclient.errors.HttpError: HttpError 500 'backend error' when requesting https://www.googleapis.com/gmail/v1/users/me/watch

When running the following minimal code to execute the GMail API watch() method. A 'Backend Error' occurs. This is true both when running on a client machine, and when run directly on Google Cloud as a Cloud Function.
from google.oauth2 import service_account
import googleapiclient.discovery
SCOPES = ["https://mail.google.com/"]
SERVICE_ACCOUNT_FILE = '<JSON KEY FILE REMOVED>'
TARGET='<GSUITE DOMAIN USER EMAIL ADDRESS>'
credentials = service_account.Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE, scopes=SCOPES)
credentials_delegated = credentials.with_subject(TARGET)
service = googleapiclient.discovery.build('gmail', 'v1', credentials=credentials_delegated)
val = { 'topicName': '<TOPIC NAME COPIED FROM GCLOUD PUB/SUB>' }
watch_resp = service.users().watch(userId='me', body=val).execute()
print(watch_resp)
Other GMail APIs such as service.users().getProfile(userId='me').execute() work as expected. This error occurs after ensuring that the service account and the delegated user have the pub/sub publisher role and that the topic name is correct and already created. The exact output of the error is as follows:
Traceback (most recent call last):
File "test.py", line 12, in <module>
watch_resp = service.users().watch(userId='me', body=val).execute()
File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/googleapiclient/_helpers.py", line 130, in positional_wrapper
return wrapped(*args, **kwargs)
File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/googleapiclient/http.py", line 842, in execute
raise HttpError(resp, content, uri=self.uri)
googleapiclient.errors.HttpError: <HttpError 500 when requesting https://www.googleapis.com/gmail/v1/users/me/watch?alt=json returned "Backend Error">

Leasing app engine task in compute engine

I'm trying to lease an app engine task from a pull queue in a compute engine instance but it keeps giving this error:
{
"error": {
"errors": [
{
"domain": "global",
"reason": "forbidden",
"message": "you are not allowed to make this api call"
}
],
"code": 403,
"message": "you are not allowed to make this api call"
}
}
This is the code I'm using:
import httplib2, json, urllib
from oauth2client.client import AccessTokenCredentials
from apiclient.discovery import build
def FetchToken():
METADATA_SERVER = ('http://metadata/computeMetadata/v1/instance/service-accounts')
SERVICE_ACCOUNT = 'default'
http = httplib2.Http()
token_uri = '%s/%s/token' % (METADATA_SERVER, SERVICE_ACCOUNT)
resp, content = http.request(token_uri, method='GET',
body=None,
headers={'Metadata-Flavor': 'Google'})
print token_uri
print content
if resp.status == 200:
d = json.loads(content)
access_token = d['access_token'] # Save the access token
credentials = AccessTokenCredentials(d['access_token'],
'my-user-agent/1.0')
autho = credentials.authorize(http)
print autho
return autho
else:
print resp.status
task_api = build('taskqueue', 'v1beta2')
lease_req = task_api.tasks().lease(project='project-name',
taskqueue='pull-queue',
leaseSecs=30,
numTasks=1)
result = lease_req.execute(http=FetchToken()) ####ERRORS HERE
item = result.items[0]
print item['payload']
It seems like an authentication issue but it gives me the exact same error if I do the same lease request using a bullshit made-up project name so I can't be sure.
I also launched the instance with taskqueue enabled.
Any help would be greatly appreciated
In case anyone else is stuck on a problem like this I'll explain how it's working now.
Firstly I'm using a different (shorter) method of authentication:
from oauth2client import gce
credentials = gce.AppAssertionCredentials('')
http = httplib2.Http()
http=credentials.authorize(http)
credentials.refresh(http)
service = build('taskqueue', 'v1beta2', http=http)
Secondly, the reason my lease request was being denied is that in queue.yaml my service account email was set as a writer email. In the documentation it's mentioned that an email ending with #gmail.com will not have the rights of a user email when set as a writer email. It's not mentioned that that extends to emails ending with #developer.gserviceaccount.com.

'OAuth2Token' object has no attribute 'redirect_uri' and save access token with Appengine and gdata

I try a simple HelloWorld with gdata, appengine and OAuth2. I read this post and the official post from Google.
Problem 1
According to the first post post, my app fails at the part 7 "Use the code to get an access token" :
Traceback (most recent call last):
File "[$PATH]/dev/outils/google_appengine/google/appengine/ext/webapp/_webapp25.py", line 701, in __call__
handler.get(*groups)
File "[$PATH]/dev/outils/google_appengine/google/appengine/ext/webapp/util.py", line 68, in check_login
handler_method(self, *args)
File "[$PATH]/dev/projets/xxx/main.py", line 76, in get
token.get_access_token(url.query)
File "[$PATH]/dev/projets/xxx/gdata/gauth.py", line 1296, in get_access_token
'redirect_uri': self.redirect_uri,
AttributeError: 'OAuth2Token' object has no attribute 'redirect_uri'
I provide the redirect_uri in the method generate_authorize_url() and i filled 2 "Redirect URIs" on Google APIs console :
http://localhost:8080/oauth2callback
http://example.com/oauth2callback
Why the redirect_uri is loosed ?
Solution : See #bossylobster answer.
Problem 2
Now, i want to save this new access token like this :
access_token_key = 'access_token_%s' % current_user.user_id()
gdata.gauth.ae_save(token, access_token_key)
Theses lines throw this exception :
Traceback (most recent call last):
File "[$PATH]/dev/outils/google_appengine/google/appengine/ext/webapp/_webapp25.py", line 701, in __call__
handler.get(*groups)
File "[$PATH]/dev/outils/google_appengine/google/appengine/ext/webapp/util.py", line 68, in check_login
handler_method(self, *args)
File "[$PATH]/dev/projets/xxx/main.py", line 89, in get
gdata.gauth.ae_save(token, access_token_key)
File "[$PATH]/dev/projets/xxx/gdata/gauth.py", line 1593, in ae_save
return gdata.alt.app_engine.set_token(key_name, token_to_blob(token))
File "[$PATH]/dev/projets/xxx/gdata/alt/app_engine.py", line 92, in set_token
if Token(key_name=unique_key, t=token_str).put():
File "[$PATH]/dev/outils/google_appengine/google/appengine/ext/db/__init__.py", line 973, in __init__
prop.__set__(self, value)
File "[$PATH]/dev/outils/google_appengine/google/appengine/ext/db/__init__.py", line 613, in __set__
value = self.validate(value)
File "[$PATH]/dev/outils/google_appengine/google/appengine/ext/db/__init__.py", line 2779, in validate
(self.name, self.data_type.__name__, err))
BadValueError: Property t must be convertible to a Blob instance (Blob() argument should be str instance, not unicode)
But gdata.gauth.access_token calls gdata.gauth.upgrade_to_access_token which return the token with some modification.
If i try with token_to_blob, i have this exception UnsupportedTokenType: Unable to serialize token of type <type 'unicode'>
** How save the new access Token ?**
main.py :
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app, login_required
from google.appengine.api import users
import gdata.gauth
import atom.http_core
SETTINGS = {
'APP_NAME': 'xxx',
'CLIENT_ID':'xxx.apps.googleusercontent.com',
'CLIENT_SECRET':'xxx',
'SCOPES': ['https://www.google.com/m8/feeds/', 'https://docs.google.com/feeds/', 'https://www.google.com/calendar/feeds/'],
'USER_AGENT' : 'xxxs',
'OAUTH2CALLBACK':'http://localhost:8080/oauth2callback'
#'OAUTH2CALLBACK':'http://example.com/oauth2callback'
}
class Home(webapp.RequestHandler):
def get(self):
"""Home"""
if users.get_current_user():
self.redirect("/step1")
else:
self.response.out.write("<a href='/step1'>Sign in google</a><br />")
class Fetcher(webapp.RequestHandler):
#login_required
def get(self):
"""This handler is responsible for fetching an initial OAuth
request token and redirecting the user to the approval page."""
current_user = users.get_current_user()
#create token
token = gdata.gauth.OAuth2Token(client_id = SETTINGS['CLIENT_ID'],
client_secret = SETTINGS['CLIENT_SECRET'],
scope = ' '.join(SETTINGS['SCOPES']),
user_agent = SETTINGS['USER_AGENT'])
url = token.generate_authorize_url(redirect_uri = SETTINGS['OAUTH2CALLBACK'])
#save token to datastore
gdata.gauth.ae_save(token, current_user.user_id())
message = """<a href="%s">
Request token for the Google Documents Scope</a>"""
self.response.out.write(message % url)
self.response.out.write(" ; redirect uri : %s" % token.redirect_uri)
class RequestTokenCallback(webapp.RequestHandler):
#login_required
def get(self):
"""When the user grants access, they are redirected back to this
handler where their authorized request token is exchanged for a
long-lived access token."""
current_user = users.get_current_user()
#get token from callback uri
url = atom.http_core.Uri.parse_uri(self.request.uri)
# get token from datastore
token = gdata.gauth.ae_load(current_user.user_id())
# SOLUTION 1
token.redirect_uri = SETTINGS['OAUTH2CALLBACK']
if isinstance(token, gdata.gauth.OAuth2Token):
if 'error' in url.query:
pass
else:
token.get_access_token(url.query)
gdata.gauth.ae_save(gdata.gauth.token_to_blob(token), "access_token_" + current_user.user_id())
def main():
application = webapp.WSGIApplication([('/', Home),
('/step1', Fetcher),
('/oauth2callback', RequestTokenCallback)],
debug = True)
run_wsgi_app(application)
if __name__ == '__main__':
main()
app.yaml :
application: xxx
version: 2
runtime: python
api_version: 1
handlers:
- url: .*
script: main.py
When you call AeLoad, you need to look at AeSave. You'll notice in the source code that token_to_blob is called.
However, in the source for token_to_blob, the redirect_uri is not saved to the blob, so you'll need to keep it around and call:
token = gdata.gauth.ae_load(current_user.user_id())
token.redirect_uri = SETTINGS['OAUTH2CALLBACK']
For reference, see another related post.
Answer to Question 2: Read the traceback: Blob() argument should be str instance, not unicode. Which version of Python are you using locally?

Resources