웹 앱에 다중 인증(MFA) 추가

Firebase Authentication with Identity Platform으로 업그레이드했다면 웹 앱에 SMS 다중 인증(MFA)을 추가할 수 있습니다.

다중 인증(MFA)을 통해 앱의 보안이 강화됩니다. 공격자는 종종 비밀번호와 소셜 미디어 계정을 유출시키지만 문자 메시지를 가로채는 것은 더 어렵습니다.

시작하기 전에

  1. 다중 인증(MFA)을 지원하는 제공업체를 하나 이상 사용 설정합니다. 전화 인증, 익명 인증, Apple Game Center를 제외한 모든 제공업체는 MFA를 지원합니다.

  2. SMS 인증을 사용할 리전을 사용 설정합니다. Firebase는 기본적으로 더 안전한 상태로 프로젝트를 만드는 데 도움이 되는 SMS 완전 차단 리전 정책을 사용합니다.

  3. 앱이 사용자 이메일을 확인하고 있는지 확인합니다. MFA를 사용하려면 이메일 인증이 필요합니다. 이를 통해 악의적인 행위자가 자신이 소유하지 않은 이메일에 서비스를 등록한 후 두 번째 단계를 추가하여 실제 소유자의 접근을 막는 일을 방지할 수 있습니다.

멀티테넌시 사용

멀티 테넌트 환경에서 사용하기 위해 다단계 인증을 사용 설정하는 경우 이 도움말의 나머지 안내와 함께 다음 단계를 완료해야 합니다.

  1. Google Cloud 콘솔에서 작업할 테넌트를 선택합니다.

  2. 코드에서 Auth 인스턴스의 tenantId 필드를 테넌트의 ID로 설정합니다. 예를 들면 다음과 같습니다.

    Web

    import { getAuth } from "firebase/auth";
    
    const auth = getAuth(app);
    auth.tenantId = "myTenantId1";
    

    Web

    firebase.auth().tenantId = 'myTenantId1';
    

다중 인증(MFA) 사용 설정

  1. Firebase Console의 인증 > 로그인 방법 페이지를 엽니다.

  2. 고급 섹션에서 SMS 다중 인증(MFA)을 사용 설정합니다.

    앱을 테스트할 전화번호도 입력해야 합니다. 선택사항이지만 개발 중 제한이 발생하지 않도록 테스트 전화번호를 등록하는 것이 좋습니다.

  3. 아직 앱 도메인을 승인하지 않았다면 Firebase Console의 인증 > 설정 페이지에서 허용 목록에 추가합니다.

등록 패턴 선택

앱에 다중 인증(MFA)이 필요한지 여부 및 사용자 등록 방법과 시기를 선택할 수 있습니다. 일반적인 패턴은 다음과 같습니다.

  • 등록 시 사용자의 두 번째 단계를 등록합니다. 앱이 모든 사용자에게 다중 인증(MFA)을 요구한다면 이 방법을 사용하세요.

  • 등록 시 건너뛸 수 있는 옵션으로 두 번째 단계를 등록하는 옵션을 제공하세요. 다중 인증(MFA)을 권고하지만 필수이지는 않은 앱은 이 방법을 사용하는 것이 좋습니다.

  • 가입 화면이 아닌 사용자의 계정 또는 프로필 관리 페이지에서 두 번째 단계를 추가할 수 있도록 합니다. 이렇게 하면 등록 프로세스 중에 발생하는 마찰을 최소화하면서도 보안에 민감한 사용자에게 다중 인증(MFA)을 제공할 수 있습니다.

  • 사용자가 보안 요구사항이 향상된 기능에 액세스하려고 할 때 두 번째 단계를 점진적으로 추가하도록 합니다.

reCAPTCHA 확인자 설정

SMS 코드를 보내려면 reCAPTCHA 확인자를 구성해야 합니다. Firebase는 reCAPTCHA를 통해 전화번호 확인 요청이 앱의 허용된 도메인 중 하나에서 발생하도록 하여 악용을 방지합니다.

reCAPTCHA 클라이언트를 수동으로 설정할 필요가 없습니다. 클라이언트 SDK의 RecaptchaVerifier 객체는 필요한 클라이언트 키 및 보안 비밀을 자동으로 만들고 초기화합니다.

보이지 않는 reCAPTCHA 사용

RecaptchaVerifier 객체는 종종 상호작용 없이 사용자를 확인할 수 있는 보이지 않는 reCAPTCHA를 지원합니다. 보이지 않는 reCAPTCHA를 사용하려면 size 매개변수가 invisible로 설정된 RecaptchaVerifier를 만들고 다단계 등록을 시작하는 UI 요소의 ID를 지정합니다.

Web

import { RecaptchaVerifier } from "firebase/auth";

const recaptchaVerifier = new RecaptchaVerifier("sign-in-button", {
    "size": "invisible",
    "callback": function(response) {
        // reCAPTCHA solved, you can proceed with
        // phoneAuthProvider.verifyPhoneNumber(...).
        onSolvedRecaptcha();
    }
}, auth);

Web

var recaptchaVerifier = new firebase.auth.RecaptchaVerifier('sign-in-button', {
'size': 'invisible',
'callback': function(response) {
  // reCAPTCHA solved, you can proceed with phoneAuthProvider.verifyPhoneNumber(...).
  onSolvedRecaptcha();
}
});

reCAPTCHA 위젯 사용

표시되는 reCAPTCHA 위젯을 사용하려면 위젯을 포함할 HTML 요소를 만든 다음 UI 컨테이너의 ID가 있는 RecaptchaVerifier 객체를 만듭니다. 선택적으로 reCAPTCHA가 해결되거나 만료될 때 호출되는 콜백을 설정할 수도 있습니다.

Web

import { RecaptchaVerifier } from "firebase/auth";

const recaptchaVerifier = new RecaptchaVerifier(
    "recaptcha-container",

    // Optional reCAPTCHA parameters.
    {
      "size": "normal",
      "callback": function(response) {
        // reCAPTCHA solved, you can proceed with
        // phoneAuthProvider.verifyPhoneNumber(...).
        onSolvedRecaptcha();
      },
      "expired-callback": function() {
        // Response expired. Ask user to solve reCAPTCHA again.
        // ...
      }
    }, auth
);

Web

var recaptchaVerifier = new firebase.auth.RecaptchaVerifier(
  'recaptcha-container',
  // Optional reCAPTCHA parameters.
  {
    'size': 'normal',
    'callback': function(response) {
      // reCAPTCHA solved, you can proceed with phoneAuthProvider.verifyPhoneNumber(...).
      // ...
      onSolvedRecaptcha();
    },
    'expired-callback': function() {
      // Response expired. Ask user to solve reCAPTCHA again.
      // ...
    }
  });

reCAPTCHA 사전 렌더링

필요에 따라 2단계 등록을 시작하기 전에 reCAPTCHA를 미리 렌더링할 수 있습니다.

Web

recaptchaVerifier.render()
    .then(function (widgetId) {
        window.recaptchaWidgetId = widgetId;
    });

Web

recaptchaVerifier.render()
  .then(function(widgetId) {
    window.recaptchaWidgetId = widgetId;
  });

render()가 확인되면 reCAPTCHA의 위젯 ID를 가져옵니다. 이 ID를 사용하여 reCAPTCHA API를 호출할 수 있습니다.

var recaptchaResponse = grecaptcha.getResponse(window.recaptchaWidgetId);

RecaptchaVerifier는 verify 메서드를 사용하여 이 로직을 추상화하므로 grecaptcha 변수를 직접 처리할 필요가 없습니다.

두 번째 단계 등록

사용자의 새로운 두 번째 단계를 등록하려면 다음 단계를 따르세요.

  1. 사용자를 다시 인증합니다.

  2. 사용자에게 전화번호를 입력하도록 요청합니다.

  3. 이전 섹션에서 설명한 대로 reCAPTCHA 확인자를 초기화합니다. RecaptchaVerifier 인스턴스가 이미 구성된 경우 이 단계를 건너뛰세요.

    Web

    import { RecaptchaVerifier } from "firebase/auth";
    
    const recaptchaVerifier = new RecaptchaVerifier('recaptcha-container-id', undefined, auth);
    

    Web

    var recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha-container-id');
    
  4. 사용자를 위한 다중 세션을 가져옵니다.

    Web

    import { multiFactor } from "firebase/auth";
    
    multiFactor(user).getSession().then(function (multiFactorSession) {
        // ...
    });
    

    Web

    user.multiFactor.getSession().then(function(multiFactorSession) {
      // ...
    })
    
  5. 사용자의 전화번호 및 다중 세션을 사용하여 PhoneInfoOptions 객체를 초기화합니다.

    Web

    // Specify the phone number and pass the MFA session.
    const phoneInfoOptions = {
      phoneNumber: phoneNumber,
      session: multiFactorSession
    };
    

    Web

    // Specify the phone number and pass the MFA session.
    var phoneInfoOptions = {
      phoneNumber: phoneNumber,
      session: multiFactorSession
    };
    
  6. 사용자 전화로 인증 코드 보내기

    Web

    import { PhoneAuthProvider } from "firebase/auth";
    
    const phoneAuthProvider = new PhoneAuthProvider(auth);
    phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
        .then(function (verificationId) {
            // verificationId will be needed to complete enrollment.
        });
    

    Web

    var phoneAuthProvider = new firebase.auth.PhoneAuthProvider();
    // Send SMS verification code.
    return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
      .then(function(verificationId) {
        // verificationId will be needed for enrollment completion.
      })
    

    필수 사항은 아니지만, 사용자에게 SMS 메시지가 전송되고 표준 요금이 적용된다는 점을 미리 알리는 것이 좋습니다.

  7. 요청이 실패하면 reCAPTCHA를 재설정하고 이전 단계를 반복하면 사용자가 다시 시도할 수 있습니다. reCAPTCHA 토큰은 일회용이기 때문에 verifyPhoneNumber()는 오류가 발생하면 reCAPTCHA를 자동으로 재설정합니다.

    Web

    recaptchaVerifier.clear();
    

    Web

    recaptchaVerifier.clear();
    
  8. SMS 코드가 전송되면 사용자에게 코드를 확인하도록 요청합니다.

    Web

    // Ask user for the verification code. Then:
    const cred = PhoneAuthProvider.credential(verificationId, verificationCode);
    

    Web

    // Ask user for the verification code. Then:
    var cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode);
    
  9. PhoneAuthCredentialMultiFactorAssertion 객체를 초기화합니다.

    Web

    import { PhoneMultiFactorGenerator } from "firebase/auth";
    
    const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(cred);
    

    Web

    var multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
    
  10. 등록을 완료합니다. 선택사항으로 두 번째 단계의 표시 이름을 지정할 수 있습니다. 인증 과정에서 전화번호가 마스킹되므로(예: +1******1234) 두 번째 단계가 여러 개인 사용자에게 유용합니다.

    Web

    // Complete enrollment. This will update the underlying tokens
    // and trigger ID token change listener.
    multiFactor(user).enroll(multiFactorAssertion, "My personal phone number");
    

    Web

    // Complete enrollment. This will update the underlying tokens
    // and trigger ID token change listener.
    user.multiFactor.enroll(multiFactorAssertion, 'My personal phone number');
    

다음 코드는 두 번째 단계를 등록하는 전체 예시를 보여줍니다.

Web

import {
    multiFactor, PhoneAuthProvider, PhoneMultiFactorGenerator,
    RecaptchaVerifier
} from "firebase/auth";

const recaptchaVerifier = new RecaptchaVerifier('recaptcha-container-id', undefined, auth);
multiFactor(user).getSession()
    .then(function (multiFactorSession) {
        // Specify the phone number and pass the MFA session.
        const phoneInfoOptions = {
            phoneNumber: phoneNumber,
            session: multiFactorSession
        };

        const phoneAuthProvider = new PhoneAuthProvider(auth);

        // Send SMS verification code.
        return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier);
    }).then(function (verificationId) {
        // Ask user for the verification code. Then:
        const cred = PhoneAuthProvider.credential(verificationId, verificationCode);
        const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(cred);

        // Complete enrollment.
        return multiFactor(user).enroll(multiFactorAssertion, mfaDisplayName);
    });

Web

var recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha-container-id');
user.multiFactor.getSession().then(function(multiFactorSession) {
  // Specify the phone number and pass the MFA session.
  var phoneInfoOptions = {
    phoneNumber: phoneNumber,
    session: multiFactorSession
  };
  var phoneAuthProvider = new firebase.auth.PhoneAuthProvider();
  // Send SMS verification code.
  return phoneAuthProvider.verifyPhoneNumber(
      phoneInfoOptions, recaptchaVerifier);
})
.then(function(verificationId) {
  // Ask user for the verification code.
  var cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode);
  var multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
  // Complete enrollment.
  return user.multiFactor.enroll(multiFactorAssertion, mfaDisplayName);
});

수고하셨습니다. 사용자의 두 번째 인증 단계가 성공적으로 등록되었습니다.

두 번째 단계 인증으로 사용자 로그인 처리

2단계 SMS 인증으로 사용자를 로그인 처리하려면 다음 안내를 따르세요.

  1. 첫 번째 단계로 사용자를 로그인한 다음 auth/multi-factor-auth-required 오류를 포착합니다. 이 오류에는 리졸버, 등록된 두 번째 단계에 대한 힌트, 사용자가 첫 번째 단계에서 성공적으로 인증되었음을 증명하는 기본 세션이 포함됩니다.

    예를 들어 사용자의 첫 번째 단계가 이메일과 비밀번호인 경우는 다음과 같습니다.

    Web

    import { getAuth, getMultiFactorResolver} from "firebase/auth";
    
    const auth = getAuth();
    signInWithEmailAndPassword(auth, email, password)
        .then(function (userCredential) {
            // User successfully signed in and is not enrolled with a second factor.
        })
        .catch(function (error) {
            if (error.code == 'auth/multi-factor-auth-required') {
                // The user is a multi-factor user. Second factor challenge is required.
                resolver = getMultiFactorResolver(auth, error);
                // ...
            } else if (error.code == 'auth/wrong-password') {
                // Handle other errors such as wrong password.
            }
    });
    

    Web

    firebase.auth().signInWithEmailAndPassword(email, password)
      .then(function(userCredential) {
        // User successfully signed in and is not enrolled with a second factor.
      })
      .catch(function(error) {
        if (error.code == 'auth/multi-factor-auth-required') {
          // The user is a multi-factor user. Second factor challenge is required.
          resolver = error.resolver;
          // ...
        } else if (error.code == 'auth/wrong-password') {
          // Handle other errors such as wrong password.
        } ...
      });
    

    사용자의 첫 번째 단계가 OAuth, SAML 또는 OIDC와 같은 제휴 공급업체라면 signInWithPopup() 또는 signInWithRedirect()를 호출한 후 오류를 포착합니다.

  2. 사용자가 보조 단계를 여러 개 등록한 경우 어떤 단계를 사용할지 질문합니다.

    Web

    // Ask user which second factor to use.
    // You can get the masked phone number via resolver.hints[selectedIndex].phoneNumber
    // You can get the display name via resolver.hints[selectedIndex].displayName
    
    if (resolver.hints[selectedIndex].factorId ===
        PhoneMultiFactorGenerator.FACTOR_ID) {
        // User selected a phone second factor.
        // ...
    } else if (resolver.hints[selectedIndex].factorId ===
               TotpMultiFactorGenerator.FACTOR_ID) {
        // User selected a TOTP second factor.
        // ...
    } else {
        // Unsupported second factor.
    }
    

    Web

    // Ask user which second factor to use.
    // You can get the masked phone number via resolver.hints[selectedIndex].phoneNumber
    // You can get the display name via resolver.hints[selectedIndex].displayName
    if (resolver.hints[selectedIndex].factorId === firebase.auth.PhoneMultiFactorGenerator.FACTOR_ID) {
      // User selected a phone second factor.
      // ...
    } else if (resolver.hints[selectedIndex].factorId === firebase.auth.TotpMultiFactorGenerator.FACTOR_ID) {
      // User selected a TOTP second factor.
      // ...
    } else {
      // Unsupported second factor.
    }
    
  3. 이전 섹션에서 설명한 대로 reCAPTCHA 확인자를 초기화합니다. RecaptchaVerifier 인스턴스가 이미 구성된 경우 이 단계를 건너뛰세요.

    Web

    import { RecaptchaVerifier } from "firebase/auth";
    
    recaptchaVerifier = new RecaptchaVerifier('recaptcha-container-id', undefined, auth);
    

    Web

    var recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha-container-id');
    
  4. 사용자의 전화번호 및 다단계 세션을 사용하여 PhoneInfoOptions 객체를 초기화합니다. 이 값은 auth/multi-factor-auth-required 오류에 전달된 resolver 객체에 포함됩니다.

    Web

    const phoneInfoOptions = {
        multiFactorHint: resolver.hints[selectedIndex],
        session: resolver.session
    };
    

    Web

    var phoneInfoOptions = {
      multiFactorHint: resolver.hints[selectedIndex],
      session: resolver.session
    };
    
  5. 사용자 전화로 인증 코드 보내기

    Web

    // Send SMS verification code.
    const phoneAuthProvider = new PhoneAuthProvider(auth);
    phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
        .then(function (verificationId) {
            // verificationId will be needed for sign-in completion.
        });
    

    Web

    var phoneAuthProvider = new firebase.auth.PhoneAuthProvider();
    // Send SMS verification code.
    return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
      .then(function(verificationId) {
        // verificationId will be needed for sign-in completion.
      })
    
  6. 요청이 실패하면 reCAPTCHA를 재설정하고 이전 단계를 반복하면 사용자가 다시 시도할 수 있습니다.

    Web

    recaptchaVerifier.clear();
    

    Web

    recaptchaVerifier.clear();
    
  7. SMS 코드가 전송되면 사용자에게 코드를 확인하도록 요청합니다.

    Web

    const cred = PhoneAuthProvider.credential(verificationId, verificationCode);
    

    Web

    // Ask user for the verification code. Then:
    var cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode);
    
  8. PhoneAuthCredentialMultiFactorAssertion 객체를 초기화합니다.

    Web

    const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(cred);
    

    Web

    var multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
    
  9. resolver.resolveSignIn()을 호출하여 2차 인증을 완료합니다. 그런 다음 표준 공급업체별 데이터 및 인증 사용자 인증 정보가 포함된 원래 로그인 결과에 액세스할 수 있습니다.

    Web

    // Complete sign-in. This will also trigger the Auth state listeners.
    resolver.resolveSignIn(multiFactorAssertion)
        .then(function (userCredential) {
            // userCredential will also contain the user, additionalUserInfo, optional
            // credential (null for email/password) associated with the first factor sign-in.
    
            // For example, if the user signed in with Google as a first factor,
            // userCredential.additionalUserInfo will contain data related to Google
            // provider that the user signed in with.
            // - user.credential contains the Google OAuth credential.
            // - user.credential.accessToken contains the Google OAuth access token.
            // - user.credential.idToken contains the Google OAuth ID token.
        });
    

    Web

    // Complete sign-in. This will also trigger the Auth state listeners.
    resolver.resolveSignIn(multiFactorAssertion)
      .then(function(userCredential) {
        // userCredential will also contain the user, additionalUserInfo, optional
        // credential (null for email/password) associated with the first factor sign-in.
        // For example, if the user signed in with Google as a first factor,
        // userCredential.additionalUserInfo will contain data related to Google provider that
        // the user signed in with.
        // user.credential contains the Google OAuth credential.
        // user.credential.accessToken contains the Google OAuth access token.
        // user.credential.idToken contains the Google OAuth ID token.
      });
    

아래 코드는 다중 인증 사용자를 위한 전체 로그인 예시를 보여줍니다.

Web

import {
    getAuth,
    getMultiFactorResolver,
    PhoneAuthProvider,
    PhoneMultiFactorGenerator,
    RecaptchaVerifier,
    signInWithEmailAndPassword
} from "firebase/auth";

const recaptchaVerifier = new RecaptchaVerifier('recaptcha-container-id', undefined, auth);

const auth = getAuth();
signInWithEmailAndPassword(auth, email, password)
    .then(function (userCredential) {
        // User is not enrolled with a second factor and is successfully
        // signed in.
        // ...
    })
    .catch(function (error) {
        if (error.code == 'auth/multi-factor-auth-required') {
            const resolver = getMultiFactorResolver(auth, error);
            // Ask user which second factor to use.
            if (resolver.hints[selectedIndex].factorId ===
                PhoneMultiFactorGenerator.FACTOR_ID) {
                const phoneInfoOptions = {
                    multiFactorHint: resolver.hints[selectedIndex],
                    session: resolver.session
                };
                const phoneAuthProvider = new PhoneAuthProvider(auth);
                // Send SMS verification code
                return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
                    .then(function (verificationId) {
                        // Ask user for the SMS verification code. Then:
                        const cred = PhoneAuthProvider.credential(
                            verificationId, verificationCode);
                        const multiFactorAssertion =
                            PhoneMultiFactorGenerator.assertion(cred);
                        // Complete sign-in.
                        return resolver.resolveSignIn(multiFactorAssertion)
                    })
                    .then(function (userCredential) {
                        // User successfully signed in with the second factor phone number.
                    });
            } else if (resolver.hints[selectedIndex].factorId ===
                       TotpMultiFactorGenerator.FACTOR_ID) {
                // Handle TOTP MFA.
                // ...
            } else {
                // Unsupported second factor.
            }
        } else if (error.code == 'auth/wrong-password') {
            // Handle other errors such as wrong password.
        }
    });

Web

var resolver;
firebase.auth().signInWithEmailAndPassword(email, password)
  .then(function(userCredential) {
    // User is not enrolled with a second factor and is successfully signed in.
    // ...
  })
  .catch(function(error) {
    if (error.code == 'auth/multi-factor-auth-required') {
      resolver = error.resolver;
      // Ask user which second factor to use.
      if (resolver.hints[selectedIndex].factorId ===
          firebase.auth.PhoneMultiFactorGenerator.FACTOR_ID) {
        var phoneInfoOptions = {
          multiFactorHint: resolver.hints[selectedIndex],
          session: resolver.session
        };
        var phoneAuthProvider = new firebase.auth.PhoneAuthProvider();
        // Send SMS verification code
        return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
          .then(function(verificationId) {
            // Ask user for the SMS verification code.
            var cred = firebase.auth.PhoneAuthProvider.credential(
                verificationId, verificationCode);
            var multiFactorAssertion =
                firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
            // Complete sign-in.
            return resolver.resolveSignIn(multiFactorAssertion)
          })
          .then(function(userCredential) {
            // User successfully signed in with the second factor phone number.
          });
      } else if (resolver.hints[selectedIndex].factorId ===
        firebase.auth.TotpMultiFactorGenerator.FACTOR_ID) {
        // Handle TOTP MFA.
        // ...
      } else {
        // Unsupported second factor.
      }
    } else if (error.code == 'auth/wrong-password') {
      // Handle other errors such as wrong password.
    } ...
  });

수고하셨습니다. 다중 인증(MFA)을 사용하여 사용자가 성공적으로 로그인했습니다.

다음 단계