Gestisci sessioni utente

Le sessioni di autenticazione Firebase durano a lungo. Ogni volta che un utente accede, le credenziali dell'utente vengono inviate al backend di autenticazione Firebase e scambiate con un token ID Firebase (un JWT) e un token di aggiornamento. I token ID Firebase hanno vita 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
  • È stata rilevata una modifica importante all'account dell'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.

Revoca i token di aggiornamento

Potresti 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 fuga su vasta scala di token attivi, puoi utilizzare 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, in questo caso il backend Firebase Authentication gestisce automaticamente la revoca. Alla revoca, l'utente viene disconnesso e gli viene richiesto di autenticarsi nuovamente.

Di seguito è riportato un esempio di implementazione che utilizza 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, puoi determinare che un token è stato revocato solo richiedendone lo stato dal backend di autenticazione Firebase. Per questo motivo, eseguire questo controllo sul tuo server è un'operazione costosa, che richiede un viaggio di andata e ritorno sulla rete aggiuntivo. Puoi evitare di effettuare questa richiesta di rete configurando le regole di sicurezza Firebase che verificano la revoca anziché utilizzare Admin SDK per effettuare il controllo.

Rileva la revoca del token ID nelle regole di sicurezza Firebase

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 nel Firebase Realtime Database.

Salvare il timestamp di revoca del token di aggiornamento. Ciò è necessario per tenere traccia della revoca del token ID tramite le regole di sicurezza Firebase. Ciò consente controlli efficienti all'interno del database. Negli esempi di codice riportati di seguito, utilizzare 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.');
});

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 Firebase

Per applicare questo controllo, imposta una regola senza accesso in scrittura del client per archiviare l'ora 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 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 è necessario verificare il token ID di un utente, è necessario passare il flag booleano aggiuntivo checkRevoked a verifyIdToken . Se il token dell'utente viene revocato, l'utente deve essere disconnesso sul client o gli deve essere chiesto di autenticarsi nuovamente 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 si trovano 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 si prevede che l'utente si autentichi nuovamente o venga 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 restrizioni sugli 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 con indirizzo IP singolo. Oppure 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 ispezionare il token ID e verificare se l'indirizzo IP della richiesta corrisponde ai 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!' })
      });
    }
  });
});