Strapi ♥ Keycloak! Now it’s security time

Andrea Chiumenti
7 min readDec 1, 2023

In the previous articles we have installed, and stated using Strapi Headless CMS, …with little customization too.

There is a “small” part missing… security!

As a security platform we’ll use Keycloak.

Keycloak is an open-source system for identity and access management, designed to facilitate authentication and authorization in web applications and services. Developed by Red Hat, it provides a comprehensive solution for identity security, including services such as authentication, authorization, session management, and identity federation.

The main strength points of Keycloak are:

  • Secure Resource Access and Streamlined Entry: Keycloak provides a platform that facilitates secure access to resources, creating a seamless experience for users.
  • Harmonious Connections: Through identity federation, Keycloak enables harmonious connections between diverse identity sources, promoting integrated and unified management.
  • Agility in Integration: With its flexibility, Keycloak seamlessly integrates with various applications, fostering a synergistic flow between systems.
  • Conscious Access Modeling: In identity management, Keycloak offers tools for consciously modeling access, allowing precise and mindful control.
  • Security Encryption: Through adherence to security standards, Keycloak provides an encrypted environment, ensuring the safety and confidentiality of information.

The easiest way to stat using Keycloak is to use docker compose, and since I love PostgreSQL here it is the link:
https://github.com/keycloak/keycloak-containers/blob/main/docker-compose-examples/keycloak-postgres.yml

We need to enable https (selfsigned certificate is ok for our purposes), so modify the file exposing the https port:

version: "3"

volumes:
postgres_data:
driver: local

services:
postgres:
image: postgres
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: password
keycloak:
image: quay.io/keycloak/keycloak:legacy
environment:
DB_VENDOR: POSTGRES
DB_ADDR: postgres
DB_DATABASE: keycloak
DB_USER: keycloak
DB_SCHEMA: public
DB_PASSWORD: password
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: Pa55w0rd
# Uncomment the line below if you want to specify JDBC parameters. The parameter below is just an example, and it shouldn't be used in production without knowledge. It is highly recommended that you read the PostgreSQL JDBC driver documentation in order to use it.
#JDBC_PARAMS: "ssl=true"
ports:
- 8080:8080
- 8443:8443
depends_on:
- postgres

Download it and then simply run:

docker-compose -f /path-to/docker-compose.yml -p pgkeycloak -d start

Then open your browser at the following address: https://localhost:8443/auth/admin/

Follow the steps to register the admin and then create a realm and set “Realm name” to : “strapitest”

Even if we could connect Keycloak to Google, github or other identity providers, let’s keep it simple and just add a user “testuser”.

And set the password in the credentials tab (turn of temporary switch)

Now we are going to create a client selecting Clients menu. Use OpenID Connect as client type and name it “strapicli”

Click “Next” button, turn “Client authentication” switch on, leave the defaults as they are, and complete the process.

Go to the Credentials tab and copy the client secret

Complete the process and save, then to the settings again because we need to instruct Keycloak to use strapi.
Add http://localhost:1337/api/connect/keycloak/callback as a valid callback url

We won’t cover in-depth Keycloak security aspects with fine tuning, so we have done everything we needed with this application.

OK, done! Let’s move to Strapi now.

As usual, create our sample project

> npx create-strapi-app@latest auth-keycloak --quickstart --typescript --no-run

Then our graphql plugin

> cd auth-keycloak
> yarn strapi install graphql

Finally run strapi yarn develop and in “Content-Type Builder” create a new collection type “Posts”

Most of Strapi power relies in its plugins and the possibility to customize nearly everything. In Strapi security is managed by “@strapi/plugin-users-permissions” that comes with the standard installation, you can find it in ./node_modules .

To override a Strapi plugin is relatively easy, you have many options the one I prefer most here is with code.

it works overriding files using ./src/extension folder. So for plugin users-permissions start creating a users-permissions folder in ./src/extension.

Users permissions plugin is a back-end plugin, so the entry point is strapi-server.ts (ts because we are using typescript).

I suggest to go and have a look at `node_modules/@strapi/plugin-users-permissions and study the code. Reverse engineering is often the best option.

The strategy here is to use a Proxy to extend the behavior of its boostrap method adding a Keycloak provider.

Start writing file ./src/extension/users-permissions/server/registry.ts as the name suggests we are adding keycloak provider to the registry.

export const doRegisterKeycloakProvider = ({ strapi }) => {
const providersRegistry = strapi.service(
"plugin::users-permissions.providers-registry"
);

providersRegistry.register("keycloak", ({ purest }) => {
return async ({ accessToken }) => {
const pluginStore = strapi.store({
type: "plugin",
name: "users-permissions",
});
const storedGrantConfig = (await pluginStore.get({ key: "grant" })) || {};
const keycloak = purest({
provider: "keycloak",
});
return keycloak
.subdomain(storedGrantConfig.keycloak.subdomain)
.get("protocol/openid-connect/userinfo")
.auth(accessToken)
.request()
.then(({ body }: { body: any }) => ({
username: body["preferred_username"],
email: body.email,
}));
};
});
};

Then the boostrapHandler for our plugin proxy, let’s write file ./src/extension/users-permissions/server/bootstrap.ts

import urlJoin from "url-join";
import { Bootstrap } from "@strapi/types/dist/types/core/plugins/config/strapi-server/lifecycle";

import { Plugin } from "@strapi/types";
import { doRegisterKeycloakProvider } from "./registry";

const getGrantConfig = (baseURL) => ({
keycloak: {
enabled: false,
icon: "keycloak",
key: "",
secret: "",
subdomain: "localhost:8443/auth/realms/strapitest",
callback: `${baseURL}/keycloak/callback`,
scope: ["email"],
},
});

export const bootstrapHandler = (bootstrap: Bootstrap): Bootstrap => {
return async ({ strapi }) => {
const pluginStore = strapi.store({
type: "plugin",
name: "users-permissions",
});

const storedGrantConfig =
((await pluginStore.get({ key: "grant" })) as Object) || {};
await bootstrap({ strapi });
const storedGrantConfigOnBootstrap =
((await pluginStore.get({ key: "grant" })) as Object) || {};

const apiPrefix = strapi.config.get("api.rest.prefix");
const baseURL = urlJoin(strapi.config.server.url, apiPrefix, "auth");
const grantConfig = getGrantConfig(baseURL);

const newGrantConfig = {
...grantConfig,
...storedGrantConfigOnBootstrap,
...storedGrantConfig,
};

await pluginStore.set({
key: "grant",
value: newGrantConfig,
});

doRegisterKeycloakProvider({ strapi });
};
};

If you notice in the file we are getting the stored grant config before and after calling the original plugin bootstrap. This is due to the fact that the code inside the original bootstrap erases the configuration of non original providers, with

const newGrantConfig = {
...grantConfig,
...storedGrantConfigOnBootstrap,
...storedGrantConfig,
};

We are ensuring to keep our settings.

Finally we crate our Proxy, editing file ./src/extension/users-permissions/strapi-server.ts

import { bootstrapHandler } from "./server/bootstrap";

export default async (plugin) => {
return new Proxy(plugin, {
get(target, prop) {
if (prop === "bootstrap") {
return bootstrapHandler(plugin.bootstrap);
}

return target[prop];
},
});
};

WOW!, no more code.

Start Strapi

NODE_TLS_REJECT_UNAUTHORIZED=0 yarn develop

Setting NODE_TLS_REJECT_UNAUTHORIZED to 0 allows us to use Keycloak self signed certificate for SSL.

Open the admin console http://localhost:1337/admin/settings/users-permissions/providers

And editkeycloak provider

NOTE: In the picture above I’ve used http://localhost:3000/auth/callback as value in The redirect URL to your front-end app, to quickly test our new authorization provider we need to skip a step, so replace the URL with http://localhost:1337/api/auth/keycloak/callback.

To understand why I quicly explay the auth lifecycle:

  1. Authenticate opening http://localhost:1337/api/connect/keycloak
  2. This will redirect to Keycloak login panel
  3. Keycloak redirect you to the provided callback, that in picture is localhost:3000 with access_soken as search parameter:
    http://localhost:3000/auth/callback?access_token=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAi
  4. You then use the access token to get jwt and user info calling: http://localhost:1337/api/auth/keycloak/callback?access_token=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAi

Since we don’t have a frontend application to play with we directly call Strapi auth callback to get jwt and useinfo.

So, for this article, change the configuration to:

Now you can authenticate opening the link http://localhost:1337/api/connect/keycloak

The result should be something like:

Now you can use this JWT to access Strapi services!

Strapi: Headless CMS walk trough

7 stories

--

--

Andrea Chiumenti

Co-founder of Red Software Systems srl — Italy, I’m a software architect with many years of experience in back end and front end development.