Le sessioni di autenticazione Firebase sono di lunga durata. Ogni volta che un utente accede, le credenziali dell'utente vengono inviate al back-end di autenticazione Firebase e scambiate con un token ID Firebase (un JWT) e un token di aggiornamento. I token ID Firebase sono di breve durata 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. Ciò include eventi come aggiornamenti di password o indirizzi e-mail.
Firebase Admin SDK offre la possibilità 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 restrizioni per impedire l'utilizzo delle sessioni in circostanze sospette, nonché un meccanismo per il ripristino da un potenziale furto di token.
Revocare i token di aggiornamento
È possibile revocare il token di aggiornamento esistente di un utente quando un utente segnala un dispositivo smarrito o rubato. Allo stesso modo, se scopri una vulnerabilità generale o sospetti una perdita su larga scala di token attivi, puoi usare l'API listUsers
per cercare tutti gli utenti e revocare i loro token per il progetto specificato.
La reimpostazione della password revoca anche i token esistenti di un utente; tuttavia, il back-end di autenticazione Firebase gestisce automaticamente la revoca in quel caso. Alla revoca, l'utente viene disconnesso e viene richiesto di riautenticarsi.
Ecco un'implementazione di esempio che usa Admin SDK per revocare il token di aggiornamento di un determinato utente. Per inizializzare l'Admin SDK, seguire le istruzioni 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}`);
});
Giava
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);
Pitone
# 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))
andare
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 senza stato, è possibile determinare che un token è stato revocato solo richiedendo lo stato del token dal back-end di autenticazione Firebase. Per questo motivo, eseguire questo controllo sul tuo server è un'operazione costosa, che richiede un viaggio di andata e ritorno della rete aggiuntivo. Puoi evitare di effettuare questa richiesta di rete impostando le regole di sicurezza Firebase che verificano la revoca anziché utilizzare l'SDK di amministrazione per effettuare il controllo.
Rileva la revoca del token ID nelle regole di sicurezza di Firebase
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 nel database Firebase Realtime.
Salva il timestamp di revoca del token di aggiornamento. Ciò è necessario per tenere traccia della revoca del token ID tramite le regole di sicurezza di Firebase. Ciò consente controlli efficienti all'interno del database. Negli esempi di codice seguenti, utilizzare l'uid e il tempo di revoca ottenuti nella sezione precedente .
Node.js
const metadataRef = getDatabase().ref('metadata/' + uid);
metadataRef.set({ revokeTime: utcRevocationTimeSecs }).then(() => {
console.log('Database updated successfully.');
});
Giava
DatabaseReference ref = FirebaseDatabase.getInstance().getReference("metadata/" + uid);
Map<String, Object> userData = new HashMap<>();
userData.put("revokeTime", revocationSecond);
ref.setValueAsync(userData);
Pitone
metadata_ref = firebase_admin.db.reference("metadata/" + uid)
metadata_ref.set({'revokeTime': revocation_second})
Aggiungi un segno di spunta alle Regole di sicurezza di Firebase
Per applicare questo controllo, imposta una regola senza accesso in scrittura client per memorizzare il tempo di revoca per utente. Questo può essere aggiornato con il timestamp UTC dell'ultima ora 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",
}
}
}
}
Tutti i dati che richiedono l'accesso autenticato devono avere la regola seguente 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 il token ID di un utente deve essere verificato, il flag booleano checkRevoked
aggiuntivo deve essere passato a verifyIdToken
. Se il token dell'utente viene revocato, l'utente deve essere disconnesso dal client o deve essere invitato a riautenticarsi utilizzando le API di riautenticazione fornite dagli SDK del client di autenticazione Firebase.
Per inizializzare l'Admin SDK per la tua piattaforma, segui le istruzioni nella pagina di configurazione . Esempi di recupero del token ID sono 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.
}
});
Giava
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.
}
}
Pitone
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
andare
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 Admin SDK, il client viene informato della revoca e l'utente deve riautenticarsi 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 restrizioni degli indirizzi IP
Un meccanismo di sicurezza comune per rilevare il furto di token consiste nel tenere traccia delle origini dell'indirizzo IP della richiesta. Ad esempio, se le richieste provengono sempre dallo stesso indirizzo IP (server che effettua la chiamata), è possibile imporre sessioni a indirizzo IP singolo. In alternativa, potresti revocare il token di un utente se rilevi che l'indirizzo IP dell'utente ha cambiato improvvisamente la geolocalizzazione o se ricevi una richiesta da un'origine sospetta.
Per eseguire controlli di sicurezza in base all'indirizzo IP, per ogni richiesta autenticata controllare il token ID e verificare se l'indirizzo IP della richiesta corrisponde a precedenti indirizzi IP attendibili o rientra in un intervallo attendibile prima di consentire l'accesso ai dati riservati. Per 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!' })
});
}
});
});