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

이 문서에서는 외부 ID 및 IAP를 사용하여 자체 인증 페이지를 빌드하는 방법을 설명합니다. 이 페이지를 직접 구성하면 인증 흐름과 사용자 환경을 완전히 제어할 수 있습니다.

UI를 완전히 맞춤설정할 필요가 없는 경우 IAP에서 로그인 페이지를 호스팅하거나 FirebaseUI를 사용하여 환경을 더욱 간소화할 수 있습니다.

개요

자체 인증 페이지를 빌드하려면 다음 단계를 따르세요.

  1. 외부 ID를 사용 설정합니다. 설정 중에 자체 UI 제공 옵션을 선택합니다.
  2. gcip-iap 라이브러리를 설치합니다.
  3. AuthenticationHandler 인터페이스를 구현하여 UI를 구성합니다. 인증 페이지에서는 다음 시나리오를 처리해야 합니다.
    • 테넌트 선정
    • 사용자 승인
    • 사용자 로그인
    • 오류 처리
  4. 선택사항: 진행률 표시줄, 로그아웃 페이지, 사용자 처리 등의 추가 기능으로 인증 페이지를 맞춤설정합니다.
  5. UI를 테스트합니다.

gcip-iap 라이브러리 설치

gcip-iap 라이브러리를 설치하려면 다음 명령어를 실행합니다.

npm install gcip-iap --save

gcip-iap NPM 모듈은 애플리케이션, IAP, Identity Platform 간의 통신을 추상화합니다. 이렇게 하면 UI와 IAP 간의 기본 교환을 관리할 필요 없이 전체 인증 흐름을 맞춤설정할 수 있습니다.

SDK 버전에 맞는 가져오기를 사용합니다.

gcip-iap v0.1.4 이하

// 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';

gcip-iap v1.0.0~v1.1.0

버전 v1.0.0부터는 gcip-iapfirebase v9 이상의 피어 종속 항목이 필요합니다. gcip-iap v1.0.0 이상으로 마이그레이션하는 경우 다음 작업을 완료합니다.

  • package.json 파일의 firebase 버전을 v9.6.0 이상으로 업데이트합니다.
  • 다음과 같이 firebase import 문을 업데이트합니다.
// Import Firebase modules.
import firebase from 'firebase/compat/app';
import 'firebase/compat/auth';
// Import the gcip-iap module.
import * as ciap from 'gcip-iap';

추가 코드 변경은 필요하지 않습니다.

gcip-iap v2.0.0

버전 v2.0.0부터 gcip-iap모듈식 SDK 형식을 사용하여 커스텀 UI 애플리케이션을 다시 작성해야 합니다. gcip-iap v2.0.0 이상으로 마이그레이션하는 경우 다음 작업을 완료해야 합니다.

  • package.json 파일의 firebase 버전을 v9.8.3 이상으로 업데이트합니다.
  • 다음과 같이 firebase import 문을 업데이트합니다.
  // Import Firebase modules.
  import { initializeApp } from 'firebase/app';
  import { getAuth, GoogleAuthProvider } 'firebase/auth';
  // Import the gcip-iap module.
  import * as ciap from 'gcip-iap';

UI 구성

UI를 구성하려면 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;
}

라이브러리는 인증 중에 AuthenticationHandler의 메서드를 자동으로 호출합니다.

테넌트 선택

테넌트를 선택하려면 selectTenant()를 구현합니다. 이 메서드를 구현하면 테넌트를 프로그래매틱 방식으로 선택하거나 사용자가 직접 선택할 수 있도록 UI를 표시할 수 있습니다.

두 경우 모두 라이브러리는 반환된 SelectedTenantInfo 객체를 사용하여 인증 흐름을 완료합니다. 선택한 테넌트의 ID, 모든 제공업체 ID, 사용자가 입력한 이메일이 포함됩니다.

프로젝트에 테넌트가 여러 개 있는 경우 테넌트 중 하나를 선택해야 사용자를 인증할 수 있습니다. 테넌트가 하나만 있거나 프로젝트 수준 인증을 사용 중인 경우 selectTenant()를 구현할 필요가 없습니다.

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

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

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

프로그래매틱 방식으로 테넌트 선택

프로그래매틱 방식으로 테넌트를 선택하려면 현재 컨텍스트를 활용하세요. 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);
  });
}

사용자가 테넌트를 선택하도록 허용

사용자가 테넌트를 선택할 수 있도록 하려면 테넌트 목록을 표시하고 사용자가 하나를 선택하도록 하거나 이메일 주소를 입력하여 도메인을 기준으로 일치하는 항목을 찾도록 요청합니다.

// 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,
          });
        });
  });
}

사용자 인증

공급업체가 있으면 getAuth()를 구현하여 제공된 API 키 및 테넌트 ID에 해당하는 인증 인스턴스를 반환합니다. 테넌트 ID가 제공되지 않으면 프로젝트 수준의 ID 공급업체를 사용합니다.

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

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

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

gcip-iap v1.0.0

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 so that
    // multiple users can be 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;
}

gcip-iap v2.0.0

import {initializeApp, getApp} from 'firebase/app';
import {getAuth} from 'firebase/auth';

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 = getAuth(getApp(tenantId || undefined));
    // Tenant ID should be already set on initialization below.
  } catch (e) {
    // Use different App names for every tenant so that
    // multiple users can be signed in at the same time (one per tenant).
    const app = initializeApp(this.config, tenantId || '[DEFAULT]');
    auth = getAuth(app);
    // Set the tenant ID on the Auth instance.
    auth.tenantId = tenantId || null;
  }
  return auth;
}

사용자 로그인

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

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

다음 예는 이메일 및 비밀번호가 있는 기존 사용자에게 맞는 startSignIn() 구현을 보여줍니다.

gcip-iap v1.0.0

startSignIn(auth, selectedTenantInfo) {
  return new Promise((resolve, reject) => {
    // Show the 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 the user for an email and password.
      // Note: 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 the error message.
        });
    });
  });
}

gcip-iap v2.0.0

import {signInWithEmailAndPassword} from 'firebase/auth';

startSignIn(auth, selectedTenantInfo) {
  return new Promise((resolve, reject) => {
    // Show the 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 the user for an email and password.
      // Note: The method of sign in may have already been determined from the
      // selectedTenantInfo object.
        signInWithEmailAndPassword(auth, email, password)
        .then((userCredential) => {
          resolve(userCredential);
        })
        .catch((error) => {
          // Show the error message.
        });
    });
  });
}

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

gcip-iap v1.0.0

startSignIn(auth, selectedTenantInfo) {
  // Show the UI to sign-in or sign-up a user.
  return new Promise((resolve, reject) => {
    // Provide the user multiple buttons to sign-in.
    // For example sign-in with popup using a SAML provider.
    // Note: 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 the 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);
  });
}

gcip-iap v2.0.0

import {signInWithPopup, SAMLAuthProvider} from 'firebase/auth';

startSignIn(auth, selectedTenantInfo) {
  // Show the UI to sign-in or sign-up a user.
  return new Promise((resolve, reject) => {
    // Provide the user multiple buttons to sign-in.
    // For example sign-in with popup using a SAML provider.
    // Note: The method of sign in might have already been determined from the
    // selectedTenantInfo object.
    const provider = new SAMLAuthProvider('saml.myProvider');
    signInWithPopup(auth, provider)
      .then((userCredential) => {
        resolve(userCredential);
      })
      .catch((error) => {
        // Show the 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.
    signInWithRedirect(auth, provider);
  });
}

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

gcip-iap v1.0.0

startSignIn(auth, selectedTenantInfo) {
  // Show the 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 address.
    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 the error message.
      });
    });
}

gcip-iap v2.0.0

import {signInWithPopup, OAuthProvider} from 'firebase/auth';

startSignIn(auth, selectedTenantInfo) {
  // Show the 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 address.
    if (selectedTenantInfo &&
        selectedTenantInfo.providerIds &&
        selectedTenantInfo.providerIds.indexOf('microsoft.com') !== -1) {
      const provider = new OAuthProvider('microsoft.com');
      provider.setCustomParameters({
        login_hint: selectedTenantInfo.email || undefined,
      });
    } else {
      // Figure out the provider used...
    }
    signInWithPopup(auth, provider)
      .then((userCredential) => {
        resolve(userCredential);
      })
      .catch((error) => {
        // Show the error message.
      });
    });
}

자세한 내용은 멀티테넌시로 인증을 참고하세요.

오류 처리

사용자에게 오류 메시지를 표시하거나 네트워크 시간 초과와 같은 오류에서 복구를 수행하려면 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이 누락되었습니다.

UI 맞춤설정

진행률 표시줄, 로그아웃 페이지와 같은 선택적 기능을 사용하여 인증 페이지를 맞춤설정할 수 있습니다.

진행률 UI 표시

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

사용자 로그아웃

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

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

사용자 처리

IAP 리소스로 리디렉션하기 전에 로그인한 사용자를 수정하려면 processUser()를 구현합니다.

이 메서드를 사용하여 다음을 수행할 수 있습니다.

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

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

gcip-iap v1.0.0

processUser(user) {
  return lastAuthUsed.getRedirectResult().then(function(result) {
    // Save additional data, or ask the 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;
  });
}

gcip-iap v2.0.0

import {getRedirectResult} from 'firebase/auth';

processUser(user) {
  return getRedirectResult(lastAuthUsed).then(function(result) {
    // Save additional data, or ask the 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 토큰 클레임에 반영된 사용자 변경 사항을 원하면 토큰을 새로고침해야 합니다.

gcip-iap v1.0.0

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;
  });
}

gcip-iap v2.0.0

import {updateProfile} from 'firebase/auth';

processUser(user) {
  return updateProfile(user, {
    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 테스트

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

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

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

다음 단계