@okta/spa-platform 0.6.0 → 0.9.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 +204 -0
- package/claude.md +562 -0
- package/dist/esm/Credential/Credential.js.map +1 -1
- package/dist/esm/Credential/CredentialCoordinator.js +25 -22
- package/dist/esm/Credential/CredentialCoordinator.js.map +1 -1
- package/dist/esm/Credential/TokenStorage.js +19 -3
- package/dist/esm/Credential/TokenStorage.js.map +1 -1
- package/dist/esm/FetchClient/index.js.map +1 -1
- package/dist/esm/core.js +25 -0
- package/dist/esm/core.js.map +1 -0
- package/dist/esm/flows/AuthorizationCodeFlow.js.map +1 -1
- package/dist/esm/flows/SessionLogoutFlow.js.map +1 -1
- package/dist/esm/flows/TransactionStorage.js.map +1 -1
- package/dist/esm/flows.js +30 -0
- package/dist/esm/flows.js.map +1 -0
- package/dist/esm/index.js +4 -13
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/orchestrators/AuthorizationCodeFlowOrchestrator.js +4 -0
- package/dist/esm/orchestrators/AuthorizationCodeFlowOrchestrator.js.map +1 -1
- package/dist/esm/orchestrators/HostOrchestrator/Host.js +9 -1
- package/dist/esm/orchestrators/HostOrchestrator/Host.js.map +1 -1
- package/dist/esm/orchestrators/HostOrchestrator/OrchestrationBridge.js.map +1 -1
- package/dist/esm/orchestrators/HostOrchestrator/SubApp.js +8 -2
- package/dist/esm/orchestrators/HostOrchestrator/SubApp.js.map +1 -1
- package/dist/esm/orchestrators/HostOrchestrator/index.js +11 -6
- package/dist/esm/orchestrators/HostOrchestrator/index.js.map +1 -1
- package/dist/esm/platform/OAuth2Client.js +4 -7
- package/dist/esm/platform/OAuth2Client.js.map +1 -1
- package/dist/esm/platform/defaults.js.map +1 -1
- package/dist/esm/platform/index.js.map +1 -1
- package/dist/esm/utils/IndexedDBStore.js +27 -18
- package/dist/esm/utils/IndexedDBStore.js.map +1 -1
- package/dist/esm/utils/SynchronizedResult.js +53 -60
- package/dist/esm/utils/SynchronizedResult.js.map +1 -1
- package/dist/esm/utils/isModernBrowser.js.map +1 -1
- package/dist/types/Credential/Credential.d.ts +8 -2
- package/dist/types/Credential/TokenStorage.d.ts +25 -1
- package/dist/types/FetchClient/index.d.ts +6 -0
- package/dist/types/core.d.ts +19 -0
- package/dist/types/flows/AuthorizationCodeFlow.d.ts +63 -13
- package/dist/types/flows/SessionLogoutFlow.d.ts +24 -0
- package/dist/types/flows/TransactionStorage.d.ts +7 -0
- package/dist/types/flows/index.d.ts +15 -2
- package/dist/types/flows.d.ts +9 -0
- package/dist/types/index.d.ts +1 -14
- package/dist/types/orchestrators/AuthorizationCodeFlowOrchestrator.d.ts +40 -6
- package/dist/types/orchestrators/HostOrchestrator/Host.d.ts +15 -0
- package/dist/types/orchestrators/HostOrchestrator/OrchestrationBridge.d.ts +4 -0
- package/dist/types/orchestrators/HostOrchestrator/SubApp.d.ts +13 -2
- package/dist/types/orchestrators/HostOrchestrator/index.d.ts +50 -16
- package/dist/types/orchestrators/index.d.ts +4 -1
- package/dist/types/platform/OAuth2Client.d.ts +9 -3
- package/dist/types/platform/defaults.d.ts +8 -0
- package/dist/types/platform/index.d.ts +6 -0
- package/dist/types/utils/IndexedDBStore.d.ts +1 -0
- package/dist/types/utils/SynchronizedResult.d.ts +1 -8
- package/dist/types/utils/isModernBrowser.d.ts +2 -1
- package/package.json +1 -1
package/claude.md
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
# @okta/spa-platform
|
|
2
|
+
|
|
3
|
+
**Package**: `@okta/spa-platform`
|
|
4
|
+
**Version**: 0.6.0
|
|
5
|
+
**Type**: Platform/Management Library
|
|
6
|
+
**Purpose**: High-level SPA platform SDK for token lifecycle management, storage, and authenticated requests.
|
|
7
|
+
|
|
8
|
+
> 📌 **Context**: This is the highest-level package in the monorepo, designed for direct use by SPA applications. See [root claude.md](../../claude.md) for monorepo architecture.
|
|
9
|
+
|
|
10
|
+
## 🎯 Package Overview
|
|
11
|
+
|
|
12
|
+
This package provides a complete, production-ready SDK for Single Page Applications to handle OAuth2 authentication, token management, and authenticated API requests.
|
|
13
|
+
|
|
14
|
+
### What This Package Provides
|
|
15
|
+
|
|
16
|
+
- **Credential Management**: High-level `Credential` abstraction for tokens with automatic refresh
|
|
17
|
+
- **Token Orchestrators**: Specialized orchestrators for different authentication patterns
|
|
18
|
+
- `AuthorizationCodeFlowOrchestrator` - Standard SPA authentication flow
|
|
19
|
+
- `HostOrchestrator` - Multi-app/iframe token delegation architecture
|
|
20
|
+
- **Browser Token Storage**: `BrowserTokenStorage` for secure token persistence
|
|
21
|
+
- **OAuth2 Client**: Browser-specific OAuth2 client with tab synchronization
|
|
22
|
+
- **Authenticated FetchClient**: HTTP client with automatic token injection
|
|
23
|
+
- **Flow Wrappers**: Browser-optimized versions of `@okta/oauth2-flows`
|
|
24
|
+
- **DPoP Support**: Browser-specific DPoP key management and signing
|
|
25
|
+
|
|
26
|
+
### What This Package Does NOT Provide
|
|
27
|
+
|
|
28
|
+
- Server-side authentication (this is SPA-only)
|
|
29
|
+
- Native mobile app support
|
|
30
|
+
- OAuth2 flow implementations (delegates to `@okta/oauth2-flows`)
|
|
31
|
+
|
|
32
|
+
## 📁 Source Code Structure
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
src/
|
|
36
|
+
├── Credential/ # Credential management and abstractions
|
|
37
|
+
├── FetchClient/ # HTTP client with token injection
|
|
38
|
+
├── flows/ # Flow integrations (wrapping @okta/oauth2-flows)
|
|
39
|
+
├── orchestrators/ # Token lifecycle orchestrators
|
|
40
|
+
├── platform/ # Browser-specific platform utilities
|
|
41
|
+
├── utils/ # Utility functions
|
|
42
|
+
└── index.ts # Public API exports
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 📦 Package Exports
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { /* platform APIs */ } from '@okta/spa-platform';
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Main exports**:
|
|
52
|
+
- Credential managers
|
|
53
|
+
- Authenticated fetch client
|
|
54
|
+
- Token orchestrators
|
|
55
|
+
- Browser-specific utilities
|
|
56
|
+
- Integrated flow wrappers
|
|
57
|
+
|
|
58
|
+
## 🔑 Key Components
|
|
59
|
+
|
|
60
|
+
### 1. Credential (`Credential/Credential.ts`)
|
|
61
|
+
|
|
62
|
+
High-level abstraction representing an OAuth2 credential (token + metadata).
|
|
63
|
+
|
|
64
|
+
**Key Features**:
|
|
65
|
+
- Wraps a `Token` with lifecycle management
|
|
66
|
+
- Automatic refresh via `refreshIfNeeded()`
|
|
67
|
+
- Static methods for finding and storing credentials
|
|
68
|
+
- Integration with `BrowserTokenStorage`
|
|
69
|
+
|
|
70
|
+
**Usage**:
|
|
71
|
+
```typescript
|
|
72
|
+
import { Credential } from '@okta/spa-platform';
|
|
73
|
+
|
|
74
|
+
// Store a credential
|
|
75
|
+
const credential = await Credential.store(token, ['app:main']);
|
|
76
|
+
|
|
77
|
+
// Find credentials by filter
|
|
78
|
+
const credentials = await Credential.find((meta) =>
|
|
79
|
+
meta.scopes?.includes('profile')
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Refresh if needed (checks expiration automatically)
|
|
83
|
+
await credential.refreshIfNeeded();
|
|
84
|
+
|
|
85
|
+
// Access the token
|
|
86
|
+
const accessToken = credential.token.accessToken;
|
|
87
|
+
|
|
88
|
+
// Remove credential
|
|
89
|
+
await credential.remove();
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 2. AuthorizationCodeFlowOrchestrator (`orchestrators/AuthorizationCodeFlowOrchestrator.ts`)
|
|
93
|
+
|
|
94
|
+
Implements `TokenOrchestrator` for standard SPA authentication flows.
|
|
95
|
+
|
|
96
|
+
**Responsibilities**:
|
|
97
|
+
- Manage authentication flow lifecycle
|
|
98
|
+
- Store and retrieve credentials
|
|
99
|
+
- Handle redirects and callbacks
|
|
100
|
+
- Automatic token refresh
|
|
101
|
+
- Event emission for UI integration
|
|
102
|
+
|
|
103
|
+
**Usage**:
|
|
104
|
+
```typescript
|
|
105
|
+
import {
|
|
106
|
+
AuthorizationCodeFlowOrchestrator,
|
|
107
|
+
AuthorizationCodeFlow
|
|
108
|
+
} from '@okta/spa-platform';
|
|
109
|
+
|
|
110
|
+
// Create flow
|
|
111
|
+
const flow = new AuthorizationCodeFlow({
|
|
112
|
+
issuer: 'https://dev-123456.okta.com/oauth2/default',
|
|
113
|
+
clientId: 'your-client-id',
|
|
114
|
+
redirectUri: 'http://localhost:8080/callback',
|
|
115
|
+
scopes: ['openid', 'profile', 'offline_access'],
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Create orchestrator
|
|
119
|
+
const orchestrator = new AuthorizationCodeFlowOrchestrator(flow, {
|
|
120
|
+
avoidPrompting: false,
|
|
121
|
+
emitBeforeRedirect: true,
|
|
122
|
+
getOriginalUri: () => window.location.pathname,
|
|
123
|
+
tags: ['main-app'],
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Listen for events
|
|
127
|
+
orchestrator.on('login_prompt_required', ({ done, params }) => {
|
|
128
|
+
console.log('Login required', params);
|
|
129
|
+
done(); // Call when ready to proceed
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
orchestrator.on('error', ({ error }) => {
|
|
133
|
+
console.error('Auth error', error);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Get token (will redirect if not authenticated)
|
|
137
|
+
const token = await orchestrator.getToken({
|
|
138
|
+
scopes: ['openid', 'profile'],
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Resume flow after redirect callback
|
|
142
|
+
await orchestrator.resumeFlow(window.location.href);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### 3. HostOrchestrator (`orchestrators/HostOrchestrator/`)
|
|
146
|
+
|
|
147
|
+
Advanced multi-app architecture for token delegation between host app and sub-apps (e.g., iframes).
|
|
148
|
+
|
|
149
|
+
**Components**:
|
|
150
|
+
- `HostOrchestrator.Host` - Receives and fulfills token requests from sub-apps
|
|
151
|
+
- `HostOrchestrator.SubApp` - Delegates token requests to host
|
|
152
|
+
- `HostOrchestrator.ProxyHost` - Adapts any orchestrator into a host
|
|
153
|
+
|
|
154
|
+
**Use Cases**:
|
|
155
|
+
- Micro-frontend architectures
|
|
156
|
+
- Main app + embedded iframes
|
|
157
|
+
- Shared authentication across multiple SPAs
|
|
158
|
+
|
|
159
|
+
**Usage - Host App**:
|
|
160
|
+
```typescript
|
|
161
|
+
import {
|
|
162
|
+
HostOrchestrator,
|
|
163
|
+
AuthorizationCodeFlowOrchestrator,
|
|
164
|
+
AuthorizationCodeFlow
|
|
165
|
+
} from '@okta/spa-platform';
|
|
166
|
+
|
|
167
|
+
// Create standard orchestrator
|
|
168
|
+
const flow = new AuthorizationCodeFlow({ /* config */ });
|
|
169
|
+
const orchestrator = new AuthorizationCodeFlowOrchestrator(flow);
|
|
170
|
+
|
|
171
|
+
// Wrap in ProxyHost to enable delegation
|
|
172
|
+
const host = new HostOrchestrator.ProxyHost('main-app', orchestrator);
|
|
173
|
+
|
|
174
|
+
// Host automatically handles token requests from sub-apps
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Usage - Sub-App (iframe)**:
|
|
178
|
+
```typescript
|
|
179
|
+
import { HostOrchestrator } from '@okta/spa-platform';
|
|
180
|
+
|
|
181
|
+
// Create SubApp orchestrator that delegates to host
|
|
182
|
+
const subApp = new HostOrchestrator.SubApp({
|
|
183
|
+
scopes: ['openid', 'profile'],
|
|
184
|
+
targetOrigin: 'https://main-app.example.com',
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Use like any orchestrator
|
|
188
|
+
const token = await subApp.getToken();
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### 4. BrowserTokenStorage (`Credential/TokenStorage.ts`)
|
|
192
|
+
|
|
193
|
+
Browser-specific token storage implementation.
|
|
194
|
+
|
|
195
|
+
**Features**:
|
|
196
|
+
- Supports `localStorage` and `sessionStorage`
|
|
197
|
+
- Automatic serialization/deserialization
|
|
198
|
+
- Tab-synchronized updates
|
|
199
|
+
- Secure storage practices
|
|
200
|
+
- Encrypts tokens at-rest
|
|
201
|
+
|
|
202
|
+
### 5. OAuth2Client (`platform/OAuth2Client.ts`)
|
|
203
|
+
|
|
204
|
+
Browser-specific OAuth2 client extending `@okta/auth-foundation` client.
|
|
205
|
+
|
|
206
|
+
**Key Feature**:
|
|
207
|
+
- **Tab-Synchronized Refresh**: Uses `SynchronizedResult` to coordinate token refresh across browser tabs
|
|
208
|
+
- Only one tab performs refresh, others wait for result
|
|
209
|
+
- Prevents duplicate refresh requests
|
|
210
|
+
|
|
211
|
+
**How It Works**:
|
|
212
|
+
```typescript
|
|
213
|
+
// Multiple tabs call refresh simultaneously
|
|
214
|
+
// Only one actually makes the request
|
|
215
|
+
// Others receive the result via storage events
|
|
216
|
+
const refreshedToken = await client.refresh(token);
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### 6. FetchClient (`FetchClient/`)
|
|
220
|
+
|
|
221
|
+
Authenticated HTTP client for making API requests.
|
|
222
|
+
|
|
223
|
+
**Features**:
|
|
224
|
+
- Automatic access token injection in `Authorization` header
|
|
225
|
+
- DPoP proof generation (if token is DPoP-bound)
|
|
226
|
+
- Token refresh on 401 responses
|
|
227
|
+
- Platform-specific fetch implementation
|
|
228
|
+
|
|
229
|
+
**Usage**:
|
|
230
|
+
```typescript
|
|
231
|
+
import {
|
|
232
|
+
FetchClient,
|
|
233
|
+
Credential,
|
|
234
|
+
AuthorizationCodeFlowOrchestrator,
|
|
235
|
+
AuthorizationCodeFlow
|
|
236
|
+
} from '@okta/spa-platform';
|
|
237
|
+
|
|
238
|
+
// Create standard orchestrator
|
|
239
|
+
const flow = new AuthorizationCodeFlow({ /* config */ });
|
|
240
|
+
const orchestrator = new AuthorizationCodeFlowOrchestrator(flow);
|
|
241
|
+
|
|
242
|
+
const client = new FetchClient(orchestrator);
|
|
243
|
+
|
|
244
|
+
// Automatically adds Authorization header
|
|
245
|
+
const response = await client.fetch('https://api.example.com/data');
|
|
246
|
+
const data = await response.json();
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### 7. Flow Wrappers (`flows/`)
|
|
250
|
+
|
|
251
|
+
Browser-optimized versions of `@okta/oauth2-flows`:
|
|
252
|
+
- `AuthorizationCodeFlow` - Extends base flow with browser storage integration
|
|
253
|
+
- `SessionLogoutFlow` - Extends base logout with browser cleanup
|
|
254
|
+
|
|
255
|
+
These wrap the core flows and add browser-specific functionality like:
|
|
256
|
+
- Automatic state/verifier storage in sessionStorage
|
|
257
|
+
- Callback parsing from `window.location`
|
|
258
|
+
- Browser redirect handling
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
## 🔧 Development
|
|
262
|
+
|
|
263
|
+
### Building
|
|
264
|
+
```bash
|
|
265
|
+
yarn build # Build ESM + types
|
|
266
|
+
yarn build:watch # Watch mode
|
|
267
|
+
yarn build:esm # Rollup bundle only
|
|
268
|
+
yarn build:types # TypeScript definitions only
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Testing
|
|
272
|
+
```bash
|
|
273
|
+
yarn test # Run all tests
|
|
274
|
+
yarn test:unit # Unit tests
|
|
275
|
+
yarn test:watch # Watch mode
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Linting
|
|
279
|
+
```bash
|
|
280
|
+
yarn lint
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## 🏗️ Architecture Patterns
|
|
284
|
+
|
|
285
|
+
### Layered Architecture
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
┌─────────────────────────────────────┐
|
|
289
|
+
│ SPA Application Code │
|
|
290
|
+
└─────────────────┬───────────────────┘
|
|
291
|
+
│
|
|
292
|
+
┌─────────────────▼───────────────────┐
|
|
293
|
+
│ @okta/spa-platform │
|
|
294
|
+
│ • Token Orchestration │
|
|
295
|
+
│ • Storage Management │
|
|
296
|
+
│ • Tab Synchronization │
|
|
297
|
+
└─────────────────┬───────────────────┘
|
|
298
|
+
│
|
|
299
|
+
┌─────────────────▼───────────────────┐
|
|
300
|
+
│ @okta/oauth2-flows │
|
|
301
|
+
│ • Authorization Code Flow │
|
|
302
|
+
│ • Logout Flows │
|
|
303
|
+
└─────────────────┬───────────────────┘
|
|
304
|
+
│
|
|
305
|
+
┌─────────────────▼───────────────────┐
|
|
306
|
+
│ @okta/auth-foundation │
|
|
307
|
+
│ • Token Primitives │
|
|
308
|
+
│ • HTTP Client │
|
|
309
|
+
│ • Crypto Utils │
|
|
310
|
+
└─────────────────────────────────────┘
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Credential-Based Architecture
|
|
314
|
+
|
|
315
|
+
This package uses a **Credential** abstraction rather than raw tokens:
|
|
316
|
+
|
|
317
|
+
```
|
|
318
|
+
Application Code
|
|
319
|
+
↓
|
|
320
|
+
Credential (high-level)
|
|
321
|
+
↓
|
|
322
|
+
Token (from @okta/auth-foundation)
|
|
323
|
+
↓
|
|
324
|
+
OAuth2 protocol primitives
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Benefits**:
|
|
328
|
+
- Automatic refresh management
|
|
329
|
+
- Storage abstraction
|
|
330
|
+
- Metadata (scopes, tags) attached to tokens
|
|
331
|
+
- Query/filter capabilities
|
|
332
|
+
|
|
333
|
+
### Orchestrator Pattern
|
|
334
|
+
|
|
335
|
+
`TokenOrchestrator` (from `@okta/auth-foundation`) defines the contract:
|
|
336
|
+
- `getToken(params)` - Get or request a token
|
|
337
|
+
- Event emission for lifecycle hooks
|
|
338
|
+
|
|
339
|
+
This package provides implementations:
|
|
340
|
+
- `AuthorizationCodeFlowOrchestrator` - Standard SPA flow
|
|
341
|
+
- `HostOrchestrator.Host` - Token delegation receiver
|
|
342
|
+
- `HostOrchestrator.SubApp` - Token delegation requestor
|
|
343
|
+
|
|
344
|
+
### Tab Synchronization
|
|
345
|
+
|
|
346
|
+
**Challenge**: Multiple browser tabs need consistent auth state
|
|
347
|
+
|
|
348
|
+
**Solution**:
|
|
349
|
+
- `BrowserTokenStorage` uses storage events
|
|
350
|
+
- `SynchronizedResult` prevents duplicate operations
|
|
351
|
+
- `OAuth2Client.prepareRefreshRequest` coordinates refresh across tabs
|
|
352
|
+
|
|
353
|
+
**Flow**:
|
|
354
|
+
1. Tab A starts token refresh
|
|
355
|
+
2. Tab A creates `SynchronizedResult` with unique key
|
|
356
|
+
3. Tab A stores "pending" state in localStorage
|
|
357
|
+
4. Tab B attempts refresh with same key
|
|
358
|
+
5. Tab B sees "pending", waits for result
|
|
359
|
+
6. Tab A completes, stores result
|
|
360
|
+
7. Tab B receives result via storage event
|
|
361
|
+
|
|
362
|
+
## 🧪 Testing Strategy
|
|
363
|
+
|
|
364
|
+
### Test Organization
|
|
365
|
+
Tests are co-located in `test/` and focus on:
|
|
366
|
+
- Token renewal logic
|
|
367
|
+
- Tab synchronization
|
|
368
|
+
- Storage operations
|
|
369
|
+
- HTTP client behavior
|
|
370
|
+
- Error handling
|
|
371
|
+
|
|
372
|
+
### Browser Environment
|
|
373
|
+
All tests simulate browser environment:
|
|
374
|
+
- Mock `localStorage` and `sessionStorage`
|
|
375
|
+
- Mock `BroadcastChannel`
|
|
376
|
+
- Mock `fetch` API
|
|
377
|
+
|
|
378
|
+
### Mocking Strategy
|
|
379
|
+
Uses `@repo/jest-helpers` to mock:
|
|
380
|
+
- Browser storage APIs
|
|
381
|
+
- Token endpoints
|
|
382
|
+
- Time (for expiration testing)
|
|
383
|
+
|
|
384
|
+
## 🔗 Integration with Other Packages
|
|
385
|
+
|
|
386
|
+
### Dependencies
|
|
387
|
+
- `@okta/auth-foundation` (peer dependency): Core primitives
|
|
388
|
+
- `@okta/oauth2-flows` (optional peer dependency): OAuth2 flows
|
|
389
|
+
|
|
390
|
+
### Typical Usage
|
|
391
|
+
```typescript
|
|
392
|
+
import {
|
|
393
|
+
FetchClient,
|
|
394
|
+
Credential,
|
|
395
|
+
AuthorizationCodeFlowOrchestrator,
|
|
396
|
+
AuthorizationCodeFlow
|
|
397
|
+
} from '@okta/spa-platform';
|
|
398
|
+
|
|
399
|
+
// Create standard orchestrator
|
|
400
|
+
const flow = new AuthorizationCodeFlow({ /* config */ });
|
|
401
|
+
const orchestrator = new AuthorizationCodeFlowOrchestrator(flow);
|
|
402
|
+
|
|
403
|
+
const client = new FetchClient(orchestrator);
|
|
404
|
+
|
|
405
|
+
// Automatically adds Authorization header
|
|
406
|
+
const response = await client.fetch('https://api.example.com/data');
|
|
407
|
+
const data = await response.json();
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## 📘 Key Concepts
|
|
411
|
+
|
|
412
|
+
### Token Renewal
|
|
413
|
+
|
|
414
|
+
**Why?**
|
|
415
|
+
- Access tokens are short-lived (typically 1 hour)
|
|
416
|
+
- Must be renewed before expiry to avoid interruptions
|
|
417
|
+
|
|
418
|
+
**How?**
|
|
419
|
+
- Orchestrator checks expiration periodically
|
|
420
|
+
- Uses refresh token to get new access token
|
|
421
|
+
- Updates storage atomically
|
|
422
|
+
- Notifies all tabs of new tokens
|
|
423
|
+
|
|
424
|
+
### Cross-Tab Authentication
|
|
425
|
+
|
|
426
|
+
**Challenges**:
|
|
427
|
+
- User might authenticate in Tab A
|
|
428
|
+
- Tab B needs to know about authentication
|
|
429
|
+
- User might logout in Tab A
|
|
430
|
+
- Tab B should also logout
|
|
431
|
+
|
|
432
|
+
**Solution**:
|
|
433
|
+
- Tab sync via storage events or BroadcastChannel
|
|
434
|
+
- Shared token storage
|
|
435
|
+
- Coordinated logout
|
|
436
|
+
|
|
437
|
+
### Secure Token Storage
|
|
438
|
+
|
|
439
|
+
**Best Practices**:
|
|
440
|
+
1. **Never store tokens in cookies** (CSRF risk)
|
|
441
|
+
2. **Encrypt at rest** (XSS protection)
|
|
442
|
+
3. **Use localStorage** only if persistence is required
|
|
443
|
+
4. **Never log tokens** (security risk)
|
|
444
|
+
5. **Clear storage on logout**
|
|
445
|
+
|
|
446
|
+
### DPoP in SPA Context
|
|
447
|
+
|
|
448
|
+
**DPoP with SPAs**:
|
|
449
|
+
- Private key must be stored securely
|
|
450
|
+
- Use Web Crypto API for key generation
|
|
451
|
+
- Store (non-extractable) key in IndexedDB (not localStorage)
|
|
452
|
+
- Same key must be used for token and API requests
|
|
453
|
+
|
|
454
|
+
## 🚨 Common Gotchas
|
|
455
|
+
|
|
456
|
+
1. **Storage Permissions**: Some browsers block storage in iframes or incognito mode
|
|
457
|
+
2. **Token Expiry**: Orchestrator must start BEFORE tokens expire
|
|
458
|
+
3. **Tab Sync Delay**: Storage events have slight delay - don't rely on instant sync
|
|
459
|
+
4. **Memory Leaks**: Always call `orchestrator.stop()` when component unmounts
|
|
460
|
+
5. **DPoP Keys**: Must persist DPoP keys across page reloads (use IndexedDB)
|
|
461
|
+
|
|
462
|
+
## 🎯 Common Use Cases
|
|
463
|
+
|
|
464
|
+
### Basic SPA Authentication
|
|
465
|
+
```typescript
|
|
466
|
+
import {
|
|
467
|
+
AuthorizationCodeFlowOrchestrator,
|
|
468
|
+
AuthorizationCodeFlow,
|
|
469
|
+
Credential
|
|
470
|
+
} from '@okta/spa-platform';
|
|
471
|
+
|
|
472
|
+
// 1. Setup
|
|
473
|
+
const flow = new AuthorizationCodeFlow({
|
|
474
|
+
issuer: 'https://dev-123456.okta.com/oauth2/default',
|
|
475
|
+
clientId: 'client-id',
|
|
476
|
+
redirectUri: 'http://localhost:8080/callback',
|
|
477
|
+
scopes: ['openid', 'profile', 'offline_access'],
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const orchestrator = new AuthorizationCodeFlowOrchestrator(flow);
|
|
481
|
+
|
|
482
|
+
// 2. Check if already authenticated
|
|
483
|
+
const token = await orchestrator.getToken();
|
|
484
|
+
// If not authenticated, this redirects to Okta
|
|
485
|
+
|
|
486
|
+
// 3. Handle callback (in /callback route)
|
|
487
|
+
if (window.location.pathname === '/callback') {
|
|
488
|
+
await orchestrator.resumeFlow();
|
|
489
|
+
// Redirect to originalUri or home
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// 4. Make API requests
|
|
493
|
+
const credential = (await Credential.find(() => true))[0];
|
|
494
|
+
await credential.refreshIfNeeded();
|
|
495
|
+
const accessToken = credential.token.accessToken;
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### Logout
|
|
499
|
+
```typescript
|
|
500
|
+
import { SessionLogoutFlow } from '@okta/oauth2-flows';
|
|
501
|
+
|
|
502
|
+
// Logout from Okta
|
|
503
|
+
const logoutFlow = new SessionLogoutFlow(config);
|
|
504
|
+
const credential = await Credential.getDefault();
|
|
505
|
+
const url = await logoutFlow.start(credential.token?.idToken);
|
|
506
|
+
window.location.href = url;
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### Multi-App Architecture
|
|
510
|
+
```typescript
|
|
511
|
+
// Host App (main-app.example.com)
|
|
512
|
+
import {
|
|
513
|
+
HostOrchestrator,
|
|
514
|
+
AuthorizationCodeFlowOrchestrator,
|
|
515
|
+
AuthorizationCodeFlow
|
|
516
|
+
} from '@okta/spa-platform';
|
|
517
|
+
|
|
518
|
+
const flow = new AuthorizationCodeFlow({ /* config */ });
|
|
519
|
+
const orchestrator = new AuthorizationCodeFlowOrchestrator(flow);
|
|
520
|
+
const host = new HostOrchestrator.ProxyHost('main', orchestrator);
|
|
521
|
+
|
|
522
|
+
// Sub-App (iframe in main app)
|
|
523
|
+
import { HostOrchestrator } from '@okta/spa-platform';
|
|
524
|
+
|
|
525
|
+
const subApp = new HostOrchestrator.SubApp({
|
|
526
|
+
scopes: ['openid', 'profile'],
|
|
527
|
+
targetOrigin: 'https://main-app.example.com',
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const token = await subApp.getToken(); // Requests from host
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
## 💡 Working with Claude
|
|
534
|
+
|
|
535
|
+
When modifying this package:
|
|
536
|
+
|
|
537
|
+
1. **Browser-Only**: All code must work in browser environment
|
|
538
|
+
2. **Storage Access**: Always handle storage errors gracefully (incognito, permissions)
|
|
539
|
+
3. **Tab Sync**: Test with multiple tabs/windows open
|
|
540
|
+
4. **Token Security**: Never log or expose tokens
|
|
541
|
+
5. **Orchestrator Lifecycle**: Always clean up (call `stop()`) to prevent memory leaks
|
|
542
|
+
|
|
543
|
+
### Important Files
|
|
544
|
+
- `src/Credential/` - Credential abstraction and storage
|
|
545
|
+
- `src/orchestrators/` - Token orchestrators
|
|
546
|
+
- `src/flows/` - Browser-specific flow wrappers
|
|
547
|
+
- `src/platform/OAuth2Client.ts` - Tab-synchronized refresh
|
|
548
|
+
- `src/index.ts` - Public API surface
|
|
549
|
+
|
|
550
|
+
### Related Documentation
|
|
551
|
+
- OAuth2 Token Refresh: https://oauth.net/2/refresh-tokens/
|
|
552
|
+
- Web Storage API: https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API
|
|
553
|
+
- BroadcastChannel API: https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel
|
|
554
|
+
|
|
555
|
+
---
|
|
556
|
+
|
|
557
|
+
**Peer Dependencies**:
|
|
558
|
+
- `@okta/auth-foundation@*` (required)
|
|
559
|
+
- `@okta/oauth2-flows@*` (optional)
|
|
560
|
+
|
|
561
|
+
**Target Environment**: Browser (SPA only)
|
|
562
|
+
**Private**: Yes (not published separately)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Credential.js","sources":["../../../../src/Credential/Credential.ts"],"sourcesContent":[null],"names":["CredentialBase"],"mappings":";;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"Credential.js","sources":["../../../../src/Credential/Credential.ts"],"sourcesContent":[null],"names":["CredentialBase"],"mappings":";;;;;;;;;;;;;;;AAyBM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAc,CAAA,CAAA;AAC5C,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;QACE,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAA,CAAA,CAAG,IAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAyB,CAAC,IAAI,CAAC,CAAA;;AASjD,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,KAAK,CAAA,CAAA,CAAA,CAAA;AAEhB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAyC,CAAA,CAAE,KAAK,CAAA,CAAA,CAAA,CAAI,CAAA;IAC5D,CAAA;AACD,CAAA;;"}
|
|
@@ -25,8 +25,7 @@ class CredentialCoordinatorImpl extends CredentialCoordinatorImpl$1 {
|
|
|
25
25
|
this.credentialDataSource = new DefaultCredentialDataSource(CredentialConstructor);
|
|
26
26
|
this.registerTabListeners();
|
|
27
27
|
this.emitter.on('credential_refreshed', ({ credential }) => {
|
|
28
|
-
|
|
29
|
-
this.broadcast('credential_refreshed', { id: token.id, value: token.toJSON() });
|
|
28
|
+
this.broadcast('credential_refreshed', { id: credential.id });
|
|
30
29
|
});
|
|
31
30
|
}
|
|
32
31
|
get tokenStorage() {
|
|
@@ -43,7 +42,7 @@ class CredentialCoordinatorImpl extends CredentialCoordinatorImpl$1 {
|
|
|
43
42
|
}
|
|
44
43
|
super.tokenStorage = tokenStorage;
|
|
45
44
|
this.tokenStorage.emitter.on('token_added', ({ token }) => {
|
|
46
|
-
this.broadcast('credential_added', { id: token.id
|
|
45
|
+
this.broadcast('credential_added', { id: token.id });
|
|
47
46
|
});
|
|
48
47
|
this.tokenStorage.emitter.on('token_removed', ({ id }) => {
|
|
49
48
|
this.broadcast('credential_removed', { id });
|
|
@@ -67,7 +66,7 @@ class CredentialCoordinatorImpl extends CredentialCoordinatorImpl$1 {
|
|
|
67
66
|
if (isFirefox()) {
|
|
68
67
|
await pause(50);
|
|
69
68
|
}
|
|
70
|
-
const { eventName, id,
|
|
69
|
+
const { eventName, id, source } = event.data;
|
|
71
70
|
if (source == this.id) {
|
|
72
71
|
return;
|
|
73
72
|
}
|
|
@@ -88,27 +87,31 @@ class CredentialCoordinatorImpl extends CredentialCoordinatorImpl$1 {
|
|
|
88
87
|
this.emitter.emit('metadata_updated', { storage: this.tokenStorage, id, metadata });
|
|
89
88
|
}
|
|
90
89
|
}
|
|
91
|
-
else {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
else {
|
|
98
|
-
this.emitter.emit('credential_removed', { dataSource: this.credentialDataSource, id });
|
|
99
|
-
}
|
|
90
|
+
else if (eventName === 'credential_added') {
|
|
91
|
+
this.emitter.emit('credential_added', { id });
|
|
92
|
+
}
|
|
93
|
+
else if (eventName === 'credential_removed') {
|
|
94
|
+
if (this.credentialDataSource.hasCredential(id)) {
|
|
95
|
+
this.credentialDataSource.remove(id);
|
|
100
96
|
}
|
|
101
97
|
else {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
98
|
+
this.emitter.emit('credential_removed', { id });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else if (eventName === 'credential_refreshed') {
|
|
102
|
+
if (!this.credentialDataSource.hasCredential(id)) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const token = await this.tokenStorage.get(id);
|
|
106
|
+
if (!token) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const credential = this.credentialDataSource.credentialFor(token);
|
|
110
|
+
if (Token.isEqual(token, credential.token)) {
|
|
111
|
+
return;
|
|
111
112
|
}
|
|
113
|
+
credential.token = token;
|
|
114
|
+
this.emitter.emit('credential_refreshed', { credential });
|
|
112
115
|
}
|
|
113
116
|
log('allIDs: ', this.allIDs(), 'size: ', this.credentialDataSource.size);
|
|
114
117
|
};
|