Ikuti semua informasi yang diumumkan di Firebase Summit, dan pelajari bagaimana Firebase dapat membantu Anda mempercepat pengembangan aplikasi dan menjalankan aplikasi dengan percaya diri. Pelajari Lebih Lanjut

Kontrol Akses dengan Klaim Kustom dan Aturan Keamanan

Firebase Admin SDK mendukung penetapan atribut khusus pada akun pengguna. Ini memberikan kemampuan untuk menerapkan berbagai strategi kontrol akses, termasuk kontrol akses berbasis peran, di aplikasi Firebase. Atribut khusus ini dapat memberikan tingkat akses (peran) yang berbeda kepada pengguna, yang diterapkan dalam aturan keamanan aplikasi.

Peran pengguna dapat ditentukan untuk kasus umum berikut:

  • Memberikan hak administratif kepada pengguna untuk mengakses data dan sumber daya.
  • Menentukan grup berbeda yang dimiliki pengguna.
  • Menyediakan akses multi-level:
    • Membedakan pelanggan berbayar/tidak berbayar.
    • Membedakan moderator dari pengguna biasa.
    • Aplikasi guru/siswa, dll.
  • Tambahkan pengenal tambahan pada pengguna. Misalnya, pengguna Firebase dapat memetakan ke UID yang berbeda di sistem lain.

Mari kita pertimbangkan kasus di mana Anda ingin membatasi akses ke node database "adminContent." Anda bisa melakukannya dengan pencarian database pada daftar pengguna admin. Namun, Anda dapat mencapai tujuan yang sama secara lebih efisien menggunakan klaim pengguna khusus bernama admin dengan aturan Realtime Database berikut:

{
  "rules": {
    "adminContent": {
      ".read": "auth.token.admin === true",
      ".write": "auth.token.admin === true",
    }
  }
}

Klaim pengguna khusus dapat diakses melalui token autentikasi pengguna. Dalam contoh di atas, hanya pengguna dengan admin yang disetel ke true dalam klaim token mereka yang akan memiliki akses baca/tulis ke node adminContent . Karena token ID sudah berisi pernyataan ini, tidak diperlukan pemrosesan atau pencarian tambahan untuk memeriksa izin admin. Selain itu, token ID adalah mekanisme tepercaya untuk mengirimkan klaim khusus ini. Semua akses yang diautentikasi harus memvalidasi token ID sebelum memproses permintaan terkait.

Contoh kode dan solusi yang dijelaskan di halaman ini diambil dari Firebase Auth API sisi klien dan API Auth sisi server yang disediakan oleh Admin SDK .

Tetapkan dan validasikan klaim pengguna khusus melalui Admin SDK

Klaim khusus dapat berisi data sensitif, oleh karena itu klaim hanya boleh disetel dari lingkungan server istimewa oleh Firebase Admin SDK.

Node.js

// Set admin privilege on the user corresponding to uid.

getAuth()
  .setCustomUserClaims(uid, { admin: true })
  .then(() => {
    // The new custom claims will propagate to the user's ID token the
    // next time a new one is issued.
  });

Jawa

// Set admin privilege on the user corresponding to uid.
Map<String, Object> claims = new HashMap<>();
claims.put("admin", true);
FirebaseAuth.getInstance().setCustomUserClaims(uid, claims);
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.

Python

# Set admin privilege on the user corresponding to uid.
auth.set_custom_user_claims(uid, {'admin': True})
# The new custom claims will propagate to the user's ID token the
# next time a new one is issued.

Pergi

// Get an auth client from the firebase.App
client, err := app.Auth(ctx)
if err != nil {
	log.Fatalf("error getting Auth client: %v\n", err)
}

// Set admin privilege on the user corresponding to uid.
claims := map[string]interface{}{"admin": true}
err = client.SetCustomUserClaims(ctx, uid, claims)
if err != nil {
	log.Fatalf("error setting custom claims %v\n", err)
}
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.

C#

// Set admin privileges on the user corresponding to uid.
var claims = new Dictionary<string, object>()
{
    { "admin", true },
};
await FirebaseAuth.DefaultInstance.SetCustomUserClaimsAsync(uid, claims);
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.

Objek klaim kustom tidak boleh berisi nama kunci yang dicadangkan OIDC atau nama yang dicadangkan Firebase . Muatan klaim kustom tidak boleh melebihi 1000 byte.

Token ID yang dikirim ke server backend dapat mengonfirmasi identitas pengguna dan tingkat akses menggunakan Admin SDK sebagai berikut:

Node.js

// Verify the ID token first.
getAuth()
  .verifyIdToken(idToken)
  .then((claims) => {
    if (claims.admin === true) {
      // Allow access to requested admin resource.
    }
  });

Jawa

// Verify the ID token first.
FirebaseToken decoded = FirebaseAuth.getInstance().verifyIdToken(idToken);
if (Boolean.TRUE.equals(decoded.getClaims().get("admin"))) {
  // Allow access to requested admin resource.
}

Python

# Verify the ID token first.
claims = auth.verify_id_token(id_token)
if claims['admin'] is True:
    # Allow access to requested admin resource.
    pass

Pergi

// Verify the ID token first.
token, err := client.VerifyIDToken(ctx, idToken)
if err != nil {
	log.Fatal(err)
}

claims := token.Claims
if admin, ok := claims["admin"]; ok {
	if admin.(bool) {
		//Allow access to requested admin resource.
	}
}

C#

// Verify the ID token first.
FirebaseToken decoded = await FirebaseAuth.DefaultInstance.VerifyIdTokenAsync(idToken);
object isAdmin;
if (decoded.Claims.TryGetValue("admin", out isAdmin))
{
    if ((bool)isAdmin)
    {
        // Allow access to requested admin resource.
    }
}

Anda juga dapat memeriksa klaim khusus pengguna yang ada, yang tersedia sebagai properti pada objek pengguna:

Node.js

// Lookup the user associated with the specified uid.
getAuth()
  .getUser(uid)
  .then((userRecord) => {
    // The claims can be accessed on the user record.
    console.log(userRecord.customClaims['admin']);
  });

Jawa

// Lookup the user associated with the specified uid.
UserRecord user = FirebaseAuth.getInstance().getUser(uid);
System.out.println(user.getCustomClaims().get("admin"));

Python

# Lookup the user associated with the specified uid.
user = auth.get_user(uid)
# The claims can be accessed on the user record.
print(user.custom_claims.get('admin'))

Pergi

// Lookup the user associated with the specified uid.
user, err := client.GetUser(ctx, uid)
if err != nil {
	log.Fatal(err)
}
// The claims can be accessed on the user record.
if admin, ok := user.CustomClaims["admin"]; ok {
	if admin.(bool) {
		log.Println(admin)
	}
}

C#

// Lookup the user associated with the specified uid.
UserRecord user = await FirebaseAuth.DefaultInstance.GetUserAsync(uid);
Console.WriteLine(user.CustomClaims["admin"]);

Anda dapat menghapus klaim khusus pengguna dengan meneruskan null untuk customClaims .

Menyebarkan klaim khusus kepada klien

Setelah klaim baru diubah pada pengguna melalui Admin SDK, klaim tersebut disebarkan ke pengguna yang diautentikasi di sisi klien melalui token ID dengan cara berikut:

  • Pengguna masuk atau mengautentikasi ulang setelah klaim khusus diubah. Token ID yang dikeluarkan sebagai hasilnya akan berisi klaim terbaru.
  • Sesi pengguna yang ada mendapatkan token ID-nya di-refresh setelah token yang lebih lama kedaluwarsa.
  • Token ID disegarkan secara paksa dengan memanggil currentUser.getIdToken(true) .

Akses klaim khusus pada klien

Klaim khusus hanya dapat diambil melalui token ID pengguna. Akses ke klaim ini mungkin diperlukan untuk mengubah UI klien berdasarkan peran atau tingkat akses pengguna. Namun, akses backend harus selalu diterapkan melalui token ID setelah memvalidasinya dan menguraikan klaimnya. Klaim khusus tidak boleh dikirim langsung ke backend, karena tidak dapat dipercaya di luar token.

Setelah klaim terbaru disebarkan ke token ID pengguna, Anda bisa mendapatkannya dengan mengambil token ID:

JavaScript

firebase.auth().currentUser.getIdTokenResult()
  .then((idTokenResult) => {
     // Confirm the user is an Admin.
     if (!!idTokenResult.claims.admin) {
       // Show admin UI.
       showAdminUI();
     } else {
       // Show regular user UI.
       showRegularUI();
     }
  })
  .catch((error) => {
    console.log(error);
  });

Android

user.getIdToken(false).addOnSuccessListener(new OnSuccessListener<GetTokenResult>() {
  @Override
  public void onSuccess(GetTokenResult result) {
    boolean isAdmin = result.getClaims().get("admin");
    if (isAdmin) {
      // Show admin UI.
      showAdminUI();
    } else {
      // Show regular user UI.
      showRegularUI();
    }
  }
});

Cepat

user.getIDTokenResult(completion: { (result, error) in
  guard let admin = result?.claims?["admin"] as? NSNumber else {
    // Show regular user UI.
    showRegularUI()
    return
  }
  if admin.boolValue {
    // Show admin UI.
    showAdminUI()
  } else {
    // Show regular user UI.
    showRegularUI()
  }
})

Objective-C

user.getIDTokenResultWithCompletion:^(FIRAuthTokenResult *result,
                                      NSError *error) {
  if (error != nil) {
    BOOL *admin = [result.claims[@"admin"] boolValue];
    if (admin) {
      // Show admin UI.
      [self showAdminUI];
    } else {
      // Show regular user UI.
      [self showRegularUI];
    }
  }
}];

Praktik terbaik untuk klaim kustom

Klaim khusus hanya digunakan untuk memberikan kontrol akses. Mereka tidak dirancang untuk menyimpan data tambahan (seperti profil dan data khusus lainnya). Meskipun ini mungkin tampak seperti mekanisme yang nyaman untuk melakukannya, namun sangat tidak disarankan karena klaim ini disimpan dalam token ID dan dapat menyebabkan masalah kinerja karena semua permintaan yang diautentikasi selalu berisi token ID Firebase yang sesuai dengan pengguna yang masuk.

  • Gunakan klaim khusus untuk menyimpan data guna mengontrol akses pengguna saja. Semua data lainnya harus disimpan secara terpisah melalui database waktu nyata atau penyimpanan sisi server lainnya.
  • Klaim kustom terbatas dalam ukuran. Melewati muatan klaim khusus yang lebih besar dari 1000 byte akan menimbulkan kesalahan.

Contoh dan kasus penggunaan

Contoh berikut mengilustrasikan klaim khusus dalam konteks kasus penggunaan Firebase tertentu.

Mendefinisikan peran melalui Fungsi Firebase pada pembuatan pengguna

Dalam contoh ini, klaim khusus ditetapkan pada pengguna saat pembuatan menggunakan Cloud Functions.

Klaim kustom dapat ditambahkan menggunakan Cloud Functions, dan segera disebarkan dengan Realtime Database. Fungsi ini dipanggil hanya saat mendaftar menggunakan pemicu onCreate . Setelah klaim khusus ditetapkan, klaim tersebut menyebar ke semua sesi yang ada dan yang akan datang. Saat berikutnya pengguna masuk dengan kredensial pengguna, token berisi klaim khusus.

Implementasi sisi klien (JavaScript)

const provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider)
.catch(error => {
  console.log(error);
});

let callback = null;
let metadataRef = null;
firebase.auth().onAuthStateChanged(user => {
  // Remove previous listener.
  if (callback) {
    metadataRef.off('value', callback);
  }
  // On user login add new listener.
  if (user) {
    // Check if refresh is required.
    metadataRef = firebase.database().ref('metadata/' + user.uid + '/refreshTime');
    callback = (snapshot) => {
      // Force refresh to pick up the latest custom claims changes.
      // Note this is always triggered on first call. Further optimization could be
      // added to avoid the initial trigger when the token is issued and already contains
      // the latest claims.
      user.getIdToken(true);
    };
    // Subscribe new listener to changes on that node.
    metadataRef.on('value', callback);
  }
});

Logika Fungsi Cloud

Sebuah node database baru (metadata/($uid)} dengan read/write dibatasi untuk pengguna yang diautentikasi ditambahkan.

const functions = require('firebase-functions');
const { initializeApp } = require('firebase-admin/app');
const { getAuth } = require('firebase-admin/auth');
const { getDatabase } = require('firebase-admin/database');

initializeApp();

// On sign up.
exports.processSignUp = functions.auth.user().onCreate(async (user) => {
  // Check if user meets role criteria.
  if (
    user.email &&
    user.email.endsWith('@admin.example.com') &&
    user.emailVerified
  ) {
    const customClaims = {
      admin: true,
      accessLevel: 9
    };

    try {
      // Set custom user claims on this newly created user.
      await getAuth().setCustomUserClaims(user.uid, customClaims);

      // Update real-time database to notify client to force refresh.
      const metadataRef = getDatabase().ref('metadata/' + user.uid);

      // Set the refresh time to the current UTC timestamp.
      // This will be captured on the client to force a token refresh.
      await  metadataRef.set({refreshTime: new Date().getTime()});
    } catch (error) {
      console.log(error);
    }
  }
});

Aturan basis data

{
  "rules": {
    "metadata": {
      "$user_id": {
        // Read access only granted to the authenticated user.
        ".read": "$user_id === auth.uid",
        // Write access only via Admin SDK.
        ".write": false
      }
    }
  }
}

Mendefinisikan peran melalui permintaan HTTP

Contoh berikut menetapkan klaim pengguna khusus pada pengguna yang baru masuk melalui permintaan HTTP.

Implementasi sisi klien (JavaScript)

const provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider)
.then((result) => {
  // User is signed in. Get the ID token.
  return result.user.getIdToken();
})
.then((idToken) => {
  // Pass the ID token to the server.
  $.post(
    '/setCustomClaims',
    {
      idToken: idToken
    },
    (data, status) => {
      // This is not required. You could just wait until the token is expired
      // and it proactively refreshes.
      if (status == 'success' && data) {
        const json = JSON.parse(data);
        if (json && json.status == 'success') {
          // Force token refresh. The token claims will contain the additional claims.
          firebase.auth().currentUser.getIdToken(true);
        }
      }
    });
}).catch((error) => {
  console.log(error);
});

Penerapan backend (Admin SDK)

app.post('/setCustomClaims', async (req, res) => {
  // Get the ID token passed.
  const idToken = req.body.idToken;

  // Verify the ID token and decode its payload.
  const claims = await getAuth().verifyIdToken(idToken);

  // Verify user is eligible for additional privileges.
  if (
    typeof claims.email !== 'undefined' &&
    typeof claims.email_verified !== 'undefined' &&
    claims.email_verified &&
    claims.email.endsWith('@admin.example.com')
  ) {
    // Add custom claims for additional privileges.
    await getAuth().setCustomUserClaims(claims.sub, {
      admin: true
    });

    // Tell client to refresh token on user.
    res.end(JSON.stringify({
      status: 'success'
    }));
  } else {
    // Return nothing.
    res.end(JSON.stringify({ status: 'ineligible' }));
  }
});

Alur yang sama dapat digunakan saat meningkatkan tingkat akses pengguna yang ada. Ambil contoh pengguna gratis yang meningkatkan ke langganan berbayar. Token ID pengguna dikirim dengan informasi pembayaran ke server backend melalui permintaan HTTP. Ketika pembayaran berhasil diproses, pengguna ditetapkan sebagai pelanggan berbayar melalui Admin SDK. Respons HTTP yang berhasil dikembalikan ke klien untuk memaksa penyegaran token.

Mendefinisikan peran melalui skrip backend

Skrip berulang (tidak dimulai dari klien) dapat disetel untuk dijalankan guna memperbarui klaim khusus pengguna:

Node.js

getAuth()
  .getUserByEmail('user@admin.example.com')
  .then((user) => {
    // Confirm user is verified.
    if (user.emailVerified) {
      // Add custom claims for additional privileges.
      // This will be picked up by the user on token refresh or next sign in on new device.
      return getAuth().setCustomUserClaims(user.uid, {
        admin: true,
      });
    }
  })
  .catch((error) => {
    console.log(error);
  });

Jawa

UserRecord user = FirebaseAuth.getInstance()
    .getUserByEmail("user@admin.example.com");
// Confirm user is verified.
if (user.isEmailVerified()) {
  Map<String, Object> claims = new HashMap<>();
  claims.put("admin", true);
  FirebaseAuth.getInstance().setCustomUserClaims(user.getUid(), claims);
}

Python

user = auth.get_user_by_email('user@admin.example.com')
# Confirm user is verified
if user.email_verified:
    # Add custom claims for additional privileges.
    # This will be picked up by the user on token refresh or next sign in on new device.
    auth.set_custom_user_claims(user.uid, {
        'admin': True
    })

Pergi

user, err := client.GetUserByEmail(ctx, "user@admin.example.com")
if err != nil {
	log.Fatal(err)
}
// Confirm user is verified
if user.EmailVerified {
	// Add custom claims for additional privileges.
	// This will be picked up by the user on token refresh or next sign in on new device.
	err := client.SetCustomUserClaims(ctx, user.UID, map[string]interface{}{"admin": true})
	if err != nil {
		log.Fatalf("error setting custom claims %v\n", err)
	}

}

C#

UserRecord user = await FirebaseAuth.DefaultInstance
    .GetUserByEmailAsync("user@admin.example.com");
// Confirm user is verified.
if (user.EmailVerified)
{
    var claims = new Dictionary<string, object>()
    {
        { "admin", true },
    };
    await FirebaseAuth.DefaultInstance.SetCustomUserClaimsAsync(user.Uid, claims);
}

Klaim khusus juga dapat diubah secara bertahap melalui Admin SDK:

Node.js

getAuth()
  .getUserByEmail('user@admin.example.com')
  .then((user) => {
    // Add incremental custom claim without overwriting existing claims.
    const currentCustomClaims = user.customClaims;
    if (currentCustomClaims['admin']) {
      // Add level.
      currentCustomClaims['accessLevel'] = 10;
      // Add custom claims for additional privileges.
      return getAuth().setCustomUserClaims(user.uid, currentCustomClaims);
    }
  })
  .catch((error) => {
    console.log(error);
  });

Jawa

UserRecord user = FirebaseAuth.getInstance()
    .getUserByEmail("user@admin.example.com");
// Add incremental custom claim without overwriting the existing claims.
Map<String, Object> currentClaims = user.getCustomClaims();
if (Boolean.TRUE.equals(currentClaims.get("admin"))) {
  // Add level.
  currentClaims.put("level", 10);
  // Add custom claims for additional privileges.
  FirebaseAuth.getInstance().setCustomUserClaims(user.getUid(), currentClaims);
}

Python

user = auth.get_user_by_email('user@admin.example.com')
# Add incremental custom claim without overwriting existing claims.
current_custom_claims = user.custom_claims
if current_custom_claims.get('admin'):
    # Add level.
    current_custom_claims['accessLevel'] = 10
    # Add custom claims for additional privileges.
    auth.set_custom_user_claims(user.uid, current_custom_claims)

Pergi

user, err := client.GetUserByEmail(ctx, "user@admin.example.com")
if err != nil {
	log.Fatal(err)
}
// Add incremental custom claim without overwriting existing claims.
currentCustomClaims := user.CustomClaims
if currentCustomClaims == nil {
	currentCustomClaims = map[string]interface{}{}
}

if _, found := currentCustomClaims["admin"]; found {
	// Add level.
	currentCustomClaims["accessLevel"] = 10
	// Add custom claims for additional privileges.
	err := client.SetCustomUserClaims(ctx, user.UID, currentCustomClaims)
	if err != nil {
		log.Fatalf("error setting custom claims %v\n", err)
	}

}

C#

UserRecord user = await FirebaseAuth.DefaultInstance
    .GetUserByEmailAsync("user@admin.example.com");
// Add incremental custom claims without overwriting the existing claims.
object isAdmin;
if (user.CustomClaims.TryGetValue("admin", out isAdmin) && (bool)isAdmin)
{
    var claims = new Dictionary<string, object>(user.CustomClaims);
    // Add level.
    claims["level"] = 10;
    // Add custom claims for additional privileges.
    await FirebaseAuth.DefaultInstance.SetCustomUserClaimsAsync(user.Uid, claims);
}