Backstage Oidc Provider

Backstage is an excellent dashboard tool for developers. Its plugin system makes it particularly flexible. However, configuring these plugins can be tedious. Backstage is constantly evolving, which sometimes makes it difficult for the documentation to keep up. This has been the case since the migration to the new Backend system. Backstage provides several plugins for connecting to most identity providers via OIDC. However, if you want to connect Backstage to a custom provider, like an enterprise Keycloak, the documentation is not very clear. That’s what we will explore in this post.

OIDC plugin configuration

Backstage provides an OIDC provider but no ready-to-use plugin. We will need to create a module to extend Backstage’s Auth plugin with our OIDC support. We use the Backstage CLI for this:

1
2
3
backstage-cli new --select backend-module
# ? Enter the ID of the plugin [required] : auth
# ? Enter the ID of the module [required] : oidc

Backstage has now generated a plugin structure in the plugins/auth-backend-module-oidc folder. Let’s start by adding the OIDC provider dependency provided by Backstage:

1
2
cd plugins/auth-backend-module-oidc
yarn add @backstage/plugin-auth-backend-module-oidc-provider

Once this is done, in the src/module.ts file, replace the content with the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { oidcAuthenticator } from '@backstage/plugin-auth-backend-module-oidc-provider';
import { authProvidersExtensionPoint, createOAuthProviderFactory } from '@backstage/plugin-auth-node'
import {
  coreServices,
  createBackendModule,
} from '@backstage/backend-plugin-api';
import { DEFAULT_NAMESPACE, stringifyEntityRef } from '@backstage/catalog-model';

export const authModuleOidc = createBackendModule({
  pluginId: 'auth',
  moduleId: 'oidc',
  register(reg) {
    reg.registerInit({
      deps: { logger: coreServices.logger, providers: authProvidersExtensionPoint },
      async init({ logger, providers }) {
        logger.info('Registering OIDC provider!');
        providers.registerProvider({
          providerId: 'sso-auth-provider',
          factory: createOAuthProviderFactory({
            authenticator: oidcAuthenticator,
            async signInResolver(info, ctx) {
              const userRef = stringifyEntityRef({
                kind: 'User',
                name: info.result.fullProfile.userinfo.sub,
                namespace: DEFAULT_NAMESPACE
              });
              return ctx.issueToken({
                claims: {
                  sub: userRef, // The user's own identity
                  ent: [userRef], // A list of identities that the user claims ownership through
                }
              });
            }
          })
        });
      },
    });
  },
});

Our module is now operational. Let’s check that the module is properly enabled in packages/backend/index.ts. The following line should be present:

1
backend.add(import('backstage-plugin-auth-backend-module-oidc'))

Api Factory configuration

Now, we will reference the plugin as the authentication API in the front end. To do this, add the following code in the packages/app/src/apis.ts file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import {
  AnyApiFactory,
  ApiRef,
  BackstageIdentityApi,
  configApiRef,
  createApiFactory,
  createApiRef,
  discoveryApiRef,
  oauthRequestApiRef,
  OpenIdConnectApi,
  ProfileInfoApi,
  SessionApi,
} from '@backstage/core-plugin-api';
import { OAuth2 } from '@backstage/core-app-api';

export const oidcAuthApiRef: ApiRef <
  OpenIdConnectApi & // The OIDC API that will handle authentification
  ProfileInfoApi & // Profile API for requesting user profile info from the auth provider in question
  BackstageIdentityApi & // Backstage Identity API to handle and associate the user profile with backstage identity
  SessionApi // Sesssion API, to handle the session the user will have while logged in
> = createApiRef({
  id: 'sso-auth-provider' // Can be anything as long as it doesn't conflict with other API ref IDs
})

export const apis: AnyApiFactory[] = [
  createApiFactory({
    api: oidcAuthApiRef,
    deps: {
      discoveryApi: discoveryApiRef,
      oauthRequestApi: oauthRequestApiRef,
      configApi: configApiRef
    },
    factory: ({ discoveryApi, oauthRequestApi, configApi }) => Oauth2.create({
      configApi,
      discoveryApi,
      oauthRequestApi,
      provider: {
        id: 'sso-auth-provider',
        title: 'Enterprise SSO',
        icon: () => null
      },
      environment: configApi.getOptionalString('auth.environment'),
      defaultScopes: ['openId', 'profile', 'email'],
      popupOptions: {
        size: {
          // fullscreen: true
          // or specify popup width and height
          width: 1000,
          height: 1000,
        }
      }
    })
  }),
  // Other createApiFactory
]

Adding the connection button into the interface

Now let’s go to the packages/app/src/App.tsx file to add the login button to Backstage.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { apis, oidcAuthApiRef } from './apis';
import { configApiRef, useApi } from '@backstage/core-plugin-api';

const app = createApp({
  // Other stuff...
  components: {
    SignInPage: props => (<SignInPage {...props} auto providers={ useApi(configApiRef).getString('auth.environment') === 'development' ? ['guest'] : [{
      id: 'sso-auth-provider',
      title: 'Enterprise SSO',
      message: 'Sign in with Enterprise SSO',
      apiRef: oidcAuthApiRef
    }]}/>)
  }
  // Other stuff...
})

and in the app-config.production.yaml file:

1
2
auth:
  environment: development

et dans le fichier app-config.production.yaml

1
2
3
4
5
6
7
8
9
auth:
  environment: production
  providers:
    guest: {}
    sso-auth-provider:
      production:
        metadataUrl: ${AUTH_REALM_URL}/.well-known/openid-configuration
        clientId: ${AUTH_MY_CLIENT_ID}
        clientSecret: ${AUTH_MY_CLIENT_SECRET}

With the above configuration, authentication will occur as a guest in development mode and via SSO in production mode. For more information, you can check the official documentation

Congratulations, your Backstage application now supports OIDC login. Also take a look at the Keycloak integration for automatique import of user and groups into Backstage.

0%