From d149c09cc82dc0635518be3b25c3ab90b9e8e6c2 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Thu, 15 Sep 2022 13:12:37 -0500 Subject: [PATCH 01/13] docs: Add OIDC section Signed-off-by: Carlos Esteban Lopez --- docs/auth/oidc.md | 235 ++++++++++++++++++++++++++++++++++++++++ microsite/sidebars.json | 1 + mkdocs.yml | 1 + 3 files changed, 237 insertions(+) create mode 100644 docs/auth/oidc.md diff --git a/docs/auth/oidc.md b/docs/auth/oidc.md new file mode 100644 index 0000000000..eb8a95afd5 --- /dev/null +++ b/docs/auth/oidc.md @@ -0,0 +1,235 @@ +--- +id: oidc +title: OIDC provider from scratch +description: This section shows how to use an OIDC provider from scrath, same steps apply for custom providers. +--- + +This section shows how to use an OIDC provider from scrath, same steps apply for custom +providers. Please note these steps are for using a provider, not how to implement one. + +## Summary + +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 a resolver so you can 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. + +### The API reference + +An API reference exist for the sake of **Dependency Injection**, it's basically an ID to +help backstage DI to identify the provider and either create a new instance of the +class/object/API identified by such ID, or if it has already been created, return the +existing instance, that way we have a singleton instance of the provider. + +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 oidcAuthApiRef: ApiRef< + OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +> = createApiRef({ + id: 'core.auth.oidc', +}); +``` + +Please note a few things, the ID can be anything you want as long as it doesn't conflict +with other refs, 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 in. + +### The API Factory + +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 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 factory to the APIs array in the `packages/app/src/apis.ts` file: + +```diff ++ import { OAuth2 } from '@backstage/core-app-api'; + +export const apis: AnyApiFactory[] = [ ++ createApiFactory({ ++ api: oidcAuthApiRef, ++ deps: { ++ discoveryApi: discoveryApiRef, ++ oauthRequestApi: oauthRequestApiRef, ++ configApi: configApiRef, ++ }, ++ factory: ({ discoveryApi, oauthRequestApi, configApi }) => ++ OAuth2.create({ ++ discoveryApi, ++ oauthRequestApi, ++ provider: { ++ id: 'oidc', ++ title: 'OIDC provider', ++ icon: () => null, ++ }, ++ environment: configApi.getOptionalString('auth.environment'), ++ defaultScopes: [ ++ 'openid', ++ 'profile', ++ 'email', ++ ], ++ }), ++ }), + +``` + +Please note we're importing the `OAuth2` class from `@backstage/core-app-api` effectively +delegating the authentication to it (yes it can handle OIDC as well). Also we're using +the `oidc` ID to tell `OAuth2` to use the OIDC protocol, and added the default scopes to +request ID, profile, email and user read permissions. + +### The Resolver + +Resolvers exist to map user identity from the 3rd party (in this case OIDC provider) to +the backstage user identity, for a detailed explanation check the [Identity Resolver][1] +page, it explains how to write a custom resolver as well as linking the built in resolvers +of backstage. + +As an example if you're setting up OIDC provider with Microsoft, you could use the built +in microsoft resolvers, or create one yourself in `packages/backend/src/plugins/auth.ts`: + +```diff +import { + DEFAULT_NAMESPACE, ++ stringifyEntityRef, +} from '@backstage/catalog-model'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + return await createRouter({ + logger: env.logger, + config: env.config, + database: env.database, + discovery: env.discovery, + tokenManager: env.tokenManager, + providerFactories: { + ...defaultAuthProviderFactories, ++ // oidc: providers.oidc.create({ ++ // signIn: { ++ // resolver: ++ // providers.microsoft.resolvers.emailMatchingUserEntityAnnotation() as any, ++ // }, ++ // }), ++ oidc: providers.oidc.create({ ++ signIn: { ++ resolver(info, ctx) { ++ const userRef = stringifyEntityRef({ ++ kind: 'User', ++ name: info.profile.email!, ++ namespace: DEFAULT_NAMESPACE, ++ }); ++ console.log(info, userRef); ++ return ctx.issueToken({ ++ claims: { ++ sub: userRef, // The user's own identity ++ ent: [userRef], // A list of identities that the user claims ownership through ++ }, ++ }); ++ }, ++ }, ++ }), + } +``` + +### The configuration + +We are using the `OAuth2` wrapper to delegate the authentication to the 3rd party using +the OIDC protocol, as such, it depends on the specific wrapper what has to be configured. + +As an example we'll configure OIDC with Microsoft, to do so we need to +[Create app registration][2] 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/oidc/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 +auth: + environment: development + ### Providing an auth.session.secret will enable session support in the auth-backend + session: + secret: ${SESSION_SECRET} + providers: + oidc: + # Note that you must define a session secret (see above) since the oidc provider requires session support. + # Note that by default, this provider will use the 'none' prompt which assumes that your are already logged on in the IDP. + # You should set prompt to: + # - auto: will let the IDP decide if you need to log on or if you can skip login when you have an active SSO session + # - login: will force the IDP to always present a login form to the user + development: + metadataUrl: ${AUTH_OIDC_METADATA_URL} + clientId: ${AUTH_OIDC_CLIENT_ID} + clientSecret: ${AUTH_OIDC_CLIENT_SECRET} + authorizationUrl: ${AUTH_OIDC_AUTH_URL} + tokenUrl: ${AUTH_OIDC_TOKEN_URL} + tokenSignedResponseAlg: ${AUTH_OIDC_TOKEN_SIGNED_RESPONSE_ALG} # default='RS256' + scope: ${AUTH_OIDC_SCOPE} # default='openid profile email' + prompt: ${AUTH_OIDC_PROMPT} # default=none (allowed values: auto, none, consent, login) +``` + +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 browser, that json will hold + these 2 urls somewhere in there. +- 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. + +### The Sign In provider + +The last step is to add the provider to the `SignInPage` so users can sign in with your +new provider, please follow the [Sing In Configuration][3] docs, here's where you import +and use the API ref we defined earlier. + +## Note + +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 factory, the code in +the resolver, and the different variables each provider needs in the YAML config or env +variables. + +[1]: https://backstage.io/docs/auth/identity-resolver +[2]: https://backstage.io/docs/auth/microsoft/provider#create-an-app-registration-on-azure +[3]: https://backstage.io/docs/auth/#sign-in-configuration diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 8e77594376..2cb7cfdc03 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -277,6 +277,7 @@ }, "auth/identity-resolver", "auth/oauth", + "auth/oidc.md", "auth/add-auth-provider", "auth/troubleshooting", "auth/glossary" diff --git a/mkdocs.yml b/mkdocs.yml index c0a8e3a182..b1f8399d78 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -162,6 +162,7 @@ nav: - Bitbucket: 'auth/bitbucket/provider.md' - Sign in resolvers: 'auth/identity-resolver.md' - OAuth and OpenID Connect: 'auth/oauth.md' + - OIDC provider from scratch: 'auth/oidc.md' - Contributing New Providers: 'auth/add-auth-provider.md' - Troubleshooting Auth: 'auth/troubleshooting.md' - Glossary: 'auth/glossary.md' From bae3d9e06f732d285ca757f8c7046518f1844e1b Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Thu, 15 Sep 2022 13:25:58 -0500 Subject: [PATCH 02/13] docs: Address Vale spelling errors Signed-off-by: Carlos Esteban Lopez --- docs/auth/oidc.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/auth/oidc.md b/docs/auth/oidc.md index eb8a95afd5..6fef6b9d4c 100644 --- a/docs/auth/oidc.md +++ b/docs/auth/oidc.md @@ -4,7 +4,7 @@ title: OIDC provider from scratch description: This section shows how to use an OIDC provider from scrath, same steps apply for custom providers. --- -This section shows how to use an OIDC provider from scrath, same steps apply for custom +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. ## Summary @@ -46,8 +46,8 @@ export const oidcAuthApiRef: ApiRef< ``` Please note a few things, the ID can be anything you want as long as it doesn't conflict -with other refs, 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 +with other refs, 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: @@ -115,7 +115,7 @@ page, it explains how to write a custom resolver as well as linking the built in of backstage. As an example if you're setting up OIDC provider with Microsoft, you could use the built -in microsoft resolvers, or create one yourself in `packages/backend/src/plugins/auth.ts`: +in Microsoft resolvers, or create one yourself in `packages/backend/src/plugins/auth.ts`: ```diff import { @@ -168,7 +168,7 @@ the OIDC protocol, as such, it depends on the specific wrapper what has to be co As an example we'll configure OIDC with Microsoft, to do so we need to [Create app registration][2] 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/microsoft/handler/frame` URL needs to change to `http://localhost:7007/api/auth/oidc/handler/frame`. Then we need to configure the env variables for the provider, based on the provider's code @@ -203,16 +203,16 @@ 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 +- `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 browser, that json will hold - these 2 urls somewhere in there. -- 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, +- `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. +- `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 +- `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. From f29577ba23e4590c791f5ce6140a3bd0c509404a Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Jaramillo Date: Mon, 19 Sep 2022 12:32:15 -0500 Subject: [PATCH 03/13] Update microsite/sidebars.json Co-authored-by: Jamie Klassen Signed-off-by: Carlos Esteban Lopez Jaramillo --- microsite/sidebars.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 2cb7cfdc03..d0c9a8592c 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -277,7 +277,7 @@ }, "auth/identity-resolver", "auth/oauth", - "auth/oidc.md", + "auth/oidc", "auth/add-auth-provider", "auth/troubleshooting", "auth/glossary" From b6e95ea96dca9b2492347b4ffdf9ccde4723a428 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Tue, 20 Sep 2022 10:34:23 -0500 Subject: [PATCH 04/13] docs: Address MR comments Signed-off-by: Carlos Esteban Lopez --- docs/auth/oidc.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/auth/oidc.md b/docs/auth/oidc.md index 6fef6b9d4c..bde1b4076d 100644 --- a/docs/auth/oidc.md +++ b/docs/auth/oidc.md @@ -23,10 +23,8 @@ We'll explain each step more in detail next. ### The API reference -An API reference exist for the sake of **Dependency Injection**, it's basically an ID to -help backstage DI to identify the provider and either create a new instance of the -class/object/API identified by such ID, or if it has already been created, return the -existing instance, that way we have a singleton instance of the provider. +An API reference exist for the sake of **Dependency Injection**, check [Utility APIs][4] +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 @@ -233,3 +231,4 @@ variables. [1]: https://backstage.io/docs/auth/identity-resolver [2]: https://backstage.io/docs/auth/microsoft/provider#create-an-app-registration-on-azure [3]: https://backstage.io/docs/auth/#sign-in-configuration +[4]: https://backstage.io/docs/api/utility-apis From 9c9938bbb5187c0f886f06f619d717f90acfe0e7 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Tue, 27 Sep 2022 10:45:21 -0500 Subject: [PATCH 05/13] docs: Remove commented diff per MR comments Signed-off-by: Carlos Esteban Lopez --- docs/auth/oidc.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/auth/oidc.md b/docs/auth/oidc.md index bde1b4076d..f1db4a1257 100644 --- a/docs/auth/oidc.md +++ b/docs/auth/oidc.md @@ -132,12 +132,6 @@ export default async function createPlugin( tokenManager: env.tokenManager, providerFactories: { ...defaultAuthProviderFactories, -+ // oidc: providers.oidc.create({ -+ // signIn: { -+ // resolver: -+ // providers.microsoft.resolvers.emailMatchingUserEntityAnnotation() as any, -+ // }, -+ // }), + oidc: providers.oidc.create({ + signIn: { + resolver(info, ctx) { From 200bdbfa2ef7cca3dbb3ac6ea2bcbee47468ef48 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Tue, 27 Sep 2022 16:17:41 -0500 Subject: [PATCH 06/13] docs: Make oidc provider example an specific provider to azure Signed-off-by: Carlos Esteban Lopez --- docs/auth/oidc.md | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/docs/auth/oidc.md b/docs/auth/oidc.md index f1db4a1257..c5d2d54c0c 100644 --- a/docs/auth/oidc.md +++ b/docs/auth/oidc.md @@ -1,11 +1,14 @@ --- id: oidc title: OIDC provider from scratch -description: This section shows how to use an OIDC provider from scrath, same steps apply for custom providers. +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. +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. ## Summary @@ -36,10 +39,10 @@ An example of such would be when you use an auth provider from a library install NPM, or any other library repository, you would import the API ref from the library. ```ts -export const oidcAuthApiRef: ApiRef< +export const azureOIDCAuthApiRef: ApiRef< OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi > = createApiRef({ - id: 'core.auth.oidc', + id: 'core.auth.azureOIDC', }); ``` @@ -74,7 +77,7 @@ Let's add our OIDC factory to the APIs array in the `packages/app/src/apis.ts` f export const apis: AnyApiFactory[] = [ + createApiFactory({ -+ api: oidcAuthApiRef, ++ api: azureOIDCAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, @@ -85,7 +88,7 @@ export const apis: AnyApiFactory[] = [ + discoveryApi, + oauthRequestApi, + provider: { -+ id: 'oidc', ++ id: 'oidc', // This has to be 'oidc' or OAuth2 will not use oidc protocol + title: 'OIDC provider', + icon: () => null, + }, @@ -107,13 +110,13 @@ request ID, profile, email and user read permissions. ### The Resolver -Resolvers exist to map user identity from the 3rd party (in this case OIDC provider) to -the backstage user identity, for a detailed explanation check the [Identity Resolver][1] -page, it explains how to write a custom resolver as well as linking the built in resolvers -of backstage. +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 +[Identity Resolver][1] page, it explains how to write a custom resolver as well as +linking the built in resolvers of backstage. -As an example if you're setting up OIDC provider with Microsoft, you could use the built -in Microsoft resolvers, or create one yourself in `packages/backend/src/plugins/auth.ts`: +As an example if you're setting up OIDC provider with Azure IDP, you could reuse +the built in resolvers, or create one yourself in `packages/backend/src/plugins/auth.ts`: ```diff import { @@ -132,7 +135,7 @@ export default async function createPlugin( tokenManager: env.tokenManager, providerFactories: { ...defaultAuthProviderFactories, -+ oidc: providers.oidc.create({ ++ azureOIDC: providers.oidc.create({ + signIn: { + resolver(info, ctx) { + const userRef = stringifyEntityRef({ @@ -158,7 +161,7 @@ export default async function createPlugin( We are using the `OAuth2` wrapper to delegate the authentication to the 3rd party using the OIDC protocol, as such, it depends on the specific wrapper what has to be configured. -As an example we'll configure OIDC with Microsoft, to do so we need to +As an example we'll configure OIDC with `azureOIDC`, to do so we need to [Create app registration][2] 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/oidc/handler/frame`. @@ -174,7 +177,7 @@ auth: session: secret: ${SESSION_SECRET} providers: - oidc: + azureOIDC: # Note that you must define a session secret (see above) since the oidc provider requires session support. # Note that by default, this provider will use the 'none' prompt which assumes that your are already logged on in the IDP. # You should set prompt to: From a29ca45d9c2d17a31a402d1c26f63e9b3af9f314 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Wed, 28 Sep 2022 19:09:24 -0500 Subject: [PATCH 07/13] docs: Separate Auth Provider concept from API Provider and resolvers Signed-off-by: Carlos Esteban Lopez --- docs/auth/oidc.md | 95 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 72 insertions(+), 23 deletions(-) diff --git a/docs/auth/oidc.md b/docs/auth/oidc.md index c5d2d54c0c..bfc77b2163 100644 --- a/docs/auth/oidc.md +++ b/docs/auth/oidc.md @@ -18,7 +18,8 @@ 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 a resolver so you can handle the result from 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. @@ -42,12 +43,16 @@ NPM, or any other library repository, you would import the API ref from the libr export const azureOIDCAuthApiRef: ApiRef< OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi > = createApiRef({ - id: 'core.auth.azureOIDC', + 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, also we're exporting this reference, as well as the `typings`, we need to +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 @@ -62,7 +67,7 @@ interfaces: 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 provider. +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 @@ -70,7 +75,7 @@ defined above, and the DI will only run this function the first time, from then 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 factory to the APIs array in the `packages/app/src/apis.ts` file: +Let's add our OIDC API factory to the APIs array in the `packages/app/src/apis.ts` file: ```diff + import { OAuth2 } from '@backstage/core-app-api'; @@ -88,8 +93,8 @@ export const apis: AnyApiFactory[] = [ + discoveryApi, + oauthRequestApi, + provider: { -+ id: 'oidc', // This has to be 'oidc' or OAuth2 will not use oidc protocol -+ title: 'OIDC provider', ++ id: 'my-auth-provider', ++ title: 'My custom auth provider', + icon: () => null, + }, + environment: configApi.getOptionalString('auth.environment'), @@ -104,9 +109,38 @@ export const apis: AnyApiFactory[] = [ ``` Please note we're importing the `OAuth2` class from `@backstage/core-app-api` effectively -delegating the authentication to it (yes it can handle OIDC as well). Also we're using -the `oidc` ID to tell `OAuth2` to use the OIDC protocol, and added the default scopes to -request ID, profile, email and user read permissions. +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 + +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). + +```diff +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + 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({ ++ }), + } +``` ### The Resolver @@ -115,8 +149,15 @@ provider) to the backstage user identity, for a detailed explanation check the [Identity Resolver][1] 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 request. + As an example if you're setting up OIDC provider with Azure IDP, you could reuse -the built in resolvers, or create one yourself in `packages/backend/src/plugins/auth.ts`: +the built in resolvers, or create one yourself in `packages/backend/src/plugins/auth.ts`. + +At the time of writing the default OIDC provider doesn't have resolvers, we could reuse +one from the Microsoft Auth provider, but we'll create one from scratch to ilustrate the +scenario where the existing resolvers from other providers don't match what we want: ```diff import { @@ -135,7 +176,7 @@ export default async function createPlugin( tokenManager: env.tokenManager, providerFactories: { ...defaultAuthProviderFactories, -+ azureOIDC: providers.oidc.create({ + 'my-auth-provider': providers.oidc.create({ + signIn: { + resolver(info, ctx) { + const userRef = stringifyEntityRef({ @@ -143,7 +184,6 @@ export default async function createPlugin( + name: info.profile.email!, + namespace: DEFAULT_NAMESPACE, + }); -+ console.log(info, userRef); + return ctx.issueToken({ + claims: { + sub: userRef, // The user's own identity @@ -152,19 +192,26 @@ export default async function createPlugin( + }); + }, + }, -+ }), + }), } ``` +We could replace the whole `signIn` object with +`providers.microsoft.resolvers.emailLocalPartMatchingUserEntityName() as any` and as long +as you have the correct user configured in the catalog it should work, note the `as any` +part, TS will error out because of typings, so we type cast it to any, it should still +work given the response of an OIDC or OAuth2 request to microsoft is very similar. + ### The configuration -We are using the `OAuth2` wrapper to delegate the authentication to the 3rd party using -the OIDC protocol, as such, it depends on the specific wrapper what has to be configured. +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). -As an example we'll configure OIDC with `azureOIDC`, to do so we need to +In this example we'll configure OIDC with `my-auth-provider`, to do so we need to [Create app registration][2] 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/oidc/handler/frame`. +`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 @@ -177,7 +224,7 @@ auth: session: secret: ${SESSION_SECRET} providers: - azureOIDC: + my-auth-provider: # Note that you must define a session secret (see above) since the oidc provider requires session support. # Note that by default, this provider will use the 'none' prompt which assumes that your are already logged on in the IDP. # You should set prompt to: @@ -210,7 +257,9 @@ check the App Registration you created: - `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. +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 Sign In provider @@ -221,9 +270,9 @@ and use the API ref we defined earlier. ## Note 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 factory, the code in -the resolver, and the different variables each provider needs in the YAML config or env -variables. +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. [1]: https://backstage.io/docs/auth/identity-resolver [2]: https://backstage.io/docs/auth/microsoft/provider#create-an-app-registration-on-azure From 92960f8fc2c101b3dc864258c66931ac1bbdb3f6 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Jaramillo Date: Sun, 2 Oct 2022 19:17:54 -0500 Subject: [PATCH 08/13] Update docs/auth/oidc.md Co-authored-by: Patrik Oldsberg Signed-off-by: Carlos Esteban Lopez Jaramillo --- docs/auth/oidc.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/auth/oidc.md b/docs/auth/oidc.md index bfc77b2163..0d71e27cf3 100644 --- a/docs/auth/oidc.md +++ b/docs/auth/oidc.md @@ -225,7 +225,6 @@ auth: secret: ${SESSION_SECRET} providers: my-auth-provider: - # Note that you must define a session secret (see above) since the oidc provider requires session support. # Note that by default, this provider will use the 'none' prompt which assumes that your are already logged on in the IDP. # You should set prompt to: # - auto: will let the IDP decide if you need to log on or if you can skip login when you have an active SSO session From b66fa896f3e0cb99c1a80b1c799c9836a0396455 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Jaramillo Date: Sun, 2 Oct 2022 19:18:18 -0500 Subject: [PATCH 09/13] Update docs/auth/oidc.md Co-authored-by: Patrik Oldsberg Signed-off-by: Carlos Esteban Lopez Jaramillo --- docs/auth/oidc.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/auth/oidc.md b/docs/auth/oidc.md index 0d71e27cf3..b82141a35d 100644 --- a/docs/auth/oidc.md +++ b/docs/auth/oidc.md @@ -196,11 +196,6 @@ export default async function createPlugin( } ``` -We could replace the whole `signIn` object with -`providers.microsoft.resolvers.emailLocalPartMatchingUserEntityName() as any` and as long -as you have the correct user configured in the catalog it should work, note the `as any` -part, TS will error out because of typings, so we type cast it to any, it should still -work given the response of an OIDC or OAuth2 request to microsoft is very similar. ### The configuration From 028eae04e224372858698f61be7b0b633b61c524 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Jaramillo Date: Sun, 2 Oct 2022 19:18:52 -0500 Subject: [PATCH 10/13] Update docs/auth/oidc.md Co-authored-by: Patrik Oldsberg Signed-off-by: Carlos Esteban Lopez Jaramillo --- docs/auth/oidc.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/docs/auth/oidc.md b/docs/auth/oidc.md index b82141a35d..5c68043d7a 100644 --- a/docs/auth/oidc.md +++ b/docs/auth/oidc.md @@ -152,12 +152,7 @@ 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 request. -As an example if you're setting up OIDC provider with Azure IDP, you could reuse -the built in resolvers, or create one yourself in `packages/backend/src/plugins/auth.ts`. - -At the time of writing the default OIDC provider doesn't have resolvers, we could reuse -one from the Microsoft Auth provider, but we'll create one from scratch to ilustrate the -scenario where the existing resolvers from other providers don't match what we want: +The OIDC provider doesn't provide any build-in resolvers, so we'll need to define our own: ```diff import { From 90101b092e15748b8e842a2a358e60fba6743881 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Jaramillo Date: Sun, 2 Oct 2022 19:21:55 -0500 Subject: [PATCH 11/13] Update docs/auth/oidc.md Co-authored-by: Patrik Oldsberg Signed-off-by: Carlos Esteban Lopez Jaramillo --- docs/auth/oidc.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/auth/oidc.md b/docs/auth/oidc.md index 5c68043d7a..321559440e 100644 --- a/docs/auth/oidc.md +++ b/docs/auth/oidc.md @@ -215,10 +215,6 @@ auth: secret: ${SESSION_SECRET} providers: my-auth-provider: - # Note that by default, this provider will use the 'none' prompt which assumes that your are already logged on in the IDP. - # You should set prompt to: - # - auto: will let the IDP decide if you need to log on or if you can skip login when you have an active SSO session - # - login: will force the IDP to always present a login form to the user development: metadataUrl: ${AUTH_OIDC_METADATA_URL} clientId: ${AUTH_OIDC_CLIENT_ID} From b04f86e85528199553ca5c948424b412aa5b4b51 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Mon, 3 Oct 2022 11:46:16 -0500 Subject: [PATCH 12/13] docs: Use correct user info name Signed-off-by: Carlos Esteban Lopez --- docs/auth/oidc.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/auth/oidc.md b/docs/auth/oidc.md index 321559440e..732a8d9c0d 100644 --- a/docs/auth/oidc.md +++ b/docs/auth/oidc.md @@ -176,7 +176,7 @@ export default async function createPlugin( + resolver(info, ctx) { + const userRef = stringifyEntityRef({ + kind: 'User', -+ name: info.profile.email!, ++ name: info.result.userinfo.sub, + namespace: DEFAULT_NAMESPACE, + }); + return ctx.issueToken({ @@ -191,7 +191,6 @@ export default async function createPlugin( } ``` - ### The configuration Since we are using our custom OIDC Auth Provider, we need to add a configuration based From cde64cb7a3d67d78dced229800067247ddb343a4 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Jaramillo Date: Mon, 3 Oct 2022 11:47:09 -0500 Subject: [PATCH 13/13] Update docs/auth/oidc.md Co-authored-by: Patrik Oldsberg Signed-off-by: Carlos Esteban Lopez Jaramillo --- docs/auth/oidc.md | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/docs/auth/oidc.md b/docs/auth/oidc.md index 732a8d9c0d..f856a2a415 100644 --- a/docs/auth/oidc.md +++ b/docs/auth/oidc.md @@ -215,14 +215,9 @@ auth: providers: my-auth-provider: development: - metadataUrl: ${AUTH_OIDC_METADATA_URL} - clientId: ${AUTH_OIDC_CLIENT_ID} - clientSecret: ${AUTH_OIDC_CLIENT_SECRET} - authorizationUrl: ${AUTH_OIDC_AUTH_URL} - tokenUrl: ${AUTH_OIDC_TOKEN_URL} - tokenSignedResponseAlg: ${AUTH_OIDC_TOKEN_SIGNED_RESPONSE_ALG} # default='RS256' - scope: ${AUTH_OIDC_SCOPE} # default='openid profile email' - prompt: ${AUTH_OIDC_PROMPT} # default=none (allowed values: auto, none, consent, login) + metadataUrl: https://example.com/.well-known/openid-configuration + clientId: ${AUTH_MY_CLIENT_ID} + clientSecret: ${AUTH_MY_CLIENT_SECRET} ``` Anything enclosed in `${}` can be replaced directly in the yaml, or provided as