Keycloak with Quarkus and Next.js/Auth.js: Full-Stack Authentication

you can also read this article in Medium -> click here
Every developer has to face this at some point — implementing authentication for a full-stack project — frontend and backend.
We’ll look closely at the process by building an application and securing it with Keycloak — an open-source identity and access management solution, that handles user login, registration, and token-based security (like OAuth 2.0 or OpenID Connect. We’ll focus on something practical here — Keycloak is used by companies worldwide and provides a set of really killer features. Chances are, you’re going to face Keycloak as an authentication solution at some point in your career, so let’s build something small and play around with it, to understand how it works and what it can do for us.
We’ll use Next.js for our frontend and Quarkus for the backend. Next.js is particularly interesting when it comes to authentication, because it can mix server-side rendering (SSR) and client-side interactions, which can create some challenges when getting the user session data. Quarkus has become my go-to backend technology for building fast, scalable, and modern APIs. Together, they create a seamless flow: Keycloak authenticates users and issues tokens, Quarkus validates those tokens and secures the API, and Next.js consumes the API while delivering a smooth sign-in user experience — ensuring your application is secure. Here is a diagram of how all three components play together:

Keep in mind that this is somewhat simplified and doesn’t discuss some of the steps (like token exchange or the signing of JWT tokens in the backend) in detail.
Now let’s start with the backend and zoom in on the diagram a bit.
Backend
We’ll create a simple Quarkus backend application and use its Dev Services to spin up a Keycloak container on start.
The backend will serve a single endpoint /protected
to demonstrate the Keycloak integration.
Here is our diagram, but zoomed in on the backend part:

JWT tokens, obtained by our frontend through the user login, are signed with a private key by Keycloak.
The tokens are then sent on every request to the backend and Quarkus verifies the token’s signature using Keycloak’s public key, which it obtains beforehand
(e.g., request to JWKS endpoint, usually /auth/realms/MY-REALM/protocol/openid-connect/certs
). The key is usually cached (See steps 2.1, 2.2., 2.3 from the diagram)
to avoid repeated requests.
We’re going to use the quarkus-oidc extension where the verification of the JWT token happens under the hood automatically.
That’s actually pretty neat — we just need the right configuration in our applciation.properties
and get this out of the box.
Let’s go into some details and set up our backend.
1. Create a Quarkus Java Application
Let’s start by creating a Quarkus Java Application for our example. Go to https://code.quarkus.io/:
- Name your project
- Choose
Gradle
as Build Tool (optional, if you want to follow along.Maven
would of course also work) - Preferably — Java 21
- Add the
quarkus-rest-jackson
and thequarkus-oidc
dependencies.

Download the project and open it in your favourite IDE — I used IntelliJ.
In src/main/java
you should be able to see some automatically generated code we get from the starter, probably GreetingResource
— delete it and add a new class with the following content:
package com.tsvetkov;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/protected")
public class ProtectedResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String protectedInfo() {
return "This is highly sensitive private protected secret information";
}
}
your folder structure should look something like this:

Then put the following in your src/resources/application.properties
:
quarkus.keycloak.devservices.port=8082
quarkus.oidc.client-id=backend
quarkus.oidc.application-type=service
quarkus.http.auth.permission.authenticated.paths=/*
quarkus.http.auth.permission.authenticated.policy=authenticated
Let’s explain what these properties do:
quarkus.keycloak.devservices.port
— Because we added the quarkus-oidc
extension, the Keycloak Dev Service will be enabled and
a Keycloak docker container will be started automatically on start of the application. Through this property, we set the port on which the Keycloak container will be started, in our case — 8082
quarkus.oidc.client-id=backend
— this is the client-id for our application — will get automatically created when the container starts.
quarkus.oidc.application-type=service
— specifies how the application interacts with OpenID Connect (OIDC).
We use type service
(for backend services using client credentials), but there are other options here like webapp
for browser-based login,
or hybrid
(supporting both webapp and service authentication).
The last two properties configure all of our endpoints to be protected and need authentication.
2. Run Quarkus Backend App
Now, we can start our Quarkus application and check if everything works as expected. Just go to the terminal in the project folder and execute:
./gradlew quarkusDev
As already mentioned, this will automatically start a Keycloak container for us. We can go to http://localhost:8082 (username: admin, password: admin) and access the Keycloak UI!

The Keycloak Dev Services set up some things for us already!
A new realm quarkus
got created, along with two roles — admin
and user
and two users:
Alice who has admin
and user
roles assigned, and Bob who only has the user
role. The client-id
“backend” we set in our application.properties
also got
automatically created. That’s already a pretty nice Keycloak server setup for us to play around with.
Now let’s try to hit the /protected
endpoint of our app (which is running on http://localhost:8080). You can use Postman or any other API Platform to test this:

Exactly as expected — we get a 401 Unauthorized. What we need now is to authenticate ourselves. We need to hit this endpoint with a valid JWT Token in order for our backend to verify we’re an allowed user. We can obtain a JWT token from the Keycloak token endpoint, which looks like this:
http://<KEYCLOAK_SERVER>/realms/<REALM_NAME>/protocol/openid-connect/token
Let’s use Postman again to get a valid JWT token. We’ll use grant_type
password for this. This is not recommended
for production apps, but here we just want to test our endpoint. Normally the JWT token will be available in our frontend after login, and
that’s where we’ll send the requests to the backend in our Full-Stack Application. We’ll get to this in a minute, but first, let’s get a valid JWT token to test our /protected
endpoint

Here we send a request to the Keycloak token endpoint with our
backend client-id and user credentials (user: alice, password: alice). Now we can use the access_token
to send a request to our /protected
endpoint:

That’s how easy it is to set up Keycloak with Quarkus for development purposes. We already have our Backend App and a Keycloak Instance running!
Our backend can validate tokens issued from our Keycloak instance — great! Now let’s zoom in on the frontend part. We now need to allow our users to open the browser, navigate to our website, authenticate with valid credentials, see some content, and get the protected data from our backend.
Frontend
We’ll create a simple Next.js application and use the free open-source Auth.js authentication library.
Why use Auth.js? Well, it simplifies a lot of stuff for us. Here are a couple of very useful features we’ll be using:
-
Auth.js provides a built-in Keycloak provider. That means, we don’t have to manually implement OAuth 2.0 or OpenID Connect (OIDC) flows. It also automatically exposes a REST API with endpoints like
/api/auth/signin
that trigger the sign-in flow of the respective providers, or/api/auth/callback/:provider
(in our case:provider
is Keycloak) that can handle the returning requests from OAuth services during sign-in. -
Auth.js provides the useSession hook and convenient functions to access the session state on both the client and server sides. This is especially useful in Next.js, where developers often mix server-side rendering (SSR), static site generation (SSG), and client-side interactions.
-
Token refresh: Keycloak issues short-lived access tokens and longer-lived refresh tokens. Auth.js can automatically handle token refreshing for us, which is awesome.
-
Middleware for Route Protection: Auth.js integrates with Next.js middleware to protect certain routes. We can easily restrict access to specific pages.
It’s also worth mentioning, that the library also poses some challenges and could create some difficulties, but we’ll talk about these later.
Let’s explore how Keycloak, Next.js, and Auth.js work together in more detail:

Let’s discuss this briefly, as this diagram shows some details that weren’t mentioned in the previous drawings (e.g. Authorization Code Flow).
- User requests protected page (e.g.
/protected-page
) - Auth.js automatically checks for valid session
- Initiate the authentication flow
Auth.js redirects the user to its own authentication endpoint — /api/auth/signin
which initiates the Keycloak authentication process.
It contains the logic to construct an OpenID Connect authorization URL which contains information like Redirect URI
, Client ID
, Response type
(typically code
for authorization code flow) and Required scopes
-
Keycloak Authentication The user is then redirected to the Keycloak’s auth endpoint with the above-mentioned parameters and the URL looks like this:
https://<KEYCLOAK_SERVER>/realms/<REALM_NAME>/protocol/openid-connect/auth
-
User enters credentials and Keycloak validates them against its user database. If valid — Keycloak generates an authorization code and redirects back to the Next.js application —
/api/auth/callback/keycloak
, where the authorization code is included as a query parameter.
(I won’t get into details in this tutorial on how the authorization code flow works, but you can read more about it here.)
- Exchange the Authorization code for Tokens Auth.js receives the authorization code and makes a server-side request to Keycloak’s token endpoint. The request includes the authorization code among other things like the Redirect URI.
Keycloak validates the request and returns the Access token (JWT), the refresh token, and the ID token.
- Session Tokens Auth.js validates the tokens and extracts the user information from the ID token. It then creates the session and sets the HttpOnly cookie in the browser. After that, the user is redirected to the requested protected page.
1. Create a simple Next.js App with Auth.js
Alright, now let’s create a Next.js Application and play around with our setup.
Go to your terminal and execute:
npx create-next-app@latest
Choose a name for your project and then select “yes” on all the prompted questions.
Then open the project in your favourite editor and create the following files:
app/nav.tsx
and app/protected-page/page.tsx
your folder structure should look like this:

Let’s do things step by step and just write some minimal initial code for our application.
2. Initial code
Let’s start from our layout.tsx which should look like this in order for you to follow along:
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import Nav from "./nav";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<Nav></Nav>
<main className="container mx-auto p-4">{children}</main>
</body>
</html>
);
}
You notice that this file contains the Nav element from /app/nav.tsx
so let’s code that next:
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import React from "react";
function Nav() {
const pathname = usePathname(); // Gets the current route
return (
<nav className="bg-gray-800 p-4">
<div className="flex justify-center gap-x-60 items-center">
<div className="flex gap-x-5">
<Link href="/" className={`${pathname === "/" ? "text-yellow-500" : "text-white"} hover:text-gray-300`}>
Home
</Link>
<Link href="/protected-page" className={`${pathname === "/protected-page" ? "text-yellow-500" : "text-white"} hover:text-gray-300`}>
Protected
</Link>
</div>
<button onClick={() => {}} className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded">
Login
</button>
</div>
</nav>
);
}
export default Nav;
The app/page.tsx
component will be our public home page. We’ll keep things minimal too:
export default function Home() {
const username = "You're not logged in.";
return (
<div className="text-center">
<h1 className="text-3xl font-bold mb-4">Welcome Home</h1>
<h2 className="text-3xl text-orange-400 mb-4"> {username}</h2>
<p className="text-gray-600">This is a public page anyone can see</p>
</div>
);
}
and lastly, our /protected-page/page.tsx
which will set up a page that we want
to secure with Keycloak at http://localhost:3000/protected-page (Our navigation has a link to it too [see two code blocks above]):
import React from "react";
function ProtectedPage() {
return (
<div className="text-center">
<h1 className="text-3xl font-bold mb-4">Protected Page</h1>
<p className="text-gray-800">
Welcome <span className="text-green-800">(Some User)!</span>
<br />
<br />
This is a protected page.
</p>
</div>
);
}
export default ProtectedPage;
Now go to your terminal, execute npm run dev and you should see this:


3. Add Auth.js
Now it gets interesting — we will install Auth.js and set up its Keycloak provider. Go to your terminal and execute:
npm install next-auth@beta
Auth.js CLI also provides a neat way to generate secrets. We’ll need to create one for our setup, so let’s do this now. Execute the following, which will create an .env.local file for us and generate a secret there:
npx auth secret
Your .env.local
file should then look like this:

We’ll use a public Keycloak client for our frontend, so we won’t need a secret actually, but the Auth.js Provider needs one (even if its null
, see here)
Next, we’ll create an auth.js
file in the root of our project and set up the actual configuration of the Keycloak provider:
import NextAuth, { NextAuthResult } from "next-auth";
import KeycloakProvider from "next-auth/providers/keycloak";
const authData: NextAuthResult = NextAuth({
providers: [
KeycloakProvider({
clientId: process.env.KEYCLOAK_CLIENT_ID,
issuer: process.env.KEYCLOAK_ISSUER,
}),
],
});
export const { handlers, signIn, signOut, auth } = authData;
Also, add a Route Handler under /app/api/auth/[...nextauth]/route.ts
:
import { handlers } from "@/auth";
export const { GET, POST } = handlers;
These are used to expose the REST API endpoints (such /api/auth/signin
for redirecting users to the Keycloak login page).
You probably noticed some environmental variables here, which we do need to add to our .env.local
file.
KEYCLOAK_CLIENT_ID=frontend
KEYCLOAK_ISSUER=http://localhost:8082/realms/quarkus
The KEYCLOAK_ISSUER
is our Keycloak endpoint and the KEYCLOAK_CLIENT_ID
points to our Keycloak client — “frontend”. You probably
guessed this — we need to create this ourselves in the Keycloak UI. Let’s go to http://localhost:8082/ again and do this:




I hope this is clear enough and you have managed to create your** “frontend” **client 👍
4. Sign-in
Now it’s time to add the Sign-in functionality to our web app. Go to our nav.tsx
and let’s use Auth.js’ signIn
function to log in when we click the Login button:
<button onClick={() => signIn("keycloak")} className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded">
Login
</button>
Now go to our application, click on the Login button in our naviagtion and you will see the Keycloak Login Screen! Use the user “alice” to log in and you can even see your session in the Keycloak UI under (http://localhost:8082/admin/master/console/#/quarkus/sessions)


Now let’s use some of that login information in our application!
It would be nice to show our user's username on the home page.
Our Home
component (/app/page.tsx
)is a server-rendered component, so let’s see how to get our user information in a server-side component:
import { auth } from "@/auth";
export default async function Home() {
const session = await auth();
const username = session?.user ? session.user.name : "You're not logged in.";
return (
<div className="text-center">
<h1 className="text-3xl font-bold mb-4">Welcome Home</h1>
<h2 className="text-3xl text-orange-400 mb-4"> {username}</h2>
<p className="text-gray-600">This is a public page anyone can see</p>
</div>
);
}
We use Auth.js’ auth()
function to get information about our session, and from there, we can get our username.
If we’re not logged in, we just show the “You’re not logged in.” message from before. If you go to our application on http://localhost:3000/ you’ll be able to see our username “alice” in the Home
component!

5. Sign-out
Signing out looks similar to what we had for our login — Let’s go to our nav.tsx
component and change the Login button
to “Logout” if the user is logged in. That means that we need user information in our Nav
component, which is a client-side component, so let’s see how to get our user information on the client side:
"use client";
import { signIn, signOut, useSession } from "next-auth/react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import React from "react";
function Nav() {
const pathname = usePathname(); // Gets the current route
const session = useSession();
return (
<nav className="bg-gray-800 p-4">
<div className="flex justify-center gap-x-60 items-center">
<div className="flex gap-x-5">
<Link href="/" className={`${pathname === "/" ? "text-yellow-500" : "text-white"} hover:text-gray-300`}>
Home
</Link>
<Link href="/protected-page" className={`${pathname === "/protected-page" ? "text-yellow-500" : "text-white"} hover:text-gray-300`}>
Protected
</Link>
</div>
{!session.data ? (
<button onClick={() => signIn("keycloak")} className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded">
Login
</button>
) : (
<button onClick={() => signOut()} className="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded">
Logout
</button>
)}
</div>
</nav>
);
}
export default Nav;
We used Auth.js' useSession
hook to get the login information of our user and added a condition to show the login button if we’re logged-out and the logout button
if we’re logged in. We also added the signOut
call on click on the logout button.
We also need to wrap all our components that access the useSession
hook in the Auth.js’ <SessionProvider>
. Go to layout.tsx
and add it there:
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<SessionProvider>
<Nav></Nav>
<main className="container mx-auto p-4">{children}</main>
</SessionProvider>
</body>
</html>
Now if we log in, we see the logout button in the navigation:

That’s already pretty good! But we can still go to the /protected-page
even if we’re logged out 🤔
6. Authorize access to a page
In order to forbid the access to /protected-page
for unauthenticated users, we need to add the following middleware.ts
to the root of our project:
import { NextResponse } from "next/server";
import { auth } from "./auth";
export default auth((req) => {
if (!req.auth && req.nextUrl.pathname === "/protected-page") {
return NextResponse.redirect(new URL(`/api/login?callbackUrl=${encodeURIComponent(req.nextUrl.pathname)}`, req.nextUrl.origin));
}
});
export const config = {
matcher: ["/((?!api|_next/static|_next/image|.*\\.png$).*)"],
};
Let’s see what happens here, and why it’s a bit tricky when we use Auth.js with providers when it comes to the middleware.
We use the auth
configuration object as a wrapper for our Middleware,
following the Auth.js documentation. Inside we write our little logic that checks if a user
is unauthenticated and wants to access the /protected-page
=> In this case, we redirect to /api/login
with a callback URL as
a parameter (in this case — /protected-page
— this is where we want to land after the login).
The /api/login
is a route that we need to create in order to call the signIn
method with “Keycloak” as a provider.
The NextResponse.redirect
code from above makes a GET Request to our own route which we can configure to trigger the signIn
function from Auth.js.
I hope that was clear enough, but if you want to dig a bit deeper into why this is the most elegant way to go for now, you can read the discussion here.
So now create a route.ts
file under /api/login
with the following:
import { signIn } from "@/auth";
import { NextRequest } from "next/server";
export async function GET(req: NextRequest) {
const callback = req.nextUrl.searchParams.get("callbackUrl");
return signIn("keycloak", callback ? { redirectTo: callback } : undefined);
}
This implements the logic, explained above.
7. Display data from the backend (challenges)
Alright, now that we have protected our /protected-route
, let’s display some sensitive data in it from our backend. We’ll modify our /protected-page/page.tsx
:
import { auth } from "@/auth";
import React from "react";
const fetchBackendData = async () => {
const response = await fetch("http://localhost:8080/protected");
if (response.status === 401) {
return "Unauthorized: Please login again";
}
if (!response.ok) {
throw new Error(`Error! Status: ${response.status}`);
}
const backendData = await response.json();
return backendData;
};
async function ProtectedPage() {
const session = await auth();
const username = session?.user?.name;
const backendData = await fetchBackendData();
return (
<div className="text-center">
<h1 className="text-3xl font-bold mb-4">Protected Page</h1>
<p className="text-gray-800">
Welcome <span className="text-green-800">{username}!</span>
<br />
<br />
This is a protected page.
<br />
<br />
</p>
<div>
<div>This is the data from the backend:</div>
<div className="text-blue-500">{backendData}</div>
</div>
</div>
);
}
export default ProtectedPage;
This stays a server component and we can now easily take our session data from the auth
object.
We’ll display the username here too, like on the Home page, but we’ll also make a request to our backend and display the data from there.
🤔 🤔 🤔 Hm, the code above looks suspicious …
I hope you already noticed why this component will now always return “Unauthorized: Please login again” for the backend data. We are making an unauthorized request to our backend!
Now we come to the most challenging part — we have a couple of options here.
Options for secure communication with the backend
-
Direct API Calls with Bearer Tokens: Store tokens in the session (Tokens are exposed in client-side code)
-
Secure Cookie-Based Authentication: Use HTTP-only, secure cookies: The browser sends the cookie automatically with every request, but additional configuration is needed on the backend.
-
Backend-for-Frontend (BFF) Pattern: Using Next.js API Routes as a Secure Proxy and accessing the token only server-side. Create API routes in Next.js that proxy requests to our Quarkus backend. The access token is added to the request on the server side and this way sensitive token operations do not happen in the browser. (Can increase complexity and latency though)
At the time of writing, Auth.js recommends only the first way in its documentation. As briefly mentioned in the explanation above — this way is not perfect and we should understand that it hides some security risks (access token available client-side). I’ll discuss only this strategy in this article because our goal is to play around here, but keep in mind that this way is definitely not mature enough for real-world applications.
Anyways, let’s get into it:
Direct API Calls with Bearer Tokens
Once a user is authenticated via Auth.js with Keycloak, we can obtain the JWT token,
include it in our session, and add this token in the headers of our requests (e.g., Authorization: Bearer <access_token>
Here is a link if you’d like a quick refresher on the basics).
In order to implement this approach, we actually have to save the token in
our session object. Here is how this can happen in Auth.js — we’ll use the jwt
and session
callbacks. The jwt
one, does the following:
This callback is called whenever a JSON Web Token is created (i.e. at sign in) or updated (i.e whenever a session is accessed in the client). Anything you return here will be saved in the JWT and forwarded to the session callback. There you can control what should be returned to the client. Anything else will be kept from your frontend. The JWT is encrypted by default via your AUTH_SECRET environment variable.
Let’s modify our auth.ts
accordingly:
import NextAuth, { NextAuthResult } from "next-auth";
import KeycloakProvider from "next-auth/providers/keycloak";
const authData: NextAuthResult = NextAuth({
providers: [
KeycloakProvider({
clientId: process.env.KEYCLOAK_CLIENT_ID,
issuer: process.env.KEYCLOAK_ISSUER,
}),
],
callbacks: {
jwt({ token, account }) {
if (account) token.accessToken = account!.access_token;
return token;
},
session({ session, token }) {
if (token.accessToken) session.accessToken = token.accessToken;
return session;
},
},
});
export const { handlers, signIn, signOut, auth } = authData;
Briefly explained: We get the token from the account which contains information about the provider being used (Keycloak in our case) and also the different tokens returned by Keycloak. Then we add it to the token object, which gets passed to the session callback, and there we attach it to the session and make it available in our application.
Now let’s get back to our /protected-page/page.tsx
and modify the backend call to include the Authorization Header:
import { auth } from "@/auth";
import React from "react";
const fetchBackendData = async (accessToken: string) => {
const response = await fetch("http://localhost:8080/protected", {
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (response.status === 401) {
return "Unauthorized: Please login again";
}
if (!response.ok) {
throw new Error(`Error! Status: ${response.status}`);
}
const backendData = await response.text();
return backendData;
};
async function ProtectedPage() {
const session = await auth();
const username = session?.user?.name;
const backendData = await fetchBackendData(session.accessToken);
return (
<div className="text-center">
<h1 className="text-3xl font-bold mb-4">Protected Page</h1>
<p className="text-gray-800">
Welcome <span className="text-green-800">{username}!</span>
<br />
<br />
This is a protected page.
<br />
<br />
</p>
<div>
<div>This is the data from the backend:</div>
<div className="text-blue-500">{backendData}</div>
</div>
</div>
);
}
export default ProtectedPage;
Keep in mind that the security risks we talked about before are mitigated as this here is a server component,
but if we use the useSession()
hook in a client-side component, a malicious script could potentially extract the
token from the session object, as it is accessible in the client-side JavaScript context in the Browser.
Almost everything looks well now:

that looks very good, but:
You will notice one small thing — the logout doesn’t quite work:
If you click on Logout -> The Auth.js session is destroyed but Keycloak maintains his session, which keeps the user weirdly logged in.
To fix this, we need to call the Keycloak-Logout URL additionally, after signOut from Auth.js. To correctly call the Keycloak-Logout URL, which is:
http://<KEYCLOAK_URL>/realms/<KEYCLOAK_REALM>/protocol/openid-connect/logout
one must also add the id_token
as the id_token_hint
parameter. This means, we need to extract the id_token
from the account
in our jwt
callback as well.
Here is how this comes together at the end in our auth.ts:
import NextAuth, { NextAuthResult } from "next-auth";
import KeycloakProvider from "next-auth/providers/keycloak";
const authData: NextAuthResult = NextAuth({
providers: [
KeycloakProvider({
clientId: process.env.KEYCLOAK_CLIENT_ID,
issuer: process.env.KEYCLOAK_ISSUER,
}),
],
callbacks: {
jwt({ token, account }) {
if (account) {
token.accessToken = account!.access_token;
token.id_token = account!.id_token;
}
return token;
},
session({ session, token }) {
if (token.accessToken) session.accessToken = token.accessToken;
return session;
},
},
events: {
async signOut({ token }) {
const logOutUrl = new URL(`${process.env.KEYCLOAK_ISSUER}/protocol/openid-connect/logout`);
logOutUrl.searchParams.set("id_token_hint", token.id_token!);
const response = await fetch(logOutUrl);
if (!response.ok) {
console.error("Logout failed");
}
},
},
});
export const { handlers, signIn, signOut, auth } = authData;
We call the Keycloak-Logout URL on signOut
and add the id_token
to the token object, in order for it to be available in our signOut
event.
Now we have everything!
Final Thoughts
Implementing full-stack authentication with Keycloak, Quarkus, and Next.js (via Auth.js) is not really a breeze — it involves multiple frameworks, covers backend and frontend paradigms, and battles with Auth.js specifics. In this article, we’ve walked through a working example with real code to demonstrate how these pieces can come together and I hope the additional diagrams and explanations prove useful to developers who want to gain a deeper understanding of the topic.
That said, it's important to mention that Auth.js, while perfect for some use cases, still feels relatively immature for complex enterprise scenarios like this.
Injecting the access token into the session object to make it available to client-side hooks like useSession
might look convenient but it
does pose some risks by exposing sensitive tokens to the browser. So while it's technically possible, it's not always advisable—especially in applications, where high security is a priority.
I'd love to hear about other developers' experiences with this stack as well!
Authentication and authorization are complex topics, although many people seem to underestimate them and approach them with little understanding because there are many great frameworks nowadays that abstract the great complexity that's hidden behind the scenes here. However, with the right approach — and a deep understanding of the limitations and trade-offs — this can become a great strength and a valuable skill in a developer's toolset
Thanks for reading!