Skip to main content

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 vanilla
cd my-app
npm install

Add the SDK from inside my-app:

npm install @tidecloak/js

This package exports:

  • IAMService: high-level wrapper and lifecycle manager
  • TideCloak: lower-level Keycloak-style adapter instance

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:

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​

public/auth/redirect.html
<!DOCTYPE html>
<html>
<head><title>Redirecting...</title></head>
<body>
<p>Redirecting, please wait...</p>
<script>
// Let your app handle state after reload
window.location.href = "/";
</script>
</body>
</html>

Default redirectUri is ${origin}/auth/redirect. Ensure this path exists or override redirectUri in code. The Vite dev server answers /auth/redirect with index.html, so main.js completes the login there; this file covers static hosts that serve redirect.html for 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:

src/main.js
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:

index.html
<!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 like public/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 data property must be either a string or a Uint8Array (raw bytes).
When you encrypt a string, decryption returns a string.
When you encrypt a Uint8Array, decryption returns a Uint8Array.

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 encryption
const 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.selfencrypt and _tide_suburb.selfencrypt roles.


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 state
IAMService.isLoggedIn(); // boolean
// Retrieve tokens
await IAMService.getToken(); // string (access token)
IAMService.getIDToken(); // string (ID token)
// Inspect token metadata
IAMService.getTokenExp(); // seconds until expiry
IAMService.getName(); // preferred_username claim
// Role checks
IAMService.hasRealmRole("admin"); // boolean
IAMService.hasClientRole("editor"); // boolean
// Custom claims
IAMService.getValueFromToken("foo"); // any
IAMService.getValueFromIDToken("bar");// any
// Force a token update
await IAMService.updateIAMToken(); // boolean (whether refreshed)
await IAMService.forceUpdateToken(); // boolean
// Programmatic login / logout
IAMService.doLogin(); // redirects to SSO
IAMService.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 initIAM only 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: kcToken is set automatically; ensure server-side middleware reads this cookie.
  • Error Handling: Listen to initError and authError to gracefully recover.
  • Silent Refresh: Built-in; you only need to call updateIAMToken if 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()