Gérer les sessions utilisateur

Les sessions Firebase Authentication sont de longue durée. Chaque fois qu'un utilisateur se connecte, ses identifiants sont envoyés au backend Firebase Authentication et échangés contre un jeton d'ID Firebase (un jeton Web JSON) et un jeton d'actualisation. Les jetons d'ID 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'ID. Les jetons d'actualisation n'expirent que dans les cas suivants:

  • Le compte utilisateur est supprimé
  • Utilisateur désactivé
  • Une modification majeure du compte de l'utilisateur est détectée. Cela inclut des événements tels que la modification du mot de passe ou de l'adresse e-mail.

Le SDK Firebase Admin permet de révoquer les jetons d'actualisation pour un utilisateur spécifié. De plus, une API permettant de vérifier la révocation du jeton d'identification est également disponible. Ces fonctionnalités vous permettent de mieux contrôler les sessions utilisateur. Le SDK permet d'ajouter des restrictions pour empêcher l'utilisation des sessions dans des circonstances suspectes, ainsi qu'un mécanisme de récupération en cas de vol potentiel de jetons.

Révoquer des jetons d'actualisation

Vous pouvez révoquer le jeton d'actualisation existant d'un utilisateur lorsqu'il signale la perte ou le vol d'un appareil. De même, si vous découvrez une faille 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. Toutefois, le backend Firebase Authentication gère automatiquement la révocation dans ce cas. En cas de révocation, l'utilisateur est déconnecté et invité à s'authentifier à nouveau.

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 administrateur, suivez les instructions de la page de configuration.

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

Détecter la révocation des jetons d'ID

Étant donné que les jetons d'identification Firebase sont des JWT sans état, vous ne pouvez déterminer si un jeton a été révoqué qu'en demandant son état au backend Firebase Authentication. C'est pourquoi effectuer cette vérification sur votre serveur est une opération coûteuse, qui nécessite un aller-retour réseau supplémentaire. Vous pouvez éviter d'effectuer cette requête réseau en configurant Firebase Security Rules pour vérifier la révocation plutôt que d'utiliser le SDK Admin pour effectuer la vérification.

Détecter la révocation d'un jeton d'ID dans Firebase Security Rules

Pour pouvoir détecter la révocation du jeton d'identification à l'aide de 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 Firebase Realtime Database.

Enregistrez l'horodatage de révocation du jeton d'actualisation. Cela est nécessaire pour suivre la révocation du jeton d'ID via Firebase Security Rules. Cela permet des vérifications efficaces dans 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.

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

Ajouter une vérification à Firebase Security Rules

Pour appliquer cette vérification, configurez une règle sans accès en écriture au client pour stocker l'heure de révocation par utilisateur. Vous pouvez le mettre à jour avec l'horodatage UTC de la dernière 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",
      }
    }
  }
}

La règle suivante doit être configurée pour toutes les données nécessitant un accès authentifié. Cette logique n'autorise que les utilisateurs authentifiés disposant de jetons d'identité non révoqués à 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'ID 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'ID d'un utilisateur doit être validé, 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é à s'authentifier à nouveau à l'aide des API de réauthentification fournies par les SDK client Firebase Authentication.

Pour initialiser le SDK Admin pour votre plate-forme, suivez les instructions de la page de configuration. Vous trouverez des exemples de récupération du jeton d'ID dans la section 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.
    }
}

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

Si le jeton est révoqué via le SDK Admin, le client en est informé et l'utilisateur doit se réauthentifier ou se déconnecter:

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 des restrictions d'adresses IP

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

Pour effectuer des vérifications de sécurité en fonction de l'adresse IP, inspectez le jeton d'identité pour chaque requête authentifiée et vérifiez si l'adresse IP de la requête correspond à des adresses IP approuvées précédentes ou si elle se trouve dans une plage approuvée avant d'autoriser l'accès aux données limitées. 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!' })
      });
    }
  });
});