Firebase Summit에서 발표된 모든 내용을 살펴보고 Firebase로 앱을 빠르게 개발하고 안심하고 앱을 실행하는 방법을 알아보세요. 자세히 알아보기

웹 앱에 다단계 인증 추가

Identity Platform을 사용한 Firebase 인증으로 업그레이드한 경우 웹 앱에 SMS 다단계 인증을 추가할 수 있습니다.

다단계 인증은 앱의 보안을 강화합니다. 공격자는 종종 암호와 소셜 계정을 손상시키지만 문자 메시지를 가로채는 것은 더 어렵습니다.

시작하기 전에

  1. 다단계 인증을 지원하는 하나 이상의 공급자를 활성화합니다. 전화 인증, 익명 인증 및 Apple Game Center를 제외한 모든 제공업체는 MFA를 지원합니다.

  2. 앱이 사용자 이메일을 확인하고 있는지 확인하십시오. MFA는 이메일 확인이 필요합니다. 이렇게 하면 악의적인 행위자가 자신이 소유하지 않은 이메일로 서비스에 등록한 다음 두 번째 요소를 추가하여 실제 소유자를 잠그는 것을 방지할 수 있습니다.

멀티 테넌시 사용

다중 테넌트 환경에서 사용하기 위해 다중 요소 인증을 활성화하는 경우 이 문서의 나머지 지침과 함께 다음 단계를 완료해야 합니다.

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

  2. 코드에서 Auth 인스턴스의 tenantId 필드를 테넌트의 ID로 설정합니다. 예를 들어:

    Web version 9

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

    Web version 8

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

다단계 인증 활성화

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

  2. 고급 섹션에서 SMS 다단계 인증 을 활성화합니다.

    앱을 테스트할 전화번호도 입력해야 합니다. 선택 사항이지만 개발 중 스로틀링을 방지하려면 테스트 전화 번호를 등록하는 것이 좋습니다.

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

등록 패턴 선택

앱에 다단계 인증이 필요한지 여부와 사용자를 등록하는 방법과 시기를 선택할 수 있습니다. 몇 가지 일반적인 패턴은 다음과 같습니다.

  • 등록의 일부로 사용자의 두 번째 요소를 등록합니다. 앱이 모든 사용자에 대해 다단계 인증을 요구하는 경우 이 방법을 사용하십시오.

  • 등록하는 동안 두 번째 요소를 등록하기 위해 건너뛸 수 있는 옵션을 제공합니다. 다단계 인증을 권장하지만 요구하지 않는 앱은 이 접근 방식을 선호할 수 있습니다.

  • 가입 화면 대신 사용자 계정 또는 프로필 관리 페이지에서 두 번째 요소를 추가하는 기능을 제공합니다. 이렇게 하면 등록 프로세스 중 마찰을 최소화하면서 보안에 민감한 사용자가 다중 요소 인증을 계속 사용할 수 있습니다.

  • 사용자가 보안 요구 사항이 강화된 기능에 액세스하려는 경우 두 번째 요소를 점진적으로 추가해야 합니다.

reCAPTCHA 검증기 설정

SMS 코드를 보내려면 먼저 reCAPTCHA 검증기를 구성해야 합니다. Firebase는 reCAPTCHA를 사용하여 앱의 허용 도메인 중 하나에서 전화번호 확인 요청이 오도록 하여 남용을 방지합니다.

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

보이지 않는 reCAPTCHA 사용

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

Web version 9

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 version 8

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 version 9

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 version 8

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 version 9

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

Web version 8

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

render() 가 해결되면 reCAPTCHA API 를 호출하는 데 사용할 수 있는 reCAPTCHA의 위젯 ID를 얻습니다.

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

두 번째 요소 등록

사용자에 대한 새 보조 요소를 등록하려면:

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

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

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

    Web version 9

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

    Web version 8

    var recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha-container-id');
    
  4. 사용자를 위한 다단계 세션 가져오기:

    Web version 9

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

    Web version 8

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

    Web version 9

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

    Web version 8

    // Specify the phone number and pass the MFA session.
    var phoneInfoOptions = {
      phoneNumber: phoneNumber,
      session: multiFactorSession
    };
    
  6. 사용자의 전화로 확인 메시지 보내기:

    Web version 9

    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 version 8

    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 토큰은 1회용이므로 오류가 발생하면 verifyPhoneNumber() 가 자동으로 reCAPTCHA를 재설정합니다.

    Web version 9

    grecaptcha.reset(window.recaptchaWidgetId);
    
    // Or, if you haven't stored the widget ID:
    recaptchaVerifier.render()
      .then(function(widgetId) {
        grecaptcha.reset(widgetId);
      });
    

    Web version 8

    grecaptcha.reset(window.recaptchaWidgetId);
    
    // Or, if you haven't stored the widget ID:
    recaptchaVerifier.render()
      .then(function(widgetId) {
        grecaptcha.reset(widgetId);
      });
    
  8. SMS 코드가 전송되면 사용자에게 코드를 확인하도록 요청합니다.

    Web version 9

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

    Web version 8

    // Ask user for the verification code. Then:
    var cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode);
    
  9. PhoneAuthCredential 을 사용하여 MultiFactorAssertion 개체를 초기화합니다.

    Web version 9

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

    Web version 8

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

    Web version 9

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

    Web version 8

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

아래 코드는 두 번째 요소 등록의 전체 예를 보여줍니다.

Web version 9

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 version 8

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 version 9

    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 version 8

    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() ) 를 호출한 후 오류를 catch합니다.

  2. 사용자가 여러 개의 보조 요소를 등록한 경우 사용할 보조 요소를 묻습니다.

    Web version 9

    // 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 {
        // Unsupported second factor.
        // Note that only phone second factors are currently supported.
    }
    

    Web version 8

    // 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 {
      // Unsupported second factor.
      // Note that only phone second factors are currently supported.
    }
    
  3. 이전 섹션에서 설명한 대로 reCAPTCHA 검증기를 초기화합니다. RecaptchaVerifier 인스턴스가 이미 구성된 경우 이 단계를 건너뜁니다.

    Web version 9

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

    Web version 8

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

    Web version 9

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

    Web version 8

    var phoneInfoOptions = {
      multiFactorHint: resolver.hints[selectedIndex],
      session: resolver.session
    };
    
  5. 사용자의 전화로 확인 메시지 보내기:

    Web version 9

    // 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 version 8

    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 version 9

    grecaptcha.reset(window.recaptchaWidgetId);
    
    // Or, if you haven't stored the widget ID:
    recaptchaVerifier.render()
      .then(function(widgetId) {
        grecaptcha.reset(widgetId);
      });
    

    Web version 8

    grecaptcha.reset(window.recaptchaWidgetId);
    
    // Or, if you haven't stored the widget ID:
    recaptchaVerifier.render()
      .then(function(widgetId) {
        grecaptcha.reset(widgetId);
      });
    
  7. SMS 코드가 전송되면 사용자에게 코드를 확인하도록 요청합니다.

    Web version 9

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

    Web version 8

    // Ask user for the verification code. Then:
    var cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode);
    
  8. PhoneAuthCredential 을 사용하여 MultiFactorAssertion 개체를 초기화합니다.

    Web version 9

    const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(cred);
    

    Web version 8

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

    Web version 9

    // 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 version 8

    // 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 version 9

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 {
                // Unsupported second factor.
            }
        } else if (error.code == 'auth/wrong-password') {
            // Handle other errors such as wrong password.
        }
    });

Web version 8

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 {
        // Unsupported second factor.
      }
    } else if (error.code == 'auth/wrong-password') {
      // Handle other errors such as wrong password.
    } ...
  });

축하합니다! 다단계 인증을 사용하여 사용자를 성공적으로 로그인했습니다.

무엇 향후 계획

  • Admin SDK를 사용하여 프로그래밍 방식으로 다단계 사용자 를 관리합니다.