Skip to main content
Version: Next

OIDC provider from scratch

info

This documentation is written for the new backend system which is the default since Backstage version 1.24. If you are still on the old backend system, you may want to read its own article instead, and consider migrating!

This section shows how to enable and use the Backstage OIDC provider.

Summary

OIDC is a protocol which has numerous implementations. It's likely that many of your users won't know what the OIDC protocol is, but they will recognise your OIDC implementation. Backstage supplies a generic oidc authorization strategy. You should re-badge this with the name and branding of your OIDC implementation, so that your users will recognise it on the Backstage sign-in page.

For example, if your organization uses Keycloak, you would re-badge the OIDC provider as Keycloak and tell users to Sign In using Keycloak.

Steps

The Backstage OIDC provider is not enabled by default. You need to manually enable the provider, and tell it which OIDC server you want to use.

To enable the Backstage OIDC provider:

  • Create an API reference to identify the provider.
  • Create the API factory that will handle the authentication.
  • Add or reuse an auth provider so you can authenticate.
  • Add or reuse a resolver to handle the result from the authentication.
  • Configure the provider to access your 3rd party auth solution.
  • Add the provider to the Backstage sign-in page.

For simplicity, we assume that you only have a single OIDC provider in your Backstage installation. (If you need to have multiple OIDC providers in Backstage, the steps will be different.)

We'll explain each step more in detail next.

The API Reference

An API reference exists to enable Dependency Injection. (See Utility APIs for an extended explanation.)

In this example, we'll create the API ref directly in the packages/app/src/apis.ts file. It is not a requirement to put the ref in this file. Any location will do as long as it's available to be imported to where the API factory is, as well as easily accessible to the rest of the application so any package and plugin can inject the API instance when necessary.

export const keycloakAuthApiRef: ApiRef<
OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
> = createApiRef({
id: 'auth.keycloak',
});

The id of the API ref can be anything you want, as long as it doesn't conflict with other refs. Backstage recommends to use a custom name that references your custom provider.

TypeScript Note

As we're exporting this API reference, as well as the TypeScript types, we need to be able to import this reference anywhere in the app. The types will tell TypeScript what instance we're getting from DI when injecting the API. In this case we are defining an API for authentication, so we tell TS that this instance complies with 4 API interfaces:

  • The OIDC API that will handle authentication.
  • Profile API for requesting user profile info from the auth provider in question.
  • Backstage identity API to handle and associate the user profile with backstage identity.
  • Session API, to handle the session the user will have while signed in.

The API Factory (and auth provider)

The Backstage API factories are part of the Backstage Dependency Injection system. The factory function runs once, when something in your Backstage app first attempts to use an instance of the API it provides. The instance is then cached by the DI system for subsequent lookups.

Let's add a new API factory to the apis array in the packages/app/src/apis.ts file. We will tell it to use the OIDC auth provider internally.

packages/app/src/apis.ts
import { OAuth2 } from '@backstage/core-app-api';

export const apis: AnyApiFactory[] = [
createApiFactory({
api: keycloakAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
// delegate auth to the OAuth2 strategy
OAuth2.create({
configApi,
discoveryApi,
oauthRequestApi,
provider: {
// this value MUST be 'oidc'
// it maps our Keycloak-branded sign-in provider onto Backstage's generic OIDC auth strategy
id: 'oidc',
title: 'Keycloak',
icon: () => null,
},
environment: configApi.getOptionalString('auth.environment'),
defaultScopes: ['openid', 'profile', 'email'],
popupOptions: {
// optional, used to customize sign-in window size
size: {
fullscreen: true,
},
/**
* or specify popup width and height
* size: {
width: 1000,
height: 1000,
}
*/
},
}),
}),
];

The Resolver

Resolvers exist to map the user identity from the 3rd party (in this case Keycloak) to the Backstage user identity.

The default OIDC provider has a choice of built-in resolvers, here is how you configure them:

app-config.yaml
auth:
environment: development
providers:
oidc:
development:
# ...
signIn:
resolvers:
- resolver: emailMatchingUserEntityProfileEmail

If none of the built-in resolvers are suitable, you can alternatively write a custom resolver. See an example below:

in packages/backend/src/index.ts
import { createBackendModule } from '@backstage/backend-plugin-api';
import {
authProvidersExtensionPoint,
createOAuthProviderFactory,
} from '@backstage/plugin-auth-node';
import { oidcAuthenticator } from '@backstage/plugin-auth-backend-module-oidc-provider';

const myAuthProviderModule = createBackendModule({
// This ID must be exactly "auth" because that's the plugin it targets
pluginId: 'auth',
// This ID must be unique, but can be anything
moduleId: 'keycloak-auth-provider',
register(reg) {
reg.registerInit({
deps: { providers: authProvidersExtensionPoint },
async init({ providers }) {
providers.registerProvider({
// This ID must match the actual provider config, e.g. addressing
// auth.providers.keycloak means that this must be "keycloak".
providerId: 'keycloak',
// Use createProxyAuthProviderFactory instead if it's one of the proxy
// based providers rather than an OAuth based one
factory: createOAuthProviderFactory({
// For more info about authenticators please see https://backstage.io/docs/auth/add-auth-provider/#adding-an-oauth-based-provider
authenticator: oidcAuthenticator,
async signInResolver(info, ctx) {
const userRef = stringifyEntityRef({
kind: 'User',
name: info.result.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
},
});
},
}),
});
},
});
},
});
//...
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(myAuthProviderModule);
//...

For a more detailed explanation about resolvers check the Identity Resolver page.

The Configuration

We will now configure our Keycloak-branded OIDC Auth Provider in Backstage, so that it can talk to our Keycloak server.

The first step is to register an OIDC client app for Backstage in your Keycloak server.

Then we need to configure the provider. Based on the provider's code in plugins/auth-backend/src/providers/oidc/provider.ts we need the following parameters in the app-config.yaml:

app-config.yaml
auth:
environment: development
session:
secret: ${AUTH_SESSION_SECRET}
providers:
oidc:
development:
metadataUrl: https://example.com/.well-known/openid-configuration
clientId: ${AUTH_OIDC_CLIENT_ID}
clientSecret: ${AUTH_OIDC_CLIENT_SECRET}

Anything enclosed in ${} can be replaced directly in the YAML, or provided as environment variables.

Required Parameters

These parameters must always be set.

  • clientId: Grab from the Overview page.
  • clientSecret: Can only be seen when creating the secret, if you lose it you'll need a new secret.
  • metadataUrl: In Overview > Endpoints tab, grab OpenID Connect metadata document URL.

The OIDC provider also requires the auth.session.secret to be set.

Optional Parameters

These parameters have implicit default values. Don't override them unless you know what you're doing.

  • authorizationUrl and tokenUrl: Open the metadataUrl in a browser, that json will hold these 2 urls somewhere in there.
  • tokenEndpointAuthMethod
  • tokenSignedResponseAlg
  • scope: Only used if we didn't specify defaultScopes in the provider's factory, basically the same thing.
  • prompt: Recommended to use auto so the browser will request sign-in to the IDP if the user has no active session.
  • sessionDuration: Lifespan of the user session.
Config Reloading

Backstage does not yet support hot reloading of auth provider configuration. Any changes to this YAML file require a restart of Backstage.

The Sign-In Page

The last step is to add the provider to the sign-in page, so users can sign in with your new provider.

If you are using the standard Backstage SignInPage component, you can just add it to the providers array like this:

in packages/app/src/identityProviders.ts
export const providers = [
// other providers...
{
id: 'keycloak-auth-provider',
title: 'Keycloak',
message: 'Sign In using Keycloak',
apiRef: keycloakAuthApiRef,
},
];
Note

These steps apply to most auth providers. The main difference between providers will be the contents of the API factory, the code in the Auth Provider Factory, the resolver, and the different variables each provider needs in the YAML config or env variables.