Keycloak Provider Integration Issue in Strapi v5 After Upgrade
Bug report
Required System information
- Node.js version: v20.18.0
- NPM version: pnpm 9.13.2
- Strapi version: 5.4.0
- Database: postgres
- Operating system: macOS, Linux
- Is your project Javascript or Typescript: Typescript
Describe the bug
Hello Strapi Community,
I'm trying to enable the Keycloak provider in Strapi 5. After some research, I found this article on integrating Keycloak with Strapi. The instructions work well for Strapi v4, but after upgrading to Strapi v5, I ran into issues and haven’t been able to resolve them.
Here’s my code. If anyone has experience with Strapi v5 and can help troubleshoot, I'd greatly appreciate it. Many thanks!
src/extensions/users-permissions/server/registry.ts
import { Core } from '@strapi/strapi';
interface GrantConfig {
keycloak: {
subdomain: string;
clientId: string;
clientSecret: string;
callback: string;
enabled: boolean;
};
}
export const doRegisterKeycloakProvider = ({ strapi }: { strapi: Core.Strapi }) => {
// Get the providers-registry service using the full path
// const providersRegistry = strapi.container.get('plugin::users-permissions.providers');
// const providersRegistry = strapi.plugin('users-permissions').service('providers-registry');
const providersRegistry = strapi.service(
'plugin::users-permissions.providers-registry',
);
// also i have try to pass
// { name: 'keycloak', pluginId: 'keycloak-provider' },
providersRegistry.register('keycloak', ({ purest }) => {
return async ({ accessToken }) => {
const pluginStore = strapi.store({
type: 'plugin',
name: 'users-permissions',
});
const config = (await pluginStore.get({ key: 'grant' })) as GrantConfig;
const keycloakConfig = config?.keycloak;
if (!keycloakConfig) {
throw new Error('Keycloak configuration is missing');
}
const keycloak = purest({
provider: 'keycloak',
defaults: {
baseUrl: `https://${keycloakConfig.subdomain}`,
},
});
try {
const { body } = await keycloak
.get('protocol/openid-connect/userinfo')
.auth(accessToken)
.request();
return {
username: body.preferred_username,
email: body.email,
provider: 'keycloak',
};
} catch (error) {
throw new Error('Failed to fetch user info from Keycloak');
}
};
});
};
src/extensions/users-permissions/server/bootstrap.ts
import { Core } from '@strapi/strapi';
import { doRegisterKeycloakProvider } from './registry';
interface KeycloakConfig {
enabled: boolean;
icon: string;
key: string;
secret: string;
subdomain: string;
callback: string;
scope: string[];
}
interface GrantConfig {
keycloak: KeycloakConfig;
[key: string]: any; // For other providers if needed
}
const getGrantConfig = (baseURL: string): GrantConfig => ({
keycloak: {
enabled: false,
icon: 'keycloak',
key: process.env.KEYCLOAK_CLIENT_ID || '',
secret: process.env.KEYCLOAK_CLIENT_SECRET || '',
subdomain: `${process.env.IDENTITY_ISSUER}`,
callback: `${baseURL}/keycloak/callback`,
scope: ['email'],
},
});
export const bootstrapHandler = (
bootstrap: (params: { strapi: Core.Strapi }) => Promise<void>,
) => {
return async ({ strapi }: { strapi: Core.Strapi }) => {
const pluginStore = strapi.store({
type: 'plugin',
name: 'users-permissions',
});
const storedGrantConfig =
((await pluginStore.get({ key: 'grant' })) as Partial<GrantConfig>) || {};
await bootstrap({ strapi });
const storedGrantConfigOnBootstrap =
((await pluginStore.get({ key: 'grant' })) as Partial<GrantConfig>) || {};
const apiPrefix = strapi.config.get('api.rest.prefix');
const baseURL = `${strapi.config.server.url}${apiPrefix}/auth`;
const grantConfig = getGrantConfig(baseURL);
// Merge configurations with type safety
const newGrantConfig: GrantConfig = {
...grantConfig,
...storedGrantConfigOnBootstrap,
...storedGrantConfig,
keycloak: {
...grantConfig.keycloak,
...(storedGrantConfigOnBootstrap.keycloak || {}),
...(storedGrantConfig.keycloak || {}),
},
};
// Validate required fields
if (newGrantConfig.keycloak.enabled) {
if (!newGrantConfig.keycloak.key || !newGrantConfig.keycloak.secret) {
console.warn('Keycloak is enabled but client ID or secret is missing');
}
if (!process.env.IDENTITY_PROVIDER) {
console.warn('IDENTITY_PROVIDER environment variable is not set');
}
}
await pluginStore.set({
key: 'grant',
value: newGrantConfig,
});
doRegisterKeycloakProvider({ strapi });
};
};
src/extensions/users-permissions/strapi-server.ts
import { Core } from '@strapi/strapi';
import { bootstrapHandler } from './server/bootstrap';
interface StrapiPlugin {
bootstrap: (params: { strapi: Core.Strapi }) => Promise<void>;
[key: string]: any;
}
export default async (plugin: StrapiPlugin) => {
return new Proxy(plugin, {
get(target: StrapiPlugin, prop: string | symbol): any {
if (prop === 'bootstrap') {
return bootstrapHandler(target.bootstrap);
}
// Use type assertion for symbol access
return Reflect.get(target, prop);
},
});
};
Error: TypeError: providersRegistry.register is not a function at doRegisterKeycloakProvider
Expected behavior
Can strart strapi without any issues
Thanks for reporting this issue.
The error you're seeing:
TypeError: providersRegistry.register is not a function
is due to a breaking change in Strapi v5. In Strapi v4, custom providers were registered using .register(), but in Strapi v5 this has changed. You now need to use .add() inside the register() lifecycle method, placed in src/index.ts.
Fix for Strapi v5
Replace your custom provider logic with this structure:
// src/index.ts
export default {
register({ strapi }) {
strapi
.plugin("users-permissions")
.service("providers-registry")
.add("keycloak", {
icon: "keycloak",
enabled: true,
grantConfig: {
key: process.env.KEYCLOAK_CLIENT_ID || '',
secret: process.env.KEYCLOAK_CLIENT_SECRET || '',
callback: `${strapi.config.server.url}/api/auth/keycloak/callback`,
subdomain: process.env.IDENTITY_ISSUER || '',
scope: ["email"],
},
async authCallback({ accessToken, purest }) {
const keycloak = purest({
provider: 'keycloak',
defaults: {
baseUrl: `https://${process.env.IDENTITY_ISSUER}`,
},
});
const { body } = await keycloak
.get('protocol/openid-connect/userinfo')
.auth(accessToken)
.request();
return {
username: body.preferred_username,
email: body.email,
};
},
});
},
};
This follows the new pattern for registering providers in Strapi 5.
For more details, please refer to the official documentation:
- Creating a custom provider: https://docs.strapi.io/cms/configurations/users-and-permissions-providers/new-provider-guide
- Keycloak integration guide: https://docs.strapi.io/cms/configurations/users-and-permissions-providers/keycloak
Let us know if you need further assistance.
Transferring this to documentation as this likely needs the new method documented.
And a breaking change added