커스텀 로그인 페이지 만들기

IAP(Identity-Aware Proxy)로 외부 ID를 사용하려면 앱에 로그인 페이지가 필요합니다. IAP는 사용자를 보안 리소스에 액세스하기 전에 이 페이지로 리디렉션하여 인증합니다.

이 도움말에서는 인증 페이지를 처음부터 새로 만드는 방법을 설명합니다. 이 페이지를 직접 구성하는 것은 복잡하지만 인증 흐름과 사용자 환경을 완전히 제어할 수 있습니다.

UI를 완전히 맞춤설정할 필요가 없는 경우 코드를 단순화하기 위해 IAP에서 로그인 페이지를 호스팅하거나 FirebaseUI를 대신 사용하는 것이 좋습니다.

시작하기 전에

외부 ID를 사용설정하고 설정 중에 자체 UI 제공 옵션을 선택합니다.

gcip-iap 라이브러리 설치

gcip-iap NPM 모듈은 애플리케이션, IAP, Identity Platform 간의 통신을 추상화합니다.

라이브러리를 사용하는 것이 좋습니다. UI와 IAP 간의 기본 교환에 대한 걱정 없이 전체 인증 흐름을 맞춤설정할 수 있습니다.

라이브러리를 다음과 같은 종속 항목으로 포함합니다.

// Import Firebase/GCIP dependencies. These are installed on npm install.
import * as firebase from 'firebase/app';
import 'firebase/auth';
// Import GCIP/IAP module.
import * as ciap from 'gcip-iap';

AuthenticationHandler 구현

gcip-iap 모듈은 AuthenticationHandler라는 인터페이스를 정의합니다. 라이브러리는 적절한 시점에 자동으로 메서드를 호출하여 인증을 처리합니다. 인터페이스는 다음과 같습니다.

interface AuthenticationHandler {
  languageCode?: string | null;
  getAuth(apiKey: string, tenantId: string | null): FirebaseAuth;
  startSignIn(auth: FirebaseAuth, match?: SelectedTenantInfo): Promise<UserCredential>;
  selectTenant?(projectConfig: ProjectConfig, tenantIds: string[]): Promise<SelectedTenantInfo>;
  completeSignOut(): Promise<void>;
  processUser?(user: User): Promise<User>;
  showProgressBar?(): void;
  hideProgressBar?(): void;
  handleError?(error: Error | CIAPError): void;
}

UI를 맞춤설정하려면 인터페이스를 구현하는 커스텀 클래스를 만들어야 합니다. 사용하는 자바스크립트 프레임워크에는 제한이 없습니다. 아래 섹션에서는 각 메서드를 빌드하는 방법에 대한 추가 정보를 제공합니다.

테넌트 선택

멀티 테넌시를 사용하는 경우 테넌트를 선택해야 사용자를 인증할 수 있습니다. 테넌트가 하나만 있거나 프로젝트 수준 인증을 사용 중인 경우 이 단계를 건너뛸 수 있습니다.

IAP는 다음과 같은 Identity Platform과 동일한 제공업체를 지원합니다.

  • 이메일 및 비밀번호
  • OAuth(Google, Facebook, Twitter, GitHub, Microsoft 등)
  • SAML
  • OIDC
  • 전화번호
  • 맞춤
  • 익명

멀티 테넌시에는 전화번호, 커스텀, 익명 인증이 지원되지 않습니다.

테넌트를 선택하기 위해 라이브러리는 selectTenant() 콜백을 호출합니다. 이 메서드를 구현하면 테넌트를 프로그래매틱 방식으로 선택하거나, 사용자가 직접 선택할 수 있도록 UI를 표시할 수 있습니다.

프로그래매틱 방식으로 테넌트를 선택하려면 현재 컨텍스트를 활용하세요. Authentication 클래스에는 인증 전에 사용자가 액세스한 URL을 반환하는 getOriginalURL() 메서드가 포함되어 있습니다. 이 옵션을 사용하면 연결된 테넌트 목록에서 일치 항목을 찾을 수 있습니다.

// Select provider programmatically.
selectTenant(projectConfig, tenantIds) {
  return new Promise((resolve, reject) => {
    // Show UI to select the tenant.
    auth.getOriginalURL()
      .then((originalUrl) => {
        resolve({
          tenantId: getMatchingTenantBasedOnVisitedUrl(originalUrl),
          // If associated provider IDs can also be determined,
          // populate this list.
          providerIds: [],
        });
      })
      .catch(reject);
  });
}

UI를 표시하도록 선택하면 사용할 제공업체를 결정하는 다양한 방법이 있습니다. 예를 들어, 테넌트 목록을 표시하고 사용자가 하나를 선택하도록 하거나 이메일 주소를 입력한 다음 도메인을 기준으로 일치하는 항목을 찾도록 요청할 수 있습니다.

// Select provider by showing UI.
selectTenant(projectConfig, tenantIds) {
  return new Promise((resolve, reject) => {
    // Show UI to select the tenant.
    renderSelectTenant(
        tenantIds,
        // On tenant selection.
        (selectedTenantId) => {
          resolve({
            tenantId: selectedTenantId,
            // If associated provider IDs can also be determined,
            // populate this list.
            providerIds: [],
            // If email is available, populate this field too.
            email: undefined,
          });
        });
  });
}

방법에 관계없이 반환된 SelectedTenantInfo 객체는 나중에 인증 흐름을 완료하는 데 사용됩니다. 선택한 테넌트의 ID, 모든 제공업체 ID, 사용자가 입력한 이메일이 포함됩니다.

인증 객체 가져오기

제공업체가 있으면 Auth 객체를 얻는 방법이 필요합니다. 제공된 API 키 및 테넌트 ID에 해당하는 firebase.auth.Auth 인스턴스를 반환하도록 getAuth() 콜백을 구현합니다. 테넌트 ID가 제공되지 않으면 대신 프로젝트 수준 ID 공급업체를 사용해야 합니다.

getAuth()는 제공된 구성에 해당하는 사용자가 저장된 위치를 추적하는 데 사용됩니다. 또한 사용자가 사용자 인증 정보를 다시 입력하지 않아도 이전에 인증된 사용자의 Identity Platform ID 토큰을 자동으로 새로 고칠 수 있습니다.

서로 다른 테넌트에 여러 IAP 리소스를 사용하는 경우 각 인스턴스에 고유한 Auth 인스턴스를 사용하는 것이 좋습니다. 이렇게 하면 구성이 서로 다른 여러 리소스가 동일한 인증 페이지를 사용할 수 있습니다. 또한 이전 사용자를 로그아웃하지 않고도 여러 사용자가 동시에 로그인할 수 있습니다.

다음은 getAuth()를 구현하는 방법의 예시입니다.

getAuth(apiKey, tenantId) {
  let auth = null;
  // Make sure the expected API key is being used.
  if (apiKey !== expectedApiKey) {
    throw new Error('Invalid project!');
  }
  try {
    auth = firebase.app(tenantId || undefined).auth();
    // Tenant ID should be already set on initialization below.
  } catch (e) {
    // Use different App names for every tenant. This makes it possible to have
    // multiple users signed in at the same time (one per tenant).
    const app = firebase.initializeApp(this.config, tenantId || '[DEFAULT]');
    auth = app.auth();
    // Set the tenant ID on the Auth instance.
    auth.tenantId = tenantId || null;
  }
  return auth;
}

사용자 로그인

로그인을 처리하려면 startSignIn() 콜백을 구현합니다. 사용자가 인증할 UI를 표시한 다음 완료 시 로그인한 사용자에 대해 UserCredential을 반환해야 합니다.

멀티 테넌트 환경에서 사용 가능한 인증 방법은 제공된 경우 SelectedTenantInfo에서 확인할 수 있습니다. 이 변수에는 selectTenant() 콜백에서 반환된 동일한 정보가 포함됩니다.

다음 예시는 이메일 및 비밀번호로 기존 사용자에 대해 startSignIn()을 구현하는 방법을 보여줍니다.

startSignIn(auth, selectedTenantInfo) {
  return new Promise((resolve, reject) => {
    // Show UI to sign-in or sign-up a user.
    $('#sign-in-form').on('submit', (e) => {
      const email = $('#email').val();
      const password = $('#password').val();
      // Example: ask user for email and password.
      // The method of sign in may have already been determined from the
      // selectedTenantInfo object.
      auth.signInWithEmailAndPassword(email, password)
        .then((userCredential) => {
          resolve(userCredential);
        })
        .catch((error) => {
          // Show error message.
        });
    });
  });
}

팝업 또는 리디렉션을 사용하여 SAML 또는 OIDC와 같은 제휴 제공업체로 사용자를 로그인할 수도 있습니다.

startSignIn(auth, selectedTenantInfo) {
  // Show UI to sign-in or sign-up a user.
  return new Promise((resolve, reject) => {
    // Provide user multiple buttons to sign-in.
    // For example sign-in with popup using a SAML provider.
    // The method of sign in may have already been determined from the
    // selectedTenantInfo object.
    const provider = new firebase.auth.SAMLAuthProvider('saml.myProvider');
    auth.signInWithPopup(provider)
      .then((userCredential) => {
        resolve(userCredential);
      })
      .catch((error) => {
        // Show error message.
       });
    // Using redirect flow. When the page redirects back and sign-in completes,
    // ciap will detect the result and complete sign-in without any additional
    // action.
    auth.signInWithRedirect(provider);
  });
}

일부 OAuth 제공업체는 로그인을 위한 로그인 힌트 전달을 지원합니다.

startSignIn(auth, selectedTenantInfo) {
  // Show UI to sign-in or sign-up a user.
  return new Promise((resolve, reject) => {
    // Use selectedTenantInfo to determine the provider and pass the login hint
    // if that provider supports it and the user specified an email.
    if (selectedTenantInfo &&
        selectedTenantInfo.providerIds &&
        selectedTenantInfo.providerIds.indexOf('microsoft.com') !== -1) {
      const provider = new firebase.auth.OAuthProvider('microsoft.com');
      provider.setCustomParameters({
        login_hint: selectedTenantInfo.email || undefined,
      });
    } else {
      // Figure out the provider used...
    }
    auth.signInWithPopup(provider)
      .then((userCredential) => {
        resolve(userCredential);
      })
      .catch((error) => {
        // Show error message.
       });
    });
}

사용자 인증에 대한 자세한 내용은 Identity Platform 설명서를 참조하세요.

사용자 처리

선택적 processUser() 메서드를 사용하면 IAP 리소스로 다시 리디렉션하기 전에 로그인한 사용자를 수정할 수 있습니다. 이것을 사용하여 다음을 수행할 수 있습니다.

  • 추가 제공업체에 연결
  • 사용자 프로필 업데이트
  • 등록 후 사용자에게 추가 데이터 요청
  • signInWithRedirect()를 호출한 후 getRedirectResult()에서 반환된 OAuth 액세스 토큰 처리

다음은 processUser()를 구현하는 예시입니다.

processUser(user) {
  return lastAuthUsed.getRedirectResult().then(function(result) {
    // Save additional data, or ask user for additional profile information
    // to store in database, etc.
    if (result) {
      // Save result.additionalUserInfo.
      // Save result.credential.accessToken for OAuth provider, etc.
    }
    // Return the user.
    return user;
  });
}

IAP에서 앱으로 전파한 ID 토큰 클레임에 반영된 사용자 변경 사항을 원하면 토큰을 새로고침해야 합니다.

processUser(user) {
  return user.updateProfile({
    photoURL: 'https://example.com/profile/1234/photo.png',
  }).then(function() {
    // To reflect updated photoURL in the ID token, force token
    // refresh.
    return user.getIdToken(true);
  }).then(function() {
    return user;
  });
}

진행률 UI 표시

gcip-iap 모듈이 장기 실행 네트워크 태스크를 실행할 때마다 사용자에게 커스텀 진행률 UI를 표시하도록 선택적 showProgressBar()hideProgressBar() 콜백을 구현합니다.

오류 처리

handleError()는 오류 처리를 위한 선택적 콜백입니다. 사용자에게 오류 메시지를 표시하거나 특정 오류(예: 네트워크 시간 초과)에서 복구하려고 시도하세요.

다음 예시는 handleError()를 구현하는 방법을 보여줍니다.

handleError(error) {
  showAlert({
    code: error.code,
    message: error.message,
    // Whether to show the retry button. This is only available if the error is
    // recoverable via retrial.
    retry: !!error.retry,
  });
  // When user clicks retry, call error.retry();
  $('.alert-link').on('click', (e) => {
    error.retry();
    e.preventDefault();
    return false;
  });
}

아래 표에는 발생할 수 있는 IAP 관련 오류 코드가 나열되어 있습니다. Identity Platform에서도 오류가 발생할 수 있습니다. firebase.auth.Auth에 대한 설명서를 참조하세요.

오류 코드 설명
invalid-argument 클라이언트가 잘못된 인수를 지정했습니다.
failed-precondition 현재 시스템 상태에서는 요청을 실행할 수 없습니다.
out-of-range 클라이언트가 잘못된 범위로 지정되었습니다.
unauthenticated OAuth 토큰이 누락되었거나, 잘못되었거나, 만료되어 요청을 인증할 수 없습니다.
permission-denied 클라이언트에게 충분한 권한이 없거나 UI가 승인되지 않은 도메인에서 호스팅됩니다.
not-found 지정한 리소스를 찾을 수 없습니다.
aborted 읽기-수정-쓰기 충돌 같은 동시 실행 충돌이 발생했습니다.
already-exists 클라이언트가 만들려고 했던 리소스가 이미 존재합니다.
resource-exhausted 리소스 할당량이 부족하거나 비율 제한에 도달했습니다.
cancelled 클라이언트에서 요청을 취소했습니다.
data-loss 복구할 수 없는 데이터 손실 또는 손상이 발생했습니다.
unknown 알 수 없는 서버 오류가 발생했습니다.
internal 내부 서버 오류입니다.
not-implemented 서버에서 API 메소드를 구현하지 않았습니다.
unavailable 서비스를 사용할 수 없습니다.
restart-process 이 페이지로 리디렉션된 URL을 다시 방문하여 인증 프로세스를 다시 시작하세요.
deadline-exceeded 요청 기한이 지났습니다.
authentication-uri-fail 인증 URI를 생성할 수 없습니다.
gcip-token-invalid 제공된 GCIP ID 토큰이 잘못되었습니다.
gcip-redirect-invalid 잘못된 리디렉션 URL입니다.
get-project-mapping-fail 프로젝트 ID를 가져오지 못했습니다.
gcip-id-token-encryption-error GCIP ID 토큰 암호화 오류입니다.
gcip-id-token-decryption-error GCIP ID 토큰 복호화 오류입니다.
gcip-id-token-unescape-error 웹 안전 base64 이스케이프 제거가 실패했습니다.
resource-missing-gcip-sign-in-url 지정된 IAP 리소스의 GCIP 인증 URL이 누락되었습니다.

사용자 로그아웃

경우에 따라 사용자가 동일한 인증 URL을 공유하는 모든 현재 세션에서 로그아웃하도록 할 수 있습니다.

사용자가 로그아웃하면 다시 리디렉션할 URL이 없을 수 있습니다(일반적으로 사용자가 로그인 페이지와 연결된 모든 테넌트에서 로그아웃할 때 발생합니다). 이 경우 completeSignOut() 콜백을 구현하여 사용자가 성공적으로 로그아웃했다는 메시지를 표시합니다. 그렇지 않으면 빈 페이지가 표시됩니다.

커스텀 UI 사용

AuthenticationHandler를 구현하는 클래스를 만든 후에는 이를 사용하여 새 Authentication 인스턴스를 만들고 시작할 수 있습니다.

// Implement interface AuthenticationHandler.
// const authHandlerImplementation = ....
const ciapInstance = new ciap.Authentication(authHandlerImplementation);
ciapInstance.start();

애플리케이션을 배포하고 인증 페이지로 이동하세요. 커스텀 로그인 UI가 표시되어야 합니다.

다음 단계