Skip to content

Commit

Permalink
feat: Add Node.js User Authorization App sample (googleworkspace#344)
Browse files Browse the repository at this point in the history
  • Loading branch information
gtondello authored Jan 2, 2025
1 parent c8fb5cf commit cf5ab5c
Show file tree
Hide file tree
Showing 13 changed files with 3,701 additions and 0 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ Python, Java, Apps Script) of the following code samples and more:
implemented.
- **Selection input app**: This app demonstrates how to use external data
sources to dynamically provide selection items in card widgets.
- **User Auth app**: This app demonstrates how to obtain authorization to call
Chat API with user credentials and store the user tokens in a database to be
reused later.
- **Webhook app**: This app demonstrates how to send messages to Google Chat
with incoming webhooks.

Expand Down
29 changes: 29 additions & 0 deletions node/user-auth-app/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Copyright 2025 Google LLC
*
* 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.
*/

module.exports = {
"extends": ["eslint:recommended", "google"],
"env": {
"node": true,
},
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
},
"rules": {
"no-console": "off",
},
};
18 changes: 18 additions & 0 deletions node/user-auth-app/.gcloudignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# This file specifies files that are *not* uploaded to Google Cloud
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
# $ gcloud topic gcloudignore
#
.gcloudignore
README.md
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore

# Node.js dependencies:
node_modules/
2 changes: 2 additions & 0 deletions node/user-auth-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
client_secrets.json
124 changes: 124 additions & 0 deletions node/user-auth-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Google Chat User Authorization App

This sample demonstrates how to create a Google Chat app that requests
authorization from the user to make calls to Chat API on their behalf. The first
time the user interacts with the app, it requests offline OAuth tokens for the
user and saves them to a Firestore database. If the user interacts with the app
again, the saved tokens are used so the app can call Chat API on behalf of the
user without asking for authorization again. Once saved, the OAuth tokens could
even be used to call Chat API without the user being present.

This app is built using Node.js on Google App Engine (Standard Environment) and
leverages Google's OAuth2 for authorization and Firestore for data storage.

**Key Features:**

* **User Authorization:** Securely requests user consent to call Chat API with
their credentials.
* **Chat API Integration:** Calls Chat API to post messages on behalf of the
user.
* **Google Chat Integration:** Responds to DMs or @mentions in Google Chat. If
necessary, request configuration to start an OAuth authorization flow.
* **App Engine Deployment:** Provides step-by-step instructions for deploying
to App Engine.
* **Cloud Firestore:** Stores user tokens in a Firestore database.

## Prerequisites

* **Node.js:** [Download](https://www.nodejs.org/)
* **Google Cloud SDK:** [Install](https://cloud.google.com/sdk/docs/install)
* **Google Cloud Project:** [Create](https://console.cloud.google.com/projectcreate)

## Deployment Steps

1. **Enable APIs:**

* Enable the Cloud Firestore and Google Chat APIs using the
[console](https://console.cloud.google.com/apis/enableflow?apiid=firestore.googleapis.com,chat.googleapis.com)
or gcloud:

```bash
gcloud services enable firestore.googleapis.com chat.googleapis.com
```

1. **Initiate Deployment to App Engine:**

* Go to [App Engine](https://console.cloud.google.com/appengine) and
initialize an application.

* Deploy the User Authorization app to App Engine:

```bash
gcloud app deploy
```

1. **Create and Use OAuth Client ID:**

* Get the app hostname:

```bash
gcloud app describe | grep defaultHostname
```

* 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 `<hostname from the previous step>/oauth2` to `Authorized redirect URIs`.
* Download the JSON file and rename it to `client_secrets.json` in your
project directory.
* Redeploy the app with the file `client_secrets.json`:

```bash
gcloud app deploy
```

1. **Create a Firestore Database:**

* Create a Firestore database in native mode named `auth-data` using the
[console](https://console.cloud.google.com/firestore) or gcloud:

```bash
gcloud firestore databases create \
--database=auth-data \
--location=REGION \
--type=firestore-native
```

Replace `REGION` with a
[Firestore location](https://cloud.google.com/firestore/docs/locations#types)
such as `nam5` or `eur3`.

## Create the Google Chat app

* Go to
[Google Chat API](https://console.cloud.google.com/apis/api/chat.googleapis.com/hangouts-chat)
and click `Configuration`.
* In **App name**, enter `User Auth App`.
* In **Avatar URL**, enter `https://developers.google.com/chat/images/quickstart-app-avatar.png`.
* In **Description**, enter `Quickstart app`.
* Under Functionality, select **Receive 1:1 messages** and
**Join spaces and group conversations**.
* Under **Connection settings**, select **HTTP endpoint URL** and enter your App
Engine app's URL (obtained in the previous deployment steps).
* In **Authentication Audience**, select **HTTP endpoint URL**.
* Under **Visibility**, select **Make this Google Chat app available to specific
people and groups in your domain** and enter your email address.
* Click **Save**.
The Chat app is ready to receive and respond to messages on Chat.
## Interact with the App
* Add the app to a Google Chat space.
* @mention the app.
* Follow the authorization link to grant the app access to your account.
* Once authorization is complete, the app will post a message to the space using
your credentials.
* If you @mention the app again, it will post a new message to the space with
your credentials using the saved tokens, without asking for authorization again.
## Related Topics
* [Authenticate and authorize as a Google Chat user](https://developers.google.com/workspace/chat/authenticate-authorize-chat-user)
* [Receive and respond to user interactions](https://developers.google.com/workspace/chat/receive-respond-interactions)
15 changes: 15 additions & 0 deletions node/user-auth-app/app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright 2025 Google LLC
#
# 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.

runtime: nodejs22
71 changes: 71 additions & 0 deletions node/user-auth-app/firestore-service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Copyright 2025 Google LLC
*
* 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.
*/

/**
* @fileoverview Service that handles database operations.
*/

import {Firestore} from '@google-cloud/firestore';

/**
@typedef Tokens
@type {Object}
@property {string} accessToken The OAuth access token for the user.
@property {string} refreshToken The OAuth refresh token for the user.
*/

/** The prefix used by the Google Chat API in the User resource name. */
const USERS_PREFIX = 'users/';

/** The name of the users collection in the database. */
const USERS_COLLECTION = 'users';

// Initialize the Firestore database using Application Default Credentials.
const db = new Firestore({databaseId: 'auth-data'});

/** Service that saves and loads OAuth user tokens on Firestore. */
export const FirestoreService = {
/**
* Saves the user's OAuth2 tokens to storage.
* @param {!string} userName The resource name of the user.
* @param {!string} accessToken The OAuth2 access token.
* @param {!string} refreshToken The OAuth2 refresh token.
* @return {Promise<void>}
*/
saveUserToken: async function(userName, accessToken, refreshToken) {
const docRef = db
.collection(USERS_COLLECTION)
.doc(userName.replace(USERS_PREFIX, ''));
await docRef.set({accessToken, refreshToken});
},

/**
* Fetches the user's OAuth2 tokens from storage.
* @param {!string} userName The resource name of the user.
* @return {Promise<Tokens | null>} The fetched tokens or null if the user is
* not found in the database.
*/
getUserToken: async function(userName) {
const doc = await db
.collection(USERS_COLLECTION)
.doc(userName.replace(USERS_PREFIX, ''))
.get();
if (doc.exists) {
return doc.data();
}
return null;
},
};
78 changes: 78 additions & 0 deletions node/user-auth-app/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* Copyright 2025 Google LLC
*
* 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.
*/

/**
* @fileoverview The main script for the project, which starts an Express app
* to listen to HTTP requests from Chat events and the OAuth flow callback.
*/

import express from 'express';
import {oauth2callback} from './oauth-flow.js';
import {verifyGoogleChatRequest} from './request-verifier.js';
import {postWithUserCredentials} from './user-auth-post.js';

/**
* Processes invocation events from Chat.
* @param {!Object} event The event received from Google Chat.
* @return {Promise<Object>} A response message to send back to Chat.
*/
async function processChatEvent(event) {
const message = event.message;
if (!message) {
// Ignore events that don't contain a message.
return {};
}
// Post a message back to the same Chat space using user credentials.
return postWithUserCredentials(event);
}

// Initialize an Express app to handle routing.
const app = express()
.use(express.urlencoded({extended: false}))
.use(express.json())
.enable('trust proxy');

/** App route that handles unsupported GET requests. */
app.get('/', (_, res) => {
res.send('Hello! This endpoint is meant to be called from Google Chat.');
});

/**
* App route that handles callback requests from the OAuth2 authorization flow.
* The handler exhanges the code received from the OAuth2 server with a set of
* credentials, stores the authentication and refresh tokens in the database,
* and redirects the request to the config complete URL provided in the request.
*/
app.get('/oauth2', async (req, res) => {
await oauth2callback(req, res);
});

/** App route that responds to interaction events from Google Chat. */
app.post('/', async (req, res) => {
if (!(await verifyGoogleChatRequest(req))) {
res.send('Hello! This endpoint is meant to be called from Google Chat.');
return;
}
const event = req.body;
const responseMessage = await processChatEvent(event);
res.json(responseMessage);
});

// Start listening for requests.
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`Server is running in port - ${PORT}`);
});
Loading

0 comments on commit cf5ab5c

Please sign in to comment.