차단 함수를 사용하여 인증 흐름 맞춤설정

이 문서에서는 Cloud Functions 차단을 사용하여 Identity Platform 인증을 확장하는 방법을 설명합니다.

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

시작하기 전에

Identity Platform으로 앱을 만듭니다. 자세한 방법은 빠른 시작을 참조하세요.

차단 함수 이해

다음과 같은 두 가지 이벤트에 차단 함수를 등록할 수 있습니다.

  • beforeCreate: 신규 사용자가 Identity Platform 데이터베이스에 저장되기 전, 클라이언트 앱에 토큰이 반환되기 전에 트리거됩니다.

  • beforeSignIn: 사용자 인증 정보가 확인되었지만 Identity Platform이 클라이언트 앱에 ID 토큰을 반환하기 전에 트리거됩니다. 앱에서 다단계 인증을 사용하는 경우 사용자가 2단계 인증을 확인한 후에 함수가 트리거됩니다. 신규 사용자를 만들면 beforeCreate 이외에 beforeSignIn도 트리거됩니다.

차단 함수를 사용할 때는 다음 사항에 유의하세요.

  • 함수는 7초 이내에 응답해야 합니다. 7초가 지나면 Identity Platform이 오류를 반환하고 클라이언트 작업이 실패합니다.

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

  • 함수는 테넌트에 포함된 사용자를 포함하여 프로젝트의 모든 사용자에게 적용됩니다. Identity Platform은 사용자가 속한 테넌트를 비롯하여 함수에 대한 정보를 제공하므로 그에 따라 응답할 수 있습니다.

  • 다른 ID 공급업체를 계정에 연결하면 등록된 beforeSignIn 함수가 다시 트리거됩니다.

  • 익명 및 커스텀 인증은 차단 기능을 지원하지 않습니다.

차단 함수 만들기

다음 단계는 차단 함수를 만드는 방법을 보여줍니다.

  1. Cloud Console에서 Identity Platform 설정 페이지로 이동합니다.

    설정 페이지로 이동

  2. 트리거 탭을 선택합니다.

  3. 사용자 등록을 위한 차단 함수를 만들려면 만들기 전(beforeCreate) 아래에서 함수 드롭다운을 선택한 다음 함수 만들기fmf 선택합니다. 사용자 로그인을 위한 차단 함수를 만들려면 로그인 전 (beforeSignIn) 아래에서 함수를 만듭니다.

  4. 새 함수를 만듭니다.

    1. 함수의 이름을 입력합니다.

    2. 트리거 필드에서 HTTP를 선택합니다.

    3. 인증 필드에서 인증되지 않은 호출 허용을 선택합니다.

    4. 다음을 클릭합니다.

  5. 인라인 편집기를 사용하여 index.js을 엽니다. 예시 helloWorld 코드를 삭제하고 다음 중 하나로 바꿉니다.

    등록에 응답하려면 다음 안내를 따르세요.

    import * as gcipCloudFunctions from 'gcip-cloud-functions';
    
    const authClient = new gcipCloudFunctions.Auth();
    
    exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
      // TODO
    });
    

    로그인에 응답하려면 다음 안내를 따르세요.

    import * as gcipCloudFunctions from 'gcip-cloud-functions';
    
    const authClient = new gcipCloudFunctions.Auth();
    
    exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
      // TODO
    });
    
  6. package.json을 열고 다음을 추가합니다.

    "dependencies": {
      "gcip-cloud-functions": "^0.0.1"
    }
    
  7. 배포를 클릭하여 함수를 게시합니다.

  8. Identity Platform 차단 함수 페이지에서 저장을 클릭합니다.

함수를 구현하는 방법은 다음 섹션을 참조하세요. 함수를 업데이트할 때마다 함수를 다시 배포해야 합니다.

gcloud 명령줄 도구 또는 REST API를 사용하여 함수를 만들고 관리할 수도 있습니다. gcloud 명령줄 도구를 사용하여 함수를 배포하는 방법은 Cloud Functions 문서를 참조하세요.

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

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 Identity Platform 프로젝트 또는 테넌트. projects/project-id/tenants/tenant-id
timestamp 이벤트가 트리거된 시간으로, RFC 3339 문자열 형식입니다. Tue, 23 Jul 2019 21:10:57 GMT
additionalUserInfo 사용자에 대한 정보가 포함된 객체입니다. AdditionalUserInfo
credential 사용자의 사용자 인증 정보에 대한 정보가 포함된 객체입니다. AuthCredential

등록 또는 로그인 차단

등록 또는 로그인 시도를 차단하려면 함수에 HttpsError을 발생시킵니다. 예를 들면 다음과 같습니다.

Node.js

throw new gcipCloudFunctions.https.HttpsError('permission-denied');

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

이름 코드 Message
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 요청 기한이 지났습니다.

커스텀 오류 메시지를 지정할 수도 있습니다.

Node.js

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

다음 예시에서는 특정 도메인의 구성원이 아닌 사용자가 앱에 등록하는 것을 차단하는 방법을 보여줍니다.

Node.js

// Import the Cloud Auth Admin module.
const gcipCloudFunctions = require('gcip-cloud-functions');
// Initialize the Auth client.
const authClient = new gcipCloudFunctions.Auth();
// Http trigger with Cloud Functions.
exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
  // If the user is authenticating within a tenant context, the tenant ID can be determined from
  // user.tenantId or from context.resource, eg. '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 gcipCloudFunctions.https.HttpsError('invalid-argument', `Unauthorized email "${user.email}"`);
});

기본 또는 커스텀 메시지 사용 여부에 관계없이 Cloud Functions는 오류를 래핑하고 내부 오류로 클라이언트에게 반환합니다. 함수에서 다음 오류가 발생하는 경우를 예시로 들겠습니다.

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

다음과 유사한 오류가 클라이언트 앱에 반환됩니다(클라이언트 SDK를 사용하는 경우 오류는 내부 오류로 래핑됨).

{
  "error": {
    "code": 400,
    "message": "BLOCKING_FUNCTION_ERROR_RESPONSE : HTTP Cloud Function returned an error. Code: 400, Status: \"INVALID_ARGUMENT\", Message: \"Unauthorized email user@evil.com\"",
    "errors": [
      {
        "message": "BLOCKING_FUNCTION_ERROR_RESPONSE : HTTP Cloud Function returned an error. Code: 400, Status: \"INVALID_ARGUMENT\", Message: \"Unauthorized email user@evil.com\"",
        "domain": "global",
        "reason": "invalid"
      }
    ]
  }
}

앱이 오류를 포착하여 적절히 처리해야 합니다. 예를 들면 다음과 같습니다.

자바스크립트

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

사용자 수정

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

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

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

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

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

Node.js

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

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

예를 들어 sessionClaims가 설정된 경우 beforeSignInbeforeCreate 클레임과 함께 이를 반환하고 병합합니다. 병합되면 sessionClaims 키가 customClaims의 키와 일치하면 일치하는 customClaimssessionClaims 키에 의해 토큰 클레임에서 덮어쓰기됩니다. 그러나 향후 요청 시 덮어쓰인 customClaims 키가 데이터베이스에서 계속 유지됩니다.

지원되는 OAuth 사용자 인증 정보 및 데이터

OAuth 사용자 인증 정보와 데이터를 여러 ID 공급업체의 차단 함수에 전달할 수 있습니다. 다음 표에서는 각 ID 공급업체에서 지원되는 사용자 인증 정보와 데이터를 보여줍니다.

ID 공급업체 ID 토큰 액세스 토큰 만료 시간 토큰 보안 비밀 갱신 토큰 로그인 클레임
Google 있음 있음 없음 있음 없음
Facebook 없음 있음 있음 아니요 아니요 없음
Twitter 없음 있음 없음 있음 아니요 없음
GitHub 없음 있음 아니요 아니요 아니요 없음
Microsoft 있음 있음 없음 있음 없음
LinkedIn 없음 있음 있음 아니요 아니요 없음
Yahoo 있음 있음 없음 있음 없음
Apple 있음 있음 없음 있음 없음
SAML 없음 아니요 아니요 아니요 없음
OIDC : OpenID Connect 있음 있음 없음 있음

갱신 토큰

차단 함수에서 갱신 토큰을 사용하려면 먼저 Cloud Console의 토큰 사용자 인증 정보 포함 드롭다운 메뉴에서 트리거 섹션의 체크박스를 선택해야 합니다.

ID 토큰이나 액세스 토큰과 같은 OAuth 사용자 인증 정보를 사용하여 직접 로그인하면 ID 공급업체에서 갱신 토큰이 반환되지 않습니다. 이 경우 동일한 클라이언트 측 OAuth 사용자 인증 정보가 차단 함수로 전달됩니다. 하지만 3-legged 흐름의 경우 ID 공급업체가 지원하는 경우 갱신 토큰을 사용할 수 있습니다.

다음 섹션에서는 각 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으로 로그인하면 다음 사용자 인증 정보가 전달됩니다.

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

GitHub

사용자가 GitHub로 로그인하면 다음 사용자 인증 정보가 전달됩니다.

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

Microsoft

사용자가 Microsoft로 로그인하면 다음 사용자 인증 정보가 전달됩니다.

  • ID 토큰
  • 액세스 토큰
  • 갱신 토큰: offline_access 범위가 선택된 경우 차단 함수에 전달됩니다.

예:

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

Yahoo

사용자가 Yahoo로 로그인하면 커스텀 매개변수 또는 범위 없이 다음 사용자 인증 정보가 전달됩니다.

  • ID 토큰
  • 액세스 토큰
  • 갱신 토큰

LinkedIn

사용자가 LinkedIn으로 로그인하면 다음 사용자 인증 정보가 전달됩니다.

  • 액세스 토큰

Apple

사용자가 Apple 계정으로 로그인하면 커스텀 매개변수 또는 범위 없이 다음 사용자 인증 정보가 전달됩니다.

  • ID 토큰
  • 액세스 토큰
  • 갱신 토큰

일반적인 시나리오

다음 예시는 차단 함수의 일반적인 사용 사례를 보여줍니다.

특정 도메인의 등록만 허용

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

Node.js

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

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

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

Node.js

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

등록 시 이메일 확인 필요

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

Node.js

export.beforeCreate = authClient.functions().beforeCreateHandler((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);
    });
  }
});

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

특정 ID 공급업체 이메일을 인증된 것으로 처리

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

Node.js

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

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

다음 예시에서는 특정 IP 주소 범위에서 로그인을 차단하는 방법을 보여줍니다.

Node.js

export.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
  if (isSuspiciousIpAddress(context.ipAddress)) {
    throw new gcipCloudFunctions.https.HttpsError(
      'permission-denied', 'Unauthorized access!');
  }
});

커스텀 및 세션 클레임 설정

다음 예시에서는 커스텀 및 세션 클레임을 설정하는 방법을 보여줍니다.

Node.js

export.beforeSignIn = authClient.functions().beforeCreateHandler((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 주소를 추적합니다.

    Node.js

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

    Node.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!' })
          });
        }
      });
    });
    

사용자 사진 검사

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

Node.js

export.beforeCreate = authClient.functions().beforeCreateHandler((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 Calendar API를 호출하는 방법을 보여줍니다. 갱신 토큰은 오프라인 액세스를 위해 저장됩니다.

Node.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
);

export.beforeCreate = authClient.functions().beforeCreateHandler((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();
          });
      });
    })
  }
});

다음 단계