Gérer les sessions utilisateur

Les sessions d'authentification Firebase durent longtemps. Chaque fois qu'un utilisateur se connecte, les informations d'identification de l'utilisateur sont envoyées au backend d'authentification Firebase et échangées contre un jeton d'identification Firebase (un JWT) et un jeton d'actualisation. Les jetons d'identification Firebase sont de courte durée et durent une heure ; le jeton d'actualisation peut être utilisé pour récupérer de nouveaux jetons d'identification. Les jetons d'actualisation expirent uniquement lorsque l'un des événements suivants se produit :

  • L'utilisateur est supprimé
  • L'utilisateur est désactivé
  • Un changement de compte majeur est détecté pour l'utilisateur. Cela inclut des événements tels que les mises à jour de mots de passe ou d’adresses e-mail.

Le SDK Firebase Admin offre la possibilité de révoquer les jetons d'actualisation pour un utilisateur spécifié. De plus, une API permettant de vérifier la révocation des jetons d'identification est également mise à disposition. Grâce à ces fonctionnalités, vous avez plus de contrôle sur les sessions utilisateur. Le SDK offre la possibilité d'ajouter des restrictions pour empêcher l'utilisation de sessions dans des circonstances suspectes, ainsi qu'un mécanisme de récupération en cas de vol potentiel de jetons.

Révoquer les jetons d'actualisation

Vous pouvez révoquer le jeton d'actualisation existant d'un utilisateur lorsqu'un utilisateur signale un appareil perdu ou volé. De même, si vous découvrez une vulnérabilité générale ou soupçonnez une fuite à grande échelle de jetons actifs, vous pouvez utiliser l'API listUsers pour rechercher tous les utilisateurs et révoquer leurs jetons pour le projet spécifié.

Les réinitialisations de mot de passe révoquent également les jetons existants d'un utilisateur ; cependant, le backend d'authentification Firebase gère automatiquement la révocation dans ce cas. En cas de révocation, l'utilisateur est déconnecté et invité à se réauthentifier.

Voici un exemple d'implémentation qui utilise le SDK Admin pour révoquer le jeton d'actualisation d'un utilisateur donné. Pour initialiser le SDK Admin, suivez les instructions sur la page de configuration .

Noeud.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))

Aller

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

Détecter la révocation du jeton d'identification

Étant donné que les jetons d'identification Firebase sont des JWT sans état, vous pouvez déterminer qu'un jeton a été révoqué uniquement en demandant son statut au backend d'authentification Firebase. Pour cette raison, effectuer cette vérification sur votre serveur est une opération coûteuse, nécessitant un aller-retour réseau supplémentaire. Vous pouvez éviter de faire cette demande réseau en configurant des règles de sécurité Firebase qui vérifient la révocation plutôt que d'utiliser le SDK Admin pour effectuer la vérification.

Détecter la révocation du jeton d'identification dans les règles de sécurité Firebase

Pour pouvoir détecter la révocation du jeton d'identification à l'aide des règles de sécurité, nous devons d'abord stocker certaines métadonnées spécifiques à l'utilisateur.

Mettez à jour les métadonnées spécifiques à l'utilisateur dans la base de données Firebase Realtime.

Enregistrez l’horodatage de révocation du jeton d’actualisation. Ceci est nécessaire pour suivre la révocation du jeton d'identification via les règles de sécurité Firebase. Cela permet des contrôles efficaces au sein de la base de données. Dans les exemples de code ci-dessous, utilisez l'uid et l'heure de révocation obtenus dans la section précédente .

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

Ajouter une vérification aux règles de sécurité Firebase

Pour appliquer cette vérification, configurez une règle sans accès client en écriture pour stocker le temps de révocation par utilisateur. Celui-ci peut être mis à jour avec l'horodatage UTC de la dernière heure de révocation, comme indiqué dans les exemples précédents :

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

Toutes les données nécessitant un accès authentifié doivent avoir la règle suivante configurée. Cette logique permet uniquement aux utilisateurs authentifiés disposant de jetons d'identification non révoqués d'accéder aux données protégées :

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

Détectez la révocation du jeton d’identification dans le SDK.

Sur votre serveur, implémentez la logique suivante pour la révocation du jeton d'actualisation et la validation du jeton d'ID :

Lorsque le jeton d'identification d'un utilisateur doit être vérifié, l'indicateur booléen checkRevoked supplémentaire doit être transmis à verifyIdToken . Si le jeton de l'utilisateur est révoqué, l'utilisateur doit être déconnecté du client ou invité à se réauthentifier à l'aide des API de réauthentification fournies par les SDK du client Firebase Authentication.

Pour initialiser le SDK Admin pour votre plateforme, suivez les instructions sur la page de configuration . Des exemples de récupération du jeton d'identification se trouvent dans la section verifyIdToken .

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

Aller

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

Répondre à la révocation du jeton sur le client

Si le jeton est révoqué via le SDK Admin, le client est informé de la révocation et l'utilisateur doit se réauthentifier ou est déconnecté :

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

Sécurité avancée : appliquer les restrictions d'adresse IP

Un mécanisme de sécurité courant pour détecter le vol de jetons consiste à garder une trace de l’origine des adresses IP des demandes. Par exemple, si les demandes proviennent toujours de la même adresse IP (le serveur effectuant l'appel), des sessions avec une seule adresse IP peuvent être appliquées. Vous pouvez également révoquer le jeton d'un utilisateur si vous détectez que l'adresse IP de l'utilisateur a soudainement changé de géolocalisation ou si vous recevez une demande d'origine suspecte.

Pour effectuer des contrôles de sécurité basés sur l'adresse IP, pour chaque demande authentifiée, inspectez le jeton d'identification et vérifiez si l'adresse IP de la demande correspond aux adresses IP de confiance précédentes ou se situe dans une plage de confiance avant d'autoriser l'accès aux données restreintes. Par exemple:

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