Firebase Authentication sessions sont de longue durée. Chaque fois qu'un utilisateur se connecte, ses identifiants sont envoyés au Firebase Authentication backend et échangés contre un jeton d'ID Firebase (un JWT) et un jeton d'actualisation. Les jetons d'ID Firebase sont de courte durée et expirent au bout d'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 l'un des cas suivants :
- L'utilisateur est supprimé.
- L'utilisateur est désactivé.
- Une modification importante 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 Admin Firebase permet de révoquer les jetons d'actualisation d'un utilisateur spécifié. De plus, une API permettant de vérifier la révocation des jetons d'ID 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 de jeton potentiel.
Révoquer des jetons d'actualisation
Vous pouvez révoquer le jeton d'actualisation existant d'un utilisateur lorsqu'il signale un appareil perdu ou volé. De même, si vous découvrez une faille générale ou si vous soupçonnez une
fuite à grande échelle de jetons actifs, vous pouvez utiliser l'
listUsers
API pour rechercher tous les utilisateurs et révoquer leurs jetons pour le projet spécifié.
La réinitialisation du mot de passe révoque également les jetons existants d'un utilisateur. Toutefois, dans ce cas, le Firebase Authentication backend gère automatiquement la révocation. Lors de la 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.
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(f'Tokens revoked at: {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'ID Firebase sont des JWT sans état, vous ne pouvez déterminer qu'un jeton a été révoqué qu'en demandant l'état du jeton au Firebase Authentication backend. Pour cette raison, 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 qui vérifient la révocation au lieu d'utiliser le SDK Admin pour effectuer la vérification.
Détecter la révocation des jetons d'ID dans Firebase Security Rules
Pour pouvoir détecter la révocation des jetons d'ID à l'aide des règles de sécurité, nous devons d'abord stocker des métadonnées spécifiques à l'utilisateur.
Mettre à jour les métadonnées spécifiques à l'utilisateur dans Firebase Realtime Database
Enregistrez l'horodatage de la révocation du jeton d'actualisation. Cela est nécessaire pour suivre la révocation des jetons d'ID via Firebase Security Rules. Cela permet d'effectuer 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 client afin de stocker l'heure de révocation par utilisateur. Vous pouvez la mettre à 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 n'autorise que les utilisateurs authentifiés avec des jetons d'ID 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étecter la révocation des jetons d'ID dans le SDK
Sur votre serveur, implémentez la logique suivante pour la révocation des jetons d'actualisation et la validation des jetons d'ID :
Lorsqu'un jeton d'ID d'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é sur le client ou invité à se réauthentifier
à l'aide des API de réauthentification fournies par les Firebase Authentication SDK client.
Pour initialiser le SDK Admin pour votre plate-forme, suivez les instructions de la
page de configuration. Des exemples de récupération du jeton d'ID
sont disponibles dans la
verifyIdToken section.
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 des jetons 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 des restrictions d'adresse 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), vous pouvez appliquer des sessions d'adresse IP unique. 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 d'une origine suspecte.
Pour effectuer des contrôles de sécurité basés sur l'adresse IP, inspectez le jeton d'ID pour chaque requête authentifiée et vérifiez si l'adresse IP de la requête correspond aux adresses IP de confiance précédentes ou si elle se trouve dans une plage de confiance avant d'autoriser l'accès aux données restreintes. 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!' })
});
}
});
});