Last active
August 5, 2020 13:40
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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