Kullanıcı Oturumlarını Yönetme

Firebase Authentication oturumları uzun ömürlüdür. Kullanıcı her oturum açtığında, kullanıcı kimlik bilgileri Firebase Authentication arka ucuna gönderilir ve Firebase kimliği jetonu (JWT) ile yenileme jetonuyla değiştirilir. Firebase kimlik jetonları kısa ömürlüdür ve süresi bir saattir. Yenileme jetonu, yeni kimlik jetonları almak için kullanılabilir. Yenileme jetonlarının süresi yalnızca aşağıdakilerden biri gerçekleştiğinde sona erer:

  • Kullanıcı silinir.
  • Kullanıcı devre dışı bırakılmışsa
  • Kullanıcı için önemli bir hesap değişikliği algılandı. Şifre veya e-posta adresi güncellemeleri gibi etkinlikler de buna dahildir.

Firebase Admin SDK'sı, belirtilen bir kullanıcının yenileme jetonlarını iptal etme olanağı sunar. Ayrıca, kimlik jetonunun iptal edilip edilmediğini kontrol etmek için bir API de kullanıma sunulmuştur. Bu özellikler sayesinde kullanıcı oturumları üzerinde daha fazla kontrole sahip olursunuz. SDK, oturumların şüpheli durumlarda kullanılmasını önlemek için kısıtlamalar eklemenin yanı sıra olası jeton hırsızlığından kurtarmak için bir mekanizma sağlar.

Yenileme jetonlarını iptal etme

Kullanıcı kayıp veya çalıntı cihaz bildirdiğinde kullanıcının mevcut yenileme jetonunu iptal edebilirsiniz. Benzer şekilde, genel bir güvenlik açığı keşfederseniz veya etkin jetonların geniş ölçekte sızdırıldığından şüpheleniyorsanız tüm kullanıcıları aramak ve belirtilen proje için jetonlarını iptal etmek üzere listUsers API'yi kullanabilirsiniz.

Şifre sıfırlama işlemi, kullanıcının mevcut jetonlarını da iptal eder. Ancak bu durumda Firebase Authentication arka ucu, iptal işlemini otomatik olarak gerçekleştirir. İzin iptal edildiğinde kullanıcının oturumu kapatılır ve yeniden kimlik doğrulaması yapması istenir.

Belirli bir kullanıcının yenileme jetonunu iptal etmek için Yönetici SDK'sını kullanan örnek bir uygulama aşağıda verilmiştir. Yönetici SDK'sını başlatmak için kurulum sayfasındaki talimatları uygulayın.

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

Kimlik jetonu iptalini algılama

Firebase kimlik jetonları durum bilgisi içermeyen JWT'ler olduğundan, bir jetonun iptal edilip edilmediğini yalnızca arka uçtan jetonun durumunu isteyerek belirleyebilirsiniz. Bu nedenle, bu kontrolü sunucunuzda gerçekleştirmek pahalı bir işlemdir ve ek bir ağ gidiş dönüşünü gerektirir. Kontrolü yapmak için Yönetici SDK'sını kullanmak yerine, iptal olup olmadığını kontrol eden Firebase Security Rules'yi ayarlayarak bu ağ isteğini göndermekten kaçınabilirsiniz.

Firebase Security Rules içinde kimlik jetonu iptalini tespit et

Güvenlik Kuralları'nı kullanarak kimlik jetonu iptalini tespit edebilmek için öncelikle kullanıcıya özgü bazı meta verileri depolamamız gerekir.

Firebase Realtime Database'te kullanıcıya özgü meta verileri güncelleyin.

Yenileme jetonunun iptal zaman damgasını kaydedin. Bu, kimlik jetonunun Firebase Security Rules üzerinden iptal edilmesini izlemek için gereklidir. Bu sayede veritabanında etkili kontroller yapılabilir. Aşağıdaki kod örneklerinde, önceki bölümde elde edilen uid ve iptal süresini kullanın.

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

Firebase Security Rules için kontrol ekleyin

Bu kontrolü zorunlu kılmak için kullanıcı başına iptal süresini depolayacak, istemci yazma erişimi olmayan bir kural oluşturun. Bu, önceki örneklerde gösterildiği gibi son iptal zamanının UTC zaman damgasıyla güncellenebilir:

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

Kimlik doğrulaması gerektiren tüm verilerde aşağıdaki kuralın yapılandırılması gerekir. Bu mantık yalnızca iptal edilmemiş kimlik jetonlarına sahip kimliği doğrulanmış kullanıcıların korunan verilere erişmesine izin verir:

{
  "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()
        )",
      }
    }
  }
}

SDK'da kimlik jetonunun iptal edildiğini algılama.

Sunucunuzda, yenileme jetonunun iptal edilmesi ve kimlik jetonunun doğrulanması için aşağıdaki mantığı uygulayın:

Bir kullanıcının kimlik jetonu doğrulanacağı zaman ek checkRevokedboolean işaretinin verifyIdToken'a iletilmesi gerekir. Kullanıcının jetonu iptal edilirse kullanıcıdan istemcide oturumu kapatılmalı veya Firebase Authentication istemci SDK'ları tarafından sağlanan yeniden kimlik doğrulama API'lerini kullanarak yeniden kimlik doğrulaması yapması istenecektir.

Platformunuz için Yönetici SDK'sını başlatmak üzere kurulum sayfasındaki talimatları uygulayın. Kimlik jetonunu alma örneklerini verifyIdToken bölümünde bulabilirsiniz.

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

İstemcide jeton iptallerine yanıt verme

Jeton, Yönetici SDK'sı aracılığıyla iptal edilirse müşteriye iptal hakkında bilgi verilir ve kullanıcının kimliğini yeniden doğrulaması veya oturumunun kapatılması beklenir:

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

Gelişmiş Güvenlik: IP adresi kısıtlamalarını zorunlu kılma

Jeton hırsızlığını tespit etmek için yaygın olarak kullanılan bir güvenlik mekanizması, istek IP adresi kaynaklarını takip etmektir. Örneğin, istekler her zaman aynı IP adresinden (çağrıyı yapan sunucu) geliyorsa tek IP adresi oturumları zorunlu kılınabilir. Kullanıcının IP adresinin coğrafi konumun aniden değiştiğini tespit ederseniz veya şüpheli bir kaynaktan istek alırsanız kullanıcının jetonunu iptal edebilirsiniz.

IP adresine dayalı güvenlik kontrolleri yapmak için kimlik doğrulaması yapılan her istek için kimlik jetonunu inceleyin ve kısıtlanmış verilere erişime izin vermeden önce isteğin IP adresinin önceki güvenilir IP adresleriyle eşleşip eşleşmediğini veya güvenilir bir aralık içinde olup olmadığını kontrol edin. Örneğin:

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