Le sessioni Firebase Authentication sono di lunga durata. Ogni volta che un utente accede, le sue credenziali vengono inviate al backend di Firebase Authentication e scambiate con un token ID Firebase (un JWT) e un token di aggiornamento. I token ID di 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 è stato disattivato
- È stata rilevata una modifica sostanziale dell'account dell'utente. Sono inclusi eventi quali aggiornamenti della password o dell'indirizzo email.
L'SDK Firebase Admin offre la possibilità di revocare i token di aggiornamento per un utente specificato. Inoltre, è stata resa disponibile un'API per verificare la revoca dell'ID token. Con queste funzionalità, hai un maggiore controllo sulle sessioni degli utenti. 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.
Revocare i token di aggiornamento
Potresti revocare il token di aggiornamento esistente di un utente quando segnala un dispositivo smarrito o
rubato. Analogamente, se scopri una vulnerabilità generale o sospetti una утечка 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.
La reimpostazione della password comporta anche la revoca dei token esistenti di un utente. Tuttavia, in questo caso il backend di Firebase Authentication gestisce automaticamente la revoca. Al momento della 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('Tokens revoked at: {0}'.format(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 dell'ID token
Poiché i token ID Firebase sono JWT senza stato, puoi stabilire se un token è stato revocato solo richiedendo il relativo stato dal backend Firebase Authentication. Per questo motivo, eseguire questo controllo sul server è un'operazione costosa che richiede un viaggio di andata e ritorno aggiuntivo sulla rete. Puoi evitare di effettuare questa richiesta di rete configurando Firebase Security Rules per verificare la revoca invece di utilizzare l'SDK Admin per eseguire il controllo.
Rilevare la revoca del token ID in Firebase Security Rules
Per poter rilevare la revoca dell'ID token utilizzando le regole di sicurezza, dobbiamo prima memorizzare alcuni metadati specifici per l'utente.
Aggiorna i metadati specifici dell'utente in Firebase Realtime Database.
Salva il timestamp della revoca del token di aggiornamento. Questo è 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 la data 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 il momento della revoca per utente. Questo valore può essere aggiornato con il timestamp UTC dell'ultima data e 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",
}
}
}
}
Per qualsiasi dato che richiede l'accesso autenticato deve essere configurata la seguente regola. Questa logica consente solo agli utenti autenticati con token identificativi 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 l'ID token 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 deve essere richiesto 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 dell'ID
token 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.
}
}
Rispondere alla revoca del token sul client
Se il token viene revocato tramite l'SDK Admin, il cliente viene informato della revoca e l'utente deve eseguire nuovamente l'autenticazione o viene eseguito il logout:
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 per gli 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 (il server che effettua la chiamata), è possibile applicare sessioni con un singolo indirizzo IP. In alternativa, puoi revocare il token di un utente se rilevi che l'indirizzo IP dell'utente ha improvvisamente modificato 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 esamina il token ID e controlla se l'indirizzo IP della richiesta corrisponde a indirizzi IP attendibili precedenti o se rientra in un intervallo attendibile prima di consentire l'accesso ai dati con limitazioni. 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!' })
});
}
});
});