Le sessioni Firebase Authentication sono di lunga durata. Ogni volta che un utente accede, le credenziali utente 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 è stato eliminato
- L'utente è disattivato
- Viene rilevata una modifica importante dell'account per l'utente. Sono inclusi eventi quali aggiornamenti della password o dell'indirizzo email.
L'SDK Firebase Admin consente di revocare i token di aggiornamento per un utente specifico. Inoltre, viene resa disponibile anche un'API per verificare la revoca del 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 dei token di aggiornamento
Potresti revocare il token di aggiornamento esistente di un utente quando quest'ultimo segnala lo smarrimento o
il furto di un dispositivo. Allo stesso modo, 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.
I reimpostazioni della password revocano anche i token esistenti di un utente; tuttavia, il Firebase Authentication backend gestisce automaticamente la revoca in questo caso. Al momento della revoca, l'utente viene disconnesso e gli viene chiesto di eseguire nuovamente l'autenticazione.
Ecco un'implementazione di esempio 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);
Rilevare la revoca del token ID
Poiché i token ID Firebase sono JWT stateless, puoi determinare se un token è stato revocato solo richiedendo lo stato del token dal backend Firebase Authentication. Per questo motivo, l'esecuzione di questo controllo sul server è un'operazione costosa, che richiede un ulteriore round trip di rete. 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.
Rilevare la revoca del token ID in Firebase Security Rules
Per poter rilevare la revoca del token ID utilizzando le regole di sicurezza, dobbiamo prima memorizzare alcuni metadati specifici dell'utente.
Aggiorna i metadati specifici dell'utente in Firebase Realtime Database.
Salva il timestamp di revoca del token di aggiornamento. È necessario per monitorare la revoca del token ID tramite Firebase Security Rules. Ciò consente controlli efficienti all'interno del database. 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 memorizzare l'ora di revoca per utente. Questo valore può essere aggiornato con il timestamp UTC dell'ultimo orario di 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",
}
}
}
}
Per tutti i dati che richiedono l'accesso autenticato deve essere configurata la seguente regola. 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 è necessario verificare 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 uscire dal client o gli deve essere chiesto di eseguire nuovamente l'autenticazione
utilizzando le API di riautenticazione fornite dagli SDK client 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 disponibili 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.
}
}
Rispondere alla revoca del 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 è tenere traccia delle origini degli indirizzi IP delle richieste. Ad esempio, se le richieste provengono sempre dallo stesso indirizzo IP (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 in base all'indirizzo IP, per ogni richiesta autenticata ispeziona il token ID e verifica se l'indirizzo IP della richiesta corrisponde a indirizzi IP attendibili precedenti o rientra in un intervallo attendibile prima di consentire l'accesso a dati con accesso limitato. 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!' })
});
}
});
});