Le sessioni Firebase Authentication sono di lunga durata. Ogni volta che un utente accede, le sue credenziali vengono inviate al backend Firebase Authentication e scambiate con un token ID Firebase (un JWT) e un token di aggiornamento. I token ID Firebase hanno una durata breve e durano un'ora; il token di aggiornamento può essere utilizzato per recuperare nuovi token ID. I token di aggiornamento scadono solo quando si verifica una delle seguenti condizioni:
- L'utente viene eliminato
- L'utente è disabilitato
- Viene rilevata una modifica importante dell'account per l'utente. Sono inclusi eventi come gli aggiornamenti della password o dell'indirizzo email.
L'SDK Firebase Admin consente di revocare i token di aggiornamento per un utente specificato. Inoltre, è disponibile anche un'API per verificare la revoca dei token ID. Con queste funzionalità, hai un maggiore controllo sulle sessioni utente. L'SDK offre la possibilità di aggiungere limitazioni per impedire l'utilizzo delle sessioni in circostanze sospette, nonché un meccanismo di recupero in caso di potenziale furto di token.
Revoca token di aggiornamento
Puoi revocare il token di aggiornamento esistente di un utente quando quest'ultimo segnala lo smarrimento o il furto di un dispositivo. Analogamente, se scopri una vulnerabilità generale o sospetti una fuga di token attivi su larga scala, puoi utilizzare l'API listUsers per cercare tutti gli utenti e revocare i loro token per il progetto specificato.
Anche le reimpostazioni delle password revocano i token esistenti di un utente; tuttavia, in questo caso il backend Firebase Authentication gestisce la revoca automaticamente. In caso di revoca, l'utente viene disconnesso e gli viene chiesto di eseguire nuovamente l'autenticazione.
Ecco un esempio di implementazione che utilizza l'SDK Admin per revocare il token di aggiornamento di un determinato utente. Per inizializzare l'SDK Admin, segui le istruzioni riportate nella pagina di configurazione.
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}')
Vai
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);
Rileva la revoca del token ID
Poiché i token ID Firebase sono JWT stateless, puoi determinare se un token è stato revocato solo richiedendone lo stato dal backend Firebase Authentication. Per questo motivo, l'esecuzione di questo controllo sul server è un'operazione costosa, che richiede un'andata e ritorno di rete aggiuntiva. Puoi evitare di effettuare questa richiesta di rete configurando Firebase Security Rules che controllano la revoca anziché utilizzare l'SDK Admin per eseguire il controllo.
Rileva la revoca del token ID in Firebase Security Rules
Per poter rilevare la revoca del token ID utilizzando le regole di sicurezza, dobbiamo prima archiviare alcuni metadati specifici dell'utente.
Aggiorna i metadati specifici dell'utente in Firebase Realtime Database.
Salva il timestamp di revoca del token di aggiornamento. Questo è necessario per monitorare la revoca dei token ID tramite Firebase Security Rules. In questo modo, i controlli all'interno del database sono più efficienti. Negli esempi di codice riportati di seguito, utilizza l'UID e l'ora di revoca ottenuti nella sezione precedente.
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})
Aggiungi un controllo a Firebase Security Rules
Per applicare questo controllo, configura una regola senza accesso in scrittura del client per archiviare l'ora di revoca per utente. Questo valore può essere aggiornato con il timestamp UTC dell'ora dell'ultima revoca, come mostrato negli esempi precedenti:
{
"rules": {
"metadata": {
"$user_id": {
// this could be false as it is only accessed from backend or rules.
".read": "$user_id === auth.uid",
".write": "false",
}
}
}
}
Tutti i dati che richiedono l'accesso autenticato devono avere la seguente regola configurata. Questa logica consente solo agli utenti autenticati con token ID non revocati di accedere ai dati protetti:
{
"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()
)",
}
}
}
}
Rileva la revoca del token ID nell'SDK.
Nel tuo server, implementa la seguente logica per la revoca del token di aggiornamento e la convalida del token ID:
Quando deve essere verificato il token ID di un utente, il flag booleano aggiuntivo checkRevoked deve essere passato a verifyIdToken. Se il token dell'utente viene revocato, l'utente deve essere disconnesso sul client o deve essere invitato a ripetere l'autenticazione utilizzando le API di autenticazione fornite dagli SDK client di Firebase Authentication.
Per inizializzare l'SDK Admin per la tua piattaforma, segui le istruzioni riportate nella pagina di configurazione. Esempi di recupero del token ID sono riportati nella sezione 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
Vai
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.
}
}
Rispondi alla revoca dei token sul client
Se il token viene revocato tramite l'SDK Admin, il client viene informato della revoca e l'utente deve eseguire nuovamente l'autenticazione o viene disconnesso:
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.
});
}
Sicurezza avanzata: applica le limitazioni degli indirizzi IP
Un meccanismo di sicurezza comune per rilevare il furto di token consiste nel tenere traccia delle origini degli indirizzi IP delle richieste. Ad esempio, se le richieste provengono sempre dallo stesso indirizzo IP (il server che effettua la chiamata), è possibile applicare sessioni con un singolo indirizzo IP. In alternativa, potresti revocare il token di un utente se rilevi che la geolocalizzazione dell'indirizzo IP dell'utente è cambiata improvvisamente o se ricevi una richiesta da un'origine sospetta.
Per eseguire controlli di sicurezza basati sull'indirizzo IP, per ogni richiesta autenticata ispeziona il token ID e controlla se l'indirizzo IP della richiesta corrisponde agli indirizzi IP attendibili precedenti o se rientra in un intervallo attendibile prima di consentire l'accesso ai dati limitati. Ad esempio:
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!' })
});
}
});
});