Mengelola Sesi Pengguna

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

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

Firebase Admin SDK memberikan kemampuan untuk mencabut token refresh bagi pengguna tertentu. Selain itu, tersedia juga API untuk mendeteksi pencabutan token ID. Dengan kemampuan ini, Anda memiliki kendali yang lebih besar atas sesi pengguna. SDK ini memberikan kemampuan untuk menambahkan batasan guna mencegah digunakannya sesi dalam keadaan mencurigakan, serta sebagai mekanisme pemulihan dari kemungkinan terjadinya pencurian token.

Mencabut token refresh

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

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

Berikut ini contoh implementasi yang menggunakan Admin SDK untuk mencabut token refresh dari pengguna tertentu. Untuk menginisialisasi Admin SDK, ikuti petunjuk pada 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.
admin.auth().revokeRefreshTokens(uid)
    .then(() => {
      return admin.auth().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)

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 akan menjadi operasi yang mahal dan menambah traffic jaringan. Anda dapat menghindari permintaan jaringan ini dengan menyiapkan Aturan Firebase yang akan mendeteksi pencabutan token, dan bukan menggunakan Admin SDK untuk melakukan pemeriksaan.

Mendeteksi pencabutan token ID di Aturan Database

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

Memperbarui metadata khusus pengguna di Firebase Realtime Database.

Simpan stempel waktu pencabutan token refresh. Ini diperlukan untuk melacak pencabutan token ID melalui aturan 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 = admin.database().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 Database

Untuk menerapkan pemeriksaan ini, buat aturan tanpa akses tulis klien untuk menyimpan waktu pencabutan per pengguna. Aturan ini dapat diupdate 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 mengonfigurasi aturan berikut. Logika ini hanya memberikan akses ke data terlindungi kepada pengguna terautentikasi yang token ID-nya tidak dicabut:

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

Mendeteksi pencabutan token ID di SDK.

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

Jika token ID pengguna perlu diverifikasi, tanda boolean checkRevoked tambahan harus diteruskan ke verifyIdToken. Jika token pengguna ini dicabut, pengguna tersebut harus logout dari klien atau melakukan autentikasi ulang menggunakan API autentikasi ulang yang disediakan oleh SDK klien Firebase Authentication.

Guna menginisialisasi Admin SDK untuk platform Anda, ikuti petunjuk pada 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;
admin.auth().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.getErrorCode().equals("id-token-revoked")) {
    // 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.AuthError as exc:
    if exc.code == 'ID_TOKEN_REVOKED':
        # Token revoked, inform the user to reauthenticate or signOut().
        pass
    else:
        # 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)

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 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 batasan alamat IP

Mekanisme keamanan yang umum untuk mendeteksi pencurian token adalah dengan melacak alamat IP asal permintaan. Misalnya, jika permintaan selalu berasal dari alamat IP yang sama (server 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 Anda menerima permintaan dari asal yang mencurigakan.

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

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

Kirim masukan tentang...

Butuh bantuan? Kunjungi halaman dukungan kami.