Skip to main content

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 under src/, write those files to src/app/... instead, and add one ../ to the relative tidecloak.json imports, 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 verifyTideCloakToken rejects by default.
  • In the tide-console (http://localhost:8080/realms/myrealm/tide-console/), go to Apps → open myclient and set Valid redirect URIs to http://localhost:3000/* and Web origins to http://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.json and save it in your project root as tidecloak.json.

2) Install the SDK​

npm install @tidecloak/nextjs

Create public/silent-check-sso.html (required for silent SSO) if the install did not add it:

public/silent-check-sso.html
<html>
<body>
<script>parent.postMessage(location.href, location.origin)</script>
</body>
</html>

3) Wrap your app with <TideCloakProvider>​

App Router​

app/layout.tsx
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​

pages/_app.tsx
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):

app/auth/redirect/page.tsx
'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:

app/Header.tsx
'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:

app/layout.tsx
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.

proxy.ts
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:

middleware.ts
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​

pages/api/secure.ts
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​

app/api/secure/route.ts
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>.selfencrypt to encrypt; _tide_<tag>.selfdecrypt to decrypt.
  • Data types: data must be a string or Uint8Array (objects will fail).

9) Try it​

Start the dev server:

npm run dev
  1. Open http://localhost:3000. The header shows a Log In button.
  2. Without signing in, request http://localhost:3000/api/secure. It returns 401.
  3. Without signing in, request http://localhost:3000/protected. The proxy returns 403.
  4. Click Log In.
  5. Create an account (or sign in) on the Tide login page.
  6. You return to / and the header shows Logged in with the token expiry time.
  7. Open http://localhost:3000/api/secure in the same browser. It returns {"data":"Secure data response"}.
  8. Go back to http://localhost:3000 and click Log Out.
  9. The Log In button is shown again.