documentation icon indicating copy to clipboard operation
documentation copied to clipboard

Keycloak Provider Integration Issue in Strapi v5 After Upgrade

Open nasraldin opened this issue 1 year ago • 3 comments

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

nasraldin avatar Nov 15 '24 08:11 nasraldin

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.

raulbalestra avatar Apr 11 '25 21:04 raulbalestra

Transferring this to documentation as this likely needs the new method documented.

derrickmehaffy avatar Apr 15 '25 03:04 derrickmehaffy

And a breaking change added

derrickmehaffy avatar Apr 15 '25 03:04 derrickmehaffy