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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
Focus on using Backstage, rather than building and maintaining it.