Mengelola Sesi Pengguna

Sesi Firebase Authentication berumur panjang. Setiap kali pengguna login, kredensial pengguna tersebut dikirim ke backend Firebase Authentication dan ditukar dengan token ID Firebase (berupa JWT) serta token refresh. Token ID Firebase berumur pendek dan hanya bertahan selama satu jam. Token refresh dapat digunakan untuk mengambil token ID baru. Token refresh menjadi tidak berlaku lagi hanya jika terjadi salah satu hal berikut:

  • Pengguna dihapus
  • Pengguna dinonaktifkan
  • Terdeteksi perubahan signifikan pada akun pengguna, seperti pembaruan sandi atau alamat email.

Firebase Admin SDK memberikan kemampuan untuk mencabut token refresh bagi pengguna yang ditentukan. Selain itu, API untuk memeriksa pencabutan token ID juga tersedia. Dengan kemampuan ini, Anda memiliki kendali yang lebih besar atas sesi pengguna. SDK ini memberikan kemampuan untuk menambahkan batasan agar sesi tidak digunakan dalam keadaan yang mencurigakan, serta sebagai mekanisme pemulihan dari kemungkinan pencurian token.

Mencabut token refresh

Anda bisa mencabut token refresh yang ada pada pengguna saat pengguna melaporkan perangkatnya hilang atau dicuri. Demikian pula, jika menemukan kerentanan umum atau menduga telah terjadi kebocoran token aktif dalam skala yang luas, Anda dapat menggunakan listUsers API untuk menelusuri semua pengguna dan mencabut token mereka untuk project yang ditentukan.

Reset sandi juga akan mencabut token yang ada milik pengguna. Namun, dalam kasus ini, backend Firebase Authentication akan menangani pencabutan tersebut secara otomatis. Begitu token dicabut, pengguna akan dibuat logout dan diminta untuk melakukan autentikasi ulang.

Berikut ini contoh implementasi yang menggunakan Admin SDK untuk mencabut token refresh pengguna tertentu. Untuk menginisialisasi Admin SDK, ikuti petunjuk di halaman penyiapan.

Node.js

// Revoke all refresh tokens for a specified user for whatever reason.
// Retrieve the timestamp of the revocation, in seconds since the epoch.
getAuth()
  .revokeRefreshTokens(uid)
  .then(() => {
    return getAuth().getUser(uid);
  })
  .then((userRecord) => {
    return new Date(userRecord.tokensValidAfterTime).getTime() / 1000;
  })
  .then((timestamp) => {
    console.log(`Tokens revoked at: ${timestamp}`);
  });

Java

FirebaseAuth.getInstance().revokeRefreshTokens(uid);
UserRecord user = FirebaseAuth.getInstance().getUser(uid);
// Convert to seconds as the auth_time in the token claims is in seconds too.
long revocationSecond = user.getTokensValidAfterTimestamp() / 1000;
System.out.println("Tokens revoked at: " + revocationSecond);

Python

# Revoke tokens on the backend.
auth.revoke_refresh_tokens(uid)
user = auth.get_user(uid)
# Convert to seconds as the auth_time in the token claims is in seconds.
revocation_second = user.tokens_valid_after_timestamp / 1000
print('Tokens revoked at: {0}'.format(revocation_second))

Go

client, err := app.Auth(ctx)
if err != nil {
	log.Fatalf("error getting Auth client: %v\n", err)
}
if err := client.RevokeRefreshTokens(ctx, uid); err != nil {
	log.Fatalf("error revoking tokens for user: %v, %v\n", uid, err)
}
// accessing the user's TokenValidAfter
u, err := client.GetUser(ctx, uid)
if err != nil {
	log.Fatalf("error getting user %s: %v\n", uid, err)
}
timestamp := u.TokensValidAfterMillis / 1000
log.Printf("the refresh tokens were revoked at: %d (UTC seconds) ", timestamp)

C#

await FirebaseAuth.DefaultInstance.RevokeRefreshTokensAsync(uid);
var user = await FirebaseAuth.DefaultInstance.GetUserAsync(uid);
Console.WriteLine("Tokens revoked at: " + user.TokensValidAfterTimestamp);

Mendeteksi pencabutan token ID

Karena token ID Firebase adalah JWT stateless, Anda dapat mengetahui apakah token telah dicabut atau tidak hanya dengan meminta status token tersebut dari backend Firebase Authentication. Karena alasan ini, menjalankan pemeriksaan tersebut di server Anda adalah operasi yang mahal dan memerlukan perjalanan bolak-balik jaringan tambahan. Anda dapat menghindari permintaan jaringan ini dengan menyiapkan Aturan Keamanan Firebase yang dapat memeriksa pencabutan token, bukannya menggunakan Admin SDK untuk memeriksanya.

Mendeteksi pencabutan token ID di Aturan Keamanan Firebase

Agar dapat mendeteksi pencabutan token ID menggunakan Aturan Keamanan, simpan beberapa metadata khusus pengguna terlebih dahulu.

Memperbarui metadata khusus pengguna di Firebase Realtime Database.

Simpan stempel waktu pencabutan token refresh. Stempel waktu ini diperlukan untuk melacak pencabutan token ID melalui Aturan Keamanan Firebase. Dengan begitu, pemeriksaan yang efisien bisa dilakukan dalam database. Pada contoh kode di bawah ini, gunakan uid dan waktu pencabutan yang diperoleh di bagian sebelumnya.

Node.js

const metadataRef = getDatabase().ref('metadata/' + uid);
metadataRef.set({ revokeTime: utcRevocationTimeSecs }).then(() => {
  console.log('Database updated successfully.');
});

Java

DatabaseReference ref = FirebaseDatabase.getInstance().getReference("metadata/" + uid);
Map<String, Object> userData = new HashMap<>();
userData.put("revokeTime", revocationSecond);
ref.setValueAsync(userData);

Python

metadata_ref = firebase_admin.db.reference("metadata/" + uid)
metadata_ref.set({'revokeTime': revocation_second})

Menambahkan pemeriksaan ke Aturan Keamanan Firebase

Untuk menerapkan pemeriksaan ini, buat aturan tanpa akses tulis klien untuk menyimpan waktu pencabutan per pengguna. Aturan ini dapat diperbarui dengan stempel waktu UTC dari waktu pencabutan terakhir seperti ditunjukkan pada contoh sebelumnya:

{
  "rules": {
    "metadata": {
      "$user_id": {
        // this could be false as it is only accessed from backend or rules.
        ".read": "$user_id === auth.uid",
        ".write": "false",
      }
    }
  }
}

Setiap data yang memerlukan akses terautentikasi harus sudah memiliki konfigurasi aturan berikut. Logika ini hanya memberikan akses ke data terlindungi kepada pengguna terautentikasi yang token ID-nya tidak dicabut:

{
  "rules": {
    "users": {
      "$user_id": {
        ".read": "auth != null && $user_id === auth.uid && (
            !root.child('metadata').child(auth.uid).child('revokeTime').exists()
          || auth.token.auth_time > root.child('metadata').child(auth.uid).child('revokeTime').val()
        )",
        ".write": "auth != null && $user_id === auth.uid && (
            !root.child('metadata').child(auth.uid).child('revokeTime').exists()
          || auth.token.auth_time > root.child('metadata').child(auth.uid).child('revokeTime').val()
        )",
      }
    }
  }
}

Mendeteksi pencabutan token ID di SDK

Di server, terapkan logika berikut untuk pencabutan token refresh dan validasi token ID:

Saat token ID pengguna akan diverifikasi, tanda boolean checkRevoked tambahan harus diteruskan ke verifyIdToken. Jika token ini dicabut, pengguna akan diproses agar logout pada klien atau diminta melakukan autentikasi ulang menggunakan API autentikasi ulang yang disediakan oleh SDK klien Firebase Authentication.

Guna menginisialisasi Admin SDK untuk platform Anda, ikuti petunjuk di halaman penyiapan. Contoh pengambilan token ID ada di bagian verifyIdToken.

Node.js

// Verify the ID token while checking if the token is revoked by passing
// checkRevoked true.
let checkRevoked = true;
getAuth()
  .verifyIdToken(idToken, checkRevoked)
  .then((payload) => {
    // Token is valid.
  })
  .catch((error) => {
    if (error.code == 'auth/id-token-revoked') {
      // Token has been revoked. Inform the user to reauthenticate or signOut() the user.
    } else {
      // Token is invalid.
    }
  });

Java

try {
  // Verify the ID token while checking if the token is revoked by passing checkRevoked
  // as true.
  boolean checkRevoked = true;
  FirebaseToken decodedToken = FirebaseAuth.getInstance()
      .verifyIdToken(idToken, checkRevoked);
  // Token is valid and not revoked.
  String uid = decodedToken.getUid();
} catch (FirebaseAuthException e) {
  if (e.getAuthErrorCode() == AuthErrorCode.REVOKED_ID_TOKEN) {
    // Token has been revoked. Inform the user to re-authenticate or signOut() the user.
  } else {
    // Token is invalid.
  }
}

Python

try:
    # Verify the ID token while checking if the token is revoked by
    # passing check_revoked=True.
    decoded_token = auth.verify_id_token(id_token, check_revoked=True)
    # Token is valid and not revoked.
    uid = decoded_token['uid']
except auth.RevokedIdTokenError:
    # Token revoked, inform the user to reauthenticate or signOut().
    pass
except auth.UserDisabledError:
    # Token belongs to a disabled user record.
    pass
except auth.InvalidIdTokenError:
    # Token is invalid
    pass

Go

client, err := app.Auth(ctx)
if err != nil {
	log.Fatalf("error getting Auth client: %v\n", err)
}
token, err := client.VerifyIDTokenAndCheckRevoked(ctx, idToken)
if err != nil {
	if err.Error() == "ID token has been revoked" {
		// Token is revoked. Inform the user to reauthenticate or signOut() the user.
	} else {
		// Token is invalid
	}
}
log.Printf("Verified ID token: %v\n", token)

C#

try
{
    // Verify the ID token while checking if the token is revoked by passing checkRevoked
    // as true.
    bool checkRevoked = true;
    var decodedToken = await FirebaseAuth.DefaultInstance.VerifyIdTokenAsync(
        idToken, checkRevoked);
    // Token is valid and not revoked.
    string uid = decodedToken.Uid;
}
catch (FirebaseAuthException ex)
{
    if (ex.AuthErrorCode == AuthErrorCode.RevokedIdToken)
    {
        // Token has been revoked. Inform the user to re-authenticate or signOut() the user.
    }
    else
    {
        // Token is invalid.
    }
}

Menanggapi pencabutan token pada klien

Jika token dicabut melalui Admin SDK, klien akan diberi tahu tentang pencabutan itu dan pengguna diharapkan melakukan autentikasi ulang atau diproses agar logout:

function onIdTokenRevocation() {
  // For an email/password user. Prompt the user for the password again.
  let password = prompt('Please provide your password for reauthentication');
  let credential = firebase.auth.EmailAuthProvider.credential(
      firebase.auth().currentUser.email, password);
  firebase.auth().currentUser.reauthenticateWithCredential(credential)
    .then(result => {
      // User successfully reauthenticated. New ID tokens should be valid.
    })
    .catch(error => {
      // An error occurred.
    });
}

Keamanan Lanjutan: Memberlakukan pembatasan alamat IP

Mekanisme keamanan yang umum untuk mendeteksi pencurian token adalah dengan melacak asal alamat IP permintaan. Misalnya, jika permintaan selalu berasal dari alamat IP yang sama (server yang sama yang melakukan panggilan), sesi alamat IP tunggal dapat diberlakukan. Atau, Anda dapat mencabut token pengguna jika mendeteksi bahwa geolokasi alamat IP pengguna tersebut tiba-tiba berubah, atau jika menerima permintaan dari asal yang mencurigakan.

Untuk menjalankan pemeriksaan keamanan berdasarkan alamat IP, bagi setiap permintaan yang terautentikasi, periksalah token ID-nya dan pastikan alamat IP permintaan cocok dengan alamat IP yang tepercaya sebelumnya, atau berada dalam rentang yang tepercaya, sebelum mengizinkan akses ke data yang dibatasi. Contoh:

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 the user's previous IP addresses, previously saved.
    return getPreviousUserIpAddresses(claims.sub);
  }).then(previousIpAddresses => {
    // Get the request IP address.
    const requestIpAddress = req.connection.remoteAddress;
    // Check if the request IP address origin is suspicious relative to previous
    // 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 location change in a
    // short period of time, then it will give stronger signals of possible abuse.
    if (!isValidIpAddress(previousIpAddresses, requestIpAddress)) {
      // Invalid IP address, take action quickly and revoke all user's refresh tokens.
      revokeUserTokens(claims.uid).then(() => {
        res.status(401).send({error: 'Unauthorized access. Please login again!'});
      }, error => {
        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!' })
      });
    }
  });
});