Project: Backstage (App Definition and Development / Application Definition & Image Build)
File: oidc.md

id: oidc title: OIDC provider from scratch description: This section shows how to use an OIDC provider from scratch, same steps apply for custom providers. This section shows how to use an OIDC provider from scratch, same steps apply for custom providers. Please note these steps are for using a provider, not how to implement one, and Backstage recommends creating custom providers specific to the IDP, so we'll use a `azureOIDC` provider throughout this example, feel free to change any of those refs to your provider name. To add providers not enabled by default like OIDC, we need to follow some steps, we assume you already have a sign in page to which we'll add the provider so users can sign in through the provider. In simple steps here's how you enable the 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 sign in page so users can login with it. We'll explain each step more in detail next. An API reference exist for the sake of Dependency Injection, check for extended explanation. In this OIDC example, we'll create the API reference directly in the `packages/app/src/apis.ts` file, it is not a requirement to put the reference 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. An example of such would be when you use an auth provider from a library installed with NPM, or any other library repository, you would import the API ref from the library. ```ts export const azureOIDCAuthApiRef: ApiRef< OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi = createApiRef({ id: 'auth.my-custom-provider', }); ``` Please note a few things, the ID 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, for example we are using OIDC protocol with Azure, so we could use something like `auth.azure.oidc` as well. Also we're exporting this reference, as well as the `typings`, we need to be able to import this reference anywhere in the app, and the `typings` 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 OICD 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 logged

A factory is a function that can take some parameters or dependencies and return an instance of something, in our case it will be a function that requests some backstage APIs and use them to create an instance of an OIDC API provider. Please note that this function only runs (creates the instance) when somewhere else in the app you request the DI to give you an instance of the OIDC provider using the API ref defined above, and the DI will only run this function the first time, from then on any other DI injection will just receive the same instance created the first time, basically the instance is cached by the DI library, a singleton. Let's add our OIDC API factory to the APIs array in the `packages/app/src/apis.ts` file: ```ts title="packages/app/src/apis.ts" / highlight-add-next-line / import { OAuth2 } from '@backstage/core-app-api'; export const apis: AnyApiFactory[] = [ / highlight-add-start / createApiFactory({ api: azureOIDCAuthApiRef, deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef, configApi: configApiRef, }, factory: ({ discoveryApi, oauthRequestApi, configApi }) => OAuth2.create({ configApi, discoveryApi, oauthRequestApi, provider: { id: 'my-auth-provider', title: 'My custom auth provider', icon: () => null, }, environment: configApi.getOptionalString('auth.environment'), defaultScopes: ['openid', 'profile', 'email'], popupOptions: { // optional, used to customize login in popup size size: { fullscreen: true, }, / or specify popup width and height size: { width: 1000, height: 1000, } */ }, }), }), / highlight-add-end / // .. ]; ``` Please note we're importing the `OAuth2` class from `@backstage/core-app-api` effectively delegating the authentication to it. Also we're using the `my-auth-provider` ID to tell `OAuth2` to use the auth provider we'll define in the next section, and added the default scopes to request ID, profile, email and user read permissions. The Auth Provider is responsible for authenticating with the 3rd party service, and give us back the credentials, here's where you pick which protocol to use, be it Auth0, OAuth2, OIDC, SAML or any other that your 3rd party IDP provider supports. For this example we'll use OIDC, we pass a factory to the `providerFactories` object with the ID you picked to represent the Auth provider, this ID has to match with the provider's `id` inside the API factory, the yaml config provider key under `auth.providers`, and the callback URI provider segment (you'll have to configure your IDP to handle the callback URI properly). ```ts export default async function createPlugin( env: PluginEnvironment, ): Promise<Router> { return await createRouter({ logger: env.logger, config: env.config, database: env.database, discovery: env.discovery, tokenManager: env.tokenManager, providerFactories: { ...defaultAuthProviderFactories, / highlight-add-next-line / 'my-auth-provider': providers.oidc.create({}), }, // .. }) ``` Resolvers exist to map user identity from the 3rd party (in this case an azure IDP provider) to the backstage user identity, for a detailed explanation check the page, it explains how to write a custom resolver as well as linking the built in resolvers of backstage. The default OIDC provider does not support SignIn, we need to add such support by adding a resolver for a SignIn

The OIDC provider doesn't provide any build-in resolvers, so we'll need to define our own: ```ts import { DEFAULT_NAMESPACE, / highlight-add-next-line / stringifyEntityRef, } from '@backstage/catalog-model'; export default async function createPlugin( env: PluginEnvironment, ): Promise<Router> { return await createRouter({ logger: env.logger, config: env.config, database: env.database, discovery: env.discovery, tokenManager: env.tokenManager, providerFactories: { ...defaultAuthProviderFactories, 'my-auth-provider': providers.oidc.create({ / highlight-add-start / signIn: { resolver(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 }, }); }, }, / highlight-add-end / }), }, // .. }) ``` Since we are using our custom OIDC Auth Provider, we need to add a configuration based on the provider used, in this case based on OIDC protocol (remember the 3rd party has to support the protocol). In this example we'll configure OIDC with `my-auth-provider`, to do so we need to in the Azure console, the only difference is that the `http://localhost:7007/api/auth/microsoft/handler/frame` URL needs to change to `http://localhost:7007/api/auth/my-auth-provider/handler/frame`. Then we need to configure the env variables for the provider, based on the provider's code in `plugins/auth-backend/src/providers/oidc/provider.ts` we need the following variables in the `app-config.yaml`: ```yaml title="app-config.yaml" auth: environment: development session: secret: ${SESSION_SECRET} providers: my-auth-provider: development: metadataUrl: https://example.com/.well-known/openid-configuration clientId: ${AUTHMYCLIENT_ID} clientSecret: ${AUTHMYCLIENT_SECRET} ``` Anything enclosed in `${}` can be replaced directly in the yaml, or provided as environment variables, the way you obtain all these except `scope` and `prompt` is to check the App Registration you created: `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. `authorizationUrl` and `tokenUrl`: Open the `metadataUrl` in a browser, that json will hold these 2 urls somewhere in there. `tokenEndpointAuthMethod`: Don't define it, use the default unless you know what it does. `tokenSignedResponseAlg`: Don't define it, use the default unless you know what it does. `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 login to the IDP if the user has no active session. Note that for the time being, any change in this yaml file requires a restart of the app, also you need to have the `session.secret` part to use OIDC (some other providers might need this as well) to support user sessions. The last step is to add the provider to the `SignInPage` so users can sign in with your new provider, please follow the docs, here's where you import and use the API reference we defined earlier. These steps apply to most if not all the providers, including custom providers, the main difference between different 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.