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

Cloud Functions 차단으로 Firebase 인증 확장

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.

Identity Platform을 사용하여 Firebase 인증으로 업그레이드한 경우 Cloud Functions 차단을 사용하여 Firebase 인증을 확장할 수 있습니다.

차단 기능을 사용하면 사용자가 앱에 등록하거나 로그인한 결과를 수정하는 사용자 지정 코드를 실행할 수 있습니다. 예를 들어 사용자가 특정 기준을 충족하지 않는 경우 인증하지 못하도록 하거나 클라이언트 앱으로 반환하기 전에 사용자 정보를 업데이트할 수 있습니다.

시작하기 전에

차단 기능을 사용하려면 Firebase 프로젝트를 Identity Platform을 통한 Firebase 인증으로 업그레이드해야 합니다. 아직 업그레이드하지 않았다면 먼저 업그레이드하십시오.

차단 기능 이해

두 가지 이벤트에 대한 차단 기능을 등록할 수 있습니다.

  • beforeCreate : 새 사용자가 Firebase 인증 데이터베이스에 저장되기 전과 토큰이 클라이언트 앱으로 반환되기 전에 트리거됩니다.

  • beforeSignIn : 사용자 자격 증명이 확인된 후 Firebase 인증이 클라이언트 앱에 ID 토큰을 반환하기 전에 트리거됩니다. 앱에서 다단계 인증을 사용하는 경우 사용자가 두 번째 요소를 확인한 후 함수가 트리거됩니다. 새 사용자를 만들면 beforeSignIn 외에 beforeCreate 도 트리거됩니다.

차단 기능을 사용할 때 다음 사항에 유의하십시오.

  • 함수는 7초 이내에 응답해야 합니다. 7초 후 Firebase 인증이 오류를 반환하고 클라이언트 작업이 실패합니다.

  • 200 이외의 HTTP 응답 코드는 클라이언트 앱에 전달됩니다. 클라이언트 코드가 함수가 반환할 수 있는 오류를 처리하는지 확인합니다.

  • 기능은 테넌트 에 포함된 사용자를 포함하여 프로젝트의 모든 사용자에게 적용됩니다. Firebase 인증은 사용자가 속한 테넌트를 포함하여 사용자에 대한 정보를 함수에 제공하므로 적절하게 응답할 수 있습니다.

  • 다른 ID 공급자를 계정에 연결하면 등록된 모든 beforeSignIn 기능이 다시 트리거됩니다.

  • 익명 및 사용자 지정 인증은 차단 기능을 트리거하지 않습니다.

차단 기능 배포 및 등록

사용자 인증 흐름에 사용자 지정 코드를 삽입하려면 차단 기능을 배포하고 등록합니다. 차단 기능이 배포 및 등록되면 인증 및 사용자 생성이 성공하려면 사용자 지정 코드가 성공적으로 완료되어야 합니다.

차단 기능 배포

기능을 배포할 때와 동일한 방식으로 차단 기능을 배포합니다. (자세한 내용은 Cloud Functions 시작하기 페이지 참조). 요약해서 말하자면:

  1. beforeCreate 이벤트, beforeSignIn 이벤트 또는 둘 다를 처리하는 Cloud Functions를 작성합니다.

    예를 들어 시작하려면 index.js 에 다음 무작동 기능을 추가할 수 있습니다.

    const functions = require('firebase-functions');
    
    exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
      // TODO
    });
    
    exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
      // TODO
    });
    

    위의 예에서는 사용자 지정 인증 로직의 구현을 생략했습니다. 차단 기능을 구현하는 방법과 특정 예에 대한 일반적인 시나리오 를 알아보려면 다음 섹션을 참조하세요.

  2. Firebase CLI를 사용하여 함수를 배포합니다.

    firebase deploy --only functions
    

    업데이트할 때마다 함수를 다시 배포해야 합니다.

차단 기능 등록

  1. Firebase 콘솔에서 Firebase 인증 설정 페이지로 이동합니다.

  2. 차단 기능 탭을 선택합니다.

  3. 계정 생성 전(beforeCreate) 또는 로그인 전 (beforeSignIn) 아래의 드롭다운 메뉴에서 차단 기능을 선택하여 등록합니다.

  4. 변경 사항을 저장합니다.

사용자 및 컨텍스트 정보 가져오기

beforeSignInbeforeCreate 이벤트는 로그인하는 사용자에 대한 정보가 포함된 UserEventContext 개체를 제공합니다. 코드에서 이러한 값을 사용하여 작업을 계속할 수 있는지 여부를 결정합니다.

User 개체에서 사용할 수 있는 속성 목록은 UserRecord API 참조를 참조 하세요.

EventContext 개체에는 다음 속성이 포함됩니다.

이름 설명 예시
locale 애플리케이션 로케일. 클라이언트 SDK를 사용하거나 REST API에서 로케일 헤더를 전달하여 로케일을 설정할 수 있습니다. fr 또는 sv-SE
ipAddress 최종 사용자가 등록하거나 로그인하는 장치의 IP 주소입니다. 114.14.200.1
userAgent 차단 기능을 트리거하는 사용자 에이전트. Mozilla/5.0 (X11; Linux x86_64)
eventId 이벤트의 고유 식별자입니다. rWsyPtolplG2TBFoOkkgyg
eventType 이벤트 유형입니다. 이는 beforeSignIn 또는 beforeCreate 와 같은 이벤트 이름과 Google 또는 이메일/비밀번호와 같이 사용된 관련 로그인 방법에 대한 정보를 제공합니다. providers/cloud.auth/eventTypes/user.beforeSignIn:password
authType 항상 USER . USER
resource Firebase 인증 프로젝트 또는 테넌트입니다. projects/ project-id /tenants/ tenant-id
timestamp 이벤트가 트리거된 시간으로, RFC 3339 문자열로 형식이 지정됩니다. Tue, 23 Jul 2019 21:10:57 GMT
additionalUserInfo 사용자에 대한 정보를 포함하는 개체입니다. AdditionalUserInfo
credential 사용자의 자격 증명에 대한 정보를 포함하는 개체입니다. AuthCredential

등록 또는 로그인 차단

등록 또는 로그인 시도를 차단하려면 함수에서 HttpsError 를 발생시킵니다. 예를 들어:

노드.js

throw new functions.auth.HttpsError('permission-denied');

다음 표에는 기본 오류 메시지와 함께 발생할 수 있는 오류가 나열되어 있습니다.

이름 암호 메시지
invalid-argument 400 클라이언트가 잘못된 인수를 지정했습니다.
failed-precondition 400 현재 시스템 상태에서는 요청을 실행할 수 없습니다.
out-of-range 400 클라이언트가 잘못된 범위를 지정했습니다.
unauthenticated 401 OAuth 토큰이 없거나 유효하지 않거나 만료되었습니다.
permission-denied 403 클라이언트에게 충분한 권한이 없습니다.
not-found 404 지정된 리소스를 찾을 수 없습니다.
aborted 409 읽기-수정-쓰기 충돌과 같은 동시성 충돌.
already-exists 409 클라이언트가 생성하려고 시도한 리소스가 이미 있습니다.
resource-exhausted 429 리소스 할당량이 부족하거나 속도 제한에 도달했습니다.
cancelled 499 클라이언트가 요청을 취소했습니다.
data-loss 500 복구할 수 없는 데이터 손실 또는 데이터 손상.
unknown 500 알 수 없는 서버 오류입니다.
internal 500 인터넷 서버 오류.
not-implemented 501 API 메소드가 서버에서 구현되지 않았습니다.
unavailable 503 서비스를 사용할 수 없습니다.
deadline-exceeded 504 요청 기한이 초과되었습니다.

사용자 지정 오류 메시지를 지정할 수도 있습니다.

노드.js

throw new functions.auth.HttpsError('permission-denied', 'Unauthorized request origin!');

다음 예는 특정 도메인에 있지 않은 사용자가 앱에 등록하는 것을 차단하는 방법을 보여줍니다.

노드.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  // (If the user is authenticating within a tenant context, the tenant ID can be determined from
  // user.tenantId or from context.resource, e.g. 'projects/project-id/tenant/tenant-id-1')

  // Only users of a specific domain can sign up.
  if (user.email.indexOf('@acme.com') === -1) {
    throw new functions.auth.HttpsError('invalid-argument', `Unauthorized email "${user.email}"`);
  }
});

기본 메시지를 사용하는지 맞춤 메시지를 사용하는지에 관계없이 Cloud Functions는 오류를 래핑하고 내부 오류로 클라이언트에 반환합니다. 예를 들어:

throw new functions.auth.HttpsError('invalid-argument', `Unauthorized email user@evil.com}`);

앱은 오류를 포착하고 그에 따라 처리해야 합니다. 예를 들어:

자바스크립트

// Blocking functions can also be triggered in a multi-tenant context before user creation.
// firebase.auth().tenantId = 'tenant-id-1';
firebase.auth().createUserWithEmailAndPassword('johndoe@example.com', 'password')
  .then((result) => {
    result.user.getIdTokenResult()
  })
  .then((idTokenResult) => {
    console.log(idTokenResult.claim.admin);
  })
  .catch((error) => {
    if (error.code !== 'auth/internal-error' && error.message.indexOf('Cloud Function') !== -1) {
      // Display error.
    } else {
      // Registration succeeds.
    }
  });

사용자 수정

등록 또는 로그인 시도를 차단하는 대신 작업을 계속하도록 허용할 수 있지만 Firebase 인증의 데이터베이스에 저장되고 클라이언트에 반환되는 User 개체를 수정할 수 있습니다.

사용자를 수정하려면 수정할 필드가 포함된 이벤트 핸들러에서 객체를 반환합니다. 다음 필드를 수정할 수 있습니다.

  • displayName
  • disabled
  • emailVerified
  • photoURL
  • customClaims
  • sessionClaims ( beforeSignIn 만 해당)

sessionClaims 를 제외하고 수정된 모든 필드는 Firebase 인증의 데이터베이스에 저장됩니다. 즉, 응답 토큰에 포함되고 사용자 세션 간에 유지됩니다.

다음 예에서는 기본 표시 이름을 설정하는 방법을 보여줍니다.

노드.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  return {
    // If no display name is provided, set it to "Guest".
    displayName: user.displayName || 'Guest';
  };
});

beforeCreatebeforeSignIn 모두에 대한 이벤트 핸들러를 등록하는 경우 beforeCreatebeforeSignIn 이후에 실행됩니다. beforeSignIn 에서 업데이트된 사용자 필드는 beforeCreate 에서 볼 수 있습니다. 두 이벤트 핸들러에서 sessionClaims 이외의 필드를 설정하면 beforeCreate 에 설정된 값이 beforeSignIn 에 설정된 값을 덮어씁니다. sessionClaims 의 경우에만 현재 세션의 토큰 클레임으로 전파되지만 데이터베이스에 지속되거나 저장되지 않습니다.

예를 들어 sessionClaims 이 설정된 경우 beforeSignIn 은 이를 beforeCreate 클레임과 함께 반환하고 병합합니다. 병합될 때 sessionClaims 키가 customClaims 의 키와 일치하는 경우 일치하는 customClaimssessionClaims 키로 토큰 클레임에서 덮어씁니다. 그러나 초과된 customClaims 키는 향후 요청을 위해 데이터베이스에 계속 유지됩니다.

지원되는 OAuth 자격 증명 및 데이터

다양한 ID 제공자의 차단 기능에 OAuth 자격 증명 및 데이터를 전달할 수 있습니다. 다음 표는 각 자격 증명 공급자에 대해 지원되는 자격 증명 및 데이터를 보여줍니다.

ID 제공자 ID 토큰 액세스 토큰 만료 시간 토큰 비밀 토큰 새로 고침 로그인 클레임
Google 아니 아니
페이스북 아니 아니 아니 아니
트위터 아니 아니 아니 아니
깃허브 아니 아니 아니 아니 아니
마이크로소프트 아니 아니
링크드인 아니 아니 아니 아니
야후 아니 아니
사과 아니 아니
SAML 아니 아니 아니 아니 아니
OIDC 아니

토큰 새로 고침

차단 기능에서 새로 고침 토큰을 사용하려면 먼저 Firebase 콘솔의 차단 기능 페이지에서 확인란을 선택해야 합니다.

ID 토큰 또는 액세스 토큰과 같은 OAuth 자격 증명으로 직접 로그인할 때 ID 공급자는 새로 고침 토큰을 반환하지 않습니다. 이 상황에서 동일한 클라이언트 측 OAuth 자격 증명이 차단 기능에 전달됩니다.

다음 섹션에서는 각 ID 제공자 유형과 지원되는 자격 증명 및 데이터에 대해 설명합니다.

일반 OIDC 공급자

사용자가 일반 OIDC 공급자로 로그인하면 다음 자격 증명이 전달됩니다.

  • ID 토큰 : id_token 흐름을 선택한 경우 제공됩니다.
  • 액세스 토큰 : 코드 흐름을 선택한 경우 제공됩니다. 코드 흐름은 현재 REST API를 통해서만 지원됩니다.
  • 새로 고침 토큰 : offline_access 범위 가 선택된 경우 제공됩니다.

예시:

const provider = new firebase.auth.OAuthProvider('oidc.my-provider');
provider.addScope('offline_access');
firebase.auth().signInWithPopup(provider);

Google

사용자가 Google로 로그인하면 다음 자격 증명이 전달됩니다.

  • ID 토큰
  • 액세스 토큰
  • 새로 고침 토큰 : 다음 사용자 정의 매개변수가 요청된 경우에만 제공됩니다.
    • access_type=offline
    • prompt=consent , 사용자가 이전에 동의했고 새 범위가 요청되지 않은 경우

예시:

const provider = new firebase.auth.GoogleAuthProvider();
provider.setCustomParameters({
  'access_type': 'offline',
  'prompt': 'consent'
});
firebase.auth().signInWithPopup(provider);

Google 갱신 토큰 에 대해 자세히 알아보세요.

페이스북

사용자가 Facebook으로 로그인하면 다음 자격 증명이 전달됩니다.

  • 액세스 토큰 : 다른 액세스 토큰으로 교환할 수 있는 액세스 토큰을 반환합니다. Facebook에서 지원하는 다양한 유형의 액세스 토큰 과 이를 장기 토큰으로 교환하는 방법에 대해 자세히 알아보세요.

깃허브

사용자가 GitHub로 로그인하면 다음 자격 증명이 전달됩니다.

  • 액세스 토큰 : 해지하지 않는 한 만료되지 않습니다.

마이크로소프트

사용자가 Microsoft에 로그인하면 다음 자격 증명이 전달됩니다.

  • ID 토큰
  • 액세스 토큰
  • 새로 고침 토큰 : offline_access 범위 가 선택된 경우 차단 기능에 전달됩니다.

예시:

const provider = new firebase.auth.OAuthProvider('microsoft.com');
provider.addScope('offline_access');
firebase.auth().signInWithPopup(provider);

야후

사용자가 Yahoo로 로그인하면 사용자 정의 매개변수나 범위 없이 다음 자격 증명이 전달됩니다.

  • ID 토큰
  • 액세스 토큰
  • 토큰 새로 고침

링크드인

사용자가 LinkedIn에 로그인하면 다음 자격 증명이 전달됩니다.

  • 액세스 토큰

사과

사용자가 Apple로 로그인하면 사용자 정의 매개변수나 범위 없이 다음 자격 증명이 전달됩니다.

  • ID 토큰
  • 액세스 토큰
  • 토큰 새로 고침

일반적인 시나리오

다음 예는 차단 기능에 대한 몇 가지 일반적인 사용 사례를 보여줍니다.

특정 도메인에서만 등록 허용

다음 예는 example.com 도메인에 속하지 않은 사용자가 앱에 등록하지 못하도록 하는 방법을 보여줍니다.

노드.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (!user.email || user.email.indexOf('@example.com') === -1) {
    throw new functions.auth.HttpsError(
      'invalid-argument', `Unauthorized email "${user.email}"`);
  }
});

이메일이 확인되지 않은 사용자의 등록 차단

다음 예는 확인되지 않은 이메일을 가진 사용자가 앱에 등록하는 것을 방지하는 방법을 보여줍니다.

노드.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (user.email && !user.emailVerified) {
    throw new functions.auth.HttpsError(
      'invalid-argument', `Unverified email "${user.email}"`);
  }
});

등록 시 이메일 확인 필요

다음 예는 사용자가 등록 후 이메일을 확인하도록 요구하는 방법을 보여줍니다.

노드.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  const locale = context.locale;
  if (user.email && !user.emailVerified) {
    // Send custom email verification on sign-up.
    return admin.auth().generateEmailVerificationLink(user.email).then((link) => {
      return sendCustomVerificationEmail(user.email, link, locale);
    });
  }
});

exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
 if (user.email && !user.emailVerified) {
   throw new functions.auth.HttpsError(
     'invalid-argument', `"${user.email}" needs to be verified before access is granted.`);
  }
});

특정 ID 제공자 이메일을 확인된 것으로 취급

다음 예는 특정 ID 제공자의 사용자 이메일을 확인된 것으로 처리하는 방법을 보여줍니다.

노드.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (user.email && !user.emailVerified && context.eventType.indexOf(':facebook.com') !== -1) {
    return {
      emailVerified: true,
    };
  }
});

특정 IP 주소에서 로그인 차단

다음은 특정 IP 주소 범위에서 로그인을 차단하는 방법의 예입니다.

노드.js

exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
  if (isSuspiciousIpAddress(context.ipAddress)) {
    throw new functions.auth.HttpsError(
      'permission-denied', 'Unauthorized access!');
  }
});

사용자 지정 및 세션 클레임 설정

다음 예는 사용자 지정 및 세션 클레임을 설정하는 방법을 보여줍니다.

노드.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (context.credential &&
      context.credential.providerId === 'saml.my-provider-id') {
    return {
      // Employee ID does not change so save in persistent claims (stored in
      // Auth DB).
      customClaims: {
        eid: context.credential.claims.employeeid,
      },
      // Copy role and groups to token claims. These will not be persisted.
      sessionClaims: {
        role: context.credential.claims.role,
        groups: context.credential.claims.groups,
      }
    }
  }
});

의심스러운 활동을 모니터링하기 위해 IP 주소 추적

사용자가 로그인한 IP 주소를 추적하고 후속 요청에서 IP 주소와 비교하여 토큰 도난을 방지할 수 있습니다. 요청이 의심스러운 경우(예: IP가 다른 지역에서 온 경우) 사용자에게 다시 로그인하도록 요청할 수 있습니다.

  1. 세션 클레임을 사용하여 사용자가 로그인하는 IP 주소를 추적합니다.

    노드.js

    exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
      return {
        sessionClaims: {
          signInIpAddress: context.ipAddress,
        },
      };
    });
    
  2. 사용자가 Firebase 인증을 통한 인증이 필요한 리소스에 액세스하려고 하면 요청의 IP 주소를 로그인에 사용된 IP와 비교합니다.

    노드.js

    app.post('/getRestrictedData', (req, res) => {
      // Get the ID token passed.
      const idToken = req.body.idToken;
      // Verify the ID token, check if revoked and decode its payload.
      admin.auth().verifyIdToken(idToken, true).then((claims) => {
        // Get request IP address
        const requestIpAddress = req.connection.remoteAddress;
        // Get sign-in IP address.
        const signInIpAddress = claims.signInIpAddress;
        // Check if the request IP address origin is suspicious relative to
        // the session IP addresses. The current request timestamp and the
        // auth_time of the ID token can provide additional signals of abuse,
        // especially if the IP address suddenly changed. If there was a sudden
        // geographical change in a short period of time, then it will give
        // stronger signals of possible abuse.
        if (!isSuspiciousIpAddressChange(signInIpAddress, requestIpAddress)) {
          // Suspicious IP address change. Require re-authentication.
          // You can also revoke all user sessions by calling:
          // admin.auth().revokeRefreshTokens(claims.sub).
          res.status(401).send({error: 'Unauthorized access. Please login again!'});
        } else {
          // Access is valid. Try to return data.
          getData(claims).then(data => {
            res.end(JSON.stringify(data);
          }, error => {
            res.status(500).send({ error: 'Server error!' })
          });
        }
      });
    });
    

사용자 사진 심사

다음 예는 사용자의 프로필 사진을 삭제하는 방법을 보여줍니다.

노드.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (user.photoURL) {
    return isPhotoAppropriate(user.photoURL)
      .then((status) => {
        if (!status) {
          // Sanitize inappropriate photos by replacing them with guest photos.
          // Users could also be blocked from sign-up, disabled, etc.
          return {
            photoURL: PLACEHOLDER_GUEST_PHOTO_URL,
          };
        }
      });
});

이미지를 감지하고 삭제하는 방법에 대해 자세히 알아보려면 Cloud Vision 문서를 참조하세요.

사용자의 ID 제공자 OAuth 자격 증명에 액세스

다음 예는 Google로 로그인한 사용자에 대한 새로 고침 토큰을 얻고 이를 사용하여 Google 캘린더 API를 호출하는 방법을 보여줍니다. 새로 고침 토큰은 오프라인 액세스를 위해 저장됩니다.

노드.js

const {OAuth2Client} = require('google-auth-library');
const {google} = require('googleapis');
// ...
// Initialize Google OAuth client.
const keys = require('./oauth2.keys.json');
const oAuth2Client = new OAuth2Client(
  keys.web.client_id,
  keys.web.client_secret
);

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (context.credential &&
      context.credential.providerId === 'google.com') {
    // Store the refresh token for later offline use.
    // These will only be returned if refresh tokens credentials are included
    // (enabled by Cloud console).
    return saveUserRefreshToken(
        user.uid,
        context.credential.refreshToken,
        'google.com'
      )
      .then(() => {
        // Blocking the function is not required. The function can resolve while
        // this operation continues to run in the background.
        return new Promise((resolve, reject) => {
          // For this operation to succeed, the appropriate OAuth scope should be requested
          // on sign in with Google, client-side. In this case:
          // https://www.googleapis.com/auth/calendar
          // You can check granted_scopes from within:
          // context.additionalUserInfo.profile.granted_scopes (space joined list of scopes).

          // Set access token/refresh token.
          oAuth2Client.setCredentials({
            access_token: context.credential.accessToken,
            refresh_token: context.credential.refreshToken,
          });
          const calendar = google.calendar('v3');
          // Setup Onboarding event on user's calendar.
          const event = {/** ... */};
          calendar.events.insert({
            auth: oauth2client,
            calendarId: 'primary',
            resource: event,
          }, (err, event) => {
            // Do not fail. This is a best effort approach.
            resolve();
          });
      });
    })
  }
});