Skip to content

Commit cf5ab5c

Browse files
authored
feat: Add Node.js User Authorization App sample (#344)
1 parent c8fb5cf commit cf5ab5c

File tree

13 files changed

+3701
-0
lines changed

13 files changed

+3701
-0
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ Python, Java, Apps Script) of the following code samples and more:
4343
implemented.
4444
- **Selection input app**: This app demonstrates how to use external data
4545
sources to dynamically provide selection items in card widgets.
46+
- **User Auth app**: This app demonstrates how to obtain authorization to call
47+
Chat API with user credentials and store the user tokens in a database to be
48+
reused later.
4649
- **Webhook app**: This app demonstrates how to send messages to Google Chat
4750
with incoming webhooks.
4851

node/user-auth-app/.eslintrc.cjs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
module.exports = {
18+
"extends": ["eslint:recommended", "google"],
19+
"env": {
20+
"node": true,
21+
},
22+
"parserOptions": {
23+
"ecmaVersion": 2020,
24+
"sourceType": "module",
25+
},
26+
"rules": {
27+
"no-console": "off",
28+
},
29+
};

node/user-auth-app/.gcloudignore

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# This file specifies files that are *not* uploaded to Google Cloud
2+
# using gcloud. It follows the same syntax as .gitignore, with the addition of
3+
# "#!include" directives (which insert the entries of the given .gitignore-style
4+
# file at that point).
5+
#
6+
# For more information, run:
7+
# $ gcloud topic gcloudignore
8+
#
9+
.gcloudignore
10+
README.md
11+
# If you would like to upload your .git directory, .gitignore file or files
12+
# from your .gitignore file, remove the corresponding line
13+
# below:
14+
.git
15+
.gitignore
16+
17+
# Node.js dependencies:
18+
node_modules/

node/user-auth-app/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules/
2+
client_secrets.json

node/user-auth-app/README.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Google Chat User Authorization App
2+
3+
This sample demonstrates how to create a Google Chat app that requests
4+
authorization from the user to make calls to Chat API on their behalf. The first
5+
time the user interacts with the app, it requests offline OAuth tokens for the
6+
user and saves them to a Firestore database. If the user interacts with the app
7+
again, the saved tokens are used so the app can call Chat API on behalf of the
8+
user without asking for authorization again. Once saved, the OAuth tokens could
9+
even be used to call Chat API without the user being present.
10+
11+
This app is built using Node.js on Google App Engine (Standard Environment) and
12+
leverages Google's OAuth2 for authorization and Firestore for data storage.
13+
14+
**Key Features:**
15+
16+
* **User Authorization:** Securely requests user consent to call Chat API with
17+
their credentials.
18+
* **Chat API Integration:** Calls Chat API to post messages on behalf of the
19+
user.
20+
* **Google Chat Integration:** Responds to DMs or @mentions in Google Chat. If
21+
necessary, request configuration to start an OAuth authorization flow.
22+
* **App Engine Deployment:** Provides step-by-step instructions for deploying
23+
to App Engine.
24+
* **Cloud Firestore:** Stores user tokens in a Firestore database.
25+
26+
## Prerequisites
27+
28+
* **Node.js:** [Download](https://www.nodejs.org/)
29+
* **Google Cloud SDK:** [Install](https://cloud.google.com/sdk/docs/install)
30+
* **Google Cloud Project:** [Create](https://console.cloud.google.com/projectcreate)
31+
32+
## Deployment Steps
33+
34+
1. **Enable APIs:**
35+
36+
* Enable the Cloud Firestore and Google Chat APIs using the
37+
[console](https://console.cloud.google.com/apis/enableflow?apiid=firestore.googleapis.com,chat.googleapis.com)
38+
or gcloud:
39+
40+
```bash
41+
gcloud services enable firestore.googleapis.com chat.googleapis.com
42+
```
43+
44+
1. **Initiate Deployment to App Engine:**
45+
46+
* Go to [App Engine](https://console.cloud.google.com/appengine) and
47+
initialize an application.
48+
49+
* Deploy the User Authorization app to App Engine:
50+
51+
```bash
52+
gcloud app deploy
53+
```
54+
55+
1. **Create and Use OAuth Client ID:**
56+
57+
* Get the app hostname:
58+
59+
```bash
60+
gcloud app describe | grep defaultHostname
61+
```
62+
63+
* In your Google Cloud project, go to
64+
[APIs & Services > Credentials](https://console.cloud.google.com/apis/credentials).
65+
* Click `Create Credentials > OAuth client ID`.
66+
* Select `Web application` as the application type.
67+
* Add `<hostname from the previous step>/oauth2` to `Authorized redirect URIs`.
68+
* Download the JSON file and rename it to `client_secrets.json` in your
69+
project directory.
70+
* Redeploy the app with the file `client_secrets.json`:
71+
72+
```bash
73+
gcloud app deploy
74+
```
75+
76+
1. **Create a Firestore Database:**
77+
78+
* Create a Firestore database in native mode named `auth-data` using the
79+
[console](https://console.cloud.google.com/firestore) or gcloud:
80+
81+
```bash
82+
gcloud firestore databases create \
83+
--database=auth-data \
84+
--location=REGION \
85+
--type=firestore-native
86+
```
87+
88+
Replace `REGION` with a
89+
[Firestore location](https://cloud.google.com/firestore/docs/locations#types)
90+
such as `nam5` or `eur3`.
91+
92+
## Create the Google Chat app
93+
94+
* Go to
95+
[Google Chat API](https://console.cloud.google.com/apis/api/chat.googleapis.com/hangouts-chat)
96+
and click `Configuration`.
97+
* In **App name**, enter `User Auth App`.
98+
* In **Avatar URL**, enter `https://developers.google.com/chat/images/quickstart-app-avatar.png`.
99+
* In **Description**, enter `Quickstart app`.
100+
* Under Functionality, select **Receive 1:1 messages** and
101+
**Join spaces and group conversations**.
102+
* Under **Connection settings**, select **HTTP endpoint URL** and enter your App
103+
Engine app's URL (obtained in the previous deployment steps).
104+
* In **Authentication Audience**, select **HTTP endpoint URL**.
105+
* Under **Visibility**, select **Make this Google Chat app available to specific
106+
people and groups in your domain** and enter your email address.
107+
* Click **Save**.
108+
109+
The Chat app is ready to receive and respond to messages on Chat.
110+
111+
## Interact with the App
112+
113+
* Add the app to a Google Chat space.
114+
* @mention the app.
115+
* Follow the authorization link to grant the app access to your account.
116+
* Once authorization is complete, the app will post a message to the space using
117+
your credentials.
118+
* If you @mention the app again, it will post a new message to the space with
119+
your credentials using the saved tokens, without asking for authorization again.
120+
121+
## Related Topics
122+
123+
* [Authenticate and authorize as a Google Chat user](https://developers.google.com/workspace/chat/authenticate-authorize-chat-user)
124+
* [Receive and respond to user interactions](https://developers.google.com/workspace/chat/receive-respond-interactions)

node/user-auth-app/app.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
runtime: nodejs22
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* @fileoverview Service that handles database operations.
19+
*/
20+
21+
import {Firestore} from '@google-cloud/firestore';
22+
23+
/**
24+
@typedef Tokens
25+
@type {Object}
26+
@property {string} accessToken The OAuth access token for the user.
27+
@property {string} refreshToken The OAuth refresh token for the user.
28+
*/
29+
30+
/** The prefix used by the Google Chat API in the User resource name. */
31+
const USERS_PREFIX = 'users/';
32+
33+
/** The name of the users collection in the database. */
34+
const USERS_COLLECTION = 'users';
35+
36+
// Initialize the Firestore database using Application Default Credentials.
37+
const db = new Firestore({databaseId: 'auth-data'});
38+
39+
/** Service that saves and loads OAuth user tokens on Firestore. */
40+
export const FirestoreService = {
41+
/**
42+
* Saves the user's OAuth2 tokens to storage.
43+
* @param {!string} userName The resource name of the user.
44+
* @param {!string} accessToken The OAuth2 access token.
45+
* @param {!string} refreshToken The OAuth2 refresh token.
46+
* @return {Promise<void>}
47+
*/
48+
saveUserToken: async function(userName, accessToken, refreshToken) {
49+
const docRef = db
50+
.collection(USERS_COLLECTION)
51+
.doc(userName.replace(USERS_PREFIX, ''));
52+
await docRef.set({accessToken, refreshToken});
53+
},
54+
55+
/**
56+
* Fetches the user's OAuth2 tokens from storage.
57+
* @param {!string} userName The resource name of the user.
58+
* @return {Promise<Tokens | null>} The fetched tokens or null if the user is
59+
* not found in the database.
60+
*/
61+
getUserToken: async function(userName) {
62+
const doc = await db
63+
.collection(USERS_COLLECTION)
64+
.doc(userName.replace(USERS_PREFIX, ''))
65+
.get();
66+
if (doc.exists) {
67+
return doc.data();
68+
}
69+
return null;
70+
},
71+
};

node/user-auth-app/index.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* @fileoverview The main script for the project, which starts an Express app
19+
* to listen to HTTP requests from Chat events and the OAuth flow callback.
20+
*/
21+
22+
import express from 'express';
23+
import {oauth2callback} from './oauth-flow.js';
24+
import {verifyGoogleChatRequest} from './request-verifier.js';
25+
import {postWithUserCredentials} from './user-auth-post.js';
26+
27+
/**
28+
* Processes invocation events from Chat.
29+
* @param {!Object} event The event received from Google Chat.
30+
* @return {Promise<Object>} A response message to send back to Chat.
31+
*/
32+
async function processChatEvent(event) {
33+
const message = event.message;
34+
if (!message) {
35+
// Ignore events that don't contain a message.
36+
return {};
37+
}
38+
// Post a message back to the same Chat space using user credentials.
39+
return postWithUserCredentials(event);
40+
}
41+
42+
// Initialize an Express app to handle routing.
43+
const app = express()
44+
.use(express.urlencoded({extended: false}))
45+
.use(express.json())
46+
.enable('trust proxy');
47+
48+
/** App route that handles unsupported GET requests. */
49+
app.get('/', (_, res) => {
50+
res.send('Hello! This endpoint is meant to be called from Google Chat.');
51+
});
52+
53+
/**
54+
* App route that handles callback requests from the OAuth2 authorization flow.
55+
* The handler exhanges the code received from the OAuth2 server with a set of
56+
* credentials, stores the authentication and refresh tokens in the database,
57+
* and redirects the request to the config complete URL provided in the request.
58+
*/
59+
app.get('/oauth2', async (req, res) => {
60+
await oauth2callback(req, res);
61+
});
62+
63+
/** App route that responds to interaction events from Google Chat. */
64+
app.post('/', async (req, res) => {
65+
if (!(await verifyGoogleChatRequest(req))) {
66+
res.send('Hello! This endpoint is meant to be called from Google Chat.');
67+
return;
68+
}
69+
const event = req.body;
70+
const responseMessage = await processChatEvent(event);
71+
res.json(responseMessage);
72+
});
73+
74+
// Start listening for requests.
75+
const PORT = process.env.PORT || 8080;
76+
app.listen(PORT, () => {
77+
console.log(`Server is running in port - ${PORT}`);
78+
});

0 commit comments

Comments
 (0)