@jezweb/oauth-token-manager 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -0
- package/SECURITY.md +162 -0
- package/dist/crypto.d.ts +43 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +107 -0
- package/dist/crypto.js.map +1 -0
- package/dist/errors.d.ts +75 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +117 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/github.d.ts +45 -0
- package/dist/providers/github.d.ts.map +1 -0
- package/dist/providers/github.js +70 -0
- package/dist/providers/github.js.map +1 -0
- package/dist/providers/google.d.ts +24 -0
- package/dist/providers/google.d.ts.map +1 -0
- package/dist/providers/google.js +63 -0
- package/dist/providers/google.js.map +1 -0
- package/dist/providers/microsoft.d.ts +29 -0
- package/dist/providers/microsoft.d.ts.map +1 -0
- package/dist/providers/microsoft.js +72 -0
- package/dist/providers/microsoft.js.map +1 -0
- package/dist/providers/types.d.ts +7 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +7 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/storage/d1.d.ts +22 -0
- package/dist/storage/d1.d.ts.map +1 -0
- package/dist/storage/d1.js +31 -0
- package/dist/storage/d1.js.map +1 -0
- package/dist/storage/kv.d.ts +38 -0
- package/dist/storage/kv.d.ts.map +1 -0
- package/dist/storage/kv.js +143 -0
- package/dist/storage/kv.js.map +1 -0
- package/dist/storage/types.d.ts +7 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +7 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/token-manager.d.ts +88 -0
- package/dist/token-manager.d.ts.map +1 -0
- package/dist/token-manager.js +199 -0
- package/dist/token-manager.js.map +1 -0
- package/dist/types.d.ts +158 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +88 -0
package/README.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# @jezweb/oauth-token-manager
|
|
2
|
+
|
|
3
|
+
OAuth token management for Cloudflare Workers. Store, refresh, and retrieve tokens for downstream API access.
|
|
4
|
+
|
|
5
|
+
## The Problem
|
|
6
|
+
|
|
7
|
+
When your application needs to call APIs on behalf of users (Google Calendar, GitHub, Xero, etc.), you need to:
|
|
8
|
+
|
|
9
|
+
1. **Store** OAuth tokens securely (encrypted at rest)
|
|
10
|
+
2. **Refresh** expired tokens automatically
|
|
11
|
+
3. **Retrieve** valid tokens for API calls
|
|
12
|
+
4. **Handle** errors gracefully (expired, revoked, insufficient scopes)
|
|
13
|
+
|
|
14
|
+
Most auth libraries focus on **identity** ("who is this user?") not **API access** ("act on their behalf"). This package fills that gap.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @jezweb/oauth-token-manager
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { TokenManager, KVStorage } from '@jezweb/oauth-token-manager';
|
|
26
|
+
|
|
27
|
+
// Initialize
|
|
28
|
+
const tokens = new TokenManager({
|
|
29
|
+
storage: new KVStorage({
|
|
30
|
+
namespace: env.TOKEN_KV,
|
|
31
|
+
encryptionKey: env.TOKEN_ENCRYPTION_KEY,
|
|
32
|
+
}),
|
|
33
|
+
providers: {
|
|
34
|
+
google: {
|
|
35
|
+
clientId: env.GOOGLE_CLIENT_ID,
|
|
36
|
+
clientSecret: env.GOOGLE_CLIENT_SECRET,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Store token after OAuth callback
|
|
42
|
+
await tokens.store({
|
|
43
|
+
userId: 'user-123',
|
|
44
|
+
provider: 'google',
|
|
45
|
+
accessToken: 'ya29.xxx',
|
|
46
|
+
refreshToken: '1//xxx',
|
|
47
|
+
expiresAt: Date.now() + 3600000,
|
|
48
|
+
scopes: ['https://www.googleapis.com/auth/calendar'],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Get valid token (auto-refreshes if expired)
|
|
52
|
+
const { accessToken } = await tokens.get({
|
|
53
|
+
userId: 'user-123',
|
|
54
|
+
provider: 'google',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Use token for API call
|
|
58
|
+
const response = await fetch('https://www.googleapis.com/calendar/v3/calendars', {
|
|
59
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Features
|
|
64
|
+
|
|
65
|
+
- **Encrypted storage** - Tokens encrypted at rest using AES-256-GCM
|
|
66
|
+
- **Automatic refresh** - Tokens refreshed before expiry (5 min buffer by default)
|
|
67
|
+
- **Scope validation** - Verify required scopes before returning tokens
|
|
68
|
+
- **Built-in providers** - Google, Microsoft, GitHub out of the box
|
|
69
|
+
- **Cloudflare-native** - Built specifically for Workers + KV
|
|
70
|
+
- **Clear errors** - Typed errors guide recovery actions
|
|
71
|
+
|
|
72
|
+
## API
|
|
73
|
+
|
|
74
|
+
### `TokenManager`
|
|
75
|
+
|
|
76
|
+
Main class for token management.
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const tokens = new TokenManager({
|
|
80
|
+
storage: TokenStorage, // KVStorage or custom (handles encryption)
|
|
81
|
+
providers: { // Provider configs for refresh
|
|
82
|
+
google?: ProviderConfig,
|
|
83
|
+
microsoft?: ProviderConfig,
|
|
84
|
+
github?: ProviderConfig,
|
|
85
|
+
},
|
|
86
|
+
defaultRefreshBuffer?: number, // ms before expiry to refresh (default: 5 min)
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
#### Methods
|
|
91
|
+
|
|
92
|
+
| Method | Description |
|
|
93
|
+
|--------|-------------|
|
|
94
|
+
| `store(options)` | Store a new token or update existing |
|
|
95
|
+
| `get(options)` | Get valid token (auto-refreshes) |
|
|
96
|
+
| `list(options)` | List connected providers for a user |
|
|
97
|
+
| `revoke(options)` | Delete a token |
|
|
98
|
+
| `has(userId, provider)` | Check if token exists |
|
|
99
|
+
|
|
100
|
+
### Error Types
|
|
101
|
+
|
|
102
|
+
| Error | Meaning | Recovery |
|
|
103
|
+
|-------|---------|----------|
|
|
104
|
+
| `TokenNotFoundError` | No token for user/provider | Redirect to OAuth |
|
|
105
|
+
| `TokenExpiredError` | Token expired, refresh failed | Redirect to OAuth |
|
|
106
|
+
| `InsufficientScopesError` | Missing required scopes | Redirect to OAuth with incremental consent |
|
|
107
|
+
| `ProviderNotConfiguredError` | Provider not in config | Add provider config |
|
|
108
|
+
|
|
109
|
+
## Supported Providers
|
|
110
|
+
|
|
111
|
+
| Provider | Refresh Support | Token Lifetime | Notes |
|
|
112
|
+
|----------|-----------------|----------------|-------|
|
|
113
|
+
| Google | ✅ Yes | ~1 hour | Requires `access_type=offline` |
|
|
114
|
+
| Microsoft | ✅ Yes | ~1 hour | Token rotation by default |
|
|
115
|
+
| GitHub | ❌ No | Never expires | Tokens valid until revoked |
|
|
116
|
+
|
|
117
|
+
## Storage Adapters
|
|
118
|
+
|
|
119
|
+
### KV Storage (Recommended)
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { KVStorage } from '@jezweb/oauth-token-manager/storage/kv';
|
|
123
|
+
|
|
124
|
+
const storage = new KVStorage({
|
|
125
|
+
namespace: env.TOKEN_KV,
|
|
126
|
+
encryptionKey: env.TOKEN_ENCRYPTION_KEY,
|
|
127
|
+
keyPrefix: 'tokens', // optional, default: 'tokens'
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### D1 Storage (Coming Soon)
|
|
132
|
+
|
|
133
|
+
D1 adapter for stronger consistency and complex queries.
|
|
134
|
+
|
|
135
|
+
## Wrangler Setup
|
|
136
|
+
|
|
137
|
+
```toml
|
|
138
|
+
# wrangler.toml
|
|
139
|
+
kv_namespaces = [
|
|
140
|
+
{ binding = "TOKEN_KV", id = "your-kv-id" }
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
[vars]
|
|
144
|
+
# Store encryption key as secret, not here!
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# Set encryption key (generate with: openssl rand -base64 32)
|
|
149
|
+
echo "your-32-byte-key" | wrangler secret put TOKEN_ENCRYPTION_KEY
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Use Cases
|
|
153
|
+
|
|
154
|
+
- **MCP Servers** - Call Google Calendar, GitHub, etc. on behalf of users
|
|
155
|
+
- **CRM integrations** - Sync with external calendars, email
|
|
156
|
+
- **Social media tools** - Post to Twitter, LinkedIn
|
|
157
|
+
- **Accounting apps** - Connect to Xero, QuickBooks
|
|
158
|
+
|
|
159
|
+
## Architecture
|
|
160
|
+
|
|
161
|
+
This package handles **outbound** OAuth (your app calling external APIs).
|
|
162
|
+
|
|
163
|
+
For **inbound** OAuth (clients authenticating to your app), use:
|
|
164
|
+
- [`@cloudflare/workers-oauth-provider`](https://github.com/cloudflare/workers-oauth-provider)
|
|
165
|
+
- [better-auth](https://better-auth.com)
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
┌─────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
169
|
+
│ MCP Client │────▶│ Your App │────▶│ External API │
|
|
170
|
+
│ (Claude.ai) │ │ │ │ (Google, etc) │
|
|
171
|
+
└─────────────┘ └─────────────────┘ └─────────────────┘
|
|
172
|
+
│ │ │
|
|
173
|
+
▼ ▼ ▼
|
|
174
|
+
Inbound auth Token Manager External OAuth
|
|
175
|
+
(who is client?) (this package) (act on behalf)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Security
|
|
179
|
+
|
|
180
|
+
See [SECURITY.md](./SECURITY.md) for security considerations.
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
MIT © [Jezweb](https://jezweb.com.au)
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Security Considerations
|
|
2
|
+
|
|
3
|
+
This document describes the security model of `@jezweb/oauth-token-manager`.
|
|
4
|
+
|
|
5
|
+
## Token Encryption
|
|
6
|
+
|
|
7
|
+
### Algorithm
|
|
8
|
+
|
|
9
|
+
- **Encryption**: AES-256-GCM (Galois/Counter Mode)
|
|
10
|
+
- **Key Derivation**: PBKDF2 with SHA-256, 100,000 iterations
|
|
11
|
+
- **IV**: Random 12 bytes per encryption
|
|
12
|
+
- **Salt**: Random 16 bytes per encryption
|
|
13
|
+
|
|
14
|
+
### What's Encrypted
|
|
15
|
+
|
|
16
|
+
| Field | Encrypted | Reason |
|
|
17
|
+
|-------|-----------|--------|
|
|
18
|
+
| `accessToken` | ✅ Yes | Sensitive credential |
|
|
19
|
+
| `refreshToken` | ✅ Yes | Sensitive credential |
|
|
20
|
+
| `userId` | ❌ No | Needed for lookup |
|
|
21
|
+
| `provider` | ❌ No | Needed for lookup |
|
|
22
|
+
| `scopes` | ❌ No | Not sensitive |
|
|
23
|
+
| `expiresAt` | ❌ No | Useful for auditing |
|
|
24
|
+
| `createdAt` | ❌ No | Useful for auditing |
|
|
25
|
+
| `updatedAt` | ❌ No | Useful for auditing |
|
|
26
|
+
|
|
27
|
+
### Security Properties
|
|
28
|
+
|
|
29
|
+
1. **Confidentiality**: Tokens cannot be read without the encryption key
|
|
30
|
+
2. **Integrity**: GCM authentication tag detects tampering
|
|
31
|
+
3. **Forward secrecy**: Each encryption uses a unique salt + IV
|
|
32
|
+
4. **No key exposure**: Encryption key never stored, only used
|
|
33
|
+
|
|
34
|
+
## Encryption Key Management
|
|
35
|
+
|
|
36
|
+
### Requirements
|
|
37
|
+
|
|
38
|
+
- **Length**: 32+ bytes recommended (256 bits)
|
|
39
|
+
- **Randomness**: Use cryptographically secure random generation
|
|
40
|
+
- **Storage**: Store as Wrangler secret, never in code or env vars
|
|
41
|
+
|
|
42
|
+
### Generating a Key
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Generate a secure key
|
|
46
|
+
openssl rand -base64 32
|
|
47
|
+
|
|
48
|
+
# Store as Wrangler secret
|
|
49
|
+
echo "your-key" | wrangler secret put TOKEN_ENCRYPTION_KEY
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Key Rotation
|
|
53
|
+
|
|
54
|
+
Key rotation is NOT currently supported. If you need to rotate:
|
|
55
|
+
|
|
56
|
+
1. Deploy new version with new key
|
|
57
|
+
2. Users must re-authenticate to get new tokens
|
|
58
|
+
3. Old tokens become unreadable
|
|
59
|
+
|
|
60
|
+
Future versions may support key rotation with re-encryption.
|
|
61
|
+
|
|
62
|
+
## Storage Security
|
|
63
|
+
|
|
64
|
+
### KV Storage
|
|
65
|
+
|
|
66
|
+
- Tokens stored with user-specific keys: `tokens:{userId}:{provider}`
|
|
67
|
+
- Index stored separately: `token-index:{userId}`
|
|
68
|
+
- No cross-user data access possible with correct key structure
|
|
69
|
+
|
|
70
|
+
### Access Control
|
|
71
|
+
|
|
72
|
+
- Your Worker has full access to the KV namespace
|
|
73
|
+
- Implement authorization in your Worker to control which users can access which tokens
|
|
74
|
+
- Never expose TokenManager methods directly to untrusted input
|
|
75
|
+
|
|
76
|
+
## Provider Credentials
|
|
77
|
+
|
|
78
|
+
### Storage
|
|
79
|
+
|
|
80
|
+
- Provider `clientId` and `clientSecret` should be stored as Wrangler secrets
|
|
81
|
+
- Never hardcode credentials in source code
|
|
82
|
+
- Use environment variables via Wrangler bindings
|
|
83
|
+
|
|
84
|
+
### Exposure Risk
|
|
85
|
+
|
|
86
|
+
If provider credentials are compromised:
|
|
87
|
+
|
|
88
|
+
1. Attacker could refresh tokens (if they also have refresh tokens)
|
|
89
|
+
2. Attacker could NOT decrypt stored tokens without encryption key
|
|
90
|
+
3. Revoke compromised credentials immediately in provider console
|
|
91
|
+
|
|
92
|
+
## Attack Vectors
|
|
93
|
+
|
|
94
|
+
### Storage Breach
|
|
95
|
+
|
|
96
|
+
If KV storage is compromised:
|
|
97
|
+
|
|
98
|
+
| Data Exposed | Risk | Mitigation |
|
|
99
|
+
|--------------|------|------------|
|
|
100
|
+
| Encrypted tokens | Low | Cannot decrypt without key |
|
|
101
|
+
| User IDs | Medium | Consider hashing user IDs |
|
|
102
|
+
| Scopes | Low | Not sensitive |
|
|
103
|
+
| Timestamps | Low | Audit trail only |
|
|
104
|
+
|
|
105
|
+
### Encryption Key Breach
|
|
106
|
+
|
|
107
|
+
If encryption key is compromised:
|
|
108
|
+
|
|
109
|
+
| Risk | Impact |
|
|
110
|
+
|------|--------|
|
|
111
|
+
| Decrypt all tokens | High - full API access |
|
|
112
|
+
| Impersonate users | High - act as any user |
|
|
113
|
+
|
|
114
|
+
**Mitigation**: Rotate key immediately, invalidate all tokens.
|
|
115
|
+
|
|
116
|
+
### Provider Token Theft
|
|
117
|
+
|
|
118
|
+
If decrypted tokens are stolen:
|
|
119
|
+
|
|
120
|
+
| Token Type | Risk | Mitigation |
|
|
121
|
+
|------------|------|------------|
|
|
122
|
+
| Access token | Time-limited (~1h) | Short expiry |
|
|
123
|
+
| Refresh token | Long-lived | Revoke at provider |
|
|
124
|
+
|
|
125
|
+
## Best Practices
|
|
126
|
+
|
|
127
|
+
### Do
|
|
128
|
+
|
|
129
|
+
- ✅ Use strong encryption keys (32+ bytes, random)
|
|
130
|
+
- ✅ Store encryption key as Wrangler secret
|
|
131
|
+
- ✅ Store provider credentials as secrets
|
|
132
|
+
- ✅ Validate user authorization before token access
|
|
133
|
+
- ✅ Log token access for audit (without logging tokens)
|
|
134
|
+
- ✅ Monitor for unusual access patterns
|
|
135
|
+
|
|
136
|
+
### Don't
|
|
137
|
+
|
|
138
|
+
- ❌ Log tokens or encryption keys
|
|
139
|
+
- ❌ Include tokens in error messages
|
|
140
|
+
- ❌ Store encryption key in source code
|
|
141
|
+
- ❌ Use predictable encryption keys
|
|
142
|
+
- ❌ Skip user authorization checks
|
|
143
|
+
|
|
144
|
+
## Reporting Vulnerabilities
|
|
145
|
+
|
|
146
|
+
If you discover a security vulnerability:
|
|
147
|
+
|
|
148
|
+
1. **Do not** open a public GitHub issue
|
|
149
|
+
2. Email security concerns to jeremy@jezweb.net
|
|
150
|
+
3. Include steps to reproduce
|
|
151
|
+
4. Allow 90 days for fix before disclosure
|
|
152
|
+
|
|
153
|
+
## Compliance
|
|
154
|
+
|
|
155
|
+
This package:
|
|
156
|
+
|
|
157
|
+
- Uses industry-standard encryption (AES-256-GCM)
|
|
158
|
+
- Does not transmit tokens to third parties
|
|
159
|
+
- Does not store encryption keys
|
|
160
|
+
- Provides audit trail via timestamps
|
|
161
|
+
|
|
162
|
+
For specific compliance requirements (GDPR, SOC2, etc.), consult your compliance team about overall system architecture.
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cryptographic utilities for token encryption at rest
|
|
3
|
+
*
|
|
4
|
+
* Uses Web Crypto API (available in Cloudflare Workers, browsers, Node 18+)
|
|
5
|
+
* Algorithm: AES-256-GCM (authenticated encryption)
|
|
6
|
+
*
|
|
7
|
+
* Security properties:
|
|
8
|
+
* - Confidentiality: Tokens are encrypted and unreadable without the key
|
|
9
|
+
* - Integrity: Tampering with ciphertext is detected (GCM auth tag)
|
|
10
|
+
* - Key derivation: PBKDF2 derives strong key from your secret
|
|
11
|
+
* - Random IVs: Same plaintext produces different ciphertext each time
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Encrypt plaintext using AES-256-GCM
|
|
15
|
+
*
|
|
16
|
+
* Output format: base64(salt + iv + ciphertext + authTag)
|
|
17
|
+
* - salt: 16 bytes (for key derivation)
|
|
18
|
+
* - iv: 12 bytes (initialization vector)
|
|
19
|
+
* - ciphertext: variable length
|
|
20
|
+
* - authTag: 16 bytes (included in ciphertext by Web Crypto)
|
|
21
|
+
*
|
|
22
|
+
* @param plaintext - Data to encrypt
|
|
23
|
+
* @param encryptionKey - Secret key/password for encryption
|
|
24
|
+
* @returns Base64-encoded encrypted data
|
|
25
|
+
*/
|
|
26
|
+
export declare function encrypt(plaintext: string, encryptionKey: string): Promise<string>;
|
|
27
|
+
/**
|
|
28
|
+
* Decrypt data encrypted with encrypt()
|
|
29
|
+
*
|
|
30
|
+
* @param encryptedData - Base64-encoded encrypted data
|
|
31
|
+
* @param encryptionKey - Secret key/password used for encryption
|
|
32
|
+
* @returns Decrypted plaintext
|
|
33
|
+
*/
|
|
34
|
+
export declare function decrypt(encryptedData: string, encryptionKey: string): Promise<string>;
|
|
35
|
+
/**
|
|
36
|
+
* Encrypt an object as JSON
|
|
37
|
+
*/
|
|
38
|
+
export declare function encryptObject<T>(obj: T, encryptionKey: string): Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* Decrypt JSON back to an object
|
|
41
|
+
*/
|
|
42
|
+
export declare function decryptObject<T>(encryptedData: string, encryptionKey: string): Promise<T>;
|
|
43
|
+
//# sourceMappingURL=crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AA0CH;;;;;;;;;;;;GAYG;AACH,wBAAsB,OAAO,CAC3B,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,CA8BjB;AAED;;;;;;GAMG;AACH,wBAAsB,OAAO,CAC3B,aAAa,EAAE,MAAM,EACrB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,CA0BjB;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,CAAC,EACnC,GAAG,EAAE,CAAC,EACN,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,CAEjB;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,CAAC,EACnC,aAAa,EAAE,MAAM,EACrB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,CAAC,CAAC,CAGZ"}
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cryptographic utilities for token encryption at rest
|
|
3
|
+
*
|
|
4
|
+
* Uses Web Crypto API (available in Cloudflare Workers, browsers, Node 18+)
|
|
5
|
+
* Algorithm: AES-256-GCM (authenticated encryption)
|
|
6
|
+
*
|
|
7
|
+
* Security properties:
|
|
8
|
+
* - Confidentiality: Tokens are encrypted and unreadable without the key
|
|
9
|
+
* - Integrity: Tampering with ciphertext is detected (GCM auth tag)
|
|
10
|
+
* - Key derivation: PBKDF2 derives strong key from your secret
|
|
11
|
+
* - Random IVs: Same plaintext produces different ciphertext each time
|
|
12
|
+
*/
|
|
13
|
+
import { CryptoError } from './errors';
|
|
14
|
+
// AES-GCM parameters
|
|
15
|
+
const ALGORITHM = 'AES-GCM';
|
|
16
|
+
const KEY_LENGTH = 256; // bits
|
|
17
|
+
const IV_LENGTH = 12; // bytes (96 bits, recommended for GCM)
|
|
18
|
+
const SALT_LENGTH = 16; // bytes
|
|
19
|
+
const PBKDF2_ITERATIONS = 100000;
|
|
20
|
+
/**
|
|
21
|
+
* Derive a cryptographic key from a password/secret using PBKDF2
|
|
22
|
+
*/
|
|
23
|
+
async function deriveKey(secret, salt) {
|
|
24
|
+
// Import the secret as a key for PBKDF2
|
|
25
|
+
const keyMaterial = await crypto.subtle.importKey('raw', new TextEncoder().encode(secret), 'PBKDF2', false, ['deriveKey']);
|
|
26
|
+
// Derive AES key using PBKDF2
|
|
27
|
+
return crypto.subtle.deriveKey({
|
|
28
|
+
name: 'PBKDF2',
|
|
29
|
+
salt,
|
|
30
|
+
iterations: PBKDF2_ITERATIONS,
|
|
31
|
+
hash: 'SHA-256',
|
|
32
|
+
}, keyMaterial, { name: ALGORITHM, length: KEY_LENGTH }, false, ['encrypt', 'decrypt']);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Encrypt plaintext using AES-256-GCM
|
|
36
|
+
*
|
|
37
|
+
* Output format: base64(salt + iv + ciphertext + authTag)
|
|
38
|
+
* - salt: 16 bytes (for key derivation)
|
|
39
|
+
* - iv: 12 bytes (initialization vector)
|
|
40
|
+
* - ciphertext: variable length
|
|
41
|
+
* - authTag: 16 bytes (included in ciphertext by Web Crypto)
|
|
42
|
+
*
|
|
43
|
+
* @param plaintext - Data to encrypt
|
|
44
|
+
* @param encryptionKey - Secret key/password for encryption
|
|
45
|
+
* @returns Base64-encoded encrypted data
|
|
46
|
+
*/
|
|
47
|
+
export async function encrypt(plaintext, encryptionKey) {
|
|
48
|
+
try {
|
|
49
|
+
// Generate random salt and IV
|
|
50
|
+
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
|
51
|
+
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
|
52
|
+
// Derive key from secret
|
|
53
|
+
const key = await deriveKey(encryptionKey, salt);
|
|
54
|
+
// Encrypt the data
|
|
55
|
+
const encoded = new TextEncoder().encode(plaintext);
|
|
56
|
+
const ciphertext = await crypto.subtle.encrypt({ name: ALGORITHM, iv }, key, encoded);
|
|
57
|
+
// Combine salt + iv + ciphertext into single buffer
|
|
58
|
+
const combined = new Uint8Array(salt.length + iv.length + ciphertext.byteLength);
|
|
59
|
+
combined.set(salt, 0);
|
|
60
|
+
combined.set(iv, salt.length);
|
|
61
|
+
combined.set(new Uint8Array(ciphertext), salt.length + iv.length);
|
|
62
|
+
// Return as base64
|
|
63
|
+
return btoa(String.fromCharCode(...combined));
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
throw new CryptoError('encrypt', error instanceof Error ? error : undefined);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Decrypt data encrypted with encrypt()
|
|
71
|
+
*
|
|
72
|
+
* @param encryptedData - Base64-encoded encrypted data
|
|
73
|
+
* @param encryptionKey - Secret key/password used for encryption
|
|
74
|
+
* @returns Decrypted plaintext
|
|
75
|
+
*/
|
|
76
|
+
export async function decrypt(encryptedData, encryptionKey) {
|
|
77
|
+
try {
|
|
78
|
+
// Decode base64
|
|
79
|
+
const combined = Uint8Array.from(atob(encryptedData), (c) => c.charCodeAt(0));
|
|
80
|
+
// Extract salt, iv, and ciphertext
|
|
81
|
+
const salt = combined.slice(0, SALT_LENGTH);
|
|
82
|
+
const iv = combined.slice(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
|
|
83
|
+
const ciphertext = combined.slice(SALT_LENGTH + IV_LENGTH);
|
|
84
|
+
// Derive key from secret
|
|
85
|
+
const key = await deriveKey(encryptionKey, salt);
|
|
86
|
+
// Decrypt the data
|
|
87
|
+
const decrypted = await crypto.subtle.decrypt({ name: ALGORITHM, iv }, key, ciphertext);
|
|
88
|
+
return new TextDecoder().decode(decrypted);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
throw new CryptoError('decrypt', error instanceof Error ? error : undefined);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Encrypt an object as JSON
|
|
96
|
+
*/
|
|
97
|
+
export async function encryptObject(obj, encryptionKey) {
|
|
98
|
+
return encrypt(JSON.stringify(obj), encryptionKey);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Decrypt JSON back to an object
|
|
102
|
+
*/
|
|
103
|
+
export async function decryptObject(encryptedData, encryptionKey) {
|
|
104
|
+
const json = await decrypt(encryptedData, encryptionKey);
|
|
105
|
+
return JSON.parse(json);
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error types for OAuth Token Manager
|
|
3
|
+
*
|
|
4
|
+
* These errors provide clear, actionable information about what went wrong
|
|
5
|
+
* and what the application should do to recover.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Base error class for all token manager errors
|
|
9
|
+
*/
|
|
10
|
+
export declare class TokenManagerError extends Error {
|
|
11
|
+
readonly code: string;
|
|
12
|
+
constructor(message: string, code: string);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Token not found for the given user and provider
|
|
16
|
+
*
|
|
17
|
+
* Recovery: Redirect user to OAuth flow to connect this provider
|
|
18
|
+
*/
|
|
19
|
+
export declare class TokenNotFoundError extends TokenManagerError {
|
|
20
|
+
readonly userId: string;
|
|
21
|
+
readonly provider: string;
|
|
22
|
+
constructor(userId: string, provider: string);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Token has expired and refresh failed or no refresh token available
|
|
26
|
+
*
|
|
27
|
+
* Recovery: Redirect user to OAuth flow to re-authenticate
|
|
28
|
+
*/
|
|
29
|
+
export declare class TokenExpiredError extends TokenManagerError {
|
|
30
|
+
readonly userId: string;
|
|
31
|
+
readonly provider: string;
|
|
32
|
+
readonly reason: 'no_refresh_token' | 'refresh_failed' | 'refresh_token_expired';
|
|
33
|
+
constructor(userId: string, provider: string, reason: 'no_refresh_token' | 'refresh_failed' | 'refresh_token_expired');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Token exists but doesn't have the required scopes
|
|
37
|
+
*
|
|
38
|
+
* Recovery: Redirect user to OAuth flow with incremental consent for missing scopes
|
|
39
|
+
*/
|
|
40
|
+
export declare class InsufficientScopesError extends TokenManagerError {
|
|
41
|
+
readonly userId: string;
|
|
42
|
+
readonly provider: string;
|
|
43
|
+
readonly requiredScopes: string[];
|
|
44
|
+
readonly grantedScopes: string[];
|
|
45
|
+
constructor(userId: string, provider: string, requiredScopes: string[], grantedScopes: string[]);
|
|
46
|
+
get missingScopes(): string[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Provider is not configured in the token manager
|
|
50
|
+
*
|
|
51
|
+
* Recovery: Add provider configuration to TokenManager constructor
|
|
52
|
+
*/
|
|
53
|
+
export declare class ProviderNotConfiguredError extends TokenManagerError {
|
|
54
|
+
readonly provider: string;
|
|
55
|
+
constructor(provider: string);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Encryption/decryption failed
|
|
59
|
+
*
|
|
60
|
+
* Recovery: Check encryption key is correct and hasn't changed
|
|
61
|
+
*/
|
|
62
|
+
export declare class CryptoError extends TokenManagerError {
|
|
63
|
+
readonly cause?: Error | undefined;
|
|
64
|
+
constructor(operation: 'encrypt' | 'decrypt', cause?: Error | undefined);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Storage operation failed
|
|
68
|
+
*
|
|
69
|
+
* Recovery: Check storage backend (KV/D1) is available and configured correctly
|
|
70
|
+
*/
|
|
71
|
+
export declare class StorageError extends TokenManagerError {
|
|
72
|
+
readonly cause?: Error | undefined;
|
|
73
|
+
constructor(operation: 'get' | 'set' | 'delete' | 'list', cause?: Error | undefined);
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;aAGxB,IAAI,EAAE,MAAM;gBAD5B,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM;CAK/B;AAED;;;;GAIG;AACH,qBAAa,kBAAmB,SAAQ,iBAAiB;aAErC,MAAM,EAAE,MAAM;aACd,QAAQ,EAAE,MAAM;gBADhB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM;CAQnC;AAED;;;;GAIG;AACH,qBAAa,iBAAkB,SAAQ,iBAAiB;aAEpC,MAAM,EAAE,MAAM;aACd,QAAQ,EAAE,MAAM;aAChB,MAAM,EAAE,kBAAkB,GAAG,gBAAgB,GAAG,uBAAuB;gBAFvE,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,kBAAkB,GAAG,gBAAgB,GAAG,uBAAuB;CAa1F;AAED;;;;GAIG;AACH,qBAAa,uBAAwB,SAAQ,iBAAiB;aAE1C,MAAM,EAAE,MAAM;aACd,QAAQ,EAAE,MAAM;aAChB,cAAc,EAAE,MAAM,EAAE;aACxB,aAAa,EAAE,MAAM,EAAE;gBAHvB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,EAAE,EACxB,aAAa,EAAE,MAAM,EAAE;IAUzC,IAAI,aAAa,IAAI,MAAM,EAAE,CAE5B;CACF;AAED;;;;GAIG;AACH,qBAAa,0BAA2B,SAAQ,iBAAiB;aACnC,QAAQ,EAAE,MAAM;gBAAhB,QAAQ,EAAE,MAAM;CAO7C;AAED;;;;GAIG;AACH,qBAAa,WAAY,SAAQ,iBAAiB;aAG9B,KAAK,CAAC,EAAE,KAAK;gBAD7B,SAAS,EAAE,SAAS,GAAG,SAAS,EAChB,KAAK,CAAC,EAAE,KAAK,YAAA;CAQhC;AAED;;;;GAIG;AACH,qBAAa,YAAa,SAAQ,iBAAiB;aAG/B,KAAK,CAAC,EAAE,KAAK;gBAD7B,SAAS,EAAE,KAAK,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,EAC5B,KAAK,CAAC,EAAE,KAAK,YAAA;CAQhC"}
|