Mengontrol Akses dengan Klaim Kustom dan Aturan Keamanan

Firebase Admin SDK mendukung penetapan atribut khusus pada akun pengguna. Hal ini memberikan kemampuan untuk menerapkan berbagai strategi kontrol akses, termasuk kontrol akses berbasis peran, pada aplikasi Firebase. Atribut kustom ini dapat memberi pengguna beberapa tingkat akses (peran), yang diberlakukan dalam aturan keamanan aplikasi.

Peran pengguna dapat ditetapkan untuk kasus umum berikut ini:

  • Memberikan hak istimewa administratif kepada pengguna untuk mengakses data dan resource.
  • Menetapkan berbagai grup yang mencakup pengguna.
  • Menyediakan akses multilevel:
    • Membedakan pelanggan berbayar/tidak berbayar.
    • Membedakan moderator dari pengguna biasa.
    • Aplikasi guru/siswa, dll.
  • Menambahkan ID tambahan pada pengguna. Misalnya, pengguna Firebase dapat dipetakan ke UID berbeda dalam sistem lain.

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

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

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

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

Menetapkan dan memvalidasi klaim pengguna kustom melalui Admin SDK

Karena dapat berisi data sensitif, sebaiknya klaim kustom hanya ditetapkan dari lingkungan server dengan hak 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.
  });

Java

// 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.

Go

// 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 apa pun yang dicadangkan untuk OIDC atau nama apa pun yang dicadangkan untuk Firebase. Payload klaim kustom tidak boleh melebihi 1.000 byte.

Token ID yang dikirim ke server backend dapat mengonfirmasi identitas dan tingkat akses pengguna 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.
    }
  });

Java

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

Go

// 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 kustom yang ada milik pengguna, 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']);
  });

Java

// 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'))

Go

// 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 kustom pengguna dengan meneruskan null untuk customClaims.

Menerapkan klaim kustom ke klien

Setelah dimodifikasi pada pengguna melalui Admin SDK, klaim baru akan diterapkan ke pengguna terautentikasi pada sisi klien melalui token ID dengan cara berikut:

  • Pengguna melakukan login atau autentikasi ulang setelah klaim kustom dimodifikasi. Token ID yang dikeluarkan sebagai hasilnya akan berisi klaim terbaru.
  • Token ID untuk sesi pengguna yang ada akan di-refresh setelah masa berlaku token lama habis.
  • Token ID di-refresh paksa dengan memanggil currentUser.getIdToken(true).

Mengakses klaim kustom pada klien

Klaim kustom hanya dapat diambil melalui token ID pengguna. Akses ke klaim ini mungkin diperlukan untuk memodifikasi UI klien berdasarkan peran atau tingkat akses pengguna. Namun, akses backend harus selalu diberlakukan melalui token ID setelah memvalidasinya dan mengurai klaimnya. Klaim kustom sebaiknya tidak dikirim langsung ke backend karena tidak tepercaya di luar token tersebut.

Setelah klaim terbaru diterapkan ke token ID pengguna, Anda dapat memperoleh klaim tersebut 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();
    }
  }
});

Swift

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 kustom hanya digunakan untuk memberikan kontrol akses. Klaim kustom tidak dirancang untuk menyimpan data tambahan (seperti profil dan data kustom lainnya). Meskipun terlihat praktis, mekanisme ini sangat tidak dianjurkan mengingat klaim ini disimpan dalam token ID dan dapat menimbulkan masalah performa karena semua permintaan terautentikasi selalu berisi token ID Firebase yang terkait dengan pengguna yang sudah login.

  • Gunakan klaim kustom untuk menyimpan data guna mengontrol akses pengguna saja. Semua data lainnya sebaiknya disimpan terpisah melalui database real-time atau penyimpanan sisi server lainnya.
  • Klaim kustom memiliki ukuran terbatas. Error akan terjadi jika Anda meneruskan payload klaim kustom di atas 1.000 byte.

Contoh dan kasus penggunaan

Contoh berikut ini menggambarkan klaim kustom dalam konteks kasus penggunaan Firebase tertentu.

Menetapkan peran melalui Firebase Functions saat membuat pengguna

Dalam contoh ini, klaim kustom ditetapkan pada saat membuat pengguna menggunakan Cloud Functions.

Klaim kustom dapat ditambahkan menggunakan Cloud Functions dan segera diterapkan dengan Realtime Database. Fungsi ini hanya dipanggil saat mendaftar menggunakan pemicu onCreate. Begitu ditetapkan, klaim kustom akan diterapkan ke semua sesi yang ada dan sesi yang akan datang. Saat berikutnya pengguna login dengan kredensial pengguna, token akan memuat klaim kustom.

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 Cloud Functions

Node database baru (metadata/($uid)} dengan akses baca/tulis yang dibatasi untuk pengguna terautentikasi akan 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 database

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

Menetapkan peran melalui permintaan HTTP

Contoh berikut menetapkan klaim pengguna kustom pada pengguna yang baru login 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);
});

Implementasi 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 mengupgrade tingkat akses pengguna yang ada. Misalnya, saat mengupgrade pengguna gratis menjadi pelanggan berbayar. Token ID pengguna dikirim bersama informasi pembayaran ke server backend melalui permintaan HTTP. Jika pembayaran berhasil diproses, pengguna akan ditetapkan sebagai pelanggan berbayar melalui Admin SDK. Respons HTTP yang berhasil akan ditampilkan ke klien untuk me-refresh paksa token.

Menetapkan peran melalui skrip backend

Skrip berulang (tidak dimulai oleh klien) dapat ditetapkan agar dijalankan untuk memperbarui klaim kustom 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);
  });

Java

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
    })

Go

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 kustom juga dapat dimodifikasi secara inkremental 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);
  });

Java

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)

Go

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