Skip to content

Instantly share code, notes, and snippets.

@davidwtbuxton
Last active August 5, 2020 13:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save davidwtbuxton/525924b7f06f56b8530947d55bad1c21 to your computer and use it in GitHub Desktop.
Save davidwtbuxton/525924b7f06f56b8530947d55bad1c21 to your computer and use it in GitHub Desktop.
Getting an auth token with custom scopes for the default service account on Google App Engine's Python 3 runtime
# Copyright David Buxton 2020
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Example for a custom service account credentials class that uses the
# metadata service on the Python 3 App Engine standard runtime with custom
# auth scopes.
#
# This app has a root request handler that takes an "id" request parameter,
# which is a spreadsheet ID. Share your spreadsheet with the default service
# account email for your App Engine project.
#
# This works, but I haven't tested it much!
import datetime
import os
import traceback
# pip install flask google-auth google-api-python-client
import flask
import google.auth
from google.auth import _helpers
from google.auth import credentials
from google.auth.compute_engine import _metadata
from googleapiclient import discovery
app = flask.Flask(__name__)
class ServiceAccountCredentials(credentials.Scoped, credentials.Credentials):
"""Credentials for App Engine runtime using the metadata service.
In production `google.auth.default()` returns an instance of the Compute
Engine credentials class, which does not currently support custom oauth
scopes, even though it uses the metadata service which does.
"""
def __init__(self, scopes=None, service_account_id="default"):
super().__init__()
self._scopes = scopes
self._service_account_id =service_account_id
def refresh(self, request):
data = self._get_token(request, self._scopes)
seconds = data["expires_in"]
token_expiry = _helpers.utcnow() + datetime.timedelta(seconds=seconds)
self.token = data["access_token"]
self.expiry = token_expiry
@classmethod
def _get_token(cls, request, scopes=None):
token_url = "instance/service-accounts/default/token"
if scopes:
if not isinstance(scopes, str):
scopes = ",".join(scopes)
token_url = _helpers.update_query(token_url, {"scopes": scopes})
token_data = _metadata.get(request, token_url)
return token_data
@property
def requires_scopes(self):
return not self._scopes
def with_scopes(self, scopes):
return self.__class__(
scopes=scopes, service_account_id=self._service_account_id
)
def in_production():
return os.getenv('GAE_ENV', '').startswith('standard')
def new_creds(scopes=None):
"""Create credentials wih scopes.
For local development this uses the default credentials. On App Engine
this uses the default service account.
"""
if not in_production():
# Local development.
creds, _ = google.auth.default(scopes=scopes)
return creds
creds = ServiceAccountCredentials(scopes=scopes)
return creds
@app.route('/')
def home():
"""Get a spreadsheet using the Sheets API.
Make a request like /?id=xyz where xyz is a spreadsheet ID and the
spreadsheet is readable by this app's service account.
"""
# Auth scopes for Google API requests, adjust to taste.
scopes = ["https://www.googleapis.com/auth/spreadsheets"]
try:
sheet_id = flask.request.args['id']
creds = new_creds(scopes=scopes)
service = discovery.build("sheets", "v4", credentials=creds)
request = service.spreadsheets().get(spreadsheetId=sheet_id)
response = request.execute()
error = None
except Exception:
error = traceback.format_exc()
response = None
context = {
'response': response,
'error': error,
}
return flask.jsonify(context)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment