5-Minute Tutorial: TideCloak + Vanilla JS (Vite)
This tutorial scaffolds a minimal Vanilla JS app and wires in TideCloak auth + silent SSO.
1) Prerequisites
- Node.js 20.19+ or 22.12+ (required by current Vite)
- A running TideCloak server (free dev tier OK) with a realm and a client for this app
In the tide-console, open Apps → your app and set:
- Valid redirect URIs:
http://localhost:5173/* - Web origins:
http://localhost:5173
If IGA (QEA) is enabled on your realm, these edits are filed as change requests. Approve them under QEA Approvals; they take effect only once committed.
2) Scaffold a project
npm create vite@latest my-app -- --template vanillacd my-appnpm install
Add the SDK from inside my-app:
- NPM
- Yarn
npm install @tidecloak/js
yarn add @tidecloak/js
This package exports:
IAMService: high-level wrapper and lifecycle managerTideCloak: lower-level Keycloak-style adapter instance
Note: Installing this package automatically adds a
silent-check-sso.htmlfile to yourpublicdirectory. This file is required for silent SSO checks; if it doesn't exist, create it manually atpublic/silent-check-sso.htmlwith the following content, otherwise the app will break:public/silent-check-sso.html<html><body><script>parent.postMessage(location.href, location.origin)</script></body></html>
3) Add tidecloak.json
Export your adapter from the TideCloak tide-console → Apps → your app → Credentials → Tide adapter → Download tidecloak.json. Save it in the project root, next to package.json.
4) Create /auth/redirect.html
<!DOCTYPE html><html><head><title>Redirecting...</title></head><body><p>Redirecting, please wait...</p><script>// Let your app handle state after reloadwindow.location.href = "/";</script></body></html>
Default
redirectUriis${origin}/auth/redirect. Ensure this path exists or overrideredirectUriin code. The Vite dev server answers/auth/redirectwithindex.html, somain.jscompletes the login there; this file covers static hosts that serveredirect.htmlfor that path.
5) Wire up src/main.js
Replace the contents of src/main.js to initialize IAM and register lifecycle listeners. Every listener receives the event name first, followed by the event's arguments:
import { IAMService } from "@tidecloak/js";import config from "../tidecloak.json";const loginBtn = document.getElementById("login-btn");const logoutBtn = document.getElementById("logout-btn");const statusEl = document.getElementById("status");loginBtn.onclick = () => IAMService.doLogin();logoutBtn.onclick = () => IAMService.doLogout();function updateUI(authenticated) {loginBtn.style.display = authenticated ? "none" : "inline-block";logoutBtn.style.display = authenticated ? "inline-block" : "none";statusEl.textContent = authenticated ? "Authenticated" : "Please log in";}IAMService.on("ready", (_event, authenticated) => updateUI(authenticated)).on("authError", (_event, err) => {statusEl.textContent = `Auth error: ${err.message}`;}).on("logout", () => {console.log("User logged out");updateUI(false);}).on("tokenExpired", () => {alert("Session expired, please log in again");updateUI(false);});(async () => {try {await IAMService.initIAM(config); // You can add redirectUri here if customizing} catch (err) {console.error("Failed to initialize IAM:", err);statusEl.textContent = "Initialization error";}})();
Replace index.html so the page has the buttons and status element that main.js uses:
<!doctype html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>my-app</title></head><body><button id="login-btn">Log In</button><button id="logout-btn" style="display:none">Log Out</button><div id="status">Initializing...</div><script type="module" src="/src/main.js"></script></body></html>
6) Run it
Your project should now contain these files (Vite's other template files can stay):
my-app/├── index.html├── package.json├── tidecloak.json├── public/│ ├── silent-check-sso.html│ └── auth/│ └── redirect.html└── src/└── main.js
Start the dev server:
npm run dev
Open the URL Vite prints (e.g., http://localhost:5173). You should see a Log In button.
Click Log In, create an account (or sign in) on the Tide login page, and return to the app.
The status now reads Authenticated and the Log Out button is shown.
Click Log Out. The app returns to its signed-out state: the status goes back to Please log in and the Log In button is shown again.
Redirect URI Handling
TideCloak will redirect users after login/logout to a URI defined in your adapter config.
If not explicitly set, the default value is:
`${window.location.origin}/auth/redirect`
This means your app must contain a static file or route at
/auth/redirect. In Vite, this typically means adding a file likepublic/auth/redirect.html(see step 4).
You can override this behavior by passing a redirectUri to initIAM():
await IAMService.initIAM({...config,redirectUri: "https://yourdomain.com/auth/callback"});
⚠️ Regardless of the value used, the actual route or file must exist in your deployed project, and the URI must be listed in the client's Valid redirect URIs. If the redirect target doesn't exist, users will land on a 404 page after login/logout.
Encrypting & Decrypting Data
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).
Syntax Overview
// Encrypt one or more payloads:const encryptedArray = await IAMService.doEncrypt([{ data: /* string or Uint8Array */, tags: ['tag1', 'tag2'] },]);// Decrypt one or more encrypted blobs:const decryptedArray = await IAMService.doDecrypt([{ encrypted: /* string from doEncrypt() */, tags: ['tag1', 'tag2'] },]);
Important: The
dataproperty must be either a string or aUint8Array(raw bytes).
When you encrypt a string, decryption returns a string.
When you encrypt aUint8Array, decryption returns aUint8Array.
Valid Example
// Before testing below, ensure you've set up the necessary roles:const multi_encrypted_addresses = await IAMService.doEncrypt([{data: "10 Smith Street",tags: ["street"]},{data: "Southport",tags: ["suburb"]},{data: "20 James Street - Burleigh Heads",tags: ["street", "suburb"]}]);
Invalid (will fail):
// Prepare data for encryptionconst dataToEncrypt = {title: noteData.title,content: noteData.content};// Encrypt the note data using TideCloak (this will error)const encryptedArray = await IAMService.doEncrypt([{ data: dataToEncrypt, tags: ['note'] }]);
- Permissions: Encryption requires
_tide_<tag>.selfencrypt; decryption requires_tide_<tag>.selfdecrypt. - Order guarantee: Output preserves input order.
- Realm: Encryption needs a Tide realm with IGA (QEA) enabled.
- Modes: Encryption and decryption run in the browser (the default front-channel mode). They are not available in hybrid mode, and native mode does not support a decryption policy.
Encryption Example
import { IAMService } from "@tidecloak/js";async function encryptExamples() {// Simple single-item encryption:const [encryptedDob] = await IAMService.doEncrypt([{ data: '2005-03-04', tags: ['dob'] }]);// Multi-field encryption:const encryptedFields = await IAMService.doEncrypt([{ data: '10 Smith Street', tags: ['street'] },{ data: 'Southport', tags: ['suburb'] },{ data: '20 James Street - Burleigh Heads', tags: ['street', 'suburb'] }]);}
Permissions: Users need roles matching every tag on a payload. A payload tagged
['street','suburb']requires both the_tide_street.selfencryptand_tide_suburb.selfencryptroles.
Decryption Example
import { IAMService } from "@tidecloak/js";async function decryptExamples(encryptedFields) {// Single-item decryption:const [decryptedDob] = await IAMService.doDecrypt([{ encrypted: encryptedFields[0], tags: ['dob'] }]);// Multi-field decryption:const decryptedFields = await IAMService.doDecrypt([{ encrypted: encryptedFields[0], tags: ['street'] },{ encrypted: encryptedFields[1], tags: ['suburb'] },{ encrypted: encryptedFields[2], tags: ['street','suburb'] }]);}
Permissions: Like encryption, decryption requires the same tag-based roles (
_tide_street.selfdecrypt,_tide_suburb.selfdecrypt, etc.).
Core Methods
After initialization, you can call these methods anywhere:
// Check login stateIAMService.isLoggedIn(); // boolean// Retrieve tokensawait IAMService.getToken(); // string (access token)IAMService.getIDToken(); // string (ID token)// Inspect token metadataIAMService.getTokenExp(); // seconds until expiryIAMService.getName(); // preferred_username claim// Role checksIAMService.hasRealmRole("admin"); // booleanIAMService.hasClientRole("editor"); // boolean// Custom claimsIAMService.getValueFromToken("foo"); // anyIAMService.getValueFromIDToken("bar");// any// Force a token updateawait IAMService.updateIAMToken(); // boolean (whether refreshed)await IAMService.forceUpdateToken(); // boolean// Programmatic login / logoutIAMService.doLogin(); // redirects to SSOIAMService.doLogout(); // clears cookie & redirects// Data encryption / decryption (TideCloak service)await IAMService.doEncrypt([{ data: "secret", tags: ["tag1"] }]);await IAMService.doDecrypt([{ encrypted: "...", tags: ["tag1"] }]);
Tips & Best Practices
- Single Init: Call
initIAMonly once on page load or app bootstrap. - Event handlers: Handlers are called as
(eventName, ...args), so read the event's value from the second argument. - Token Cookie:
kcTokenis set automatically; ensure server-side middleware reads this cookie. - Error Handling: Listen to
initErrorandauthErrorto gracefully recover. - Silent Refresh: Built-in; you only need to call
updateIAMTokenif you want manual control. - Event Cleanup: Use
.off(...)in SPAs before component unmount. - Redirect URI: If using a custom
redirectUri, ensure the route or file exists.
Next steps
- Protect API calls with your backend's JWT verification
- Add role checks and E2EE via
IAMService.doEncrypt()/doDecrypt()