diff --git a/python/auth-app/README.md b/python/auth-app/README.md index 8cbcfdc9..5349cf30 100644 --- a/python/auth-app/README.md +++ b/python/auth-app/README.md @@ -1,103 +1,131 @@ -# Google Chat authorization app - -This code sample creates a Google Chat app that requests additional -authorizations from the user. This app retrieves the user's Google profile -information from [People API](https://developers.google.com/people/), and -is performing authorization against -[Google's OAuth2](https://developers.google.com/identity/protocols/OAuth2WebServer) -endpoints. - -The sample is built using Python on Google App Engine, Standard Environment. - -For more information on connecting a Chat app with other services and tools, -please read the -[guide](https://developers.google.com/workspace/chat/connect-web-services-tools). - -## Deploy the sample - - 1. Follow the steps in [Setting Up Your Development Environment](https://cloud.google.com/appengine/docs/standard/python3/setting-up-environment) - to install Python and the Google Cloud SDK - 1. Follow the steps in [Setting Up Your GCP Resources](https://cloud.google.com/appengine/docs/standard/python3/console/#create) - to create a project and enable App Engine. - 1. Enable the People API for your project using - [this wizard](https://console.cloud.google.com/flows/enableapi?apiid=people.googleapis.com). - 1. Enable the Cloud Datastore API for your project using - [this wizard](https://console.cloud.google.com/flows/enableapi?apiid=datastore.googleapis.com). - 1. Follow [instructions](https://support.google.com/googleapi/answer/6158849?hl=en) for creating - an oauth client ID for your project. Use the type "Web application" and a redirect - URI of \ - `https://.appspot.com/auth/callback`. - 1. Download the associated JSON file, move it to this directory, and name it - `client_secret.json`. - - 1. Run the following command to deploy the app: +# Google Chat Authorization App + +This sample demonstrates how to create a Google Chat app that requests authorization from the user to access their Google profile information using the People API. This app is built using Python on Google App Engine (Standard Environment) and leverages Google's OAuth2 for authorization. + +**Key Features:** + +* **User Authorization:** Securely requests user consent to access their Google profile data. +* **People API Integration:** Retrieves and displays user profile information. +* **Google Chat Integration:** Responds to @mentions in Google Chat. +* **App Engine Deployment:** Provides step-by-step instructions for deploying to App Engine. + +## Prerequisites + +* **Python 3.7 or higher:** [Download](https://www.python.org/downloads/) +* **Google Cloud SDK:** [Install](https://cloud.google.com/sdk/docs/install) +* **Google Cloud Project:** [Create](https://console.cloud.google.com/projectcreate) +* **Basic familiarity with Google Cloud Console and command line:** + +## Deployment Steps + +1. **Enable APIs:** + * Enable the People API: [Enable People API](https://console.cloud.google.com/flows/enableapi?apiid=people.googleapis.com) + * Enable the Cloud Datastore API: [Enable Datastore API](https://console.cloud.google.com/flows/enableapi?apiid=datastore.googleapis.com) + +2. **Create OAuth Client ID:** + * In your Google Cloud project, go to [APIs & Services > Credentials](https://console.cloud.google.com/apis/credentials). + * Click "Create Credentials" > "OAuth client ID". + * Select "Web application" as the application type. + * Add `http://localhost:8080/auth/callback` to "Authorized redirect URIs" for local testing. + * Download the JSON file and rename it to `client_secrets.json` in your project directory. + +3. **Deploy to App Engine:** + * Open `app.yaml` and replace `` with the email address of your App Engine default service account (you can find this in the App Engine settings in Cloud Console). + * Deploy the app: + ```bash + gcloud app deploy ``` + * Get the app hostname: + ```bash + gcloud app describe | grep defaultHostname + ``` + * Update `client_secrets.json`: Replace `http://localhost:8080/auth/callback` in "Authorized redirect URIs" with `/auth/callback`. + * Redeploy the app: + ```bash gcloud app deploy ``` -## Configure the app for Google Chat +4. **Grant Datastore Permissions:** + * Grant the App Engine default service account permissions to access Datastore: + ```bash + PROJECT_ID=$(gcloud config list --format='value(core.project)') + SERVICE_ACCOUNT_EMAIL=$(gcloud app describe | grep serviceAccount | cut -d ':' -f 2) + gcloud projects add-iam-policy-binding $PROJECT_ID \ + --member="serviceAccount:$SERVICE_ACCOUNT_EMAIL" \ + --role="roles/datastore.owner" + ``` - 1. To configure the app to respond to @mentions in Google Chat, follow - the steps to enable the API in - [Publishing apps](https://developers.google.com/chat/how-tos/apps-publish). - 1. When configuring the app on the **Configuration** tab on the - **Google Chat API** page, enter the URL for the deployed version - of the app into the **Bot URL** text box. +## Configure Google Chat Integration -## Interact with the app +1. **Enable the Google Chat API:** [Enable Chat API](https://console.cloud.google.com/flows/enableapi?apiid=chat.googleapis.com) +2. **Create a Google Chat App:** + * Go to [Google Chat API](https://developers.google.com/chat/api/guides/quickstart/apps-script) and click "Configuration". + * Enter your App Engine app's URL (obtained in the previous deployment steps) as the **Bot URL**. + * Complete the rest of the configuration as needed. -Either add and @mention the app in a room or in a direct mention to engage with the app. +## Interact with the App -When first messaged or added to a space, the app will respond with a private rqeuest -to configure the app. Follow the link to authorize access to your profile data. Subsequent -messages will display a card with your profile. +* Add the app to a Google Chat space. +* @mention the app. +* Follow the authorization link to grant the app access to your profile. +* Send messages to the app to see your profile information. +* Type "logout" to deauthorize the app. -To deauthorize the app, message "logout" to the app. +## Run Locally -## Run the sample locally +1. **Set up Service Account:** + * Create a service account with the "Project > Editor" role. + * Download the service account key as a JSON file (`service-acct.json`). -Note: Follow the steps for deployment and configuring the app for Google Chat -before running locally. +2. **Set Environment Variable:** + ```bash + export GOOGLE_APPLICATION_CREDENTIALS=./service-acct.json +```` - 1. Create a service account for the app, as documented - [here](https://developers.google.com/chat/api/guides/auth/service-accounts). - Save the private key in a `service-acct.json` file in the working directory. - 1. Start a virtual environment - ``` - python3 -m venv python3.10 - source python3.10/bin/activate - ``` - 1. Install libraries using `pip`. - `pip install -r requirements.txt --upgrade` - 1. Run the sample. - `GOOGLE_APPLICATION_CREDENTIALS=service-acct.json python main.py` +3. **Create Virtual Environment (Recommended):** -To verify that the sample is running and responds with the correct data -to incoming requests, run the following command from the terminal: + ```bash + python3 -m venv venv + source venv/bin/activate + ``` -``` -curl -H 'Content-Type: application/json' --data '{"type": "MESSAGE", "configCompleteRedirectUrl": "https://www.example.com", "message": { "text": "header keyvalue", "thread": null }, "user": { "name": "users/123", "displayName": "me"}, "space": { "displayName": "space", "name": "spaces/-oMssgAAAAE"}}' http://127.0.0.1:8080/ -``` +4. **Install Dependencies:** -## Shut down the local environment + ```bash + pip install -r requirements.txt + ``` -``` -deactivate -``` +5. **Run the App:** -## Troubleshooting + ```bash + python main.py + ``` -Note: When running this sample, you may receive an error about -SpooledTemporaryFile class missing from the werkzeug module. To fix this, after -you've downloaded all of the support libraries to lib/ open up -lib/werkzeug/formparser.py and change the following line +6. **Test the App:** ``` -from tempfile import SpooledTemporaryFile +curl \ + -H 'Content-Type: application/json' \ + --data '{ + "type": "MESSAGE", + "configCompleteRedirectUrl": "https://www.example.com", + "message": { + "text": "header keyvalue", + "thread": null + }, + "user": { + "name": "users/123", + "displayName": "me" + }, + "space": { + "displayName": "space", + "name": "spaces/-oMssgAAAAE" + } + }' \ + http://127.0.0.1:8080/ ``` -to +## Troubleshooting -``` -from tempfile import TemporaryFile -``` + * **`SpooledTemporaryFile` Error:** If you encounter an error related to the `SpooledTemporaryFile` class, replace `from tempfile import SpooledTemporaryFile` with `from tempfile import TemporaryFile` in `lib/werkzeug/formparser.py`. + * **Other Errors:** Refer to the [Google Chat API documentation](https://www.google.com/url?sa=E&source=gmail&q=https://developers.google.com/chat/api/guides/overview) and [App Engine documentation](https://cloud.google.com/appengine/docs) for troubleshooting and common issues. diff --git a/python/auth-app/app.yaml b/python/auth-app/app.yaml index 51540237..6f85dd49 100644 --- a/python/auth-app/app.yaml +++ b/python/auth-app/app.yaml @@ -21,3 +21,5 @@ runtime: python310 env_variables: CLIENT_SECRET_PATH: "client_secret.json" SESSION_SECRET: "notasecret" + +service_account: diff --git a/python/auth-app/auth.py b/python/auth-app/auth.py index 7383ae4c..4d9711e9 100644 --- a/python/auth-app/auth.py +++ b/python/auth-app/auth.py @@ -32,21 +32,22 @@ from google.oauth2.credentials import Credentials from google_auth_oauthlib import flow -CLIENT_SECRET_PATH = os.environ.get('CLIENT_SECRET_PATH', 'client_secret.json') -JWT_SECRET = os.environ.get('SESSION_SECRET', 'notasecret') +CLIENT_SECRET_PATH = os.environ.get("CLIENT_SECRET_PATH", "client_secrets.json") +JWT_SECRET = os.environ.get("SESSION_SECRET", "notasecret") -mod = flask.Blueprint('auth', __name__) +mod = flask.Blueprint("auth", __name__) # Scopes required to access the People API. PEOPLE_API_SCOPES = [ - 'openid', - 'https://www.googleapis.com/auth/user.emails.read', - 'https://www.googleapis.com/auth/user.addresses.read', - 'https://www.googleapis.com/auth/userinfo.profile', - 'https://www.googleapis.com/auth/userinfo.email', - 'https://www.googleapis.com/auth/user.phonenumbers.read', + "openid", + "https://www.googleapis.com/auth/user.emails.read", + "https://www.googleapis.com/auth/user.addresses.read", + "https://www.googleapis.com/auth/userinfo.profile", + "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/user.phonenumbers.read", ] + class Store: """Manages storage in Google Cloud Datastore.""" @@ -62,12 +63,15 @@ def get_user_credentials(self, user_name: str) -> Credentials | None: Returns: A Credentials object, or None if the user has not authorized the app. """ - key = self.datastore_client.key('RefreshToken', user_name) - entity = self.datastore_client.get(key) - - if entity is None or 'credentials' not in entity: + try: + key = self.datastore_client.key("RefreshToken", user_name) + entity = self.datastore_client.get(key) + if entity is None or "credentials" not in entity: + return None + return Credentials(**entity["credentials"]) + except Exception as e: + logging.exception("Error retrieving credentials: %s", e) return None - return Credentials(**entity['credentials']) def put_user_credentials(self, user_name: str, creds: Credentials) -> None: """Stores OAuth2 credentials for a user. @@ -76,20 +80,25 @@ def put_user_credentials(self, user_name: str, creds: Credentials) -> None: user_name (str): The identifier for the associated user. creds (Credentials): The OAuth2 credentials obtained for the user. """ - key = self.datastore_client.key('RefreshToken', user_name) - entity = datastore.Entity(key) - entity.update({ - 'credentials': { - 'token': creds.token, - 'refresh_token': creds.refresh_token, - 'token_uri': creds.token_uri, - 'client_id': creds.client_id, - 'client_secret': creds.client_secret, - 'scopes': creds.scopes, - }, - 'timestamp': time.time() - }) - self.datastore_client.put(entity) + try: + key = self.datastore_client.key("RefreshToken", user_name) + entity = datastore.Entity(key) + entity.update( + { + "credentials": { + "token": creds.token, + "refresh_token": creds.refresh_token, + "token_uri": creds.token_uri, + "client_id": creds.client_id, + "client_secret": creds.client_secret, + "scopes": creds.scopes, + }, + "timestamp": time.time(), + } + ) + self.datastore_client.put(entity) + except Exception as e: + logging.exception("Error storing credentials: %s", e) def delete_user_credentials(self, user_name: str) -> None: """Deleted stored OAuth2 credentials for a user. @@ -97,14 +106,25 @@ def delete_user_credentials(self, user_name: str) -> None: Args: user_name (str): The identifier for the associated user. """ - key = self.datastore_client.key('RefreshToken', user_name) + try: + key = self.datastore_client.key("RefreshToken", user_name) + self.datastore_client.delete(key) + except Exception as e: + logging.exception("Error deleting credentials: %s", e) + + key = self.datastore_client.key("RefreshToken", user_name) self.datastore_client.delete(key) def get_user_credentials(user_name: str) -> Credentials: """Gets stored crednetials for a user, if it exists.""" - store = Store() - return store.get_user_credentials(user_name) + try: + store = Store() + return store.get_user_credentials(user_name) + except Exception as e: + logging.exception("Error getting credentials: %s", e) + return None + def get_config_url(event) -> Any: """Gets the authorization URL to redirect the user to. @@ -116,11 +136,14 @@ def get_config_url(event) -> Any: Returns: str: The authorization URL to direct the user to. """ - payload = { - 'completion_url': event['configCompleteRedirectUrl'] - } - token = jwt.encode(payload, JWT_SECRET, algorithm='HS256') - return flask.url_for('auth.start_auth', token=token, _external=True) + try: + payload = {"completion_url": event["configCompleteRedirectUrl"]} + token = jwt.encode(payload, JWT_SECRET, algorithm="HS256") + return flask.url_for("auth.start_auth", token=token, _external=True) + except Exception as e: + logging.exception("Error getting config URL: %s", e) + return None + def logout(user_name: str) -> None: """Logs out the user, removing their stored credentials and revoking the @@ -129,66 +152,78 @@ def logout(user_name: str) -> None: Args: user_name (str): The identifier of the user. """ - store = Store() - user_credentials = store.get_user_credentials(user_name) - if user_credentials is None: - logging.info('Ignoring logout request for user %s', user_name) - return - logging.info('Logging out user %s', user_name) - store.delete_user_credentials(user_name) - request = requests.Request() - request.post( - 'https://accounts.google.com/o/oauth2/revoke', - params={'token': user_credentials.token}, - headers={'Content-Type': 'application/x-www-form-urlencoded'}) - - -@mod.route('/start') + try: + store = Store() + user_credentials = store.get_user_credentials(user_name) + if user_credentials is None: + logging.info("Ignoring logout request for user %s", user_name) + return + logging.info("Logging out user %s", user_name) + store.delete_user_credentials(user_name) + request = requests.Request() + request.post( + "https://accounts.google.com/o/oauth2/revoke", + params={"token": user_credentials.token}, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + except Exception as e: + logging.exception("Error logging out user: %s", e) + + +@mod.route("/start") def start_auth() -> flask.Response: """Begins the oauth flow to authorize access to profile data.""" - token = flask.request.args['token'] - request = jwt.decode(token, JWT_SECRET, algorithm='HS256') - - flask.session['completion_url'] = request['completion_url'] - oauth2_flow = flow.Flow.from_client_secrets_file( - CLIENT_SECRET_PATH, - scopes=PEOPLE_API_SCOPES, - redirect_uri=flask.url_for('auth.on_oauth2_callback', _external=True)) - oauth2_url, state = oauth2_flow.authorization_url( - access_type='offline', - include_granted_scopes='true', - prompt='consent') - flask.session['state'] = state - return flask.redirect(oauth2_url) - -@mod.route('/callback') + try: + token = flask.request.args["token"] + request = jwt.decode(token, JWT_SECRET, algorithms=["HS256"]) + + flask.session["completion_url"] = request["completion_url"] + oauth2_flow = flow.Flow.from_client_secrets_file( + CLIENT_SECRET_PATH, + scopes=PEOPLE_API_SCOPES, + redirect_uri=flask.url_for("auth.on_oauth2_callback", _external=True), + ) + oauth2_url, state = oauth2_flow.authorization_url( + access_type="offline", include_granted_scopes="true", prompt="consent" + ) + flask.session["state"] = state + return flask.redirect(oauth2_url) + except Exception as e: + logging.exception("Error starting auth: %s", e) + return flask.abort(403) + + +@mod.route("/callback") def on_oauth2_callback() -> flask.Response: """Handles the OAuth callback.""" - saved_state = flask.session['state'] - state = flask.request.args['state'] - - if state != saved_state: - logging.warn('Mismatched state in oauth response') + try: + saved_state = flask.session["state"] + state = flask.request.args["state"] + + if state != saved_state: + logging.warn("Mismatched state in oauth response") + return flask.abort(403) + + redirect_uri = flask.url_for("auth.on_oauth2_callback", _external=True) + oauth2_flow = flow.Flow.from_client_secrets_file( + CLIENT_SECRET_PATH, scopes=PEOPLE_API_SCOPES, redirect_uri=redirect_uri + ) + oauth2_flow.fetch_token(authorization_response=flask.request.url) + creds = oauth2_flow.credentials + + # Use the id_token to identify the chat user. + request = requests.Request() + id_info = id_token.verify_oauth2_token(creds.id_token, request, creds.client_id) + + if id_info["iss"] != "https://accounts.google.com": + flask.abort(403) + + user_id = id_info["sub"] + user_name = "users/{user_id}".format(user_id=user_id) + store = Store() + store.put_user_credentials(user_name, creds) + completion_url = flask.session["completion_url"] + return flask.redirect(completion_url) + except Exception as e: + logging.exception("Error completing auth: %s", e) return flask.abort(403) - - redirect_uri = flask.url_for('auth.on_oauth2_callback', _external=True) - oauth2_flow = flow.Flow.from_client_secrets_file( - CLIENT_SECRET_PATH, - scopes=PEOPLE_API_SCOPES, - redirect_uri=redirect_uri) - oauth2_flow.fetch_token(authorization_response=flask.request.url) - creds = oauth2_flow.credentials - - # Use the id_token to identify the chat user. - request = requests.Request() - id_info = id_token.verify_oauth2_token(creds.id_token, request, creds.client_id) - - if id_info['iss'] != 'https://accounts.google.com': - flask.abort(403) - - user_id = id_info['sub'] - user_name = 'users/{user_id}'.format(user_id=user_id) - store = Store() - store.put_user_credentials(user_name, creds) - completion_url = flask.session['completion_url'] - return flask.redirect(completion_url) diff --git a/python/auth-app/install.sh b/python/auth-app/install.sh new file mode 100755 index 00000000..3ca6b843 --- /dev/null +++ b/python/auth-app/install.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +python3 -m venv .venv +source .venv/bin/activate +python3 -m pip install --upgrade -r requirements.txt diff --git a/python/auth-app/main.py b/python/auth-app/main.py index 2802e75f..e54b732a 100644 --- a/python/auth-app/main.py +++ b/python/auth-app/main.py @@ -31,149 +31,180 @@ import auth app = flask.Flask(__name__) -app.register_blueprint(auth.mod, url_prefix='/auth') +app.register_blueprint(auth.mod, url_prefix="/auth") app.wsgi_app = ProxyFix(app.wsgi_app) # Set the secret key for sessions -app.secret_key = os.environ.get('SESSION_SECRET', 'notasecret') +app.secret_key = os.environ.get("SESSION_SECRET", "notasecret") logging.basicConfig( level=logging.INFO, - style='{', - format='{levelname:.1}{asctime} {filename}:{lineno}] {message}') + style="{", + format="{levelname:.1}{asctime} {filename}:{lineno}] {message}", +) -@app.route('/', methods=['GET']) + +@app.route("/", methods=["GET"]) def home(): """Default home page""" - return flask.render_template('home.html') + return flask.render_template("home.html") + -@app.route('/', methods=['POST']) -def on_event() -> (Any | dict): +@app.route("/", methods=["POST"]) +def on_event() -> Any | dict: """Handler for events from Google Chat.""" if event := flask.request.get_json(): - if message := event.get('message'): - if 'logout' in message.get('text', '').lower(): - return on_logout(event) - else: - return on_mention(event) - if event['type'] == 'ADDED_TO_SPACE': - return flask.jsonify({ - 'text': ( - 'Thanks for adding me! ' - 'Try mentioning me with `@MyProfile` to see your profile.' - ) - }) - return flask.jsonify({}) - - return 'Error: Unknown action' + if message := event.get("message"): + if "logout" in message.get("text", "").lower(): + return on_logout(event) + else: + return on_mention(event) + if event["type"] == "ADDED_TO_SPACE": + return flask.jsonify( + { + "text": ( + "Thanks for adding me! " + "Try mentioning me with `@MyProfile` to see your profile." + ) + } + ) + return flask.jsonify({}) + + return "Error: Unknown action" def on_mention(event: dict) -> dict: """Handles a mention from Google Chat.""" - user_name = event['user']['name'] + user_name = event["user"]["name"] user_credentials = auth.get_user_credentials(user_name) if not user_credentials: - logging.info('Requesting credentials for user %s', user_name) - return flask.jsonify({ - 'actionResponse': { - 'type': 'REQUEST_CONFIG', - 'url': auth.get_config_url(event), - }, - }) - logging.info('Found existing auth credentials for user %s', user_name) + logging.info("Requesting credentials for user %s", user_name) + return flask.jsonify( + { + "actionResponse": { + "type": "REQUEST_CONFIG", + "url": auth.get_config_url(event), + }, + } + ) + logging.info("Found existing auth credentials for user %s", user_name) return flask.jsonify(produce_profile_message(user_credentials)) def on_logout(event) -> dict: """Handles logging out the user.""" - user_name = event['user']['name'] + user_name = event["user"]["name"] try: auth.logout(user_name) except Exception as e: logging.exception(e) - return flask.jsonify({ - 'text': 'Failed to log out user %s: ```%s```' % (user_name, e), - }) + return flask.jsonify( + { + "text": "Failed to log out user %s: ```%s```" % (user_name, e), + } + ) else: - return flask.jsonify({ - 'text': 'Logged out.', - }) + return flask.jsonify( + { + "text": "Logged out.", + } + ) def produce_profile_message(creds: Credentials) -> dict: """Generate a message containing the users profile inforamtion.""" - people_api = discovery.build('people', 'v1', credentials=creds) + people_api = discovery.build("people", "v1", credentials=creds) try: - person = people_api.people().get( - resourceName='people/me', - personFields=','.join([ - 'names', - 'addresses', - 'emailAddresses', - 'phoneNumbers', - 'photos', - ])).execute() + person = ( + people_api.people() + .get( + resourceName="people/me", + personFields=",".join( + [ + "names", + "addresses", + "emailAddresses", + "phoneNumbers", + "photos", + ] + ), + ) + .execute() + ) except Exception as e: logging.exception(e) return { - 'text': 'Failed to fetch profile info: ```%s```' % e, + "text": "Failed to fetch profile info: ```%s```" % e, } card = {} - if person.get('names') and person.get('photos'): - card.update({ - 'header': { - 'title': person['names'][0]['displayName'], - 'imageUrl': person['photos'][0]['url'], - 'imageStyle': 'AVATAR', - }, - }) - widgets = [] - for email_address in person.get('emailAddresses', []): - widgets.append({ - 'keyValue': { - 'icon': 'EMAIL', - 'content': email_address['value'], + if person.get("names") and person.get("photos"): + card.update( + { + "header": { + "title": person["names"][0]["displayName"], + "imageUrl": person["photos"][0]["url"], + "imageStyle": "AVATAR", + }, } - }) - for phone_number in person.get('phoneNumbers', []): - widgets.append({ - 'keyValue': { - 'icon': 'PHONE', - 'content': phone_number['value'], + ) + widgets = [] + for email_address in person.get("emailAddresses", []): + widgets.append( + { + "keyValue": { + "icon": "EMAIL", + "content": email_address["value"], + } } - }) - for address in person.get('addresses', []): - if 'formattedValue' in address: - widgets.append({ - 'keyValue': { - 'icon': 'MAP_PIN', - 'content': address['formattedValue'], + ) + for phone_number in person.get("phoneNumbers", []): + widgets.append( + { + "keyValue": { + "icon": "PHONE", + "content": phone_number["value"], } - }) - if widgets: - card.update({ - 'sections': [ + } + ) + for address in person.get("addresses", []): + if "formattedValue" in address: + widgets.append( { - 'widgets': widgets, + "keyValue": { + "icon": "MAP_PIN", + "content": address["formattedValue"], + } } - ] - }) + ) + if widgets: + card.update( + { + "sections": [ + { + "widgets": widgets, + } + ] + } + ) if card: - return {'cards': [card]} + return {"cards": [card]} return { - 'text': 'Hmm, no profile information found', + "text": "Hmm, no profile information found", } -if __name__ == '__main__': - os.environ.update({ - # Disable HTTPS check in oauthlib when testing locally. - 'OAUTHLIB_INSECURE_TRANSPORT': '1', - }) +if __name__ == "__main__": + os.environ.update( + { + # Disable HTTPS check in oauthlib when testing locally. + "OAUTHLIB_INSECURE_TRANSPORT": "1", + } + ) - if not os.environ['GOOGLE_APPLICATION_CREDENTIALS']: + if not os.environ["GOOGLE_APPLICATION_CREDENTIALS"]: raise Exception( - 'Set the environment variable GOOGLE_APPLICATION_CREDENTIALS with ' - 'the path to your service account JSON file.') + "Set the environment variable GOOGLE_APPLICATION_CREDENTIALS with " + "the path to your service account JSON file." + ) app.run(port=8080, debug=True) diff --git a/python/auth-app/templates/home.html b/python/auth-app/templates/home.html new file mode 100644 index 00000000..26a0a9f1 --- /dev/null +++ b/python/auth-app/templates/home.html @@ -0,0 +1,13 @@ + + + + + Home Page + + + +

Welcome to My Home Page

+

This is a simple home page.

+ + +