I'm trying to set up a Google apps script API executable with Server to server auth for a Python micro-service I'm setting up.
Using the Quickstart, I was able to make it work via Auth2, but I was unable to make it work with a service account. I gave access to script and spreadsheet to service account email. Project ID in client secret JSON matches the project ID of the app scripts. I deployed it as an API executable just the same.
This is my code below (although I don't think the code is the issue):
from oauth2client.service_account import ServiceAccountCredentials
from httplib2 import Http
from googleapiclient.discovery import build
scopes = [
'https://www.googleapis.com/auth/drive',
'https://www.googleapis.com/auth/script.external_request',
'https://www.googleapis.com/auth/script.storage',
'https://www.googleapis.com/auth/spreadsheets',
'https://www.googleapis.com/auth/userinfo.email'
]
credentials = ServiceAccountCredentials.from_json_keyfile_name('client_secret.json', scopes)
http_auth = credentials.authorize(Http())
service = build('script', 'v1', http=http_auth)
request = {'function': 'testApi'}
response = service.scripts().run(body=request, scriptId='SCRIPT_ID').execute()
print(response)
The testApi function in my App Script is a simple function that returns "It works".
I keep getting that the user does not have permission (403) when using personal account, or even 500 when using organization (G Suite account).
As mentioned earlier, the Quickstart tutorial from Google documentation worked, but this is not using a service account.
Has anyone made Google Apps Scripts API executable working with a server to server auth account flow?
You may want to check this tutorial about Using Google Service Accounts with Google Apps Script. This sample code shows how to use OAuth in Google Apps Script using Service Accounts.
For this code to work, you need to create a Google Service account with domain-wide delegation, substitute the private key and client client email with the actual values and also add the Client Id to your Google Apps admin console with the Drive API Scope. The OAuth 2.0 access tokens are stored in the Script Properties.
var JSON = {
"private_key": "Your Private Key",
"client_email": "serviceacount#project-ctrlq.iam.gserviceaccount.com",
"client_id": "1234567890",
"user_email": "amit#labnol.org"
};
function getOAuthService(user) {
return OAuth2.createService("Service Account")
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
.setPrivateKey(JSON.private_key)
.setIssuer(JSON.client_email)
.setSubject(JSON.user_email)
.setPropertyStore(PropertiesService.getScriptProperties())
.setParam('access_type', 'offline')
.setScope('https://www.googleapis.com/auth/drive');
}
function getUserFiles() {
var service = getOAuthService();
service.reset();
if (service.hasAccess()) {
var url = 'https://www.googleapis.com/drive/v2/files?pageSize=1';
var response = UrlFetchApp.fetch(url, {
headers: {
Authorization: 'Bearer ' + service.getAccessToken()
}
});
Logger.log(response.getContentText());
}
}
function reset() {
var service = getOAuthService();
service.reset();
}
Also, if you are getting the 403 Insufficient permission error, it is likely because the application is requesting access to API scopes that are not authorized in the Google Apps admin console. The invalid_grant error is likely due to incorrect date and time settings of the server that is hosting the application.
Related
I want to create a Google Cloud project using Google Cloud API Python client.
Steps:
Run gcloud beta auth application-default login command to get application default credentials by login onto the browser.
Enable Resource Manager API on my GCP console.
Here's the working Python code:
from googleapiclient import discovery
from oauth2client.client import GoogleCredentials
...
credentials = GoogleCredentials.get_application_default()
service = discovery.build('cloudresourcemanager', 'v1', credentials=credentials)
project_body = {
'name': 'Api Project',
'projectId': 'api-9837'
}
request = service.projects().create(body=project_body)
request.execute()
pprint(request)
But I can't take the first step.
In order to make it easier to help you, please share always the errors/messages you receive.
In the docs: Creating and managing projects - Creating a project:
You must have the resourcemanager.projects.create permission.
You can create a new project using the Cloud Console, the gcloud command-line tool, or the projects.create() method.
...
from googleapiclient import discovery
from oauth2client.client import OAuth2Credentials as creds
crm = discovery.build(
'cloudresourcemanager', 'v1', http=creds.authorize(httplib2.Http()))
operation = crm.projects().create(
body={
'project_id': flags.projectId,
'name': 'my project'
}).execute()
...
For Service Accounts:
You can use a service account to automate project creation. Like user accounts, service accounts can be granted permission to create projects within an organization. Service accounts are not allowed to create projects outside of an organization and must specify the parent resource when creating a project. Service accounts can create a new project using the gcloud tool or the projects.create() method.
There are slightly differences in your code vs the mentioned in the Docs.
I have a Flask app in Google App Engine Standard Environment Python, and I also have a Cloud Function with an HTTP trigger which accepts a JSON body including the URL of a file. The CF downloads the file at that URL then saves it to a GCS bucket. The GAE service account has Cloud Function Invoker permissions, yet when using urlfetch.fetch() in my GAE code to trigger CF, the App Engine code gets a 403 Forbidden error unless I make the CF trigger callable by anyone.
How do I successfully call/trigger CF's from GAE in Python? I assume the answer is one of these:
Set IAM permissions on GAE service account to {enlighten me here}
Add authentication headers in urlfetch.fetch() like so {different enlightenment}
Make CF triggerable from anywhere, but hard code some secret key so the CF code itself handles authentication.
It's well documented here: Cloud Functions Authentication
In short you have to provide your service account credentials in the authentication header.
To get your credentials use the Google Auth Client library. If you are testing from local you should create a service account JSON and load it to the environment variable GOOGLE_APPLICATION_CREDENTIALS but on App Engine it will work from scratch.
After you have gotten your token, pass it as an auth header like so:
auth_req = google.auth.transport.requests.Request()
auth_token = google.oauth2.id_token.fetch_id_token(auth_req, cloud_function_url)
response = requests.post(cloud_function_url, json=payload, headers={"Authorization" : f"Bearer {auth_token}"})
I am trying to authenticate an Azure app that has application-level permissions. All the permissions have been granted by an admin, and the app has both a client id and a client secret. I'm running the following code based on the Daemon-api documentation from Microsoft graph:
import msal
config = {
"authority": "https://login.microsoftonline.com/organizations",
"client_id": CLIENT_ID,
"scope": ["https://graph.microsoft.com/.default"],
"redirect_uri": REDIRECT_URI,
"client_secret": CLIENT_SECRET
}
app = msal.ConfidentialClientApplication(
config["client_id"], authority=config["authority"],
client_credential=config["client_secret"] )
result = app.acquire_token_silent(config["scope"], account=None)
import logging
if not result:
logging.info("No suitable token exists in cache. Let's get a new one from AAD.")
result = app.acquire_token_for_client(scopes=config["scope"])
If I print result though, it says the following:
{'error': 'unauthorized_client',
'error_description': "AADSTS700016: Application with identifier [IDENTIFIER] was not found in the directory 'microsoft.com'. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You may have sent your authentication request to the wrong tenant.\r\nTrace ID: [TRACE ID]\r\nCorrelation ID: [CORRELATION ID]\r\nTimestamp: 2019-08-28 17:14:39Z",
'error_codes': [700016],
'timestamp': '2019-08-28 17:14:39Z',
'trace_id': [TRACE ID],
'correlation_id': [CORRELATION ID],
'error_uri': 'https://login.microsoftonline.com/error?code=700016'}
The app has existed for several days and as I've mentioned, it has had all its permissions authorized by an admin. How come I'm still getting an "unauthorized" error? I check my id and secret, they're correct.
I wonder if it might have something to do with the fact that the error message says it's being sent to the microsoft.com directory? But the only microsoft information I provide is in authority and scope which the api says is needed as-is. I don't see anywhere to provide a directory id. Could that be the issue? If so, how would I rectify it?
Authority field in your config should be
https://login.microsoftonline.com/<directory_id>
I'm trying to connect to my Google Cloud Endpoints API that is running as an Appengine app:
#endpoints.api(name='helloworldendpoints', allowed_client_ids=["1234", "12345"], version='v1', auth_level=endpoints.AUTH_LEVEL.REQUIRED)
class HelloWorldApi(remote.Service):
...
The API request is as follows:
scopes = ["https://www.googleapis.com/auth/userinfo.email"]
credentials = ServiceAccountCredentials.from_json_keyfile_name("CloudEndpointsClient.json", scopes)
from httplib2 import Http
http_auth = credentials.authorize(Http())
from apiclient.discovery import build
api_root = 'https://myapp.appspot.com/_ah/api'
api = 'helloworldendpoints'
version = 'v1'
discovery_url = '%s/discovery/v1/apis/%s/%s/rest' % (api_root, api, version)
service = build(api, version, discoveryServiceUrl=discovery_url)
response = service.myFunction(myparameter = "123456").execute(http=http_auth)#
print response
The requests work well if I remove authentication requirements.
I know that authentication works since the error changes if after authenticating.
The error message I'm getting is:
googleapiclient.errors.HttpError: https://my-app.appspot.com/_ah/api/helloworldendpoints/v1/obtainScoreFromEmail?myparameter=1234&alt=json returned "Access Not Configured. has not been used in project 123456789 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/helloworldendpoints/overview?project=123456789 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.">
I cannot enable the API in my Google Cloud Project, since the API does not exist.
What I found to work is a hack around. I used a user authentication (instead of server) on the same project for the same API which worked (https://cloud.google.com/endpoints/docs/frameworks/python/access_from_python).
After I switched back to my initial server auth. method it started working.
To use the new Google Directory API we created an OAuth2 "service account" (see Using OAuth 2.0 for Server to Server Applications). This is basically a PKCS #12 file. All of our Directory API scripts work fine with this service account.
We also use the EmailSettings API (Developer's Guide Email Settings API) to manage some of our Google account settings. These scripts did not move to the new API format, so we continue to use the old OAuth1 authentication method. This has, until recently, worked fine. However, it appears that Google is no longer supporting OAuth1 authentication.
So, we need to move the EmailSettings scripts from OAuth1 to our OAuth2 service account. We are using the gdata Python libraries (GitHub google/gdata-python-client) to make calls to the EmailSettings API. This is how we currently authenticate to make EmailSettings API calls:
import gdata.apps.emailsettings.service
# self is an EmailSettingsService object (gdata.apps.emailsettings.service)
self.domain = "mydomain.com"
self.source = "my application name"
token = get OAuth1 token string from a file
self.SetOAuthInputParameters(
gdata.auth.OAuthSignatureMethod.HMAC_SHA1,
consumer_key = token.oauth_input_params._consumer.key,
consumer_secret = token.oauth_input_params._consumer.secret
)
token.oauth_input_params = self._oauth_input_params
self.SetOAuthToken(token)
Using these Python gdata libraries, how do I authenticate using our OAuth2 service account, i.e., the PKCS #12 file, to use the EmailSettings API?
There's another SO question which shows you how to do this but uses the slightly newer gdata.apps.emailsettings.client methods.
If you must stay with gdata.apps.emailsettings.service then you can "hack" OAuth 2.0 onto the object with something like:
Build your OAuth 2.0 credentials object as you are already doing for your Admin SDK Directory API calls.
Build your GData client object as you are already doing in lines 1-3 of your code.
Grab the access token from your credentials object and apply it as a header to your client object:
client.additional_headers = {
'Authorization': u'Bearer %s' % credentials.access_token}
If you get a 401 response (access token expired), repeat 1 and 3 to get and apply a new access token.