How‑to Guide: Common Tasks with TideCloak in React
Copy‑paste snippets for the tasks you'll use most in a React SPA.
Install or scaffold
New React + Vite app (TypeScript):
npm create vite@latest my-app -- --template react-tscd my-appnpm install @tidecloak/react
Required file for silent SSO ``
<html><body><script>parent.postMessage(location.href, location.origin)</script></body></html>
Serve the DPoP verification page
Only needed if you turn on DPoP. Skip this if you do not pass useDPoP.
With DPoP enabled, TideCloak asks your app to prove the browser holds the DPoP key before it issues a bound token. It does that by loading a small page from your origin, because the key lives in your app's IndexedDB and nothing on another origin can read it. See DPoP and sender-constrained tokens for what the page does.
First fetch the page into public/:
curl -o public/tide_dpop_auth.html \https://raw.githubusercontent.com/tide-foundation/tidecloak-js/main/packages/tidecloak-js/tide_dpop_auth.html
Vite's static server cannot add response headers, so serve it from a small middleware instead:
import { defineConfig } from 'vite';import react from '@vitejs/plugin-react';import { readFileSync } from 'node:fs';const CSP ="default-src 'self'; " +"script-src 'self' 'sha256-utc6UrebuHOyLd/2aiMXS/p1EDy9UZBDe/XEMKDw9Mc='; " +"style-src 'self' 'sha256-F7OJTdJYct4J+cQfuJUoDauitndqt8pAc8EbA8gwDPU='";const tideDpop = () => {const serve = (server) =>server.middlewares.use((req, res, next) => {if (!/^\/tide_dpop\/iss\/[0-9a-f]+\/aud\/[0-9a-f]+\/tide_dpop_auth\.html/i.test(req.url ?? '')) {return next();}res.setHeader('Content-Type', 'text/html');res.setHeader('Content-Security-Policy', CSP);res.setHeader('Allow-CSP-From', '*');res.end(readFileSync('public/tide_dpop_auth.html', 'utf-8'));});return { name: 'tide-dpop', configureServer: serve, configurePreviewServer: serve };};export default defineConfig({plugins: [react(), tideDpop()],});
That covers vite dev and vite preview. The sha256 values pin the page's inline script and style, so they change if the page does.
In production
This is the one part that cannot be automated. A static host serves files, and this page needs two response headers, so a built SPA needs them configured on the host or a small server in front.
Serve tide_dpop_auth.html for any path matching:
/tide_dpop/iss/<hex>/aud/<hex>/tide_dpop_auth.html
with these two headers:
Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-utc6UrebuHOyLd/2aiMXS/p1EDy9UZBDe/XEMKDw9Mc='; style-src 'self' 'sha256-F7OJTdJYct4J+cQfuJUoDauitndqt8pAc8EbA8gwDPU='Allow-CSP-From: *
The hashes pin the page's inline script and style, so they change if the page does. If you ever update tide_dpop_auth.html, recompute them from the file rather than copying them from here:
import { readFileSync } from 'node:fs';import { createHash } from 'node:crypto';const html = readFileSync('public/tide_dpop_auth.html', 'utf8');for (const tag of ['script', 'style']) {const re = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, 'g');for (const [, body] of html.matchAll(re)) {console.log(tag, `'sha256-${createHash('sha256').update(body, 'utf8').digest('base64')}'`);}}
node dpop-hashes.mjs
Do not add X-Frame-Options or a frame-ancestors directive to this response. Both block the embed the flow depends on.
Your handler should also check the issuer and client in the path against your own config and answer 403 if they do not match, so the page cannot be requested for a realm you do not serve.
Checking it works
Request the page and confirm it returns 200 with both headers. A 404 means nothing is serving the path, which is the usual cause of a slow login.
Initialize the Provider (React Router)
import React from 'react';import ReactDOM from 'react-dom/client';import { BrowserRouter, Routes, Route } from 'react-router-dom';import { TideCloakContextProvider } from '@tidecloak/react';import adapter from '../tidecloak.json';import Home from './pages/Home';import RedirectPage from './pages/auth/RedirectPage';import Dashboard from './pages/Dashboard';ReactDOM.createRoot(document.getElementById('root')!).render(<React.StrictMode><TideCloakContextProvider config={adapter}><BrowserRouter><Routes><Route path="/" element={<Home />} /><Route path="/auth/redirect" element={<RedirectPage />} /><Route path="/dashboard" element={<Dashboard />} /></Routes></BrowserRouter></TideCloakContextProvider></React.StrictMode>);
Customize redirect URI
Default (if omitted):
${window.location.origin}/auth/redirect
Override it:
<TideCloakContextProvider config={{ ...adapter, redirectUri: 'https://yourapp.com/auth/callback' }}>{/* app */}</TideCloakContextProvider>
Ensure the target route exists in your router.
Example redirect handler
import { useEffect } from 'react';import { useNavigate } from 'react-router-dom';import { useTideCloak } from '@tidecloak/react';export default function RedirectPage() {const { authenticated, isInitializing, logout } = useTideCloak();const navigate = useNavigate();useEffect(() => {const params = new URLSearchParams(window.location.search);if (params.get('auth') === 'failed') {sessionStorage.setItem('tokenExpired', 'true');logout();}}, []);useEffect(() => {if (!isInitializing) navigate(authenticated ? '/dashboard' : '/');}, [authenticated, isInitializing, navigate]);return (<div style={{ minHeight:'100vh', display:'flex', alignItems:'center', justifyContent:'center' }}><p>Waiting for authentication...</p></div>);}
Read token claims
import { useTideCloak } from '@tidecloak/react';const { getValueFromToken, getValueFromIdToken } = useTideCloak();const email = getValueFromToken('email');const username = getValueFromIdToken('preferred_username');
Check roles
import { useTideCloak } from '@tidecloak/react';const { hasRealmRole, hasClientRole } = useTideCloak();const isAdmin = hasRealmRole('admin');const isEditor = hasClientRole('editor'); // defaults to your app clientconst isBillingOnBackoffice = hasClientRole('billing:writer', 'backoffice-client');
Manually refresh token
import { useTideCloak } from '@tidecloak/react';const { refreshToken } = useTideCloak();const ok = await refreshToken();console.log(ok ? 'Token refreshed!' : 'Refresh failed.');
(Tokens also refresh automatically as they expire.)
Encrypt data before sending
import { useTideCloak } from '@tidecloak/react';const { doEncrypt } = useTideCloak();const encrypted = await doEncrypt([{ data: 'Sensitive Information', tags: ['private'] },]);
Data type:
datamust be string or Uint8Array. Permissions: require_tide_<tag>.selfencrypton the access token.
Decrypt received data
import { useTideCloak } from '@tidecloak/react';const { doDecrypt } = useTideCloak();const decrypted = await doDecrypt([{ encrypted: receivedEncryptedData, tags: ['private'] },]);
Permissions: require
_tide_<tag>.selfdecrypt. Order guarantee: output preserves input order.
Route guards and gated UI
Simple gated UI:
import { Authenticated, Unauthenticated } from '@tidecloak/react';export function Header() {return (<><Authenticated><button>Sign out</button></Authenticated><Unauthenticated><button>Sign in</button></Unauthenticated></>);}
Lightweight route guard:
import { useTideCloak } from '@tidecloak/react';import { Navigate } from 'react-router-dom';export function RequireAuth({ children }: { children: JSX.Element }) {const { authenticated, isInitializing } = useTideCloak();if (isInitializing) return null;return authenticated ? children : <Navigate to="/" replace />;}
Use it:
<Route path="/dashboard" element={<RequireAuth><Dashboard/></RequireAuth>} />
Verify tokens on the backend (Node Express)
Install @tidecloak/verify and express in your API project. Tokens must come from a Tide realm with IGA (QEA) enabled.
import express from 'express';import type { Request, Response, NextFunction } from 'express';import { verifyTideCloakToken } from '@tidecloak/verify';import adapter from '../tidecloak.json' with { type: 'json' };const app = express();function auth(requiredRoles: string[] = []) {return async (req: Request, res: Response, next: NextFunction) => {const authz = req.headers.authorization || '';const token = authz.startsWith('Bearer ') ? authz.slice(7) : '';const payload = await verifyTideCloakToken(adapter, token, requiredRoles);if (!payload) return res.status(401).json({ error: 'Unauthorized' });(req as any).user = payload;next();};}app.get('/api/secure', auth(['user']), (req, res) => {res.json({ data: 'Secure data response' });});app.listen(3001, () => console.log('API on :3001'));
Handle auth errors
import { useEffect } from 'react';import { useTideCloak } from '@tidecloak/react';export function AuthErrorToast() {const { initError } = useTideCloak();useEffect(() => {if (initError) {console.error('Authentication error:', initError);alert('Authentication failed. Please log in again.');}}, [initError]);return null;}
Quick checklist
-
public/silent-check-sso.htmlexists -
<TideCloakContextProvider config={adapter}>wraps your app - Redirect route exists (default
/auth/redirector your customredirectUri) - Roles are created and assigned in TideCloak
- Backend verifies tokens before serving secure data