How-to Guides
OAuth Token Encryption
FluxyChat token encryption provides AES-256-GCM encryption for securely storing OAuth tokens used by platform adapters (Slack, Linear, and others).
OAuth Token Encryption
FluxyChat token encryption provides AES-256-GCM encryption for securely storing OAuth tokens used by platform adapters (Slack, Linear, and others).
Overview
OAuth tokens stored in D1 or external systems should never be plaintext. The TokenCrypto class wraps the Web Crypto API's AES-256-GCM implementation with a simple encrypt/decrypt interface. Each encryption uses a random 12-byte IV, ensuring the same plaintext produces different ciphertext every time.
Source: apps/worker/src/lib/token-crypto.js
Quick Start
import { TokenCrypto } from "@fluxy-chat/sdk";
// Initialize with your master encryption key (store in Worker secrets)
const crypto = new TokenCrypto(process.env.OAUTH_ENCRYPTION_KEY);
// Encrypt an OAuth token before storing it
const encrypted = await crypto.encrypt("xoxb-slack-bot-token-...");
// { ciphertext: "base64...", iv: "base64...", tag: "base64..." }
// Store `encrypted` in D1
await db.prepare(
"INSERT INTO oauth_tokens (adapter, user_id, ciphertext, iv, tag) VALUES (?, ?, ?, ?, ?)"
).bind("slack", "U123", encrypted.ciphertext, encrypted.iv, encrypted.tag);
// Decrypt when you need to use the token
const row = await db.prepare("SELECT * FROM oauth_tokens WHERE user_id = ?").bind("U123").first();
const plaintext = await crypto.decrypt({
ciphertext: row.ciphertext,
iv: row.iv,
tag: row.tag,
});
// "xoxb-slack-bot-token-..."How It Works
- Key derivation: The master key string is SHA-256 hashed to produce a 256-bit AES-GCM
CryptoKey. - Encryption: A random 12-byte (96-bit) IV is generated per encryption. The plaintext is encrypted using AES-GCM with a 128-bit authentication tag.
- Output: Three base64-encoded values:
ciphertext,iv, andtag. - Decryption: The IV and tag are combined with the ciphertext to decrypt back to plaintext.
The Web Crypto API (crypto.subtle) is used throughout, so this works on Cloudflare Workers, browsers, and Node.js >= 19.
Lower-Level API
For cases where you already have a CryptoKey (e.g., shared across multiple operations):
import { deriveKey, encryptToken, decryptToken } from "@fluxy-chat/sdk";
const key = await deriveKey(process.env.OAUTH_ENCRYPTION_KEY);
const encrypted = await encryptToken("my-secret-token", key);
// { ciphertext, iv, tag }
const plaintext = await decryptToken(encrypted, key);
// "my-secret-token"Type Guard
import { isEncryptedTokenData } from "@fluxy-chat/sdk";
const value = JSON.parse(row.data);
if (isEncryptedTokenData(value)) {
const token = await crypto.decrypt(value);
}Adapter Integration
Adapters that handle OAuth flows (Slack, Linear, etc.) use TokenCrypto internally:
class SlackAdapter extends BaseAdapter {
constructor(env) {
this.tokenCrypto = new TokenCrypto(env.OAUTH_ENCRYPTION_KEY);
}
async storeUserToken(userId, token) {
const encrypted = await this.tokenCrypto.encrypt(token);
await this.db.prepare(
"INSERT INTO slack_oauth (user_id, ciphertext, iv, tag) VALUES (?, ?, ?, ?)"
).bind(userId, encrypted.ciphertext, encrypted.iv, encrypted.tag).run();
}
async getUserToken(userId) {
const row = await this.db.prepare(
"SELECT ciphertext, iv, tag FROM slack_oauth WHERE user_id = ?"
).bind(userId).first();
if (!row) return null;
return this.tokenCrypto.decrypt({
ciphertext: row.ciphertext,
iv: row.iv,
tag: row.tag,
});
}
}Security Notes
- Master key: Store
OAUTH_ENCRYPTION_KEYin Cloudflare Worker secrets (wrangler secret put). Never commit it to source. - Key rotation: To rotate, decrypt all tokens with the old key and re-encrypt with the new key. There's no built-in migration tool — handle this in a maintenance script.
- IV uniqueness: Each encryption call generates a cryptographically random IV. Never reuse an IV with the same key.
- Authentication tag: The GCM tag ensures ciphertext integrity. If the encrypted data is tampered with, decryption will throw.
See Also
- Adapter Errors —
AuthenticationErrorfor invalid/expired tokens - Adapter Pattern — How adapters are structured
Per-Thread Typed State
`createThreadState()` provides a typed state accessor per thread with merge/replace semantics and TTL — designed for multi-turn agent context.
Tool Approvals
By default, tools with an `execute` function run automatically when the model calls them. Use `toolApproval` on `runAgentLoop` to review, approve, or deny selec