How-to Guide: Common Tasks with TideCloak in Next.js
Common tasks you'll use most with TideCloak in a Next.js app - quick, copy-paste snippets.
How to: Install or Scaffold a Project
Scaffold a ready-to-go template:
sudo apt update && sudo apt install -y curl jqnpm init @tidecloak/nextjs@latest my-app
Part way through, the initializer prints an enrolment link and waits. Open it and complete Tide enrolment for the first admin in the enclave; the initializer polls for that account and finishes on its own. It can take a few minutes, and the app has no working realm until it does. See the quick start for the full sequence.
Install the SDK in an existing app:
- NPM
- Yarn
npm install @tidecloak/nextjs
yarn add @tidecloak/nextjs
Create public/silent-check-sso.html (required for silent SSO):
<html><body><script>parent.postMessage(location.href, location.origin)</script></body></html>
How to: Initialize the Provider (App & Pages Router)
App Router
import React from 'react';import { TideCloakProvider } from '@tidecloak/nextjs';import adapter from '../tidecloak.json';export default function RootLayout({ children }: { children: React.ReactNode }) {return (<html lang="en"><body><TideCloakProvider config={{ ...adapter }}>{children}</TideCloakProvider></body></html>);}
Pages Router
import React from 'react';import type { AppProps } from 'next/app';import { TideCloakProvider } from '@tidecloak/nextjs';import adapter from '../tidecloak.json';function MyApp({ Component, pageProps }: AppProps) {return (<TideCloakProvider config={adapter}><Component {...pageProps} /></TideCloakProvider>);}export default MyApp;
How to: Customize Redirect URI
Specify a custom redirect URI after login/logout:
<TideCloakProvider config={{ ...adapter, redirectUri: 'https://yourdomain.com/auth/callback' }}>{children}</TideCloakProvider>
Default (if omitted):
`${window.location.origin}/auth/redirect`
Example redirect page
'use client';import { useEffect } from 'react';import { useRouter } from 'next/navigation';import { useTideCloak } from '@tidecloak/nextjs';export default function RedirectPage() {const { authenticated, isInitializing, logout } = useTideCloak();const router = useRouter();useEffect(() => {const params = new URLSearchParams(window.location.search);if (params.get("auth") === "failed") {sessionStorage.setItem("tokenExpired", "true");logout();}}, []);useEffect(() => {if (!isInitializing) {router.push(authenticated ? '/home' : '/');}}, [authenticated, isInitializing, router]);return (<div style={{ minHeight:'100vh', display:'flex', alignItems:'center', justifyContent:'center' }}><p>Waiting for authentication...</p></div>);}
Ensure the path exists in your app to avoid redirect issues.
How to: Get User Info from Access/ID Tokens
Retrieve user information stored in tokens:
const { getValueFromToken, getValueFromIdToken } = useTideCloak();const userEmail = getValueFromToken('email');const preferredUsername = getValueFromIdToken('preferred_username');
How to: Check User Roles
Check realm and client roles:
const { hasRealmRole, hasClientRole } = useTideCloak();const isAdmin = hasRealmRole('admin');const isEditor = hasClientRole('editor'); // defaults to your app clientconst isBillingInOtherClient = hasClientRole('billing:writer', 'backoffice-client');
How to: Manually Refresh Token
Force a silent token refresh:
const { refreshToken } = useTideCloak();const refreshed = await refreshToken();console.log(refreshed ? 'Token refreshed!' : 'Refresh failed.');
Tokens also refresh automatically when they expire.
How to: Encrypt Data Before Sending
Encrypt sensitive data using tag-based encryption:
const { doEncrypt } = useTideCloak();const encrypted = await doEncrypt([{ data: "Sensitive Information", tags: ["private"] }]);
Note:
datamust be a string or Uint8Array. Permissions: require_tide_<tag>.selfencrypton the access token.
How to: Decrypt Received Data
Decrypt encrypted payloads:
const { doDecrypt } = useTideCloak();const decrypted = await doDecrypt([{ encrypted: receivedEncryptedData, tags: ["private"] }]);
Permissions: require
_tide_<tag>.selfdecrypton the access token. Order guarantee: returned array preserves the input order.
How to: Gate UI with Guard Components
Render UI conditionally based on auth state:
'use client';import { Authenticated, Unauthenticated } from '@tidecloak/nextjs';export default function Dashboard() {return (<><Authenticated><h1>Dashboard</h1>{/* Protected widgets */}</Authenticated><Unauthenticated><p>Please log in to access the dashboard.</p></Unauthenticated></>);}
How to: Protect Routes with a Proxy or Middleware
On Next.js 16, protect pages and APIs from proxy.ts in your project root:
import { NextResponse } from 'next/server';import { createTideCloakProxy } from '@tidecloak/nextjs/server';import type { TidecloakConfig } from '@tidecloak/nextjs/server';import tidecloakConfig from './tidecloak.json';export const proxy = createTideCloakProxy({config: tidecloakConfig as TidecloakConfig,protectedRoutes: {'/admin/*': ['admin'],'/api/private/*': ['user'],},onFailure: (_ctx, req) => NextResponse.redirect(new URL('/', req.url)),});export const config = {matcher: ['/admin/:path*', '/api/private/:path*'],};
Role names are examples; use roles that exist in your realm. Without onFailure, failed checks get a 403 JSON response.
On Next.js 13.5 to 15, use middleware.ts with the same options:
import { createTideCloakMiddleware } from '@tidecloak/nextjs/server';import type { TidecloakConfig } from '@tidecloak/nextjs/server';import tidecloakConfig from './tidecloak.json';export default createTideCloakMiddleware({config: tidecloakConfig as TidecloakConfig,protectedRoutes: {'/admin/*': ['admin'],'/api/private/*': ['user'],},});export const config = {matcher: ['/admin/:path*', '/api/private/:path*'],};
How to: 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 actually does.
Apps scaffolded with npm init @tidecloak/nextjs already have this. The template ships public/tide_dpop_auth.html and the route below, so there is nothing to do.
For an existing app, 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
Then add a catch-all route that serves it with the two headers Tide needs:
import fs from "fs";import path from "path";const htmlPath = path.join(process.cwd(), "public", "tide_dpop_auth.html");const CSP ="default-src 'self'; " +"script-src 'self' 'sha256-utc6UrebuHOyLd/2aiMXS/p1EDy9UZBDe/XEMKDw9Mc='; " +"style-src 'self' 'sha256-F7OJTdJYct4J+cQfuJUoDauitndqt8pAc8EbA8gwDPU='";export async function GET(): Promise<Response> {return new Response(fs.readFileSync(htmlPath, "utf-8"), {status: 200,headers: {"Content-Type": "text/html","Content-Security-Policy": CSP,"Allow-CSP-From": "*",},});}
The sha256 values pin the page's inline script and style, so they change if the page does. Do not add X-Frame-Options or a frame-ancestors directive: both block the embed the flow depends on.
Do not try to serve this from
proxy.tsormiddleware.ts. The usual matchers do not reach it:['/protected', '/protected/:path*']does not cover the path, and the catch-all'/((?!_next|.*\\..*).*)'excludes anything containing a dot, which this path does. A route handler has no such problem.
Checking it works
Request the page directly:
curl -I "http://localhost:3000/tide_dpop/iss/$(printf 'http://localhost:8080/realms/YOUR_REALM' | xxd -p -c 256)/aud/$(printf 'YOUR_CLIENT' | xxd -p -c 256)/tide_dpop_auth.html"
A working setup returns 200 with both Content-Security-Policy and Allow-CSP-From: *. A 404 means the route is not mounted. A 403 means it is mounted but your realm or client does not match the config it was built with.
How to: Verify Tokens in API Routes
Pages Router
import type { NextApiRequest, NextApiResponse } from 'next';import { verifyTideCloakToken } from '@tidecloak/nextjs/server';import config from '../../tidecloak.json';export default async function handler(req: NextApiRequest, res: NextApiResponse) {const token = req.cookies.kcToken || req.headers.authorization?.split(' ')[1] || '';const payload = await verifyTideCloakToken(config, token, ['user']);if (!payload) return res.status(401).json({ error: 'Unauthorized' });return res.status(200).json({ data: 'Secure data response' });}
App Router
import { NextRequest, NextResponse } from 'next/server';import { verifyTideCloakToken } from '@tidecloak/nextjs/server';import config from '../../../tidecloak.json';export async function GET(req: NextRequest) {const token = req.cookies.get('kcToken')?.value || '';const payload = await verifyTideCloakToken(config, token, ['user']);if (!payload) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });return NextResponse.json({ data: 'Secure data response' });}
How to: Handle Authentication Errors
Handle initialization errors from the provider:
'use client';import { useEffect } from 'react';import { useTideCloak } from '@tidecloak/nextjs';export default function ErrorBoundaryLite() {const { initError } = useTideCloak();useEffect(() => {if (initError) {console.error('Authentication error:', initError);alert('Authentication failed. Please log in again.');}}, [initError]);return null;}
How to: Run TideCloak locally (dev)
If you don't already have a TideCloak instance, run the dev container:
sudo docker run -d -v .:/opt/keycloak/data/h2 --name tidecloak -p 8080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=password tideorg/tidecloak-dev:latest
The dev image does not include an app realm. Create one with the template initializer (npm init @tidecloak/nextjs@latest my-app), which asks for your server URL and admin credentials, then pauses for you to complete Tide enrolment for the first admin at the link it prints.
tide-console: http://localhost:8080/realms/nextjs-test/tide-console/
What the initializer sets up with its default answers
- Realm
nextjs-test, set up as a Tide realm, with registration off. Link Tide Accountrequired action enabled.- Client myclient with the following settings:
- Redirect URIs:
http://localhost:3000,http://localhost:3000/*,http://localhost:3000/silent-check-sso.htmlandhttp://localhost:3000/auth/redirect - Web origins:
http://localhost:3000.
- Redirect URIs:
- Realm roles
appUser,_tide_message.selfencryptand_tide_message.selfdecryptadded to the default roles. - License activation, a first Tide realm admin, and IGA (QEA) enabled.
Activate a license (dev/free)
If you didn't use the initializer, in the tide-console go to Advanced → Tide → Licensing → Request License and complete the flow. The host becomes licensed/activated (free dev tier available).
Play!
- Visit
http://localhost:3000 - Log in
- Hit the protected API route and try the E2EE example page
- Logout to test Single‑Sign‑Out