Enterprise-Level Authentication in a Containerized Environment for Next.js 13
TL;DR
https://github.com/ozdemirrulass/keycloak-nextjs-mysql-docker
This article aims to provide a step by step guide for Keycloak + NextJS 13 authentication and containerization. By the end of this article you will be able to
Set up a Keycloak server based on MYSQL database for authentication.
Integrate Keycloak with a Next.js 13 application for user authentication.
Implement authentication flows such as login, registration in your Next.js app.
Containerize your Keycloak and Next.js applications using Docker for easy deployment and scalability.
Understand best practices for managing authentication tokens and sessions in a containerized environment.
From start to end we will be using compose to build our application in a containerized environment. Since we will be building our containers for development environment, output source code of this tutorial will be ready to convert multi-environment. Since the requirements and configurations of production and development environments are different, Multi-environment development is strongly advised but we will be only working on development environment for educational purposes.
Prerequisites
I did my best to keep this article as simple as possible to teach the basics but to get the most out of this article it is strongly advised to have some knowledge on Next.js, Docker, and authentication concepts.
Tools and Software: Docker, TypeScript, NodeJS, Docker Compose, Keycloak
Setting Up Development Environment
Let's start creating a new project directory named keycloak-nextjs-docker-tutorial
and open it using your favorite IDE. I'll be using VS Code.
As I mentioned earlier we will be using docker compose tool to create our containers.
For those unfamiliar with docker compose tool:
It is simply a tool for defining and running multi-container applications. To be able to run containers we must define the properties of our containers in a yml
file.
docker-compose.yml
or docker-compose.yaml
in the current directory. However, you can specify a different file using the -f
or --file
option when running Docker Compose commands.For example, if your docker-compose.yml
file is named my-compose.yml
, you can use the following command to specify the file:
docker-compose -f my-compose.yml up
We will be building a development environment so let's create a file named docker-compose.dev.yml
to define our container properties and .env
file to specify some environment values.
Typical docker-compose.yml
file contains the following definitions:
- Version: This field specifies the version of the Docker Compose file format being used. It's usually first line of the file and defines the schema of the file.
version: '3.8'
- Services: This section defines the containers that make up your application. Each service must be identified by a unique name, and its configuration must be under that name.
services:
keycloak:
...
mysql:
...
next-app:
...
Container Configuration: Within each service definition, you can configure various aspects of the container, such as:
Image: Specifies the Docker image to use for the container.
Build: Specifies the path to the Dockerfile if the image needs to be built.
Ports: Maps ports from the container to the host machine.
Volumes: Mounts volumes from the host machine into the container.
Environment Variables: Sets environment variables for the container.
Dependencies: Defines dependencies on other services within the same
docker-compose.yml
file.Networks: Configures networking options for the container.
Command: Overrides the default command specified in the Docker image.
Healthcheck: Configures a health check for the container.
Having said that let's start to configure our containers.
MySQL
Let's start with the database. We need a database for our Keycloak service to store user data. I'll be using MYSQL for this tutorial but after completing this tutorial I encourage you to try to build the same structure with different database technologies.
version: '3.8'
services:
mysql:
container_name: mysql
image: "mysql:${MYSQL_VERSION}"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
environment:
MYSQL_DATABASE: keycloak
MYSQL_USER: keycloak
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: password
volumes:
- ./mysql_data:/var/lib/mysql
We've defined a MYSQL container named mysql
and a database named keycloak
. User credentials of keycloak database are username: keycloak, password: password and the root user's password is also password.
${MYSQL_VERSION}
in the Docker Compose file. This is an example of variable substitution or interpolation.${VARIABLE_NAME}
syntax is used to reference environment variables defined either in the shell environment or in an .env
file. Docker Compose automatically detects the .env
file within the same directory.Open the .env
file we created earlier in the /keycloak-nextjs-docker-tutorial
directory and add the following variable.
MYSQL_VERSION=8.0
I'll be using mysql:8.0
Another very important part of this service definition is:
volumes:
- ./mysql_data:/var/lib/mysql
/var/lib/mysql
directory is stored on your local machine and remains accessible across container restarts or recreations.Keycloak
In this part we will be adding the Keycloak service to our compose definitions and ensure the connectivity between our database.
Open the docker-compose.dev.yml
file we created earlier in the /keycloak-nextjs-docker-tutorial
and add the following definitions after the mysql
service.
keycloak:
container_name: keycloak
image: "quay.io/keycloak/keycloak:${KC_VERSION}"
command: ["start-dev"]
restart: unless-stopped
depends_on:
- mysql
healthcheck:
test: ["CMD", "curl", "--fail", "http://localhost:8080"]
environment:
- KC_DB=mysql
- KC_DB_USERNAME=keycloak
- KC_DB_PASSWORD=password
- KC_DB_URL=jdbc:mysql://mysql:3306/keycloak
- KC_FEATURES=${KC_FEATURES}
- KEYCLOAK_ADMIN=admin
- KEYCLOAK_ADMIN_PASSWORD=${KC_PASSWORD}
ports:
- ${KC_PORT}:8080
As you can see, we have a few variables here, just like in our mysql service. Let's quickly open the .env
file we created earlier in the /keycloak-nextjs-docker-tutorial
directory and add the related variables.
KC_VERSION=17.0.1
KC_PORT=8080
KC_FEATURES=account2,admin2,account-api,token-exchange
KC_PASSWORD=keycloak
There is another very important feature of Docker I'd like to draw your attention.- KC_DB_URL=jdbc:mysql://mysql:3306/keycloak
Here we specify a database connection url using Java Database Connectivity API but as you can see instead of using localhost
or a domain or an ip address we are using the container name!
Good job! 👏 We've just prepared a Docker Compose environment for Keycloak based on MYSQL database.
Let's pause here, run our containers, and take a closer look at what we have accomplished.
To build and run our containers execute the following command in the same directory with docker-compose.dev.yml
file which in our case /keycloak-nextjs-docker-tutorial
.
docker compose -f docker-compose.dev.yml up -d
Before executing the code I'd like to explain the -f
and -d
flags.
-f
flag stands for "file." as I mentioned earlier if your compose file is not docker-compose.yml/yaml you must specify the filename.-d
flag stands for "detached mode". When you use this flag, Docker Compose will run the containers in the background, allowing you to continue using the terminal for other tasks. If you don't use -d flag containers will start in the foreground, and the logs from the containers will be streamed to your terminal. This means you'll see the output of each container's STDOUT and STDERR directly in your terminal window.
If you followed the previous steps precisely you must be seeing something like this in your terminal:
P.S. Container names must be unique and since I already have a container named mysql on my system I named my MYSQL container as mysql.
To see which containers are running open your terminal and execute
docker ps
You'll see two containers:
0.0.0.0:8080->8080/tcp
This is another important feature of docker and we mentioned it earlier but It's worth repeating. It represents the port mapping. This indicates that port 8080 on the host machine is mapped to port 8080 on the container. This means that any traffic directed to port 8080 on the host machine will be forwarded to the corresponding port on the Docker container. This means when you try to reach to service from your host machine you must use the port shows up in the left side but if another service in the same network tries to reach this service it should use the port in the right side of :
. It is simply [hostPort]:[containerPort]
Let's check if our Keycloak and MySQL integration is working.
Visit localhost:8080 on your web browser. You should see the following screen:
Seems like Keycloak running fine. Let's login with the credentials we've defined in .env
file earlier. Click to Administrator console and login with the following credentials:
Username: admin
Password: keycloak
You will see a Keycloak dashboard after successful login
Perfect! Before we proceed further, let me demonstrate how to access container terminals and execute commands within Docker environments.
docker exec -it <container_id_or_name> <command>
This is the syntax of executing a command in a container. For example, if you have a container running a bash shell and its ID is abcdef123456
, you can access its terminal like this:
docker exec -it abcdef123456 bash
This command opens an interactive terminal (-it
) within the specified container (abcdef123456
) running the bash
shell. You can replace bash
with any other command you want to execute within the container.
We will be working inside mysql container so it must be:
docker exec -it mysql bash
docker ps
command in your terminal.After executing the bash command user@host indicator will replace with bash-5.1#
like this:
This means that you've successfully accessed the bash shell of your container. Now Let's connect to a MySQL database and check tables and records.
mysql -u keycloak -p
after you execute this command in the bash shell it will require you to enter a password which we specified in docker-compose.dev.yml
as
MYSQL_USER: keycloak
MYSQL_PASSWORD: password
Let's check the databases exists in our mysql service.
Execute the following query in MySQL monitor
You will see 3 database record. 2 of them are default special databases of MySQL. The information_schema
database provides metadata about MySQL server objects, while the performance_schema
database offers insights into server performance metrics.
Important part for us is keycloak database. It is perfectly created just like we defined as MYSQL_DATABASE: keycloak
in our compose file.
To be able to see the the contents execute the following commands in order.use keycloak;
show tables;
This will show us a list of tables which our Keycloak service created automatically.
Execute exit;
to exit from the MySQL monitor and execute exit
in the bash shell to exit from the MySQL container's bash shell.
👏 Congratulations on our achievements so far! Yet, there's still more to accomplish!
Before we proceed, I'd like to introduce the concept of Multi-tenancy. Understanding the Multi-tenancy concept will help you to understand and operate Keycloak better!
A "tenant" typically refers to an individual or organization that has its own distinct set of users, data, and configuration settings within the shared software environment.
Multitenancy refers to a software architecture where a single instance of the software serves multiple clients (tenants), keeping their data and configurations separate while sharing the same underlying infrastructure.
I strongly advise you to read more on multi-tenancy and understand the concept and types of it.
There is a beautiful explanation of handling multitenant organization with Keycloak in the documentation of cloud-iam.
We will go for second for simplicity and consistency reasons.
Let's create our first REALM.
Perfect! We just created our first REALM. Let's create a client for our NextJS application now!
If all goes well you must see the following screen:
Congrats 🥳 We have a Realm and a Client for our NextJS application.
Before we proceed further with Client configuration and Access Settings we will create and containerize a NextJS application! 💃🕺
“If you're not a disruptor, you will be disrupted.” – John Chambers
So;
"Mr. Gorbachev, tear down this wall!"- Reagan
Open your terminal in the root of our project which is /keycloak-nextjs-docker-tutorial
and execute this command:
docker compose -f docker-compose.dev.yml down
This command will stop and remove all of the containers defined in our compose file as well as the docker network.
Did we made all this for nothing !? Of course NO!
Open the project directory in your favorite IDE and take a look at the folder structure of the project.
Docker Compose and our MySQL service left us a little present. Well, actually we wanted this from them...
Do you remember this part of the docker-compose.dev.yml
file?
volumes:
- ./mysql_data:/var/lib/mysql
Thanks to this piece of definition whenever we built our service again mysql will mount the latest status and contents of our databases.
We will reunite with out precious data but first Let's create our NextJS application and containerize it!
NextJS
Open the project directory in your terminal /keycloak-nextjs-docker-tutorial
and execute the following command to create a NextJS app:
npx create-next-app@latest
Once again we will open the project folder using our IDE.
Here is our next application. Isn't it adorable? No! Because it is not containerized. Let's get to work then!
But before we work on our application we should add the /next-app
directory to workplace otherwise ESLint will throw errors.
File->Add Folder to Workspace->(Choose next-app directory) and click add.
Now lets open next.config.mjs
file in our /next-app
directory
Next we add a property inside of the nextConfig const.
output: "standalone",
This declaration is to create a self-contained build of our application. This can be particularly useful for deploying our Next.js application in environments where we want to avoid installing Node.js dependencies directly on the server, such as when using Docker or deploying to a serverless environment. We will see the benefit of this when we want to write a production environment.
Until now we built and run an existing images by pulling from their resources. This time we will create a docker image for our NextJs Application.
First create a new file as dev.Dockerfile
inside the /next-app
directory and open the file on IDE.
Before writing anything let's first understand what is Dockerfile then break it down it together.
# Base Image: Typically, a Dockerfile starts with a base image upon which
# you build your application.
# This is specified using the FROM instruction.
FROM node:18-alpine
# If you set WORKDIR /app, then any commands or file operations
# will be relative to the /app directory.
WORKDIR /app
# Install dependencies based on the preferred package manager
# Copy Application Files: You copy the application code or files
# into the image using the COPY or ADD instruction.
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i; \
# Allow install without lockfile, so example works
#even without Node.js installed locally
else echo "Warning: Lockfile not found. It is recommended to commit lockfiles to version control." && yarn install; \
fi
COPY . .
# Next.js collects completely anonymous telemetry data about
# general usage. Learn more here: https://nextjs.org/telemetry
# Comment the following line to enable telemetry at run time
ENV NEXT_TELEMETRY_DISABLED 1
# Note: We could expose ports here but instead
# Compose will handle that for us
# Start Next.js in development mode based on the
# preferred package manager
CMD \
if [ -f yarn.lock ]; then yarn dev; \
elif [ -f package-lock.json ]; then npm run dev; \
elif [ -f pnpm-lock.yaml ]; then pnpm dev; \
else npm run dev; \
fi
Beautiful.
Now we will add this Dockerfile to our Compose definitions so that Compose tool will be able to build the image for us just as we defined in our Dockerfile
. Add the following block under services in docker-compose.dev.yml
.
next-app:
container_name: next-app
build:
context: ./next-app
dockerfile: dev.Dockerfile
restart: unless-stopped
environment:
- NODE_ENV=development
volumes:
- ./next-app:/app
- /app/node_modules
ports:
- 3000:3000
Before building and running the containers I'd like to mention one more concept.
Network Isolation: until now we've been using the default bridge network which Docker built for us. But let's think on this for a second.
While working on a multi-service environment docker automatically creates a bridge network and assigns each container within that network with a unique name.
What does this mean?
Does it mean when we add NextJS application in this compose file will mysql can be accessible by NextJS ?
Answer is Yes! But do we want it though? What are we supposed to do with MySQL in a NextJS front-end client? Nothing. Let's cut the bonds then!
What we have to do is isolating the connectivity between our services like in this diagram by creating custom networks:
open docker-compose.dev.yml
once again and go to very bottom of it and add following piece of definition.
networks:
frontend-network:
keycloak-network:
By adding this definition, we've created 2 new networks for our environment. As you can see I did not specify any property for our networks because the default network type is bridge
.
and this is exactly what we want.
Adding the previous piece of definition in our compose file only creates the network. To be able to use it we must introduce our containers to our new networks.
version: '3.8'
services:
mysql:
container_name: mysqlk
image: "mysql:${MYSQL_VERSION}"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
environment:
MYSQL_DATABASE: keycloak
MYSQL_USER: keycloak
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: password
volumes:
- ./mysql_data:/var/lib/mysql
networks:
- keycloak-network
keycloak:
container_name: keycloak
image: "quay.io/keycloak/keycloak:${KC_VERSION}"
command: ["start-dev"]
restart: unless-stopped
depends_on:
- mysql
healthcheck:
test: ["CMD", "curl", "--fail", "http://localhost:8080"]
environment:
- KC_DB=mysql
- KC_DB_USERNAME=keycloak
- KC_DB_PASSWORD=password
- KC_DB_URL=jdbc:mysql://mysqlk:3306/keycloak
- KC_FEATURES=${KC_FEATURES}
- KEYCLOAK_ADMIN=admin
- KEYCLOAK_ADMIN_PASSWORD=${KC_PASSWORD}
ports:
- ${KC_PORT}:8080
networks:
- keycloak-network
- frontend-network
next-app:
container_name: next-app
build:
context: ./next-app
dockerfile: dev.Dockerfile
restart: unless-stopped
environment:
- NODE_ENV=development
volumes:
- ./next-app:/app
- /app/node_modules
ports:
- 3000:3000
networks:
- frontend-network
networks:
frontend-network:
keycloak-network:
As you can see in the current version of our compose file, I added the networks to the bottom of each service.
It's time to see what we've done. Open the terminal in the root directory of our project and run the following command to build image.
docker compose -f docker-compose.dev.yml build
This will build the images you defined in the Compose file.
It's time to run our containers.
docker compose -f docker-compose.dev.yml up -d
up
command without using the build
command, Compose will check if a pre-built image with the same name exists. If it does, Compose will use that image. If it doesn't, Compose will build the image first and then start the container with it.It will take a bit more time for Keycloak to start comparing other services. In the meanwhile we can check if our next-app.
Visit localhost:3000
on your browser.
Nice work!
Let's make sure our hot load works as expected and our container applies the changes we make in our host machine through the volume.
Open /next-app/src/app/page.tsx
and replace the content with the following code:
export default function Home() {
return (
<main>
<div>It Works!</div>
</main>
);
}
Visit localhost:3000
and you must see the changes!
Now it's time to integrate Keycloak authentication for our our NextJS application.
We will be using NextAuth.
Install Next-Auth package:
npm install next-auth
Create a new directory under /next-app
directory as types
and a file inside types as node-env.d.ts
.
// /next-app/types/node-env.d.ts
declare namespace NodeJS {
export interface ProcessEnv {
NEXT_PUBLIC_KEYCLOAK_CLIENT_ID: string
KEYCLOAK_CLIENT_SECRET: string
NEXT_LOCAL_KEYCLOAK_URL: string
NEXT_PUBLIC_KEYCLOAK_REALM: string
NEXT_CONTAINER_KEYCLOAK_ENDPOINT: string
}
}
now it's time to create authentication routes.
import { AuthOptions } from "next-auth";
import NextAuth from "next-auth/next";
import KeycloakProvider from "next-auth/providers/keycloak";
export const authOptions: AuthOptions = {
providers: [
KeycloakProvider({
jwks_endpoint: `${process.env.NEXT_CONTAINER_KEYCLOAK_ENDPOINT}/realms/myrealm/protocol/openid-connect/certs`,
wellKnown: undefined,
clientId: process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID,
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
issuer: `${process.env.NEXT_LOCAL_KEYCLOAK_URL}/realms/${process.env.NEXT_PUBLIC_KEYCLOAK_REALM}`,
authorization: {
params: {
scope: "openid email profile",
},
url: `${process.env.NEXT_LOCAL_KEYCLOAK_URL}/realms/myrealm/protocol/openid-connect/auth`,
},
token: `${process.env.NEXT_CONTAINER_KEYCLOAK_ENDPOINT}/realms/myrealm/protocol/openid-connect/token`,
userinfo: `${process.env.NEXT_CONTAINER_KEYCLOAK_ENDPOINT}/realms/myrealm/protocol/openid-connect/userinfo`,
}),
],
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
Go back to root directory of your next-app and create an environment file as .env.local
NEXT_PUBLIC_KEYCLOAK_REALM=<realm-name>
NEXT_PUBLIC_KEYCLOAK_CLIENT_ID=<client-name>
KEYCLOAK_CLIENT_SECRET=<secret-from-keycloak-client>
NEXTAUTH_SECRET=<create-using-openssl>
NEXT_LOCAL_KEYCLOAK_URL="http://localhost:8080"
NEXT_CONTAINER_KEYCLOAK_ENDPOINT="http://keycloak:8080"
As I mentioned before Keycloak still has the Realm and the Client we've created before.
Visit localhost:8080
and sign in and select the Realm we created earlier:
Go to clients and select the client we created earlier.
Go to Access Settings and define Home URL and Valid redirect URI's:
Save and go to Credentials.
Copy Client secret and paste KEYCLOAK_CLIENT_SECRET
value.
KEYCLOAK_CLIENT_SECRET=<secret-from-keycloak-client>
For NEXTAUTH_SECRET
, create a secret by running the following command and add it to .env.local
. This secret is used to sign and encrypt cookies.
openssl rand -base64 32
In my case final .env.local
file looks like this:
NEXT_LOCAL_KEYCLOAK_URL="http://localhost:8080"
NEXT_CONTAINER_KEYCLOAK_ENDPOINT="http://keycloak:8080"
NEXT_PUBLIC_KEYCLOAK_REALM="myrealm"
NEXT_PUBLIC_KEYCLOAK_CLIENT_ID="next-app"
KEYCLOAK_CLIENT_SECRET="71ikzeN5p0fEwdHW6Hw5jOmlRvRIEtgO"
NEXTAUTH_SECRET="MdiNiCNlDcBP8fUmANd9ARPIB+tlKV/oy3m88W2bTHk="
Create a new folder under /next-app/src
as components
and create the following components inside the directory.
//next-app/src/components/Login.tsx
"use client"
import { signIn } from "next-auth/react";
export default function Login() {
return <button onClick={() => signIn("keycloak")}>
Signin with keycloak
</button>
}
//next-app/src/components/Logout.tsx
"use client"
import { signOut } from "next-auth/react";
export default function Logout() {
return <button onClick={() => signOut()}>
Signout of keycloak
</button>
}
Go to page.tsx file /next-app/src/app/page.tsx
and replace the content with the following block:
import { getServerSession } from 'next-auth'
import { authOptions } from './api/auth/[...nextauth]/route'
import Login from '../components/Login'
import Logout from '../components/Logout'
export default async function Home() {
const session = await getServerSession(authOptions)
if (session) {
return <div>
<div>Your name is {session.user?.name}</div>
<div><Logout /> </div>
</div>
}
return (
<div>
<Login />
</div>
)
}
We need a user to test our client.
Create the user and go to Credentials
section to set a password for the user we just created.
Now its time to test our authentication flow.
Go to our nextjs app localhost:3000
Congratulations we have successfully implemented an enterprise-level authentication in a containerized environment for Next.js 13 front-end.
For federated logout you my want to check this discussion on github: https://github.com/nextauthjs/next-auth/discussions/3938
And here is how you can activate user registration:
I'll also write how to integrate it with your backend service and how to monitor your application and keycloak using Prometheus and Grafana as soon as possible. It will also cover securing your Next.js routes using Keycloak's role-based access control.