Backstage Login에 Keycloak 연동해보기

2025. 3. 12. 01:08Devops/backstage

728x90
반응형

backstage를 이용하다보면 login에 대한 연동이 꼭 필수적이다. 대부분 backstage연동을 github을 통한 로그인이 많았던거같다. 근데 현재 사내에서는 keycloak을 통해 모든 sso연동이 되어있고 또한 k8s도 sso 연동은 keycloak으로 되어있어 backstage도 keycloak을 연동해보고 싶었다!!!

 

그래서 이번 글에서는 keycloak을 연동해보고 로그인까지해보려고한다. 그리고 추후를 위해 keycloak catalog도 설정을 미리 해놓을려고한다. auth인증흐름은 아래와 같다.

출처 : https://backstage.io/docs/auth/oauth

 

 

1. keycloak에서 backstage client 만들기

먼저 해줘야할 일은 keycloak의 backstage client이다. 저는 아래와 같이 만들었습니다.

중요한건 redirect URL인데 로컬 테스트이기때문에 localhost:7007이고 

redirect URI path는 /api/auth/keycloak/handler/frame이다. 이 부분을 꼭넣어줘야한다.

setting
credentials

 

생성되었으면 credentials의 값을 저장해둔다. keycloak 셋팅은 여기서 끝이다. 원래라면 아무래도 client scope를 통한 mapper도 만들어서 해야겠지만 현재는 kubernetes나 cicd툴등을 연동해놓지 않았기 때문에 따로 설정은 하지않는다. 이후에 kuberentes 연동기도 쓸예정이니 그때.. 하는걸로 해야겠다..

 

2. backstage catalog keycloak plugin 설치

이제 backstage로 와서 keycloak plugin을 설치해보자 해당 plugin은 catalog관련 plugin이다.

참고 : https://github.com/backstage/community-plugins/tree/main/workspaces/keycloak/plugins/catalog-backend-module-keycloak

 

community-plugins/workspaces/keycloak/plugins/catalog-backend-module-keycloak at main · backstage/community-plugins

Community plugins for Backstage. Contribute to backstage/community-plugins development by creating an account on GitHub.

github.com

 

yarn workspace backend add @backstage-community/plugin-catalog-backend-module-keycloak

 

failed with Error 라고 나오지만 설치가된것이다.. 이제 순서대로 로그인을 연동으 위해 package안에 있는 front와 backend쪽 소스에 몇가지를 추가를 해줘야한다.

 

3. backstage Config : app-config.yaml

backstage에는 app-config.yaml이 3개가 기본생성되는데 yarn dev를 실행하면 default로 app-config.yaml이 실행이된다. 그렇기에 먼저 변경은 app-config.yaml에 몇가지 추가를 해보자

# app-config.yaml
auth:
  environment: development
  session:
    secret: g8K+T3pQ9i9g6c5F+zJx1N7M3vAqz5d8B6wUJFXxA8E=  

  # see https://backstage.io/docs/auth/ to learn about auth providers
  providers:
    keycloak:
      development:
        metadataUrl: http://localhost:3300/realms/master/.well-known/openid-configuration
        clientId: backstage client Id값을 넣어주세요
        clientSecret: backstage secret 값을 넣어주세요
        prompt: auto
    #guest: {}

 

먼저 auth칸이다. auth에는 session secret 난수 생성하여 삽입을하고, keycloak의 정보를 넣습니다. 
keycloak 정보는 아까 만들었던 ID와 Secret을 넣어주면됩니다. 해당 auth의 값은  api를 사용하기 위한 값이기에 미리 작성을 해줘야합니다. 

그리고 catalog 를 추가해줍니다.

# app-config.yaml
catalog:  
  providers:
    keycloakOrg:
      default:
        baseUrl: https://localhost:3300
        realm: master
        loginRealm: master
        clientId: backstage client Id값을 넣어주세요
        clientSecret: backstage secret 값을 넣어주세요
        schedule: # Optional (defaults to the configurations below if not provided); same options as in TaskScheduleDefinition
          # supports cron, ISO duration, "human duration" as used in code
          frequency: { minutes: 30 } # Customize this to fit your needs
          # supports ISO duration, "human duration" as used in code
          timeout: { minutes: 3 } # Customize this to fit your needs

 

추가가 완료되었으면 package안에 있는 app과 backend에 keycloak이 노출이 되도록 또한 호출되도록 설정을 해줘야합니다.

 

4. backstage Frontend: package/app

이번엔 노출되는 Front쪽에 소스를 추가해야합니다. 먼저  package/app/src/api.ts를 수정해줍니다.

 

4-1. package/app/src/api.ts

import {
  ScmIntegrationsApi,
  scmIntegrationsApiRef,
  ScmAuth,
} from '@backstage/integration-react';
import {
  AnyApiFactory,
  ApiRef,
  BackstageIdentityApi,
  configApiRef,
  createApiFactory,
  createApiRef,
  discoveryApiRef,
  oauthRequestApiRef,
  OpenIdConnectApi,
  ProfileInfoApi,
  SessionApi,
} from '@backstage/core-plugin-api';
import { OAuth2 } from '@backstage/core-app-api';

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

export const apis: AnyApiFactory[] = [
  createApiFactory({
    api: keycloakAuthApiRef,
    deps: {
      discoveryApi: discoveryApiRef,
      oauthRequestApi: oauthRequestApiRef,
      configApi: configApiRef,
    },
    factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
      OAuth2.create({
        configApi,
        discoveryApi,
        oauthRequestApi,
        provider: {
          id: 'keycloak',
          title: 'Log in with keycloak',
          icon: () => null,
        },
        environment: configApi.getOptionalString('auth.environment'),
        defaultScopes: ['openid', 'profile', 'email'],

        popupOptions: {
          // optional, used to customize login in popup size
          size: {
            fullscreen: true,
          },
        },
      }),
  }),

  createApiFactory({
    api: scmIntegrationsApiRef,
    deps: { configApi: configApiRef },
    factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
  }),
  ScmAuth.createDefaultApiFactory(),
];

 

여기서 apiRef를 아까 설정한 auth.provider중 keycloak을 가져올 수 있게 해줘야한다는 점입니다. 또한 설정한 keycloakAuthApiRef는 keycloak을 인증하기 위한 api 참조이고 해당 부분을 component에서 사용하게끔 연동을 해줘야 합니다.

 

( 추가 : import { OAuth2 } from '@backstage/core-app-api'; 이부분을 잘 설정해줘야합니다.  import { OAuth2 } from  '@backstage/core-app-api/index; 으로 들어가는 경우가 있기에  경로설정을 잘해줘야합니다.)

4-2. package/app/src/App.tsx

이번엔 component를 노출해주는 작업입니다. 

# App.tsx
const app = createApp({
  apis,
  bindRoutes({ bind }) {
    bind(catalogPlugin.externalRoutes, {
      createComponent: scaffolderPlugin.routes.root,
      viewTechDoc: techdocsPlugin.routes.docRoot,
      createFromTemplate: scaffolderPlugin.routes.selectedTemplate,
    });
    bind(apiDocsPlugin.externalRoutes, {
      registerApi: catalogImportPlugin.routes.importPage,
    });
    bind(scaffolderPlugin.externalRoutes, {
      registerComponent: catalogImportPlugin.routes.importPage,
      viewTechDoc: techdocsPlugin.routes.docRoot,
    });
    bind(orgPlugin.externalRoutes, {
      catalogIndex: catalogPlugin.routes.catalogIndex,
    });
  },
 
  components: {
     // 이전
    //SignInPage: props => <SignInPage {...props} auto providers={['guest']} />,
    SignInPage: props => (
      <SignInPage
        {...props}
        auto
        providers={[
          // 'guest',
          {
            id: 'keycloak',
            title: 'Keycloak',
            message: 'Sign in using Keycloak',
            apiRef: keycloakAuthApiRef,
          },
        ]}
      />
    ),
  },
});

 

처음소스는 SignInPage: props => <SignInPage {...props} auto providers={['guest']} />으로 되어있기에 Guest로 로그인을 할 수있습니다. 하지만 우리는 Keycloak 연동을 해야하기에 해당부분은 주석처리했고, Keycloak 을 넣어줬습니다. 여기서 apiRef는 4-1에서 추가해줬던 keycloakAuthApiRef를 추가해줍니다.

 

이와같이 Front설정은 완료되었습니다. 해당 설정들은 결국 backend를 통신하기 위해 설정해준 부분입니다. 이제 backend에 모듈을 만들어주고 해당 모듈로 keycloak과 api 통신이 되도록 설정을 진행해봅시다.

 

5. backstage Backend: package/backend

backend 폴더에서는 src/index.ts 한곳에서 작업합니다.

 

5-1. package/backend/src/index.ts

Frontend에서 설정한 api 사용하기위해 모듈을 만들어줍니다. 해당 모듈이름은 keycloakAuthModule입니다. 해당 모듈은 아래와 같으며 해당 모듈을 만들고 난 후는 backend에 등록을 해줘야합니다.

# package/backend/src/index.ts

const keycloakAuthmodule = 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',
  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.azure means that this must be "azure".
          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,
                name: info?.result.fullProfile.userinfo.name as string,
                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
                },
              });
            },
          }),
        });
      },
    });
  },
});

 

해당소스가 맨 상단에 설정이 되어있다면 이제 해당 모듈과 앞서 설치해둔 catalog plugin을 넣어줍니다.

 

# package/backend/src/index.ts

const backend = createBackend();

...


// keycloak
backend.add(import('@backstage-community/plugin-catalog-backend-module-keycloak'));
backend.add(keycloakAuthmodule);


backend.start();

 

완성코드

 

/*
 * Hi!
 *
 * Note that this is an EXAMPLE Backstage backend. Please check the README.
 *
 * Happy hacking!
 */

import { createBackend } from '@backstage/backend-defaults';
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';
import {
  DEFAULT_NAMESPACE,
  stringifyEntityRef,
} from '@backstage/catalog-model';


const keycloakAuthmodule = 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',
  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.azure means that this must be "azure".
          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,
                name: info?.result.fullProfile.userinfo.name as string,
                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
                },
              });
            },
          }),
        });
      },
    });
  },
});

const backend = createBackend();

backend.add(import('@backstage/plugin-app-backend'));
backend.add(import('@backstage/plugin-proxy-backend'));
backend.add(import('@backstage/plugin-scaffolder-backend'));
backend.add(import('@backstage/plugin-scaffolder-backend-module-github'));
backend.add(import('@backstage/plugin-techdocs-backend'));
// add keycloak plugin

// auth plugin
backend.add(import('@backstage/plugin-auth-backend'));
// See https://backstage.io/docs/backend-system/building-backends/migrating#the-auth-plugin
backend.add(import('@backstage/plugin-auth-backend-module-guest-provider'));
// See https://backstage.io/docs/auth/guest/provider

// catalog plugin
backend.add(import('@backstage/plugin-catalog-backend'));
backend.add(
  import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'),
);

// See https://backstage.io/docs/features/software-catalog/configuration#subscribing-to-catalog-errors
backend.add(import('@backstage/plugin-catalog-backend-module-logs'));

// permission plugin
backend.add(import('@backstage/plugin-permission-backend'));
// See https://backstage.io/docs/permissions/getting-started for how to create your own permission policy
backend.add(
  import('@backstage/plugin-permission-backend-module-allow-all-policy'),
);

// search plugin
backend.add(import('@backstage/plugin-search-backend'));

// search engine
// See https://backstage.io/docs/features/search/search-engines
backend.add(import('@backstage/plugin-search-backend-module-pg'));

// search collators
backend.add(import('@backstage/plugin-search-backend-module-catalog'));
backend.add(import('@backstage/plugin-search-backend-module-techdocs'));

// kubernetes
backend.add(import('@backstage/plugin-kubernetes-backend'));

// keycloak
backend.add(import('@backstage-community/plugin-catalog-backend-module-keycloak'));
backend.add(keycloakAuthmodule);


backend.start();

 

기본코드에 keycloak 모듈과 backend에 추가만 해주면 끝입니다!

 

6. 로그인 연동 확인하기

이제 연동이 잘되는지 확인을 위해 실행후 접속해봅시다!

yarn dev

 

실행하면 자동으로 localhost:3000으로 된 화면이 뜨고 keycloak이라는 Title아래 Login할수있는 칸이 생깁니다.

sign in을 눌러보자!!

 

sign in을 눌러보면.. 새창이 뜨면서 keycloak익숙한 로그인 페이지가 뜨게 됩니다.

keycloak에 대한 권한 설정이 아직 되어있지 않기때문에 admin으로 로그인을 해보겠습니다.

 

정확히 admin@admin.com 즉 keycloak에 admin의 아이디가 로그인되고 profile 정보들이 들어온것을알수가있습니다.

그리고 개발자도구를 보게되면 jwt token도 해더에 실리는 것도 볼수가 있습니다.

7. 정리

keycloak 연동을 해봤는데 생각보다 어렵지 않았다. 그리고 google에도 자료도 많은거 같고.. spotify에서 공통을 잘 잡아서 그런지.. 코드 수정이라던지 가독성도 참 보기 좋았다..(역시 해외 대기업친구들이란....굿..) 그리고 jwt token의 payload를 보면 아직 추가되지 않은 부분들이 있다. group관련 룰이라던지.. 해당부분은 추후에 해보는 것으로 해도 좋을거같다. backstage를 만져다보면 정말로 잘만들었고 api doc라던지 ci/cd툴이라던지 istio라던지 등등 infra에서 개발자들이 정말 유용하게 쓸수 있을거같다는 생각이 많이 들었다. 이후에는 인프라적인 부분들도 한번 먼저 연동후 role을 해보던지 해야할거 같다.

728x90
반응형

'Devops > backstage' 카테고리의 다른 글

Backstage Local 셋팅 및 설치방법  (0) 2025.02.20
Backstage은 무엇일까?  (0) 2025.02.20