There are mainly 3 options for Credentials storage on could function
- Cloud Storage ($0.026 per GB, network free within same location, $0.05 per 10,000 operation)
- Firestore ($0.18 per GB, network free within same location, $0.06/$0.18 per 100,000 document)
- Datastore
I chose Firestore, with Cloud Functions /tmp directory as local cache.
import loggingimport osimport datetimelog = logging.getLogger(__name__)log.setLevel(logging.DEBUG)class FirestoreStorage: def __init__(self, doc_ref, content_field, created_field, modified_field, file_cache=None): self.doc_ref = doc_ref self.content_field = content_field self.created_field = created_field self.modified_field = modified_field if file_cache: self.file_cache = os.path.join(file_cache, doc_ref.id) else: self.file_cache = None def get(self): if self.file_cache: if os.path.exists(self.file_cache): log.debug(f"get from file: {self.file_cache}") with open(self.file_cache, 'rb') as f: return f.read() log.debug(f"get from firestore: {self.doc_ref.path}") doc = self.doc_ref.get() if not doc.exists: log.debug(f"firestore object does not exist") return None return doc.get(self.content_field) def update(self, content): if self.file_cache: os.makedirs(os.path.dirname(self.file_cache), exist_ok=True) with open(self.file_cache, 'wb') as f: log.debug(f"write to file: {self.file_cache}") f.write(content) data = { self.modified_field: datetime.datetime.now(), self.content_field: content } self.doc_ref.update(data) def create(self, content): if self.file_cache: os.makedirs(os.path.dirname(self.file_cache), exist_ok=True) with open(self.file_cache, 'wb') as f: log.debug(f"write to file: {self.file_cache}") f.write(content) data = { self.created_field: datetime.datetime.now(), self.content_field: content } self.doc_ref.set(data) def delete(self): if self.file_cache: if os.path.exists(self.file_cache): os.remove(self.file_cache) self.doc_ref.delete()
Usage
import pickleimport base64from googleapiclient.discovery import buildfrom google_auth_oauthlib.flow import Flow, InstalledAppFlowfrom google.auth.transport.requests import Requestfrom firebase_admin import firestoredef get_oauth2_credentials(uid, auth_code, scopes, secret_json_file): db = firestore.client() # prevent invalid chars in auth_code, convert to base64 doc_id = base64.urlsafe_b64encode(auth_code.encode('utf-8')).decode('utf-8') doc_ref = db.collection('user').document('uid').collection('oauth2_credential').document(doc_id) storage = FirestoreStorage( doc_ref=doc_ref, content_field='pickle', created_field='created', modified_field='modified', file_cache='/tmp/oauth2') creds = storage.get() if creds: creds = pickle.loads(creds) if not creds.valid: if creds.expired and creds.refresh_token: creds.refresh(Request()) storage.update(pickle.dumps(creds)) else: storage.delete() json_abort(400, message="Credentials expired/invalid and could not refresh") else: flow = Flow.from_client_secrets_file( secret_json_file, scopes=scopes) # if same auth_code is used again # oauthlib.oauth2.rfc6749.errors.InvalidGrantError: (invalid_grant) Bad Request flow.fetch_token(code=auth_code) creds = flow.credentials storage.create(pickle.dumps(creds)) return creds
NOTE: Refer Setup/Access Firestore on Cloud Functions (Python)
NOTE: Refer json_abort
from flask import jsonifyfrom googleapiclient.discovery import build@firebase_auth_requireddef google_photos_albums_list(request, decoded_token): uid = decoded_token['uid'] if not request.is_json: json_abort(400, message="Invalid request (json)") data = request.json.get('data') if not data: json_abort(400, message="Invalid request (data)") scopes = [ 'https://www.googleapis.com/auth/userinfo.profile', 'openid', 'https://www.googleapis.com/auth/photoslibrary.readonly' ] credentials = get_oauth2_credentials( uid=uid, auth_code=data['auth_code'], scopes=scopes, secret_json_file='keys/oauth2-secret.json' ) service = build('photoslibrary', 'v1', credentials=creds, cache_discovery=False) items = results.get('albums', []) ''' if not items: log.info('No items') else: for item in items: log.info(item['title']) # log.info(json.dumps(item, indent=2)) ''' data = { 'albums': items, 'next_page_token': results.get('nextPageToken', None) } return jsonify({ 'data': data })
NOTE: Refer firebase_auth_required
NOTE: Refer Android Access Google Photos API via Python REST