Backstage에서의 인증(Authentication)
정보
출처: 문서
본문
정보
이 문서는 새 프론트엔드 시스템용으로 작성되었습니다. 이전 프론트엔드 시스템을 사용 중이라면 해당 전용 문서를 읽는 것이 좋습니다.
Backstage의 인증 시스템은 두 가지 뚜렷한 목적을 제공합니다. 사용자의 로그인과 식별, 그리고 서드파티 리소스에 대한 접근 위임입니다. Backstage에는 원하는 만큼 많은 인증 공급자를 구성할 수 있지만, 일반적으로 그중 하나만 로그인에 사용되고 나머지는 외부 리소스에 대한 접근을 제공하는 데 사용됩니다.
참고
Backstage의 신원 관리와 로그인 페이지는 새 백엔드 시스템을 사용할 때, 그리고 구성에서 backend.auth.dangerouslyDisableDefaultAuthPolicy를 설정하지 않았을 때만 외부 접근을 차단합니다. 그럼에도 프론트엔드 번들은 외부 접근으로부터 보호되지 않으며, 보호하려면 실험용 public 진입점을 사용해야 합니다. 자세한 내용은 Threat Model에서 확인할 수 있습니다.
내장 인증 공급자(Built-in Authentication Providers)
Backstage의 핵심 라이브러리에는 많은 일반적인 인증 공급자가 포함되어 있습니다.
-
Auth0
-
Atlassian
-
Azure
-
Azure Easy Auth
-
Bitbucket
-
Bitbucket Server
-
Cloudflare Access
-
GitHub
-
GitLab
-
Google
-
Google IAP
-
Keycloak(커뮤니티 관리)
-
Okta
-
OAuth 2 Custom Proxy
-
OneLogin
-
OpenShift
-
VMware Cloud
이 내장 공급자들은 필수 scope, 콜백 등을 포함해 특정 서비스의 인증 흐름을 처리합니다. 이 공급자들은 각각 비슷한 방식으로 Backstage 앱에 추가됩니다.
인증 공급자 구성하기
각 내장 공급자는 app-config.yaml의 auth 섹션 아래에 구성 블록을 가집니다. 예를 들어 GitHub 공급자는 다음과 같습니다.
auth: environment: development providers: github: development: clientId: ${AUTH_GITHUB_CLIENT_ID} clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}
특정 공급자에 필요한 구성은 해당 공급자의 문서를 참조하세요.
providers 키는 여러 인증 방법이 지원되면 여러 인증 공급자를 가질 수 있습니다. 각 공급자는 서로 다른 인증 환경(development, production 등)에 대한 구성도 가질 수 있습니다. 이렇게 하면 단일 auth 백엔드가 배포된 백엔드에 대해 로컬 프론트엔드를 실행하는 것 같은 여러 환경을 제공할 수 있습니다. 로컬 auth.environment 설정과 일치하는 공급자 구성이 선택됩니다.
로그인 구성(Sign-In Configuration)
로그인에 인증 공급자를 사용하는 것은 프론트엔드 앱과 auth 백엔드 플러그인 양쪽에서 모두 구성해야 합니다. 백엔드 앱 구성 방법에 대한 정보는 Sign-in Identities and Resolvers를 참조하세요. 이 섹션의 나머지 부분은 프론트엔드 앱에서 로그인을 구성하는 방법에 초점을 맞춥니다.
로그인은 커스텀 SignInPage 앱 컴포넌트를 제공해 구성합니다. 이 컴포넌트는 앱의 다른 모든 라우트보다 먼저 렌더링되며 현재 사용자의 신원을 제공하는 역할을 합니다. SignInPage는 임의의 수의 페이지와 컴포넌트를 렌더링하거나, 백그라운드에서 로직이 실행되는 빈 공간만 렌더링할 수 있습니다. 그러나 결국에는 onSignInSuccess 콜백 prop을 통해 유효한 Backstage 사용자 신원을 제공해야 하며, 그 시점에 나머지 앱이 렌더링됩니다.
원한다면 @backstage/core-components에서 제공하는 SignInPage 컴포넌트를 사용할 수 있으며, 이 컴포넌트는 SignInProviderConfig 정의의 provider 또는 providers(배열) prop을 받습니다.
GitHub에 대한 다음 예시는 packages/app/src/App.tsx에 필요한 추가 사항을 보여주며, 내장 공급자 중 어느 것이든 적용할 수 있습니다.
packages/app/src/App.tsx
import { createApp } from '@backstage/frontend-defaults';import catalogPlugin from '@backstage/plugin-catalog/alpha';import { navModule } from './modules/nav';import { githubAuthApiRef } from '@backstage/core-plugin-api';import { SignInPageBlueprint } from '@backstage/plugin-app-react';import { SignInPage } from '@backstage/core-components';import { createFrontendModule } from '@backstage/frontend-plugin-api';const signInPage = SignInPageBlueprint.make({ params: { loader: async () => props => ( <SignInPage {...props} provider={{ id: 'github-auth-provider', title: 'GitHub', message: 'Sign in using GitHub', apiRef: githubAuthApiRef, }} /> ), },});export default createApp({ features: [ catalogPlugin, navModule, createFrontendModule({ pluginId: 'app', extensions: [signInPage], }), ],});
참고
app-config.yaml의 루트에 enableExperimentalRedirectFlow: true를 추가하면 팝업 없이 리다이렉트 흐름을 사용하도록 로그인을 구성할 수 있습니다.
여러 공급자 사용하기
providers prop을 사용해 guest 접근을 허용하는 것처럼 여러 로그인 방법을 활성화할 수도 있습니다.
packages/app/src/App.tsx
import { githubAuthApiRef } from '@backstage/core-plugin-api';import { SignInPageBlueprint } from '@backstage/plugin-app-react';import { SignInPage } from '@backstage/core-components';import { createFrontendModule } from '@backstage/frontend-plugin-api';const signInPage = SignInPageBlueprint.make({ params: { loader: async () => props => ( <SignInPage {...props} providers={[ 'guest', { id: 'github-auth-provider', title: 'GitHub', message: 'Sign in using GitHub', apiRef: githubAuthApiRef, }, ]} /> ), },});export default createApp({ features: [ catalogPlugin, navModule, createFrontendModule({ pluginId: 'app', extensions: [signInPage], }), ],});
로그인 공급자 조건부 렌더링
위 예시에서는 Guest와 GitHub 로그인 옵션을 모두 가집니다. 이는 프로덕션이 아닐 때 유용하지만, 프로덕션에서는 Guest 접근을 제공하고 싶지 않을 것입니다. 구성을 사용해 공급자를 조건부로 렌더링하는 데 도움을 받을 수 있습니다.
packages/app/src/App.tsx
import { configApiRef, githubAuthApiRef, useApi,} from '@backstage/core-plugin-api';import { SignInPageBlueprint } from '@backstage/plugin-app-react';import { SignInPage } from '@backstage/core-components';import { createFrontendModule } from '@backstage/frontend-plugin-api';const signInPage = SignInPageBlueprint.make({ params: { loader: async () => props => { const configApi = useApi(configApiRef); if (configApi.getString('auth.environment') === 'development') { return ( <SignInPage {...props} providers={[ 'guest', { id: 'github-auth-provider', title: 'GitHub', message: 'Sign in using GitHub', apiRef: githubAuthApiRef, }, ]} /> ); } return ( <SignInPage {...props} provider={{ id: 'github-auth-provider', title: 'GitHub', message: 'Sign in using GitHub', apiRef: githubAuthApiRef, }} /> ); }, },});export default createApp({ features: [ catalogPlugin, navModule, createFrontendModule({ pluginId: 'app', extensions: [signInPage], }), ],});
프록시 공급자로 로그인(Sign-In with Proxy Providers)
일부 인증 공급자는 소위 "프록시" 공급자로, 인증 프록시 뒤에서 사용되도록 만들어졌습니다. 예로는 Amazon Application Load Balancer, Azure EasyAuth, Cloudflare Access, Google Identity-Aware Proxy, OAuth2 Proxy가 있습니다.
프록시 공급자를 사용할 때는 다른 로그인 페이지를 사용하고 싶을 것입니다. 프록시에 대해 로그인한 후에는 더 이상의 사용자 상호작용이 필요 없기 때문입니다. 로그인 페이지가 해야 할 일은 auth 공급자의 /refresh 엔드포인트를 호출해 기존 세션을 얻는 것뿐이며, 이것이 정확히 ProxiedSignInPage가 하는 일입니다. ProxiedSignInPage를 구성하기 위해 필요한 것은 공급자의 ID를 전달하는 것뿐입니다.
packages/app/src/App.tsx
import { SignInPageBlueprint } from '@backstage/plugin-app-react';import { createFrontendModule } from '@backstage/frontend-plugin-api';import { ProxiedSignInPage } from '@backstage/core-components';const signInPage = SignInPageBlueprint.make({ params: { loader: async () => props => <ProxiedSignInPage {...props} provider="awsalb" />, },});export default createApp({ features: [ catalogPlugin, navModule, createFrontendModule({ pluginId: 'app', extensions: [signInPage], }), ],});
auth 백엔드의 공급자가 x-provider-token 같은 추가 헤더를 기대한다면, 선택적 headers prop을 사용해 ProxiedSignInPage에서 이를 구성하는 방법이 있습니다.
예:
<ProxiedSignInPage {...props} provider="my-custom-provider" headers={{ 'x-some-key': someValue }}/>
헤더는 비동기 방식으로도 반환될 수 있습니다.
<ProxiedSignInPage {...props} provider="my-custom-provider" headers={async () => { const someValue = await someFn(); return { 'x-some-key': someValue }; }}/>
이 방법의 단점은 로컬 개발에 설정하기가 번거로울 수 있다는 것입니다. 해결 방법으로, 앱이 실행 중인 환경에 따라 로그인 페이지를 동적으로 선택한 다음 필요하다면 로컬 개발에 다른 로그인 방법을 사용하는 것이 가능합니다. 정확한 설정에 따라 process.env.NODE_ENV 환경 변수, 현재 위치의 hostname 확인, 또는 구성 API를 통해 구성 값을 읽는 방식으로 로그인 방법을 선택할 수 있습니다. 예:
packages/app/src/App.tsx
import { configApiRef, useApi } from '@backstage/core-plugin-api';import { SignInPageBlueprint } from '@backstage/plugin-app-react';import { ProxiedSignInPage, SignInPage } from '@backstage/core-components';import { createFrontendModule, googleAuthApiRef,} from '@backstage/frontend-plugin-api';const signInPage = SignInPageBlueprint.make({ params: { loader: async () => props => { const configApi = useApi(configApiRef); if (configApi.getString('auth.environment') === 'development') { return ( <SignInPage {...props} provider={{ id: 'google-auth-provider', title: 'Google', message: 'Sign In using Google', apiRef: googleAuthApiRef, }} /> ); } return <ProxiedSignInPage {...props} provider="gcpiap" />; }, },});export default createApp({ features: [ catalogPlugin, navModule, createFrontendModule({ pluginId: 'app', extensions: [signInPage], }), ],});
이렇게 여러 인증 공급자를 사용할 때는, 사용된 방법과 관계없이 서로 다른 로그인 리졸버가 같은 신원으로 해석되도록 구성하는 것이 중요합니다.
플러그인 개발자용
Backstage 프론트엔드 핵심 API는 플러그인 개발자가 사용자 신원과 서드파티 리소스에 모두 접근하기 위해 사용할 수 있는 Utility API 세트를 제공합니다.
플러그인 개발자를 위한 신원
플러그인 개발자에게는 사용자 신원에 접근하기 위한 주요 접점이 하나 있습니다. @backstage/core-plugin-api가 identityApiRef를 통해 내보내는 IdentityApi입니다.
IdentityApi는 프론트엔드에서 로그인한 사용자의 신원에 접근할 수 있게 해줍니다. 사용자의 엔티티 참조, 가벼운 프로필 정보, 그리고 Backstage 내에서 인증된 호출을 할 때 사용자를 식별하는 Backstage 토큰에 접근할 수 있게 해줍니다.
백엔드 플러그인에 호출할 때는 @backstage/core-plugin-api에서 fetchApiRef를 통해 내보내지는 FetchApi를 사용하는 것을 권장합니다. FetchApi는 요청에 Backstage 토큰을 자동으로 포함하므로, IdentityApi를 직접 다룰 필요가 없습니다.
서드파티 리소스 접근
Backstage에서 서드파티 서비스와 통신하는 일반적인 패턴은 사용자 대 서버(user-to-server) 요청입니다. 여기서는 플러그인이 외부 서비스에 대한 인증된 호출을 위해 단기 OAuth Access Token을 요청합니다. 이러한 호출은 서비스에 직접 또는 백엔드 플러그인이나 서비스를 통해 이루어질 수 있습니다.
사용자 대 서버 호출에 의존함으로써 프론트엔드와 백엔드 사이의 결합을 낮게 유지하고, 플러그인이 서드파티 서비스를 사용하는 데 훨씬 낮은 진입 장벽을 제공합니다. 이는 예를 들어 접근 토큰이 서버 측에 저장되는 세션 기반 시스템과 비교됩니다. 그러한 솔루션은 auth 백엔드 플러그인, 그 세션 저장소, 그리고 다른 백엔드 플러그인이나 별도 서비스 사이에 훨씬 깊은 결합을 요구합니다. Backstage의 목표는 새 플러그인을 만드는 것을 가능한 한 쉽게 만드는 것이며, 사용자 대 서버 OAuth 기반 인증 솔루션이 그에 도움이 됩니다.
프론트엔드 플러그인이 서드파티 서비스에 대한 접근을 요청하는 방법은 각 서비스 공급자에 대한 Utility API를 통하는 것입니다. 이들은 모두 *AuthApiRef 접미사로 끝납니다. 예를 들어 githubAuthApiRef입니다. 공급자의 전체 목록은 @backstage/core-plugin-api 참조를 참조하세요.
커스텀 인증 공급자
OAuth2와 SAML용 범용 인증 공급자가 있습니다. 이들은 이러한 표준을 따르는 커스텀 인증 공급자를 구현하는 데 필요한 코드 양을 줄일 수 있습니다.
Backstage는 내부적으로 Passport를 사용하며, Passport는 서로 다른 공급자에 대한 광범위한 인증 전략 라이브러리를 가집니다. Passport가 지원하는 인증 방법을 추가하는 방법에 대한 자세한 내용은 Add authentication provider를 참조하세요.
커스텀 ScmAuthApi 구현
기본 ScmAuthApi는 github, gitlab, azure(Azure DevOps), bitbucketServer, bitbucketCloud를 위한 통합을 제공하며, 새 프론트엔드 시스템이 자동으로 생성하고 등록합니다.
이 통합들의 일부만 필요하다면 ScmAuthApi의 커스텀 구현이 필요합니다. 이 API는 접근되는 리소스에 기반해 서로 다른 SCM 시스템을 일반적으로 인증하는 데 사용되며, 예를 들어 Scaffolder(Software Templates)와 Catalog Import 플러그인이 사용합니다.
첫 단계는 기본 공급자를 생성하는 코드를 제거하는 것입니다.
packages/app/src/apis.ts
import { ScmIntegrationsApi, scmIntegrationsApiRef, ScmAuth,} from '@backstage/integration-react';export const apis: AnyApiFactory[] = [ ScmAuth.createDefaultApiFactory(), // ...];
그런 다음 GitHub 공급자만 있는 ApiFactory를 만드는 다음과 같은 것으로 교체합니다.
packages/app/src/apis.ts
export const apis: AnyApiFactory[] = [ createApiFactory({ api: scmAuthApiRef, deps: { githubAuthApi: githubAuthApiRef, }, factory: ({ githubAuthApi }) => ScmAuth.merge( ScmAuth.forGithub(githubAuthApi), ), });
커스텀 인증 통합을 사용한다면 ApiFactory에 새 공급자를 추가할 수 있습니다.
첫 단계는 xxxAuthApiRef 명명 규칙을 따르는 새 인증 ref를 만드는 것입니다. 아래 예시는 이 목적에만 사용된다면 앱 내부에, 또는 @internal/apis 같은 API용 공용 내부 패키지에 정의할 수 있는 새로운 GitHub enterprise 통합에 대한 것입니다.
const gheAuthApiRef: ApiRef<OAuthApi & ProfileInfoApi & SessionApi> = createApiRef({ id: 'internal.auth.ghe', });
이 새 API ref는 그에 대한 API 팩토리를 정의할 때만 작동합니다. 예:
createApiFactory({ api: gheAuthApiRef, deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef, configApi: configApiRef, }, factory: ({ discoveryApi, oauthRequestApi, configApi }) => GithubAuth.create({ configApi, discoveryApi, oauthRequestApi, provider: { id: 'ghe', title: 'GitHub Enterprise', icon: () => null }, defaultScopes: ['read:user'], environment: configApi.getOptionalString('auth.environment'), }),});
그런 다음 새 API ref를 사용해 ApiFactory에 새 공급자를 추가합니다.
createApiFactory({ api: scmAuthApiRef, deps: { gheAuthApi: gheAuthApiRef, githubAuthApi: githubAuthApiRef, }, factory: ({ githubAuthApi, gheAuthApi }) => ScmAuth.merge( ScmAuth.forGithub(githubAuthApi), ScmAuth.forGithub(gheAuthApi, { host: 'ghe.example.com', }), ),});
마지막으로, 이 예시에서 ghe인 공급자 ID를 사용해 auth-backend에 다른 공급자를 추가하고 구성해야 합니다.
import { providers } from '@backstage/plugin-auth-backend';// Add the following options to `createRouter` in packages/backend/src/plugins/auth.tsproviderFactories: { ghe: providers.github.create(),},
새 백엔드 시스템에서는 이를 위해 authProvidersExtensionPoint를 활용할 수 있습니다.
// your-auth-plugin-module.tsexport const gheAuth = 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: 'ghe-auth-provider', register(reg) { reg.registerInit({ deps: { providers: authProvidersExtensionPoint, logger: coreServices.logger, }, async init({ providers, logger }) { providers.registerProvider({ // This ID must match the actual provider config, e.g. addressing // auth.providers.ghe means that this must be "ghe". providerId: 'ghe', factory: createOAuthProviderFactory({ authenticator: githubAuthenticator, signInResolverFactories: { ...commonSignInResolvers, }, }), }); }, }); },});// backend index.tsbackend.add(gheAuth);
토큰 발행자 구성하기
기본적으로 Backstage 인증 백엔드는 발행된 모든 Backstage 토큰에 대한 자체 서명 키를 자동으로 생성하고 관리합니다. 그러나 이 키는 수명이 짧으며 인스턴스 재시작 후에도 유지되지 않습니다.
또는 사용자가 서명 토큰을 위한 자체 공개/개인 키 파일을 제공할 수 있습니다. 이는 토큰 검증 구현이 키 목록을 공격적으로 캐시하고, 알 수 없는 키 ID를 만나도 새 키를 가져오려 하지 않는 시나리오에서 유용합니다. 이 기능을 활성화하려면 구성 파일에 다음 구성을 추가하세요.
auth: keyStore: provider: 'static' static: keys: # Must be declared at least once and the first one will be used for signing - keyId: 'primary' publicKeyFile: /path/to/public.key privateKeyFile: /path/to/private.key algorithm: # Optional, algorithm used to generate the keys, defaults to ES256 # More keys can be added so with future key rotations caches already know about it - keyId: ...
개인 키는 PKCS#8 형식으로 저장해야 합니다. 공개 키는 SPKI 형식으로 저장해야 합니다. openssl과 ES256 알고리즘을 사용해 다음 단계로 공개/개인 키 쌍을 생성할 수 있습니다.
ES256 알고리즘으로 개인 키 생성
openssl ecparam -name prime256v1 -genkey -out private.ec.key
PKCS#8 형식으로 변환
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in private.ec.key -out private.key
공개 키 추출
openssl ec -inform PEM -outform PEM -pubout -in private.key -out public.key