Menyesuaikan alur autentikasi menggunakan fungsi pemblokiran

Dokumen ini menunjukkan cara memperluas autentikasi Identity Platform menggunakan pemblokiran Cloud Functions.

Fungsi pemblokiran dapat Anda gunakan untuk mengeksekusi kode kustom yang mengubah hasil pendaftaran atau login pengguna ke aplikasi Anda. Misalnya, Anda dapat mencegah pengguna melakukan autentikasi jika tidak memenuhi kriteria tertentu, atau memperbarui informasi pengguna sebelum mengembalikannya ke aplikasi klien.

Sebelum memulai

Membuat aplikasi dengan Identity Platform. Lihat Panduan Memulai untuk mempelajari caranya.

Memahami fungsi pemblokiran

Anda dapat mendaftarkan fungsi pemblokiran untuk dua peristiwa:

  • beforeCreate: Fungsi pemblokiran terpicu sebelum pengguna baru disimpan ke database Identity Platform, dan sebelum token ditampilkan ke aplikasi klien.

  • beforeSignIn: Fungsi pemblokiran terpicu setelah kredensial pengguna diverifikasi, tetapi sebelum Identity Platform menampilkan token ID ke aplikasi klien Anda. Jika aplikasi Anda menggunakan autentikasi multi-faktor, fungsi ini akan terpicu setelah pengguna memverifikasi faktor kedua. Perlu diperhatikan bahwa membuat pengguna baru juga akan memicu beforeSignIn, selain beforeCreate.

Perhatikan hal-hal berikut saat menggunakan fungsi pemblokiran:

  • Fungsi Anda harus merespons dalam 7 detik. Setelah 7 detik, Identity Platform akan menampilkan error, dan operasi klien akan gagal.

  • Kode respons HTTP selain 200 akan diteruskan ke aplikasi klien Anda. Pastikan kode klien menangani error yang dapat ditampilkan fungsi Anda.

  • Fungsi berlaku untuk semua pengguna dalam project Anda, termasuk yang terdapat dalam tenant. Identity Platform memberikan informasi tentang pengguna kepada fungsi Anda, termasuk tenant pengguna tersebut, sehingga Anda dapat merespons dengan semestinya.

  • Menautkan penyedia identitas lain ke akun akan memicu ulang semua fungsi beforeSignIn yang terdaftar. Tidak termasuk penyedia email dan sandi.

  • Autentikasi anonim dan kustom tidak mendukung fungsi pemblokiran.

  • Jika Anda juga menggunakan fungsi asinkron, objek pengguna yang diterima fungsi asinkron tidak berisi update dari fungsi pemblokiran.

Membuat fungsi pemblokiran

Langkah-langkah berikut menunjukkan cara membuat fungsi pemblokiran:

  1. Buka halaman Settings Identity Platform di Google Cloud Console.

    Buka halaman Setelan

  2. Pilih tab Pemicu.

  3. Untuk membuat fungsi pemblokiran bagi pendaftaran pengguna, pilih dropdown Function di bagian Before create (beforeCreate), lalu klik Create function. Untuk membuat fungsi pemblokiran bagi login pengguna, buat fungsi di bagian Before sign in (beforeSignIn).

  4. Buat fungsi baru:

    1. Masukkan Nama untuk fungsi Anda.

    2. Pada kolom Trigger, pilih HTTP.

    3. Di kolom Authentication, pilih Allow unauthenticated invocations.

    4. Klik Next.

  5. Dengan menggunakan editor inline, buka index.js. Hapus contoh kode helloWorld, lalu ganti dengan salah satu kode berikut:

    Untuk merespons pendaftaran:

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

    Untuk merespons login:

    import gcipCloudFunctions from 'gcip-cloud-functions';
    
    const authClient = new gcipCloudFunctions.Auth();
    
    exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
      // TODO
    });
    
  6. Buka package.json dan tambahkan blok dependensi berikut: Untuk versi SDK terbaru, lihat gcip-cloud-functions.

    {
      "type": "module",
      "name": ...,
      "version": ...,
    
      "dependencies": {
        "gcip-cloud-functions": "^0.2.0"
      }
    }
    
  7. Tetapkan titik entri fungsi ke beforeSignIn

  8. Klik Deploy untuk memublikasikan fungsi.

  9. Klik Simpan di halaman fungsi pemblokiran Identity Platform.

Lihat bagian berikut untuk mempelajari cara menerapkan fungsi Anda. Anda harus men-deploy ulang fungsi setiap kali mengupdatenya.

Anda juga dapat membuat dan mengelola fungsi menggunakan Google Cloud CLI atau REST API. Baca dokumentasi Cloud Functions untuk mempelajari cara menggunakan Google Cloud CLI untuk men-deploy fungsi.

Mendapatkan informasi pengguna dan konteks

Peristiwa beforeSignIn dan beforeCreate menyediakan objek User dan EventContext yang berisi informasi tentang proses login pengguna. Gunakan nilai ini dalam kode Anda untuk menentukan apakah akan mengizinkan operasi dilanjutkan atau tidak.

Untuk daftar properti yang tersedia pada objek User, lihat referensi API UserRecord.

Objek EventContext berisi properti berikut:

Nama Deskripsi Contoh
locale Lokalitas aplikasi. Anda dapat menetapkan lokalitas menggunakan Client SDK, atau dengan meneruskan header lokalitas di REST API. fr atau sv-SE
ipAddress Alamat IP perangkat tempat pengguna akhir mendaftar atau login. 114.14.200.1
userAgent Agen pengguna yang memicu fungsi pemblokiran. Mozilla/5.0 (X11; Linux x86_64)
eventId ID unik peristiwa. rWsyPtolplG2TBFoOkkgyg
eventType Jenis peristiwa. Properti ini memberikan informasi tentang nama peristiwa, seperti beforeSignIn atau beforeCreate, dan metode login terkait yang digunakan, seperti Google atau email/sandi. providers/cloud.auth/eventTypes/user.beforeSignIn:password
authType Selalu USER. USER
resource Project atau tenant Identity Platform. projects/project-id/tenants/tenant-id
timestamp Waktu peristiwa dipicu, yang diformat sebagai string RFC 3339. Tue, 23 Jul 2019 21:10:57 GMT
additionalUserInfo Objek yang berisi informasi tentang pengguna. AdditionalUserInfo
credential Objek yang berisi informasi tentang kredensial pengguna. AuthCredential

Memblokir pendaftaran atau login

Untuk memblokir upaya pendaftaran atau login, tampilkan HttpsError dalam fungsi Anda. Contoh:

Node.js

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

Tabel berikut berisi daftar error yang dapat Anda tampilkan, bersama dengan pesan error default-nya:

Nama Kode Pesan
invalid-argument 400 Klien menentukan argumen yang tidak valid.
failed-precondition 400 Permintaan tidak dapat dijalankan dalam status sistem saat ini.
out-of-range 400 Klien menentukan rentang yang tidak valid.
unauthenticated 401 Token OAuth tidak ada, tidak valid, atau sudah tidak berlaku.
permission-denied 403 Klien tidak memiliki izin yang memadai.
not-found 404 Resource yang ditentukan tidak ditemukan.
aborted 409 Konflik serentak, seperti konflik baca-ubah-tulis.
already-exists 409 Resource yang klien coba buat sudah ada.
resource-exhausted 429 Kuota resource telah habis atau mencapai pembatasan kapasitas.
cancelled 499 Permintaan dibatalkan oleh klien.
data-loss 500 Kehilangan data atau kerusakan data yang tidak dapat dipulihkan.
unknown 500 Terjadi error yang tidak diketahui pada server.
internal 500 Error server internal.
not-implemented 501 Metode API tidak diimplementasikan oleh server.
unavailable 503 Layanan tidak tersedia.
deadline-exceeded 504 Batas waktu permintaan terlampaui.

Anda juga dapat menentukan pesan error kustom:

Node.js

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

Contoh berikut menunjukkan cara memblokir pengguna yang berada di luar domain tertentu agar tidak mendaftar ke aplikasi Anda:

Node.js

// Import the Cloud Auth Admin module.
import gcipCloudFunctions from '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.endsWith('@acme.com')) {
    throw new gcipCloudFunctions.https.HttpsError('invalid-argument', `Unauthorized email "${user.email}"`);
  }
});

Terlepas dari apakah Anda menggunakan pesan default atau kustom, Cloud Functions menggabungkan error dan menampilkannya ke klien sebagai error internal. Misalnya, jika Anda memunculkan error berikut di fungsi Anda:

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

Error yang serupa dengan yang berikut ditampilkan ke aplikasi klien Anda (jika Anda menggunakan SDK Klien, error tersebut akan digabungkan sebagai error internal):

{
  "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"
      }
    ]
  }
}

Aplikasi Anda harus menangkap error tersebut, dan menanganinya secara semestinya. Contoh:

JavaScript

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

Mengubah pengguna

Daripada memblokir upaya pendaftaran atau login, Anda dapat mengizinkan operasi untuk dilanjutkan, tetapi mengubah objek User yang disimpan ke database Identity Platform dan ditampilkan ke klien.

Untuk mengubah pengguna, tampilkan objek dari pengendali peristiwa yang berisi kolom yang akan diubah. Anda dapat mengubah kolom berikut:

  • displayName
  • disabled
  • emailVerified
  • photoURL
  • customClaims
  • sessionClaims (beforeSignIn saja)

Dengan pengecualian sessionClaims, semua kolom yang diubah disimpan ke database Identity Platform, yang berarti kolom tersebut disertakan pada token respons dan terus dipertahankan di antara sesi pengguna.

Contoh berikut menunjukkan cara menetapkan nama tampilan default:

Node.js

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

Jika Anda mendaftarkan pengendali peristiwa untuk beforeCreate dan beforeSignIn, perhatikan bahwa beforeSignIn dijalankan setelah beforeCreate. Kolom pengguna yang diperbarui di beforeCreate dapat dilihat di beforeSignIn. Jika Anda menetapkan kolom selain sessionClaims di kedua pengendali peristiwa, nilai yang ditetapkan dalam beforeSignIn akan menimpa nilai yang ditetapkan dalam beforeCreate. Khusus untuk sessionClaims, kolom tersebut diterapkan ke klaim token sesi saat ini, tetapi tidak dipertahankan atau disimpan dalam database.

Misalnya, jika sessionClaims ditetapkan, beforeSignIn akan menampilkannya dengan klaim beforeCreate, dan keduanya akan digabungkan. Saat digabungkan, jika kunci sessionClaims cocok dengan kunci di customClaims, customClaims yang cocok akan ditimpa dalam klaim token oleh kunci sessionClaims. Namun, kunci customClaims yang ditimpa masih disimpan di database untuk permintaan selanjutnya.

Kredensial dan data OAuth yang didukung

Anda dapat meneruskan kredensial dan data OAuth ke fungsi pemblokiran dari berbagai penyedia identitas. Tabel berikut menunjukkan kredensial dan data yang didukung untuk setiap penyedia identitas:

Penyedia Identitas Token ID Token Akses Waktu Habis Masa Berlaku Secret Token Token Refresh Klaim Login
Google Ya Ya Ya Tidak Ya Tidak
Facebook Tidak Ya Ya Tidak Tidak Tidak
Twitter Tidak Ya Tidak Ya Tidak Tidak
GitHub Tidak Ya Tidak Tidak Tidak Tidak
Microsoft Ya Ya Ya Tidak Ya Tidak
LinkedIn Tidak Ya Ya Tidak Tidak Tidak
Yahoo Ya Ya Ya Tidak Ya Tidak
Apple Ya Ya Ya Tidak Ya Tidak
SAML Tidak Tidak Tidak Tidak Tidak Ya
OIDC Ya Ya Ya Tidak Ya Ya

Token refresh

Untuk menggunakan token refresh dalam fungsi pemblokiran, Anda harus terlebih dahulu mencentang kotak di bagian Triggers pada menu dropdown Include token credentials di Google Cloud Console.

Token refresh tidak akan ditampilkan oleh penyedia identitas mana pun saat login secara langsung dengan kredensial OAuth, seperti token ID atau token akses. Dalam situasi ini, kredensial OAuth sisi klien yang sama akan diteruskan ke fungsi pemblokiran. Namun, untuk alur bercabang 3, token refresh mungkin tersedia jika penyedia identitas mendukungnya.

Bagian berikut menjelaskan setiap jenis penyedia identitas serta kredensial dan datanya yang didukung.

Penyedia OIDC umum

Saat pengguna login dengan penyedia OIDC umum, kredensial berikut akan diteruskan:

  • Token ID: Diberikan jika alur id_token dipilih.
  • Token akses: Diberikan jika alur kode dipilih. Perlu diperhatikan bahwa alur kode saat ini hanya didukung melalui REST API.
  • Token refresh: Diberikan jika cakupan offline_access dipilih.

Contoh:

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

Google

Saat pengguna login dengan Google, kredensial berikut akan diteruskan:

  • Token ID
  • Token akses
  • Token refresh: Hanya diberikan jika parameter kustom berikut diminta:
    • access_type=offline
    • prompt=consent, jika sebelumnya pengguna telah memberikan izin dan tidak ada cakupan baru yang diminta

Contoh:

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

Pelajari token refresh Google lebih lanjut.

Facebook

Saat pengguna login dengan Facebook, kredensial berikut akan diteruskan:

GitHub

Saat pengguna login dengan GitHub, kredensial berikut akan diteruskan:

  • Token akses: Masa berlakunya tidak pernah berakhir kecuali jika dicabut.

Microsoft

Saat pengguna login dengan Microsoft, kredensial berikut akan diteruskan:

Contoh:

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

Yahoo

Saat pengguna login dengan Yahoo, kredensial berikut akan diteruskan tanpa cakupan atau parameter kustom apa pun:

  • Token ID
  • Token akses
  • Token refresh

LinkedIn

Saat pengguna login dengan LinkedIn, kredensial berikut akan diteruskan:

  • Token akses

Apple

Saat pengguna login dengan Apple, kredensial berikut akan diteruskan tanpa cakupan atau parameter kustom apa pun:

  • Token ID
  • Token akses
  • Token refresh

Skenario umum

Contoh berikut menunjukkan beberapa kasus penggunaan umum untuk fungsi pemblokiran:

Hanya mengizinkan pendaftaran dari domain tertentu

Contoh berikut menunjukkan cara mencegah pengguna yang bukan bagian dari domain example.com mendaftar ke aplikasi Anda:

Node.js

exports.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}"`);
  }
});

Memblokir pengguna dengan email yang belum diverifikasi agar tidak mendaftar

Contoh berikut menunjukkan cara mencegah pengguna dengan email yang belum diverifikasi agar tidak mendaftar ke aplikasi Anda:

Node.js

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

Mewajibkan verifikasi email saat pendaftaran

Contoh berikut menunjukkan cara mewajibkan pengguna memverifikasi emailnya setelah mendaftar:

Node.js

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

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

Memperlakukan email penyedia identitas tertentu sebagai terverifikasi

Contoh berikut menunjukkan cara memperlakukan email pengguna dari penyedia identitas tertentu sebagai terverifikasi:

Node.js

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

Memblokir login dari alamat IP tertentu

Contoh berikut merupakan cara memblokir login dari rentang alamat IP tertentu:

Node.js

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

Menetapkan klaim kustom dan sesi

Contoh berikut menunjukkan cara menetapkan klaim kustom dan sesi:

Node.js

exports.beforeCreate = 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,
      }
    }
  }
});

Melacak alamat IP untuk memantau aktivitas yang mencurigakan

Anda dapat mencegah pencurian token dengan melacak alamat IP yang digunakan pengguna untuk login, lalu membandingkannya dengan alamat IP pada permintaan berikutnya. Jika permintaan tersebut tampak mencurigakan — misalnya, IP berasal dari wilayah geografis yang berbeda — Anda dapat meminta pengguna untuk login lagi.

  1. Gunakan klaim sesi untuk melacak alamat IP yang digunakan pengguna untuk login:

    Node.js

    exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
      return {
        sessionClaims: {
          signInIpAddress: context.ipAddress,
        },
      };
    });
    
  2. Saat pengguna mencoba mengakses resource yang memerlukan autentikasi dengan Identity Platform, bandingkan alamat IP dalam permintaan dengan IP yang digunakan untuk login:

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

Menyeleksi foto pengguna

Contoh berikut menunjukkan cara membersihkan foto profil pengguna:

Node.js

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

Untuk mempelajari cara mendeteksi dan membersihkan gambar lebih lanjut, lihat dokumentasi Cloud Vision.

Mengakses kredensial OAuth penyedia identitas pengguna

Contoh berikut menunjukkan cara mendapatkan token refresh untuk pengguna yang login dengan Google, dan menggunakannya untuk memanggil API Google Kalender. Token refresh disimpan untuk akses offline.

Node.js

const {OAuth2Client} = require('google-auth-library');
const {google} = require('googleapis');
const gcipCloudFunctions = require('gcip-cloud-functions');

// ...
// Initialize Google OAuth client.
const keys = require('./oauth2.keys.json');
const oAuth2Client = new OAuth2Client(
  keys.web.client_id,
  keys.web.client_secret
);

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

Langkah selanjutnya