Cloud Functions Store Oauth2 Credentials

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

❤️ Is this article helpful?

Buy me a coffee ☕ or support my work via PayPal to keep this space 🖖 and ad-free.

Do send some 💖 to @d_luaz or share this article.

✨ By Desmond Lua

A dream boy who enjoys making apps, travelling and making youtube videos. Follow me on @d_luaz

👶 Apps I built

Travelopy - discover travel places in Malaysia, Singapore, Taiwan, Japan.