Okta Organization Entity Provider logo

Backstage Okta Organization Entity Provider Plugin

Created by Roadie

Okta Organization Entity Provider brings your Okta directory into Backstage. It connects to Okta and turns users and groups into Backstage entities. This gives you a single source of truth for people and team data inside the catalog. It runs in your backend and keeps data fresh on a schedule. It works well in a self hosted Backstage setup.

You can load users and groups together with one provider or choose to load only one type. The provider can build a group tree from profile fields to reflect your org chart. It offers flexible naming strategies for users and groups. You can keep the default id style or pick options like strip domain email or slugify. Filters let you narrow which users or groups are imported.

It supports API token auth or OAuth scopes. It avoids leaking secrets in logs. It resolves group membership in chunks to handle large orgs. You can plug in custom transformers to add annotations or change fields without forking. This makes it easier to fit your internal rules and data model.

Common use cases include mapping teams to service ownership, driving access rules from group membership, and giving engineers a clear view of who owns what. The provider is a backend module, so you keep control in code and can extend it when your org structure changes.

Installation Instructions

These instructions apply to self-hosted Backstage only. To use this plugin on Roadie, visit the docs.

Install the backend module dependency

Copy
yarn --cwd packages/backend add @roadiehq/catalog-backend-module-okta

Add Okta credentials in app-config.yaml. Use either an API token or OAuth two point zero

Basic config with an API token

Copy
catalog:
  providers:
    okta:
      - orgUrl: 'https://tenant.okta.com'
        token: ${OKTA_TOKEN}
        schedule:
          frequency:
            minutes: 5
          timeout:
            minutes: 10
          initialDelay:
            minutes: 1

OAuth scoped auth with minimal scopes okta.groups.read and okta.users.read

Copy
catalog:
  providers:
    okta:
      - orgUrl: 'https://tenant.okta.com'
        oauth:
          clientId: ${OKTA_OAUTH_CLIENT_ID}
          keyId: ${OKTA_OAUTH_KEY_ID}
          privateKey: ${OKTA_OAUTH_PRIVATE_KEY}
        schedule:
          frequency:
            minutes: 5
          timeout:
            minutes: 10
          initialDelay:
            minutes: 1

Key id is optional. It must be set when you use a PEM private key

You can filter users and groups

Copy
catalog:
  providers:
    okta:
      - orgUrl: 'https://tenant.okta.com'
        token: ${OKTA_TOKEN}
        userFilter: profile.department eq "engineering"
        groupFilter: profile.name eq "Everyone"
        schedule:
          frequency:
            minutes: 5
          timeout:
            minutes: 10
          initialDelay:
            minutes: 1

Set up the new backend system using the default provider modules that load both users and groups

Edit packages/backend/src/index.ts

Copy
import { createBackend } from '@backstage/backend-defaults';

const backend = createBackend();

// Required entity provider
backend.add(
  import('@roadiehq/catalog-backend-module-okta/okta-entity-provider'),
);

// Provider factory that loads users and groups
backend.add(
  import('@roadiehq/catalog-backend-module-okta/org-provider-factory'),
);

backend.start();

If you only want users

Copy
backend.add(
  import('@roadiehq/catalog-backend-module-okta/okta-entity-provider'),
);
backend.add(
  import('@roadiehq/catalog-backend-module-okta/user-provider-factory'),
);

If you only want groups

Copy
backend.add(
  import('@roadiehq/catalog-backend-module-okta/okta-entity-provider'),
);
backend.add(
  import('@roadiehq/catalog-backend-module-okta/group-provider-factory'),
);

Set up the new backend system with a custom module that controls naming plus hierarchy plus chunk size

Create a file packages/backend/src/plugins/oktaOrgProvider.ts

Copy
import {
  createBackendModule,
  coreServices,
} from '@backstage/backend-plugin-api';
import {
  oktaCatalogBackendEntityProviderFactoryExtensionPoint,
  EntityProviderFactory,
  OktaOrgEntityProvider,
} from '@roadiehq/catalog-backend-module-okta/new-backend';
import { Config } from '@backstage/config';

export const oktaOrgEntityProviderModule = createBackendModule({
  pluginId: 'catalog',
  moduleId: 'default-okta-org-entity-provider',
  register(env) {
    env.registerInit({
      deps: {
        provider: oktaCatalogBackendEntityProviderFactoryExtensionPoint,
        logger: coreServices.logger,
      },
      async init({ provider, logger }) {
        const factory: EntityProviderFactory = (oktaConfig: Config) =>
          OktaOrgEntityProvider.fromConfig(oktaConfig, {
            logger,
            userNamingStrategy: 'strip-domain-email',
            groupNamingStrategy: 'kebab-case-name',
            hierarchyConfig: {
              key: 'profile.orgId',
              parentKey: 'profile.parentOrgId',
            },
            chunkSize: 250,
          });

        provider.setEntityProviderFactory(factory);
      },
    });
  },
});

Wire this module in packages/backend/src/index.ts. Remove the default org provider factory to avoid duplicate registration

Copy
import { createBackend } from '@backstage/backend-defaults';
import { oktaOrgEntityProviderModule } from './plugins/oktaOrgProvider';

const backend = createBackend();

// Comment out the default line if it exists
// backend.add(import('@roadiehq/catalog-backend-module-okta/org-provider-factory'));

backend.add(
  import('@roadiehq/catalog-backend-module-okta/okta-entity-provider'),
);
backend.add(oktaOrgEntityProviderModule);

backend.start();

If you prefer to register users and groups as separate modules in the new backend system

Create a file packages/backend/src/plugins/oktaUserProvider.ts

Copy
import {
  createBackendModule,
  coreServices,
} from '@backstage/backend-plugin-api';
import {
  oktaCatalogBackendEntityProviderFactoryExtensionPoint,
  EntityProviderFactory,
  OktaUserEntityProvider,
} from '@roadiehq/catalog-backend-module-okta/new-backend';
import { Config } from '@backstage/config';

export const oktaUserEntityProviderModule = createBackendModule({
  pluginId: 'catalog',
  moduleId: 'default-okta-user-entity-provider',
  register(env) {
    env.registerInit({
      deps: {
        provider: oktaCatalogBackendEntityProviderFactoryExtensionPoint,
        logger: coreServices.logger,
      },
      async init({ provider, logger }) {
        const factory: EntityProviderFactory = (oktaConfig: Config) =>
          OktaUserEntityProvider.fromConfig(oktaConfig, {
            logger,
            namingStrategy: 'strip-domain-email',
          });
        provider.setEntityProviderFactory(factory);
      },
    });
  },
});

Create a file packages/backend/src/plugins/oktaGroupProvider.ts

Copy
import {
  createBackendModule,
  coreServices,
} from '@backstage/backend-plugin-api';
import {
  oktaCatalogBackendEntityProviderFactoryExtensionPoint,
  EntityProviderFactory,
  OktaGroupEntityProvider,
} from '@roadiehq/catalog-backend-module-okta/new-backend';
import { Config } from '@backstage/config';

export const oktaGroupEntityProviderModule = createBackendModule({
  pluginId: 'catalog',
  moduleId: 'default-okta-group-entity-provider',
  register(env) {
    env.registerInit({
      deps: {
        provider: oktaCatalogBackendEntityProviderFactoryExtensionPoint,
        logger: coreServices.logger,
      },
      async init({ provider, logger }) {
        const factory: EntityProviderFactory = (oktaConfig: Config) =>
          OktaGroupEntityProvider.fromConfig(oktaConfig, {
            logger,
            userNamingStrategy: 'strip-domain-email',
            namingStrategy: 'kebab-case-name',
          });
        provider.setEntityProviderFactory(factory);
      },
    });
  },
});

Add both modules in packages/backend/src/index.ts

Copy
import { createBackend } from '@backstage/backend-defaults';
import { oktaUserEntityProviderModule } from './plugins/oktaUserProvider';
import { oktaGroupEntityProviderModule } from './plugins/oktaGroupProvider';

const backend = createBackend();

backend.add(
  import('@roadiehq/catalog-backend-module-okta/okta-entity-provider'),
);
backend.add(oktaUserEntityProviderModule);
backend.add(oktaGroupEntityProviderModule);

backend.start();

Optional custom transformers for groups in the new backend system

Create a file packages/backend/src/plugins/oktaGroupTransformer.ts

Copy
import { Group } from '@okta/okta-sdk-nodejs';
import { GroupEntity } from '@backstage/catalog-model';
import {
  GroupNamingStrategy,
  OktaGroupEntityTransformer,
} from '@roadiehq/catalog-backend-module-okta';

export const myGroupTransformer: OktaGroupEntityTransformer = function (
  group: Group,
  namingStrategy: GroupNamingStrategy,
  parentGroup: Group | undefined,
  options: {
    annotations: Record<string, string>;
    members: string[];
  },
): GroupEntity {
  const entity: GroupEntity = {
    kind: 'Group',
    apiVersion: 'backstage.io/v1alpha1',
    metadata: {
      annotations: { ...options.annotations },
      name: namingStrategy(group),
      title: group.profile.description || group.profile.name,
      description: group.profile.description || '',
    },
    spec: {
      members: options.members,
      type: 'group',
      children: [],
    },
  };
  if (parentGroup) {
    entity.spec.parent = namingStrategy(parentGroup);
  }
  return entity;
};

Use the transformer in your custom org provider module

Copy
import { myGroupTransformer } from './oktaGroupTransformer';

// inside the factory in oktaOrgEntityProviderModule
const factory: EntityProviderFactory = (oktaConfig: Config) =>
  OktaOrgEntityProvider.fromConfig(oktaConfig, {
    logger,
    userNamingStrategy: 'strip-domain-email',
    groupNamingStrategy: 'kebab-case-name',
    groupTransformer: myGroupTransformer,
  });

Set up the legacy backend system

Edit your catalog backend plugin where CatalogBuilder is created. Add the provider and start it

Copy
import { CatalogBuilder } from '@backstage/plugin-catalog-backend';
import { OktaOrgEntityProvider } from '@roadiehq/catalog-backend-module-okta';

export default async function createPlugin(env: PluginEnvironment) {
  const builder = await CatalogBuilder.create(env);

  const orgProvider = OktaOrgEntityProvider.fromConfig(env.config, {
    logger: env.logger,
    userNamingStrategy: 'strip-domain-email',
    groupNamingStrategy: 'kebab-case-name',
  });

  builder.addEntityProvider(orgProvider);

  const { processingEngine, router } = await builder.build();

  orgProvider.run();

  await processingEngine.start();

  return router;
}

You can tune membership resolution parallelism. The default tries two hundred fifty groups at a time. Set chunk size if you need a different value

Copy
const factory: EntityProviderFactory = (oktaConfig: Config) =>
  OktaOrgEntityProvider.fromConfig(oktaConfig, {
    logger,
    userNamingStrategy: 'strip-domain-email',
    groupNamingStrategy: 'kebab-case-name',
    chunkSize: 100,
  });

You can choose naming strategies

User strategies in code values id kebab-case-email strip-domain-email slugify

Group strategies in code values id kebab-case-name profile-name

Be careful with profile-name for groups. The Okta field can contain characters that do not meet Backstage entity name rules

There is no frontend package for this module. It feeds the Catalog with User and Group entities. Use your existing Catalog pages to browse them after the backend starts

Set up Backstage in minutes with Roadie