Tutorial: Integrate TideCloak into an Existing Next.js App
Add TideCloak auth to an existing Next.js app - provider setup, redirect, route protection, API verification, and E2EE.
1) Prepare TideCloak
- Have an existing Next.js app. The examples use the App Router, TypeScript, and
http://localhost:3000. - The file paths below assume a root-level
app/directory. If your app keeps its code undersrc/, write those files tosrc/app/...instead, and add one../to the relativetidecloak.jsonimports, since the adapter config stays in the project root. - Ensure your realm is set up (you can use the dev container in the other tutorial). Server-side token verification needs a Tide realm with IGA (QEA) enabled; without it the realm signs tokens with RS256, which
verifyTideCloakTokenrejects by default. - In the tide-console (
http://localhost:8080/realms/myrealm/tide-console/), go to Apps → open myclient and set Valid redirect URIs tohttp://localhost:3000/*and Web origins tohttp://localhost:3000. With IGA enabled these edits are filed as change requests; approve them under QEA Approvals before continuing. - In the same app, go to Credentials → Download
tidecloak.jsonand save it in your project root astidecloak.json.
2) Install the SDK
- NPM
- Yarn
npm install @tidecloak/nextjs
yarn add @tidecloak/nextjs
Create public/silent-check-sso.html (required for silent SSO) if the install did not add it:
<html><body><script>parent.postMessage(location.href, location.origin)</script></body></html>
3) Wrap your app with <TideCloakProvider>
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;
4) Redirect URI
If not specified, the SDK assumes:
`${window.location.origin}/auth/redirect`
Create a page at that route (App Router example):
'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.replace('/');}}, [authenticated, isInitializing, router]);return <p style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>Waiting for authentication...</p>;}
Or pass an explicit redirectUri in the provider config.
5) Use the hook & guard components
Create a client component that reads the auth state:
'use client';import { useTideCloak, Authenticated, Unauthenticated } from '@tidecloak/nextjs';export default function Header() {const { login, logout, tokenExp } = useTideCloak();return (<header><Authenticated><span>Logged in{tokenExp !== null && ` (exp ${new Date(tokenExp * 1000).toLocaleTimeString()})`}</span><button onClick={logout}>Log Out</button></Authenticated><Unauthenticated><button onClick={login}>Log In</button></Unauthenticated></header>);}
Render it inside the provider in your layout:
import React from 'react';import { TideCloakProvider } from '@tidecloak/nextjs';import adapter from '../tidecloak.json';import Header from './Header';export default function RootLayout({ children }: { children: React.ReactNode }) {return (<html lang="en"><body><TideCloakProvider config={{ ...adapter }}><Header />{children}</TideCloakProvider></body></html>);}
6) Protect routes with a proxy
Next.js 16 runs route protection from proxy.ts in your project root. The proxy reads the kcToken cookie the SDK sets after login, verifies it, and checks roles. Requests that fail get a 403 JSON response unless you pass onFailure.
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: {// offline_access is granted to every signed-in user. Use your own role for real access control.'/protected/*': ['offline_access'],},});export const config = {matcher: ['/protected', '/protected/:path*'],};
On Next.js 13.5 to 15, use middleware.ts with createTideCloakMiddleware from the same @tidecloak/nextjs/server entry point instead:
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: {'/protected/*': ['offline_access'],},});export const config = {matcher: ['/protected', '/protected/:path*'],};
7) 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, ['offline_access']);if (!payload) return res.status(401).json({ error: 'Unauthorized' });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, ['offline_access']);if (!payload) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });return NextResponse.json({ data: 'Secure data response' });}
8) Optional - E2EE with tag-based encryption
doEncrypt and doDecrypt come from useTideCloak():
// Encrypt:const encryptedArray = await doEncrypt([{ data: "10 Smith Street", tags: ["street"] },]);// Decrypt:const decryptedArray = await doDecrypt([{ encrypted: encryptedArray[0], tags: ["street"] },]);
- Permissions:
_tide_<tag>.selfencryptto encrypt;_tide_<tag>.selfdecryptto decrypt. - Data types:
datamust be a string or Uint8Array (objects will fail).
9) Try it
Start the dev server:
npm run dev
- Open
http://localhost:3000. The header shows a Log In button. - Without signing in, request
http://localhost:3000/api/secure. It returns401. - Without signing in, request
http://localhost:3000/protected. The proxy returns403. - Click Log In.
- Create an account (or sign in) on the Tide login page.
- You return to
/and the header shows Logged in with the token expiry time. - Open
http://localhost:3000/api/securein the same browser. It returns{"data":"Secure data response"}. - Go back to
http://localhost:3000and click Log Out. - The Log In button is shown again.