본문 바로가기
WIKI 기술 지식 베이스

로그인 신원 및 리졸버(Sign-in Identities and Resolvers)

원문 보기 위키 갱신

기본적으로 모든 Backstage auth 공급자는 접근 위임(access delegation) 사용 사례에만 구성되어 있습니다.

출처: 문서

본문

기본적으로 모든 Backstage auth 공급자는 접근 위임(access delegation) 사용 사례에만 구성되어 있습니다. 이를 통해 Backstage는 예를 들어 CI에서 빌드를 다시 트리거하는 것처럼 사용자를 대신해 외부 시스템에 리소스와 작업을 요청할 수 있습니다.

auth 공급자를 사용자 로그인에 사용하려면 로그인이 활성화되도록 명시적으로 구성하고, 외부 신원이 Backstage 내의 사용자 신원에 어떻게 매핑되어야 하는지도 알려줘야 합니다. 이는 내장 로그인 리졸버를 선택하거나 자신의 리졸버를 제공하여 수행합니다. 두 방법 모두 아래에 나열되어 있습니다.

빠른 시작

npx @backstage/create-app으로 만든 Backstage 프로젝트는 guest auth 공급자가 구성된 상태로 제공됩니다. 이 공급자는 모든 사용자가 단일 "guest" 신원을 공유하게 합니다. 이는 테스트 목적과 로컬에서 빠르게 시작하는 데 유용하지만, 프로덕션에서 사용하기에는 안전하지 않으며 해당 공급자는 프로덕션에서 작동을 거부합니다.

이 때문에 Backstage 인스턴스를 구축할 때 가장 먼저 해야 할 일 중 하나는 프로덕션에 적합한 auth 공급자를 선택하는 것입니다. 공급자의 전체 목록과 설치 및 구성 방법은 auth 개요 페이지를 참조하세요.

Backstage 사용자 신원

Backstage 내의 사용자 신원은 두 가지 주요 정보로 구성됩니다. 사용자 엔티티 참조와 소유권 참조(ownership references) 집합입니다. 사용자가 로그인하면 Backstage 토큰이 생성되고, 이 토큰이 Backstage 생태계 내에서 사용자를 식별하는 데 사용됩니다.

사용자 엔티티 참조는 Backstage에서 로그인한 사용자를 고유하게 식별해야 합니다. 일치하는 사용자 엔티티가 Software Catalog에도 존재하는 것이 권장되지만, 필수는 아닙니다. 사용자 엔티티가 카탈로그에 존재하면 사용자에 대한 추가 데이터를 저장하는 데 사용할 수 있습니다. 일부 플러그인은 작동하려면 이를 요구할 수도 있습니다.

소유권 참조도 엔티티 참조이며, 마찬가지로 이러한 엔티티가 카탈로그에 존재하는 것이 권장되지만 필수는 아닙니다. 소유권 참조는 사용자가 소유권을 주장하는 참조 집합으로서 사용자가 무엇을 소유하는지 결정하는 데 사용됩니다. 예를 들어, 사용자 Jane(user:default/jane)은 소유권 참조 user:default/jane, group:default/team-a, group:default/admins를 가질 수 있습니다. 이러한 소유권 주장이 주어지면 user:jane, team-a, admins 중 하나가 소유한 것으로 표시된 모든 엔티티는 Jane이 소유한 것으로 간주됩니다.

소유권 주장은 종종 사용자 엔티티 참조 자체를 포함하지만 필수는 아닙니다. 소유권 주장이 maintainer 또는 operator 상태 같은 소유권과 유사한 다른 관계를 해결하는 데도 사용될 수 있다는 점도 주목할 가치가 있습니다.

사용자 신원을 캡슐화하는 Backstage 토큰은 JWT입니다. 사용자 엔티티 참조는 페이로드의 sub 클레임에 저장되며, 소유권 참조는 이전 백엔드 시스템의 커스텀 ent 클레임에 저장되지만 새 시스템에서는 auth 백엔드의 사용자 정보 API 엔드포인트를 통해 제공됩니다. 사용자 참조와 소유권 참조 모두 항상 jane이나 user:jane 같은 약어가 아닌 전체 엔티티 참조여야 합니다.

로그인 리졸버(Sign-in Resolvers)

warning

로그인 리졸버는 누가 어떤 신원으로 Backstage 인스턴스에 접근할 수 있는지를 결정하는 일부이므로 구성할 때 주의하세요. 항상 auth 공급자 중 하나에 대해 단일 로그인 리졸버만 구성하세요. 더 많은 로그인 리졸버를 갖는 유일한 이유는 사용자가 여러 방식으로 Backstage에 로그인하도록 허용하려는 경우이지만, 이는 계정 탈취의 위험을 높입니다.

사용자를 Backstage에 로그인시키려면 서드파티 auth 공급자의 사용자 신원을 Backstage 사용자 신원으로 매핑해야 합니다. 이 매핑은 조직과 auth 공급자마다 크게 다를 수 있으므로, 사용자 신원을 해결하는 기본 방법은 없습니다. 로그인에 사용하려는 auth 공급자에는 대신 로그인 리졸버를 구성해야 하며, 이 리졸버는 사용자 신원 매핑을 만드는 역할을 하는 함수입니다.

로그인 리졸버 함수의 입력은 주어진 auth 공급자로 성공한 로그인의 결과와, 사용자 조회 및 토큰 발행을 위한 다양한 헬퍼를 포함하는 컨텍스트 객체입니다. 바로 사용할 수 있는 여러 내장 로그인 리졸버도 있으며, 이는 아래 조금 더 나중에 다룹니다.

로그인에 여러 auth 공급자를 구성하는 것이 가능하지만, 그렇게 할 때는 주의해야 합니다. 서로 다른 auth 공급자가 사용자 겹침을 가지지 않거나, 여러 공급자로 로그인할 수 있는 모든 사용자가 항상 같은 Backstage 신원을 갖게 하는 것이 가장 좋습니다. 대부분의 조직에서는 단일 로그인 방법만 제공하는 것이 가장 합리적입니다.

내장 리졸버 사용하기

대부분의 auth 공급자는 선택할 수 있는 내장 로그인 리졸버 세트와 함께 제공됩니다. 이들은 가장 일반적인 사용 사례를 대상으로 하며, 요구 사항에 맞다면 코드를 전혀 작성하지 않고 하나 이상을 선택할 수 있습니다. 여전히 선택은 해야 합니다 — 위에서 언급했듯이 내장 세트가 있어도 기본으로 선택되는 것은 없습니다.

내장 로그인 리졸버는 app-config에서 각 공급자의 구성 옆에 설정합니다. GitHub의 예시는 다음과 같습니다.

예: app-config.yaml

auth:  environment: development  providers:    github:      development:        clientId: ${AUTH_GITHUB_CLIENT_ID}        clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}        enterpriseInstanceUrl: ${AUTH_GITHUB_ENTERPRISE_INSTANCE_URL}        signIn:          resolvers:            - resolver: userIdMatchingUserEntityAnnotation

사용 가능한 리졸버 목록은 공급자마다 다릅니다. 이는 종종 업스트림 공급자 서비스가 반환하는 정보 모델에 의존하기 때문입니다. 목록을 찾으려면 각 공급자의 문서를 참조하세요.

위 예시에서 userIdMatchingUserEntityAnnotation은 GitHub 공급자에 특화된 것이지만, 모든 auth 공급자에 공통인 emailMatchingUserEntityProfileEmail 또는 emailLocalPartMatchingUserEntityName 리졸버를 선택할 수도 있습니다.

warning

이메일 기반 로그인 리졸버를 사용할 때는 구성된 auth 공급자가 의도된 사용자만 로그인하도록 허용하고 각 신원에 대해 권위 있는 이메일 주소를 제공하는지 확인하세요. 주소는 공급자가 검증했거나, 불변이며 신뢰할 수 있는 조직 소스에서 프로비저닝되어야 합니다. 일치하는 카탈로그 사용자가 있다는 것 자체만으로는 로그인하는 사용자가 제공된 주소를 제어한다는 것을 입증하지 않습니다.

warning

emailLocalPartMatchingUserEntityName 리졸버를 사용할 때는 allowedDomains 옵션을 설정하여 권한 있는 사용자만 로그인할 수 있도록 하는 것을 강력히 권장합니다.

emailLocalPartMatchingUserEntityName 리졸버를 사용한다면 allowedDomains 옵션도 설정하는 것이 좋습니다. 예:

공급자 구성 내에서

auth:  providers:    github:      development:        ...        signIn:          resolvers:            - resolver: emailLocalPartMatchingUserEntityName              allowedDomains:                - acme.org

커스텀 리졸버 만들기

내장 리졸버가 작동하지 않는다면, 코드를 통해 완전히 커스텀한 로그인 리졸버를 제공할 수도 있습니다. 사용 가능한 공급자 중 하나의 설치 지침을 따랐다면 백엔드에 의존성과 함께 코드 한 줄 및 구성 일부를 추가했을 것입니다.

GitHub를 예로 들면, 이는 백엔드 코드의 관련 부분입니다.

packages/backend/src/index.ts에서

backend.add(import('@backstage/plugin-auth-backend'));backend.add(import('@backstage/plugin-auth-backend-module-github-provider'));

커스텀 로그인 리졸버를 제공하고 싶다면, 일반적인 패턴으로 그 마지막 import를 제거하고 같은 패키지의 기능을 사용해 자신만의 공급자를 구성합니다.

app-config.yaml의 auth 구성에 resolvers 필드가 없는지 확인하세요. 그렇지 않으면 그것이 우선합니다.

예: app-config.yaml

auth:  environment: development  providers:    github:      development:        clientId: ${AUTH_GITHUB_CLIENT_ID}        clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}        enterpriseInstanceUrl: ${AUTH_GITHUB_ENTERPRISE_INSTANCE_URL}        signIn:          resolvers:            - resolver: userIdMatchingUserEntityAnnotation

packages/backend/src/index.ts

import { createBackendModule } from '@backstage/backend-plugin-api';import { githubAuthenticator } from '@backstage/plugin-auth-backend-module-github-provider';import {  authProvidersExtensionPoint,  createOAuthProviderFactory,} from '@backstage/plugin-auth-node';const customAuth = 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: 'custom-auth-provider',  register(reg) {    reg.registerInit({      deps: { providers: authProvidersExtensionPoint },      async init({ providers }) {        providers.registerProvider({          providerId: 'github',          factory: createOAuthProviderFactory({            authenticator: githubAuthenticator,            async signInResolver(info, ctx) {              // ...custom logic            },          }),        });      },    });  },});backend.add(customAuth);

유효한 ID를 만드는 데 적용되는 규칙은 naming patterns 문서를 확인하세요. 이 예시에서는 모듈 선언을 직접 packages/backend/src/index.ts에 넣었지만, 그것은 단지 단순화를 위한 것입니다. 원하는 곳 어디든, 다른 패키지에도 배치하고 거기서 import할 수 있습니다.

createOAuthProviderFactory / createProxyAuthProviderFactory 함수에는 프로필 및 상태 변환을 위한 추가 옵션이 있습니다. 여기서 다루지는 않지만, 필요할 때 알아 두면 좋습니다.

그렇다면 일반적인 로그인 리졸버 콜백의 본문은 어떻게 생길까요? 예시는 다음과 같습니다.

// ...async signInResolver(info, ctx) {  const { profile: { email } } = info;  // Profiles are not always guaranteed to have an email address.  // You can also find more provider-specific information in `info.result`.  // It typically contains a `fullProfile` object as well as ID and/or access  // tokens that you can use for additional lookups.  if (!email) {    throw new Error('User profile contained no email');  }  // You can add your own custom validation logic here.  // Logins can be prevented by throwing an error.  return await ctx.signInWithCatalogUser({    entityRef: { name: email.split('@')[0] },  });}

로그인 리졸버 함수에서 오류를 던지면 로그인 시도가 즉시 거부되고 오류 세부 정보가 사용자 인터페이스에 표시됩니다.

ctx 컨텍스트에는 다양한 방식으로 토큰을 발행하는 몇 가지 유용한 함수가 있습니다.

커스텀 소유권 해결

로그인 중에 일어나는 멤버십 해결과 토큰 생성에 대해 더 많은 제어를 원한다면 ctx.signInWithCatalogUser를 일련의 더 낮은 수준의 호출로 대체할 수 있습니다.

// File: packages/backend/src/plugins/auth.ts// ...async signInResolver({ profile: { email } }, ctx) {  if (!email) {    throw new Error('User profile contained no email');  }  // This step calls the catalog to look up a user entity. You could for example  // replace it with a call to a different external system.  const { entity } = await ctx.findCatalogUser({    annotations: {      'acme.org/email': email,    },  });  // In this step we extract the ownership references from the user entity using  // the ownership relation.  const ownershipRefs = [    stringifyEntityRef(entity),    ...(entity.relations ?? [])      .filter(r => r.type === 'ownedBy')      .map(r => r.targetRef),  ];  return await ctx.issueToken({    claims: {      sub: stringifyEntityRef(entity),      ent: ownershipRefs,    },  });}

카탈로그에 사용자 없는 로그인

warning

카탈로그에서 존재를 검증하지 않고 사용자를 로그인시키는 것은 위험할 수 있습니다. 커스텀 리졸버가 이메일 도메인 확인 같은 방식으로 예상된 사용자만 로그인하도록 허용하는지 확실히 하세요.

조직 데이터로 카탈로그를 채우는 것은 소프트웨어 생태계를 탐색하는 더 강력한 방법을 열지만, 항상 실행 가능하거나 우선시되는 옵션은 아닐 수 있습니다. 그러나 카탈로그에 사용자 엔티티가 채워져 있지 않더라도 사용자를 로그인시킬 수는 있습니다.

카탈로그의 사용자 요구 사항을 우회하는 커스텀 로그인 리졸버

현재 이 시나리오에 대한 내장 로그인 리졸버가 없으므로 직접 구현하고 싶을 수 있습니다.

카탈로그에 존재하지 않는 사용자를 로그인시키는 것은 위 예시에서 카탈로그 조회 단계를 건너뛰는 것만큼 간단합니다. 사용자를 조회하는 대신 사용 가능한 어떤 정보로든 즉시 토큰을 발행합니다. 한 가지 주의점은 소유권 참조를 결정하는 것이 까다로울 수 있다는 것이지만, 예를 들어 외부 서비스에 대한 조회를 통해 달성할 수 있습니다. 일반적으로 최소한 사용자 자신을 단독 소유권 참조로 사용하고 싶을 것입니다.

더 이상 카탈로그를 사용자 허용 목록으로 사용하지 않으므로, 로그인할 수 있는 사용자를 제한하는 것이 종종 중요합니다. 이는 아래 예시처럼 간단한 이메일 도메인 확인일 수도 있고, 제공된 결과 객체의 사용자 access token을 사용해 사용자가 속한 GitHub 조직을 조회하는 것일 수도 있습니다.

import { stringifyEntityRef, DEFAULT_NAMESPACE } from '@backstage/catalog-model';// ...async signInResolver({ profile }, ctx) {  if (!profile.email) {    throw new Error(      'Login failed, user profile does not contain an email',    );  }  // Split the email into the local part and the domain.  const [localPart, domain] = profile.email.split('@');  // Next we verify the email domain. It is recommended to include this  // kind of check if you don't look up the user in an external service.  if (domain !== 'acme.org') {    throw new Error(      `Login failed, user profile email is not from a valid domain: ${domain}`,    );  }  // Instead of looking up a user in the catalog, we issue a token directly  // using the local part of the email as the user name and the user itself  // as the ownership reference.  return await ctx.issueToken({    claims: {      sub: stringifyEntityRef({        kind: 'user',        namespace: DEFAULT_NAMESPACE,        name: localPart,      }),      ent: [        stringifyEntityRef({          kind: 'user',          namespace: DEFAULT_NAMESPACE,          name: localPart,        }),      ],    },  });}
dangerouslyAllowSignInWithoutUserInCatalog 옵션 사용하기

이 요구 사항을 우회하는 또 다른 방법은 리졸버에 dangerouslyAllowSignInWithoutUserInCatalog 옵션을 활성화하는 것입니다. 사용자는 여전히 평소와 같이 인증되지만, 이 구성은 사용자가 카탈로그에 존재하는지 확인하는 검사를 우회합니다. 사용자 엔티티가 카탈로그에서 발견되지 않으면, 리졸버 수준에서 사용 가능한 식별 정보에 기반해 Backstage 사용자 토큰이 여전히 발행됩니다.

예:

공급자 구성 내에서

auth:  providers:    github:      development:        ...        signIn:          resolvers:            - resolver: emailLocalPartMatchingUserEntityName              dangerouslyAllowSignInWithoutUserInCatalog: true

warning

프로덕션에서 이 옵션을 활성화하면 보안 위험이 따릅니다.

이 옵션은 Backstage에 온보딩되지 않은 예상 밖의 사용자에게 접근을 허용할 수 있습니다. 로그인한 사용자와 연결할 사용자 엔티티가 없으므로 권한이 예상대로 적용되지 않을 수 있으며, guest 사용자와 동일한 권한을 가지게 됩니다. 특히 권한 시스템을 사용할 때 이러한 사용자에게 할당된 권한을 신중히 고려해야 합니다.

프로필 변환(Profile Transforms)

커스텀 로그인 리졸버와 유사하게, 인증 응답을 검증하고 사용자에게 표시될 프로필로 변환하는 데 사용되는 커스텀 프로필 변환 함수를 작성할 수도 있습니다. 여기서 표시 이름과 프로필 사진 같은 것을 사용자 지정할 수 있습니다.

또한 여기서 사용자에 대한 권한 부여와 검증을 수행하고, 사용자가 Backstage에 접근을 허용되어서는 안 된다면 오류를 던질 수 있습니다.

const customAuth = 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: 'custom-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.github means that this must be "github".          providerId: 'github',          factory: createProxyAuthProviderFactory({            authenticator: githubAuthenticator,            profileTransform: async (info, ctx) => {              return {                profile: {                  email: info.profile.email,                  displayName: info.profile.displayName,                },              };            },          }),        });      },    });  },});

위와 같이 생성된 모듈을 backend.add하는 것을 잊지 마세요.

일반적인 로그인 리졸버 오류

마주칠 수 있는 두 가지 일반적인 로그인 리졸버 오류가 있습니다.

첫 번째는 "'Auth Provider Name' 공급자가 로그인을 지원하도록 구성되어 있지 않습니다"입니다. GitHub Auth 공급자의 경우 다음과 같습니다.

이 오류는 다음에 의해 발생할 수 있습니다.

  • signIn.resolvers가 Auth Provider 구성에 추가되지 않았습니다. 이것을 추가하면 오류가 해결됩니다.

  • Auth Provider 구성에 구문 오류가 있습니다. yarn backstage-cli config:check --strict를 실행하면 구문 오류를 식별하는 데 도움이 됩니다.

두 번째 일반적인 오류는 "로그인 실패, 사용자 신원을 해결할 수 없음"입니다. GitHub Auth 공급자의 경우 다음과 같습니다.

이 오류는 구성한 로그인 리졸버가 카탈로그에서 일치하는 User를 찾지 못해서 발생합니다. 이 문제를 해결하려면 조직의 이 데이터에 대한 일부 진실 소스에서 User 및 Group 데이터를 import해야 합니다. 이를 위해 Entra ID(Azure AD/MS Graph), GitHub, GitLab 등을 위한 기존 Org Data 공급자 중 하나를 사용하거나, 그중 어느 것도 요구 사항에 맞지 않으면 Custom Entity Provider를 만들 수 있습니다.

더 알아보기 (Learn more)