From 56ec61f42bcdafddbd3e081f5adc6e16b0925bfc Mon Sep 17 00:00:00 2001 From: Adam Weeks Date: Thu, 2 Apr 2026 15:57:35 -0400 Subject: [PATCH] Add wxcc-token-service-firebase playbook (GCF + Firestore token sample) Vendors the WebexSamples token-service-sample as an implementation guide: Node Cloud Function, Firestore-backed OAuth for WxCC, README with six sections, APPHUB metadata, architecture diagram, and upstream notes in docs/. Made-with: Cursor --- .../wxcc-token-service-firebase/APPHUB.yaml | 131 +++++++++++++ .../wxcc-token-service-firebase/README.md | 71 +++++++ .../diagrams/architecture-diagram.md | 25 +++ .../docs/upstream-overview.md | 65 +++++++ .../wxcc-token-service-firebase/src/LICENSE | 22 +++ .../src/env.template | 17 ++ .../src/firestore.rules | 11 ++ .../wxcc-token-service-firebase/src/index.js | 183 ++++++++++++++++++ .../src/package.json | 15 ++ 9 files changed, 540 insertions(+) create mode 100644 playbooks/wxcc-token-service-firebase/APPHUB.yaml create mode 100644 playbooks/wxcc-token-service-firebase/README.md create mode 100644 playbooks/wxcc-token-service-firebase/diagrams/architecture-diagram.md create mode 100644 playbooks/wxcc-token-service-firebase/docs/upstream-overview.md create mode 100644 playbooks/wxcc-token-service-firebase/src/LICENSE create mode 100644 playbooks/wxcc-token-service-firebase/src/env.template create mode 100644 playbooks/wxcc-token-service-firebase/src/firestore.rules create mode 100644 playbooks/wxcc-token-service-firebase/src/index.js create mode 100644 playbooks/wxcc-token-service-firebase/src/package.json diff --git a/playbooks/wxcc-token-service-firebase/APPHUB.yaml b/playbooks/wxcc-token-service-firebase/APPHUB.yaml new file mode 100644 index 0000000..b351c6b --- /dev/null +++ b/playbooks/wxcc-token-service-firebase/APPHUB.yaml @@ -0,0 +1,131 @@ +# APPHUB.yaml — Playbook metadata for Webex App Hub +# Copy this file into your Playbook folder under playbooks// +# Fill in all required fields. See CONTRIBUTING.md for field rules. + +# ----------------------------------------------------------------------------- +# friendly_id — Unique identifier. Must end with -playbook (e.g. epic-ehr-playbook) +# ----------------------------------------------------------------------------- +friendly_id: "wxcc-token-service-firebase-playbook" + +# ----------------------------------------------------------------------------- +# title — Display name for the Playbook (matches ContentStack field) +# ----------------------------------------------------------------------------- +title: "Webex Contact Center — Cloud Function token service (Firestore)" + +# ----------------------------------------------------------------------------- +# tag_line — Short tagline for App Hub detail page (required, max 128 chars) +# ----------------------------------------------------------------------------- +tag_line: "Serverless WxCC tokens: GCF + Firestore, GET ?name=, passphrase header, OAuth callback and refresh." + +# ----------------------------------------------------------------------------- +# description — App Hub supports Markdown. Use a block scalar (description: |) with +# blank lines between sections: opening paragraph (bold key terms), **Why use this +# playbook** (3–5 outcome bullets), **What it does** (concrete behaviors/endpoints). +# Do not put upstream repo URLs or install-only instructions here—use README and +# src/README.md for reference playbooks. See docs/commands/import_playbook.md. +# ----------------------------------------------------------------------------- +description: | + A **Node.js** **Google Cloud Function** with **Firebase Firestore** that stores + **Webex OAuth** client settings and tokens, completes the **authorization code** + callback, and serves **GET** requests that return a fresh **Webex Contact Center** + **access token** for cloud callers (flows, automation, HTTP clients). + + **Why use this playbook** + + - **Cloud-native URL:** One **HTTPS** endpoint on **Google Cloud** instead of + operating your own VM for token storage when your stack is already GCP/Firebase. + - **Flow-friendly pattern:** Callers send **`?name=`** and a shared **`x-token-passphrase`** + header—easy to wire from **Flow Designer** or similar HTTP steps once network + access is allowed. + - **Documented Webex APIs:** Uses **`https://webexapis.com/v1/access_token`** for + code exchange and **refresh_token** grants—no undocumented endpoints. + - **Clear limits:** Describes **Firestore** layout, **`/init`** setup-only risk, + and when to repeat browser **OAuth** after **refresh** expiry. + + **What it does** + + - **`/callback`** — Exchanges **`code`** for tokens and merges them into the + **`tokens/{state}`** document in **Firestore**. + - **`/init`** — Creates an empty token document (setup only; dangerous if misused + in production). + - **GET** with **`name`** query — Validates **`TOKEN_PASSPHRASE`**, returns + **access token** if valid long enough, otherwise refreshes when **refresh token** + is still valid. + +# ----------------------------------------------------------------------------- +# product_types — Where this Playbook appears. Pick one or more. +# Valid: teams | meetings | calling | rooms | contact_center +# ----------------------------------------------------------------------------- +product_types: + - "contact_center" + +# ----------------------------------------------------------------------------- +# app_context — Where the integration runs. Pick one or more. +# Valid: space | in_meeting | call | device | contact_center | sidebar | mcp | a2a +# ----------------------------------------------------------------------------- +app_context: + - "contact_center" + +# ----------------------------------------------------------------------------- +# categories — App Hub category slugs. Pick one or more. +# Verticals: healthcare | financial-services | retail-ecommerce +# App categories (use kebab-case slugs): +# ai-agent-testing-observability | agent-supervisor-tools | analytics | +# calendar-scheduling | collaboration-management | customer-relations | +# customer-support | developer-tools | doc-management | education | +# finance | government | healthcare | human-resources | internet-of-things | +# marketing-sales | orchestration | platform | productivity | +# project-management | recording-transcriptions | security-compliance | +# self-service-bots | social-and-fun | strategy-team-planning | +# workflow-automation | workforce-optimization | other +# ----------------------------------------------------------------------------- +categories: + - "developer-tools" + - "security-compliance" + - "workflow-automation" + +# ----------------------------------------------------------------------------- +# company_name — Your company or team name +# ----------------------------------------------------------------------------- +company_name: "Webex for Developers" + +# ----------------------------------------------------------------------------- +# company_url — Your company or project URL +# ----------------------------------------------------------------------------- +company_url: "https://developer.webex.com" + +# ----------------------------------------------------------------------------- +# support_url — Issues or support link (e.g. GitHub issues) +# ----------------------------------------------------------------------------- +support_url: "https://github.com/webex/webexplaybooks/issues" + +# ----------------------------------------------------------------------------- +# product_url — Link to this Playbook in the repo (required) +# ----------------------------------------------------------------------------- +product_url: "https://github.com/webex/webexplaybooks/tree/main/playbooks/wxcc-token-service-firebase" + +# ----------------------------------------------------------------------------- +# logo — (Optional) URL to your logo image. If not provided, defaults to the +# standard Webex Playbook logo. +# ----------------------------------------------------------------------------- +logo: "https://images.contentstack.io/v3/assets/bltd14fd2a03236233f/blta2de9daa773c6604/60f71f81e2de935fc7e35dbe/download" + +# ----------------------------------------------------------------------------- +# estimated_implementation_time — e.g. "2-4 hours", "1 day" +# ----------------------------------------------------------------------------- +estimated_implementation_time: "2-4 hours" + +# ----------------------------------------------------------------------------- +# third_party_tool — (Optional) The tool being integrated (e.g. Salesforce, Epic) +# Omit for generic playbooks (e.g. "any CMS") +# ----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- +# privacy_url — Privacy policy URL (required; use Cisco default for Webex-authored) +# ----------------------------------------------------------------------------- +privacy_url: "https://www.cisco.com/c/en/us/about/legal/privacy-full.html" + +# ----------------------------------------------------------------------------- +# submission_date — (Optional) ISO date (e.g. 2025-03-01) +# ----------------------------------------------------------------------------- +submission_date: "2026-04-02" diff --git a/playbooks/wxcc-token-service-firebase/README.md b/playbooks/wxcc-token-service-firebase/README.md new file mode 100644 index 0000000..21bd6e4 --- /dev/null +++ b/playbooks/wxcc-token-service-firebase/README.md @@ -0,0 +1,71 @@ +# Webex Contact Center token service (Firebase) Playbook + +This Playbook is adapted from the [token-service-sample](https://github.com/WebexSamples/webex-contact-center-api-samples/tree/main/token-management-samples/token-service-sample) in the Webex Contact Center API samples repository on GitHub. + +## Use Case Overview + +**Webex Contact Center** flows and cloud integrations (for example Flow Designer HTTP activities, JDS, or Webex Connect) often need a **callable HTTPS endpoint** that returns a **current Webex access token** without storing client secrets inside the flow. This sample deploys a **Google Cloud Function** backed by **Firestore** so token storage and refresh run next to Google Cloud/Firebase infrastructure. + +**Target persona:** Developers or integration engineers who already use **Google Cloud** and **Firebase**, can register a **Webex Integration**, and are comfortable deploying functions and editing Firestore documents. + +**Estimated implementation time:** 2–4 hours (GCP/Firebase project, deploy function with `TOKEN_PASSPHRASE`, deploy Firestore rules, register Integration redirect URI, run `/init`, paste OAuth client fields into Firestore, complete browser OAuth, verify GET with `?name=` and header). + +**Compared to [wxcc-token-management-sample](../wxcc-token-management-sample/README.md):** that Playbook uses **Express + SQLite** on infrastructure you operate. This Playbook uses **managed Cloud Functions + Firestore** when you want a **serverless** URL for cloud-to-cloud calls. + +## Architecture + +An **HTTP-triggered Cloud Function** (`exports.tokenService` in [`src/index.js`](src/index.js)) handles three paths: + +1. **`/callback`** — OAuth redirect from Webex; exchanges `code` for tokens using `clientId`, `clientSecret`, and `redirectUri` read from the Firestore document named by the OAuth `state` parameter (your token name). +2. **`/init`** — **Setup only:** creates or merges a `tokens/{name}` document with empty OAuth client fields. Do not call in production after configuration is complete (see [docs/upstream-overview.md](docs/upstream-overview.md)). +3. **GET** (root path with query `name`) — Validates header **`x-token-passphrase`** against environment variable **`TOKEN_PASSPHRASE`**, loads the token document, returns the access token if it has more than **two hours** left, otherwise refreshes via **`https://webexapis.com/v1/access_token`** if the refresh token is still valid (at least **two minutes** remaining per upstream logic). + +**Firestore** holds per-token OAuth client credentials and tokens. **firestore.rules** denies direct client SDK access so data is intended to be maintained via the Admin SDK inside the function or the Firebase console. + +See the [architecture diagram](diagrams/architecture-diagram.md) for the sequence view. + +## Prerequisites + +- **Google Cloud** project and billing appropriate for **Cloud Functions** and **Firestore** ([creating projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects)). +- **Firebase** linked to the project ([Firebase Console](https://console.firebase.google.com)), Firestore in **Native** mode. +- **Node.js** 18 aligned with [`src/package.json`](src/package.json) for local packaging or CI checks (deployment is still to Cloud Functions). +- **Webex** org with **Contact Center** and rights to create or use a **Webex Integration** (client ID and secret); redirect URI must match your function URL + `/callback`. +- **Postman** or **curl** (or your flow’s HTTP client) to call `/init`, test token retrieval, and set headers. +- Optional: upstream [setup video and links](docs/upstream-overview.md). + +## Code Scaffold + +Runnable sample code lives under **`src/`**: + +- **`index.js`** — Cloud Function entry; OAuth callback, `/init`, and token GET/refresh. +- **`package.json`** — `firebase-admin` dependency and Node 18 engine hint. +- **`firestore.rules`** — Deny direct client reads/writes on `tokens/{id}`; deploy with Firebase rules deploy. +- **`env.template`** — Documents **`TOKEN_PASSPHRASE`** and Firestore-held fields (not a `.env` file for GCP; set variables in the function configuration). +- **`LICENSE`** — Upstream MIT (David Finnegan, 2023). + +Additional upstream context (video, API JSON examples, `/init` warning, support links) is in [docs/upstream-overview.md](docs/upstream-overview.md). + +This is **sample code**: a single shared passphrase protects the token endpoint, **CORS** allows any origin, and there is no per-caller identity or audit trail beyond Cloud logging. + +## Deployment Guide + +1. **Create** a Firebase/GCP project with **Firestore** and enable the **Cloud Functions** API as required by your console workflow. +2. **Deploy Firestore rules** from [`src/firestore.rules`](src/firestore.rules) using the Firebase CLI or console (replace default rules in dev projects carefully). +3. **Create** an HTTP Cloud Function (1st gen matches the `exports.tokenService` style) with source from [`src/`](src/): upload or connect a repo, set runtime **Node.js 18**, entry point **`tokenService`**, trigger **HTTP** (allow unauthenticated invoke at the Google layer only if you accept that model; the app still requires **`x-token-passphrase`**). +4. **Set** environment variable **`TOKEN_PASSPHRASE`** on the function to a long random string; record the same value for callers. See [`src/env.template`](src/env.template). +5. **Note** the function’s **HTTPS URL** (including any region path). Register **`{functionBaseUrl}/callback`** as the **Redirect URI** on your Webex Integration. +6. **Initialize** a token document: send **GET** `{functionBaseUrl}/init?name=YOUR_TOKEN_NAME` with header **`x-token-passphrase: YOUR_PASSPHRASE`** (adjust if your host/path layout differs; the function must see `req.path === "/init"`). Confirm a Firestore document **`tokens/YOUR_TOKEN_NAME`** appears. +7. **Edit** that document in the **Firebase console** and set **`clientId`**, **`clientSecret`**, and **`redirectUri`** (must match the Integration redirect URI exactly). +8. **Open** the Webex **authorization URL** for your Integration in a browser; set query parameter **`state`** to **`YOUR_TOKEN_NAME`** (not the placeholder `set_state_here`). Sign in and approve; you should see a success JSON from **`{functionBaseUrl}/callback`**. +9. **Test** token retrieval: **GET** `{functionBaseUrl}?name=YOUR_TOKEN_NAME` with header **`x-token-passphrase`** (root path, not `/init` or `/callback`). Expect `{"status":"200","token":"..."}`. +10. **Wire** Webex Contact Center flows or other callers to the same URL and headers; enforce TLS and network controls appropriate to your environment. + + + +## Known Limitations + +- **Not production-hardened:** Shared passphrase only; no per-client identity, OAuth for admin setup is not separately gated, **`Access-Control-Allow-Origin: *`**, and secrets in Firestore require console access control and monitoring. +- **Token expiry:** When the **refresh token** expires, repeat the browser authorization flow. Access tokens are refreshed when less than two hours remain (upstream behavior). +- **`/init` risk:** Running **`/init`** after tokens exist can damage configuration—use only during setup ([upstream note](docs/upstream-overview.md)). +- **License:** Sample derives from MIT-licensed upstream code; see [`src/LICENSE`](src/LICENSE). This Playbook’s repository [LICENSE](../../LICENSE) applies to Playbook packaging and edits. +- **Disclaimer:** This Playbook is a starting point. Webex does not guarantee the functional accuracy of the source code. Test thoroughly before use in a production environment. diff --git a/playbooks/wxcc-token-service-firebase/diagrams/architecture-diagram.md b/playbooks/wxcc-token-service-firebase/diagrams/architecture-diagram.md new file mode 100644 index 0000000..1007565 --- /dev/null +++ b/playbooks/wxcc-token-service-firebase/diagrams/architecture-diagram.md @@ -0,0 +1,25 @@ +# Architecture — WxCC token service (Firebase) + +HTTP trigger on **Google Cloud Functions** runs `tokenService` ([`src/index.js`](../src/index.js)). **Firebase Admin** reads and writes OAuth state in **Cloud Firestore** (`tokens` collection). Callers use **Webex** OAuth and `https://webexapis.com/v1/access_token` for code exchange and refresh. + +```mermaid +sequenceDiagram + participant Admin as AdminBrowser + participant Webex as WebexOAuth + participant GCF as CloudFunction + participant FS as Firestore + participant Caller as FlowOrClient + + Admin->>Webex: Authorize with state equals token name + Webex->>GCF: Redirect to callback with code + GCF->>FS: Store access and refresh tokens + Caller->>GCF: GET trigger with name query and x-token-passphrase + GCF->>FS: Read token document + alt access token has under two hours left but refresh valid + GCF->>Webex: POST access_token refresh grant + GCF->>FS: Persist new tokens + end + GCF->>Caller: JSON access token +``` + +For a self-hosted Express + SQLite alternative, see [wxcc-token-management-sample](../../wxcc-token-management-sample/README.md). diff --git a/playbooks/wxcc-token-service-firebase/docs/upstream-overview.md b/playbooks/wxcc-token-service-firebase/docs/upstream-overview.md new file mode 100644 index 0000000..1d3a11f --- /dev/null +++ b/playbooks/wxcc-token-service-firebase/docs/upstream-overview.md @@ -0,0 +1,65 @@ +# Upstream token-service-sample notes + +Material adapted from the upstream sample readme so the playbook keeps a single root +[README.md](../README.md). Canonical sample: +[token-service-sample](https://github.com/WebexSamples/webex-contact-center-api-samples/tree/main/token-management-samples/token-service-sample). + +## Setup video + +[Watch: Token Service setup video](https://app.vidcast.io/share/ed971770-49bb-47e5-96d0-7c920074fd53) + +## Related samples + +- [token-app-sample](https://github.com/WebexSamples/webex-contact-center-api-samples/tree/main/token-management-samples/token-app-sample) — self-hosted Node pattern (see also the Playbook [wxcc-token-management-sample](../../wxcc-token-management-sample/README.md)). +- [app-auth-sample](https://github.com/WebexSamples/webex-contact-center-api-samples/tree/main/authentication-samples/app-auth-sample) — OAuth with WxCC API. + +## Token lifetime (from upstream) + +- Access tokens: up to about 12 hours. +- Refresh tokens: up to about 60 days. +- The function returns a valid access token while the refresh token is still valid; after refresh expiry, repeat the browser OAuth step with the same token name in `state`. + +## HTTP API shapes + +Success: + +```json +{ + "status": "200", + "token": "example-token-here" +} +``` + +Error: + +```json +{ + "status": "500", + "message": "error-message-here" +} +``` + +## `/init` warning + +The `/init` route creates or merges a `tokens` document for the `name` query parameter. +Use it only during setup. Calling it in production after tokens are populated can +overwrite or disrupt configuration—follow the upstream guidance. + +## Useful links + +- [Webex Contact Center for Developers](https://developer.webex.com/docs/webex-contact-center) + +## Upstream disclaimer + +The upstream repository states that samples are for demos and learning, not +production-grade solutions, and that security, multi-org design, and operations +should be planned explicitly. + +## Support + +- [Webex Contact Center APIs Developer Community](https://developer.webex.com/docs/webex-contact-center) +- [How to Ask a Question or Initiate a Discussion](https://community.cisco.com/t5/contact-center/webex-contact-center-apis-developer-community-and-support/m-p/4558270) + +## Version history (upstream) + +- 1.0.0 — Initial project commit diff --git a/playbooks/wxcc-token-service-firebase/src/LICENSE b/playbooks/wxcc-token-service-firebase/src/LICENSE new file mode 100644 index 0000000..96a0acd --- /dev/null +++ b/playbooks/wxcc-token-service-firebase/src/LICENSE @@ -0,0 +1,22 @@ + +MIT License + +Copyright (c) 2023 David Finnegan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/playbooks/wxcc-token-service-firebase/src/env.template b/playbooks/wxcc-token-service-firebase/src/env.template new file mode 100644 index 0000000..723f867 --- /dev/null +++ b/playbooks/wxcc-token-service-firebase/src/env.template @@ -0,0 +1,17 @@ +# Google Cloud Function environment variables (set in Cloud Console or gcloud). +# Copy values into your deployment configuration; do not commit real secrets. + +# Shared secret: callers of GET ?name= and /init must send header +# x-token-passphrase: +TOKEN_PASSPHRASE= + +# ----------------------------------------------------------------------------- +# Firestore (collection "tokens", document id = your token name) — not env vars +# ----------------------------------------------------------------------------- +# After /init, edit the document in Firebase Console (or your admin tooling) and set: +# clientId — Webex Integration Client ID +# clientSecret — Webex Integration Client Secret +# redirectUri — Must match Integration redirect URI (function URL + /callback) +# +# OAuth tokens (accessToken, refreshToken, accessExpires, refreshExpires) are +# written by the function after browser OAuth and on refresh. diff --git a/playbooks/wxcc-token-service-firebase/src/firestore.rules b/playbooks/wxcc-token-service-firebase/src/firestore.rules new file mode 100644 index 0000000..2852719 --- /dev/null +++ b/playbooks/wxcc-token-service-firebase/src/firestore.rules @@ -0,0 +1,11 @@ +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + + // Restrict read & write access, SDK can still read & write + match /tokens/{tokenId}{ + allow read, write: if false; + } + + } +} diff --git a/playbooks/wxcc-token-service-firebase/src/index.js b/playbooks/wxcc-token-service-firebase/src/index.js new file mode 100644 index 0000000..dc94224 --- /dev/null +++ b/playbooks/wxcc-token-service-firebase/src/index.js @@ -0,0 +1,183 @@ +/** + * Webex Contact Center token helper for Google Cloud Functions + Firestore. + * + * Exposes HTTP routes to complete OAuth (callback), initialize a token doc (/init), + * and return a current access token (GET with ?name=) using https://webexapis.com/v1/access_token. + * + * What this sample does NOT do: production authz beyond a shared passphrase header, + * rate limiting, audit logging, multi-tenant isolation, or rotation of Firestore-held + * client secrets. Treat as demonstration code only. + * + * Environment (Cloud Function): + * TOKEN_PASSPHRASE — shared secret; callers must send header x-token-passphrase with this value. + * + * Configuration in Firestore (collection "tokens", document id = token name): clientId, + * clientSecret, redirectUri, plus tokens written by this function after OAuth/refresh. + */ + +const admin = require("firebase-admin"); +admin.initializeApp(); + +exports.tokenService = async (req, res) => { + res.set("Access-Control-Allow-Origin", "*"); + + // handle preflight requests here + if (req.method === "OPTIONS") { + res.set("Access-Control-Allow-Methods", "GET, POST"); + res.set("Access-Control-Allow-Headers", "Content-Type, Accept, x-token-passphrase"); + return res.status(204).send(''); + } + + // callback request + if (req.path === "/callback") { + const code = req.query.code ? req.query.code : null; + if (!code) { + return res.status(500).json({ status: "500", message: `An error occured: ${req.query.error}` }); + } + if (req.query.state === "set_state_here") { + return res.status(500).json({ status: "500", message: `Set state to your Token Name` }); + } + try { + const tokenName = req.query.state; + const tokenDoc = await admin.firestore().collection("tokens").doc(tokenName).get(); + const tokenData = tokenDoc.data(); + const payload = { + grant_type: "authorization_code", + client_id: tokenData.clientId, + client_secret: tokenData.clientSecret, + redirect_uri: tokenData.redirectUri, + code: code, + }; + const urlParams = Object.entries(payload).map((x) => `${encodeURIComponent(x[0])}=${encodeURIComponent(x[1])}`).join("&"); + + const url = "https://webexapis.com/v1/access_token"; + const options = { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "Accept": "application/json", + }, + body: urlParams, + }; + + const fetchRes = await fetch(url, options); + const responseData = await fetchRes.json(); + const timeNow = Date.now(); + + await admin.firestore().collection("tokens").doc(tokenName).set({ + accessExpires: timeNow + (responseData.expires_in * 1000), + refreshExpires: timeNow + (responseData.refresh_token_expires_in * 1000), + accessToken: responseData.access_token, + refreshToken: responseData.refresh_token, + lastRefresh: timeNow, + }, { merge: true }); + + return res.status(200).json({ status: "200", message: "Access & Refresh tokens created" }); + } catch (error) { + console.error(`Failure in /auth/callback request, error: ${error}`) + return res.status(500).json({ status: "500", message: `Failed to create tokens, error: ${error}` }); + } + } + + + // Validate query param name exists + const tokenName = req.query.name; + if (!tokenName) return res.status(500).json({ status: "500", message: "Missing Token Name" }); + + // Validate header x-token-passphrase exists + const tokenPassphrase = req.headers["x-token-passphrase"]; + if (!tokenPassphrase) return res.status(500).json({ status: "500", message: "Missing Passphrase" }); + + const expectedPassphrase = process.env.TOKEN_PASSPHRASE; + if (!expectedPassphrase) { + return res.status(500).json({ status: "500", message: "Server misconfiguration: TOKEN_PASSPHRASE not set" }); + } + if (tokenPassphrase !== expectedPassphrase) { + return res.status(500).json({ status: "500", message: "Invalid Token Passphrase" }); + } + + + // init request + if (req.path === "/init") { + try { + await admin.firestore().collection("tokens").doc(tokenName).set({ + clientId: "", + clientSecret: "", + redirectUri: "", + }, { merge: true }); + return res.status(200).json({ status: "200", message: "Token document initialized" }); + } catch (error) { + console.error(`Failure in /init request, error: ${error}`) + return res.status(500).json({ status: "500", message: `Failed to initialize token document, error: ${error}` }); + } + } + + + // main request + try { + const tokenDoc = await admin.firestore().collection("tokens").doc(tokenName).get(); + const tokenData = tokenDoc.data(); + if (!tokenData) { + return res.status(500).json({ status: "500", message: "Access Token not found" }); + } + const currentTime = Date.now(); + + // Check if access token has at least two hour remaining + if (tokenData.accessExpires > currentTime + (2 * 60 * 60 * 1000)) { + // Access Token Valid, return Access Token + return res.status(200).json({ status: "200", token: tokenData.accessToken }); + + // Check if Refresh Token has at least two minutes remaining + } else if (tokenData.refreshExpires > currentTime + (2 * 60 * 1000)) { + // Access Token expired, refreshing Access Token + try { + const payload = { + grant_type: "refresh_token", + client_id: tokenData.clientId, + client_secret: tokenData.clientSecret, + refresh_token: tokenData.refreshToken, + }; + + const urlParams = Object.entries(payload).map((x) => `${encodeURIComponent(x[0])}=${encodeURIComponent(x[1])}`).join("&"); + + const url = "https://webexapis.com/v1/access_token"; + const options = { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "Accept": "application/json", + }, + body: urlParams, + }; + + const fetchRes = await fetch(url, options); + const responseData = await fetchRes.json(); + const timeNow = Date.now(); + + await admin.firestore().collection("tokens").doc(tokenName).set({ + accessExpires: timeNow + (responseData.expires_in * 1000), + refreshExpires: timeNow + (responseData.refresh_token_expires_in * 1000), + accessToken: responseData.access_token, + refreshToken: responseData.refresh_token, + lastRefresh: timeNow, + }, { merge: true }); + + return res.status(200).json({ status: "200", token: responseData.access_token }); + } catch (error) { + console.error(`Token Refresh Failed, Error: ${error}`) + console.error(`Try reseting the service by browsing the the OAuth Authorization URL.`) + return res.status(500).json({ status: "500", message: "Token refresh failed" }); + } + + // Refresh Token has expired + } else { + console.error("Refresh Token expired, you need to browse the the OAuth Authorization URL.") + return res.status(500).json({ status: "500", message: "Refresh Token expired" }); + } + + } catch (error) { + // Access Token not found + console.error(`Invalid Access Token name: ${tokenName}. Error: ${error}`) + return res.status(500).json({ status: "500", message: "Access Token not found" }); + } +}; diff --git a/playbooks/wxcc-token-service-firebase/src/package.json b/playbooks/wxcc-token-service-firebase/src/package.json new file mode 100644 index 0000000..23f49c1 --- /dev/null +++ b/playbooks/wxcc-token-service-firebase/src/package.json @@ -0,0 +1,15 @@ +{ + "name": "token-service", + "description": "Token Service running on Cloud Functions and Firebase", + "author": { + "name": "David Finnegan" + }, + "license": "MIT", + "private": true, + "engines": { + "node": "18" + }, + "dependencies": { + "firebase-admin": "^11.8.0" + } +}