@tidecloak/js 0.13.11 → 0.13.13

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 CHANGED
@@ -1,520 +1,35 @@
1
- # TideCloak JavaScript SDK (`@tidecloak/js`)
1
+ # TideCloak JavaScript SDK
2
2
 
3
- Lightweight browser SDK for integrating TideCloak SSO into any JavaScript application-vanilla, SPA, or framework-agnostic.
4
-
5
- ## Authentication Modes
6
-
7
- The SDK supports two authentication modes:
8
-
9
- | Mode | Description | Use Case |
10
- |------|-------------|----------|
11
- | **Front-channel** | Browser handles all token operations (standard OIDC) | SPAs, simple apps |
12
- | **Hybrid/BFF** | Browser handles PKCE, backend exchanges code for tokens | Secure apps, server-side sessions |
13
-
14
- ---
15
-
16
- ## 1. Prerequisites
17
-
18
- Before you begin, ensure you have:
19
-
20
- * A bundler like [Vite](https://vite.dev/) or [Webpack](https://webpack.js.org/)
21
- * A [running](https://github.com/tide-foundation/tidecloak-gettingstarted) TideCloak server
22
- * A registered client in your realm with default user contexts committed
23
- * A valid Keycloak adapter JSON file (e.g., `tidecloak.json`)
24
- * A browser environment (SDK uses `window`, `document.cookie`, etc.)
25
-
26
- ---
27
-
28
- ## 2. Project Setup
29
-
30
- Start a new project with Vite + TideCloak:
31
-
32
- ```bash
33
- npm create vite@latest my-app -- --template vanilla
34
- cd my-app
35
- npm install
36
- npm run dev
37
- ```
38
-
39
- Folder structure:
40
-
41
- ```
42
- my-app/
43
- ├─ index.html
44
- ├─ main.js
45
- ├─ tidecloak.json
46
- ├─ public/
47
- │ └─ auth/
48
- │ └─ redirect.html
49
- ├─ package.json
50
- └─ vite.config.js
51
- ```
52
-
53
- ---
54
-
55
- ## 3. Install `@tidecloak/js`
3
+ Add TideCloak authentication to any JavaScript app.
56
4
 
57
5
  ```bash
58
6
  npm install @tidecloak/js
59
- # or
60
- yarn add @tidecloak/js
61
- ```
62
-
63
- This package exports:
64
-
65
- * `IAMService` - high-level wrapper and lifecycle manager
66
- * `TideCloak` - lower-level Keycloak-style adapter instance
67
-
68
- > **Note:** Installing this package automatically adds a `silent-check-sso.html` file to your `public` directory. This file is required for silent SSO checks; if it doesn’t exist, create it manually at `public/silent-check-sso.html` with the following content, otherwise the app will break:
69
- >
70
- > ```html
71
- > <html>
72
- > <body>
73
- > <script>parent.postMessage(location.href, location.origin)</script>
74
- > </body>
75
- > </html>
76
- > ```
77
-
78
- ---
79
-
80
- ## 4. Initialize the SDK
81
-
82
- In your main entry file, initialize IAM and register lifecycle listeners. You may also choose to handle lifecycle events such as session expiration here:
83
-
84
- **File:** `main.js`
85
-
86
- ```js
87
- import { IAMService } from "@tidecloak/js";
88
- import config from "./tidecloak.json";
89
-
90
- const loginBtn = document.getElementById("login-btn");
91
- const logoutBtn = document.getElementById("logout-btn");
92
- const statusEl = document.getElementById("status");
93
-
94
- loginBtn.onclick = () => IAMService.doLogin();
95
- logoutBtn.onclick = () => IAMService.doLogout();
96
-
97
- function updateUI(authenticated) {
98
- loginBtn.style.display = authenticated ? "none" : "inline-block";
99
- logoutBtn.style.display = authenticated ? "inline-block" : "none";
100
- statusEl.textContent = authenticated ? "✅ Authenticated" : "🔒 Please log in";
101
- }
102
-
103
- IAMService
104
- .on("ready", updateUI)
105
- .on("authError", err => statusEl.textContent = `❌ Auth error: ${err.message}`)
106
- .on("logout", () => {
107
- console.log("User logged out");
108
- updateUI(false);
109
- })
110
- .on("tokenExpired", () => {
111
- alert("Session expired, please log in again");
112
- updateUI(false);
113
- });
114
-
115
- (async () => {
116
- try {
117
- await IAMService.initIAM(config); // You can add redirectUri here if customizing
118
- } catch (err) {
119
- console.error("Failed to initialize IAM:", err);
120
- statusEl.textContent = "❌ Initialization error";
121
- }
122
- })();
123
- ```
124
-
125
- **File:** `index.html`
126
-
127
- ```html
128
- <button id="login-btn">Log In</button>
129
- <button id="logout-btn" style="display:none">Log Out</button>
130
- <div id="status">Initializing...</div>
131
- ```
132
-
133
- ---
134
-
135
- ## 5. Redirect URI Handling
136
-
137
- TideCloak will redirect users after login/logout to a URI defined in your adapter config.
138
-
139
- If not explicitly set, the default value is:
140
-
141
- ```js
142
- `${window.location.origin}/auth/redirect`
143
- ```
144
-
145
- > This means your app **must contain a static file or route** at `/auth/redirect`.
146
- > In Vite, this typically means adding a file like `public/auth/redirect.html`.
147
-
148
- You can override this behavior by passing a `redirectUri` to `initIAM()`:
149
-
150
- ```js
151
- await IAMService.initIAM({
152
- ...config,
153
- redirectUri: "https://yourdomain.com/auth/callback"
154
- });
155
- ```
156
-
157
- > ⚠️ Regardless of the value used, the **actual route or file must exist** in your deployed project. If the redirect target doesn’t exist, users will land on a 404 page after login/logout.
158
-
159
- **File:** `public/auth/redirect.html`
160
-
161
- ```html
162
- <!-- This file ensures the /auth/redirect path exists -->
163
- <!DOCTYPE html>
164
- <html>
165
- <head><title>Redirecting...</title></head>
166
- <body>
167
- <p>Redirecting, please wait...</p>
168
- <script>
169
- // Optionally show loading UI or transition
170
- // Auth state will be handled once initIAM runs again in your main.js
171
- window.location.href = "/"; // or redirect elsewhere
172
- </script>
173
- </body>
174
- </html>
175
- ```
176
-
177
- **Description:** This file ensures that the default redirect URI resolves without a 404.
178
-
179
- If you override the `redirectUri` in `initIAM`, make sure to **update the corresponding redirect path** and that it exists in `public/` or your router.
180
-
181
- ---
182
-
183
- ## 6. Hybrid/BFF Mode (Backend-For-Frontend)
184
-
185
- In hybrid mode, the browser generates PKCE and redirects to the IdP, but the **backend exchanges the authorization code for tokens**. This keeps tokens server-side for improved security—ideal for applications with server-rendered pages or strict security requirements.
186
-
187
- ### How It Works
188
-
189
- 1. **Login Page**: User clicks login → `doLogin()` generates PKCE, stores verifier in sessionStorage, redirects to IdP
190
- 2. **IdP**: User authenticates
191
- 3. **Callback Page**: IdP redirects back with `?code=...` → `initIAM()` sends code + verifier to your backend
192
- 4. **Backend**: Exchanges code for tokens, creates session cookie
193
- 5. **Success**: User is redirected to their original destination
194
-
195
- ### Configuration
196
-
197
- ```js
198
- const hybridConfig = {
199
- authMode: "hybrid",
200
- oidc: {
201
- authorizationEndpoint: "https://auth.example.com/realms/myrealm/protocol/openid-connect/auth",
202
- clientId: "my-client",
203
- redirectUri: "https://app.example.com/auth/callback",
204
- scope: "openid profile email", // optional, defaults to "openid profile email"
205
- prompt: "login" // optional
206
- },
207
- tokenExchange: {
208
- endpoint: "/api/authenticate", // Your backend endpoint
209
- provider: "tidecloak-auth", // optional, sent to backend
210
- headers: () => ({ // optional, custom headers (e.g., CSRF token)
211
- "anti-csrf-token": document.querySelector('meta[name="csrf-token"]')?.content
212
- })
213
- }
214
- };
215
- ```
216
-
217
- ### Login Page Example
218
-
219
- ```js
220
- import { IAMService } from "@tidecloak/js";
221
-
222
- // Define config (or import from shared file)
223
- const hybridConfig = { /* ... */ };
224
-
225
- // Load config on page load
226
- await IAMService.loadConfig(hybridConfig);
227
-
228
- // Trigger login when user clicks button
229
- document.getElementById("login-btn").onclick = () => {
230
- const returnUrl = new URLSearchParams(window.location.search).get("return") || "/";
231
- IAMService.doLogin(returnUrl); // returnUrl is where user goes after successful auth
232
- };
233
- ```
234
-
235
- ### Callback Page Example
236
-
237
- ```js
238
- import { IAMService } from "@tidecloak/js";
239
-
240
- const hybridConfig = { /* same config as login page */ };
241
-
242
- // initIAM automatically detects the callback (code in URL) and handles token exchange
243
- const authenticated = await IAMService.initIAM(hybridConfig);
244
-
245
- if (authenticated) {
246
- // Success - redirect to original destination
247
- const returnUrl = IAMService.getReturnUrl() || "/";
248
- window.location.assign(returnUrl);
249
- } else {
250
- // Failed - show error or redirect to login
251
- document.getElementById("error").textContent = "Login failed. Please try again.";
252
- }
253
- ```
254
-
255
- ### Backend Token Exchange
256
-
257
- Your backend receives a POST request to the configured `tokenExchange.endpoint`:
258
-
259
- ```json
260
- {
261
- "accessToken": "{\"code\":\"AUTH_CODE\",\"code_verifier\":\"PKCE_VERIFIER\",\"redirect_uri\":\"https://app.example.com/auth/callback\"}",
262
- "provider": "tidecloak-auth"
263
- }
264
- ```
265
-
266
- Your backend should:
267
- 1. Parse the `accessToken` JSON string
268
- 2. Exchange the code with the IdP's token endpoint
269
- 3. Create a session (e.g., set HTTP-only cookies)
270
- 4. Return a success response
271
-
272
- ### React Example
273
-
274
- **LoginPage.tsx**
275
- ```tsx
276
- import { useEffect, useState } from "react";
277
- import { IAMService } from "@tidecloak/js";
278
- import { useLocation } from "react-router-dom";
279
-
280
- const hybridConfig = { /* ... */ };
281
-
282
- export function LoginPage() {
283
- const [ready, setReady] = useState(false);
284
- const location = useLocation();
285
- const returnUrl = new URLSearchParams(location.search).get("return") || "/";
286
-
287
- useEffect(() => {
288
- IAMService.loadConfig(hybridConfig).then(() => setReady(true));
289
- }, []);
290
-
291
- return (
292
- <button disabled={!ready} onClick={() => IAMService.doLogin(returnUrl)}>
293
- Login with TideCloak
294
- </button>
295
- );
296
- }
297
7
  ```
298
8
 
299
- **CallbackPage.tsx**
300
- ```tsx
301
- import { useEffect, useState } from "react";
302
- import { IAMService } from "@tidecloak/js";
303
-
304
- const hybridConfig = { /* same config */ };
305
-
306
- export function CallbackPage() {
307
- const [error, setError] = useState<string | null>(null);
308
-
309
- useEffect(() => {
310
- IAMService.initIAM(hybridConfig)
311
- .then(authenticated => {
312
- if (authenticated) {
313
- window.location.assign(IAMService.getReturnUrl() || "/");
314
- } else {
315
- setError("Login failed");
316
- }
317
- })
318
- .catch(err => setError(err.message));
319
- }, []);
320
-
321
- if (error) return <div>Error: {error}</div>;
322
- return <div>Logging in...</div>;
323
- }
324
- ```
325
-
326
- ### Hybrid Mode Limitations
327
-
328
- In hybrid mode, tokens are server-side, so these methods will throw:
329
- - `getToken()`, `getIDToken()`, `getTokenExp()`
330
- - `getName()`, `hasRealmRole()`, `hasClientRole()`
331
- - `getValueFromToken()`, `getValueFromIDToken()`
332
- - `updateIAMToken()`, `forceUpdateToken()`
333
- - `doEncrypt()`, `doDecrypt()`
334
-
335
- Use `isLoggedIn()` and `getReturnUrl()` instead.
336
-
337
9
  ---
338
10
 
339
- ## 7. Encrypting & Decrypting Data (Front-channel Only)
340
-
341
- TideCloak lets you protect sensitive fields with **tag-based** encryption. You pass in an array of `{ data, tags }` objects and receive an array of encrypted strings (or vice versa for decryption).
342
-
343
- ### Syntax Overview
344
-
345
- ```ts
346
- // Encrypt one or more payloads:
347
- const encryptedArray = await doEncrypt([
348
- { data: /* any JSON-serializable value */, tags: ['tag1', 'tag2'] },
349
- ]);
350
-
351
- // Decrypt one or more encrypted blobs:
352
- const decryptedArray = await doDecrypt([
353
- { encrypted: /* string from encrypt() */, tags: ['tag1', 'tag2'] },
354
- ]);
355
- ```
356
-
357
- > **Important:** The `data` property **must** be either a string or a `Uint8Array` (raw bytes).\
358
- > When you encrypt a string, decryption returns a string.\
359
- > When you encrypt a `Uint8Array`, decryption returns a `Uint8Array`.
360
-
361
- ### Valid Example
362
- >
363
- > ```ts
364
- > // Before testing below, ensure you've set up the necessary roles:
365
- > const multi_encrypted_addresses = await doEncrypt([
366
- > {
367
- > data: "10 Smith Street",
368
- > tags: ["street"]
369
- > },
370
- > {
371
- > data: "Southport",
372
- > tags: ["suburb"]
373
- > },
374
- > {
375
- > data: "20 James Street - Burleigh Heads",
376
- > tags: ["street", "suburb"]
377
- > }
378
- > ]);
379
- > ```
380
- >
381
- ### Invalid (will fail):
382
- >
383
- > ```ts
384
- > // Prepare data for encryption
385
- > const dataToEncrypt = {
386
- > title: noteData.title,
387
- > content: noteData.content
388
- > };
389
- >
390
- > // Encrypt the note data using TideCloak (this will error)
391
- > const encryptedArray = await doEncrypt([{ data: dataToEncrypt, tags: ['note'] }]);
392
- > ```
393
-
394
- * **Permissions:** Encryption requires `_tide_<tag>.selfencrypt`; decryption requires `_tide_<tag>.selfdecrypt`.
395
- * **Order guarantee:** Output preserves input order.
11
+ ## Choose Your Mode
396
12
 
13
+ | I'm building... | Use this mode |
14
+ |-----------------|---------------|
15
+ | A web app or SPA `(Standard)` | [Front-channel](docs/FRONT_CHANNEL.md) |
16
+ | A secure app where tokens should stay on my server | [Hybrid/BFF](docs/HYBRID_MODE.md) |
17
+ | An Electron, Tauri, or React Native app | [Native](docs/NATIVE_MODE.md) |
397
18
 
398
19
  ---
399
20
 
400
- ### Encryption Example
21
+ ## Quick Comparison
401
22
 
402
- ```js
403
- import { IAMService } from "@tidecloak/js";
404
-
405
- async function encryptExamples() {
406
- // Simple single-item encryption:
407
- const [encryptedDob] = await IAMService.doEncrypt([
408
- { data: '2005-03-04', tags: ['dob'] }
409
- ]);
410
-
411
- // Multi-field encryption:
412
- const encryptedFields = await IAMService.doEncrypt([
413
- { data: '10 Smith Street', tags: ['street'] },
414
- { data: 'Southport', tags: ['suburb'] },
415
- { data: '20 James Street – Burleigh Heads', tags: ['street', 'suburb'] }
416
- ]);
417
- }
418
- ```
419
-
420
- > **Permissions**: Users need roles matching **every** tag on a payload. A payload tagged `['street','suburb']` requires both the `_tide_street.selfencrypt` and `_tide_suburb.selfencrypt` roles.
421
-
422
- ---
423
-
424
- ### Decryption Example
425
-
426
- ```js
427
- import { IAMService } from "@tidecloak/js";
428
-
429
- async function decryptExamples(encryptedFields) {
430
- // Single-item decryption:
431
- const [decryptedDob] = await IAMService.doDecrypt([
432
- { encrypted: encryptedFields[0], tags: ['dob'] }
433
- ]);
434
-
435
- // Multi-field decryption:
436
- const decryptedFields = await IAMService.doDecrypt([
437
- { encrypted: encryptedFields[0], tags: ['street'] },
438
- { encrypted: encryptedFields[1], tags: ['suburb'] },
439
- { encrypted: encryptedFields[2], tags: ['street','suburb'] }
440
- ]);
441
- }
442
- ```
443
-
444
- > **Permissions**: Like encryption, decryption requires the same tag-based roles (`_tide_street.selfdecrypt`, `_tide_suburb.selfdecrypt`, etc.).
23
+ | | Front-channel | Hybrid/BFF | Native |
24
+ |---|---|---|---|
25
+ | Tokens stored in | Browser | Server | App (secure storage) |
26
+ | Best for | Simple web apps | High-security apps | Desktop/mobile apps |
27
+ | Setup complexity | Easy | Medium | Medium |
28
+ | Works offline | No | No | Yes |
445
29
 
446
30
  ---
447
31
 
448
- ## 8. Events & Lifecycle
449
-
450
- Register handlers via `.on(event, handler)` or remove with `.off(event, handler)`:
451
-
452
- ```js
453
- IAMService
454
- .on("logout", () => console.log("User logged out"))
455
- .on("tokenExpired", () => alert("Session expired, please log in again"));
456
- ```
457
-
458
- | Event | Emitted When… |
459
- | -------------------- | ----------------------------------------------------------------------- |
460
- | `ready` | Initial silent-SSO check completes (handler receives `true` or `false`) |
461
- | `initError` | Config load or init failure |
462
- | `authSuccess` | Interactive login succeeded |
463
- | `authError` | Interactive login failed |
464
- | `authRefreshSuccess` | Silent token refresh succeeded |
465
- | `authRefreshError` | Silent token refresh failed |
466
- | `logout` | User logged out |
467
- | `tokenExpired` | Token expired before refresh |
468
-
469
- ---
470
-
471
- ## 9. Core Methods (Front-channel Only)
472
-
473
- After initialization, you can call these methods anywhere (note: most are only available in front-channel mode):
474
-
475
- ```js
476
- // Check login state
477
- IAMService.isLoggedIn(); // boolean
478
-
479
- // Retrieve tokens
480
- await IAMService.getToken(); // string (access token)
481
- IAMService.getIDToken(); // string (ID token)
482
-
483
- // Inspect token metadata
484
- IAMService.getTokenExp(); // seconds until expiry
485
- IAMService.getName(); // preferred_username claim
486
-
487
- // Role checks
488
- IAMService.hasRealmRole("admin"); // boolean
489
- IAMService.hasClientRole("editor"); // boolean
490
-
491
- // Custom claims
492
- IAMService.getValueFromToken("foo"); // any
493
- IAMService.getValueFromIDToken("bar");// any
494
-
495
- // Force a token update
496
- await IAMService.updateIAMToken(); // boolean (whether refreshed)
497
- await IAMService.forceUpdateToken(); // boolean
498
-
499
- // Programmatic login / logout
500
- IAMService.doLogin(); // redirects to SSO
501
- IAMService.doLogout(); // clears cookie & redirects
502
-
503
- // Data encryption / decryption (TideCloak service)
504
- await IAMService.doEncrypt([{ data: "secret", tags: ["tag1"] }]);
505
- await IAMService.doDecrypt([{ encrypted: "...", tags: ["tag1"] }]);
506
- ```
507
-
508
- ---
509
-
510
- ## 10. Tips & Best Practices
511
-
512
- * **Single Init**: Call `initIAM` only once on page load or app bootstrap.
513
- * **Token Cookie**: `kcToken` is set automatically; ensure server-side middleware reads this cookie.
514
- * **Error Handling**: Listen to `initError` and `authError` to gracefully recover.
515
- * **Silent Refresh**: Built-in; you only need to call `updateIAMToken` if you want manual control.
516
- * **Event Cleanup**: Use `.off(...)` in SPAs before component unmount.
517
- * **Redirect URI**: If using a custom `redirectUri`, ensure the route or file exists.
518
-
519
- ---
32
+ ## Requirements
520
33
 
34
+ - A TideCloak server ([setup guide](https://github.com/tide-foundation/tidecloak-gettingstarted))
35
+ - A registered client in your TideCloak realm