Gerenciar sessões de usuários

As sessões do Firebase Authentication são longas. Sempre que um usuário faz login, as credenciais dele são enviadas para o back-end do Firebase Authentication e trocadas por um token de ID do Firebase (um JWT) e um token de atualização. Os tokens de ID do Firebase são curtos e duram uma hora. O token de atualização pode ser usado para recuperar novos tokens de ID. Os tokens de atualização expiram somente em uma das seguintes situações:

  • O usuário é excluído.
  • O usuário é desativado.
  • Uma mudança importante na conta é detectada para o usuário. Isso inclui eventos como atualizações de senha ou de endereço de e-mail.

O SDK Admin do Firebase permite revogar os tokens de atualização de um usuário especificado. Além disso, também há uma API disponível para verificar a revogação do token de ID. Com esses recursos, você tem mais controle sobre as sessões dos usuários. O SDK permite adicionar restrições para evitar que as sessões sejam usadas em circunstâncias suspeitas, bem como um mecanismo de recuperação para possíveis roubos de token.

Revogar tokens de atualização

É possível revogar o token de atualização de um usuário quando ele informa que o dispositivo foi perdido ou roubado. Da mesma forma, se você descobrir uma vulnerabilidade geral ou suspeitar de um grande vazamento de tokens ativos, use a API listUsers para procurar todos os usuários e revogar os respectivos tokens no projeto especificado.

As redefinições de senha também revogam os tokens de um usuário. No entanto, o back-end do Firebase Authentication processa a revogação automaticamente nesse caso. Após a revogação, o usuário é desconectado e precisa se autenticar novamente.

Veja um exemplo de implementação que usa o SDK Admin para revogar o token de atualização de um usuário. Para inicializar o SDK Admin, siga as instruções na página de configuração.

Node.js

// Revoke all refresh tokens for a specified user for whatever reason.
// Retrieve the timestamp of the revocation, in seconds since the epoch.
admin.auth().revokeRefreshTokens(uid)
  .then(() => {
    return admin.auth().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))

Go

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);

Detectar a revogação do token de ID

Como os tokens de ID do Firebase são JWTs sem estado, você determina se um token foi revogado por meio da solicitação do status do token no back-end do Firebase Authentication. Por isso, executar essa verificação no seu servidor tem um alto custo, porque exige uma operação de ida e volta adicional. Evite essa solicitação de rede configurando as regras do Firebase que verificam a revogação em vez de usar o SDK Admin para fazer a verificação.

Detectar uma revogação do token de ID nas regras do Database

Para detectar a revogação do token de ID usando as regras do banco de dados, primeiro precisamos armazenar alguns metadados específicos do usuário.

Atualizar metadados específicos do usuário no Firebase Realtime Database

Salve o carimbo de data/hora da revogação do token de atualização. Isso é necessário para rastrear a revogação do token de ID por meio das regras do Firebase. Assim, é possível realizar verificações eficientes no banco de dados. Nas amostras de código abaixo, use o uid e o horário da revogação encontrados na seção anterior.

Node.js

const metadataRef = admin.database().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})

Adicionar uma verificação nas regras do Database

Para realizar essa verificação, configure uma regra sem acesso de gravação ao cliente a fim de armazenar o horário da revogação por usuário. Ele pode ser atualizado com o carimbo de data/hora em UTC do último horário de revogação, como mostrado nos exemplos anteriores:

{
  "rules": {
    "metadata": {
      "$user_id": {
        // this could be false as it is only accessed from backend or rules.
        ".read": "$user_id === auth.uid",
        ".write": "false",
      }
    }
  }
}

Qualquer dado que exija acesso autenticado precisa ter a seguinte regra configurada. Esta lógica só permite que os usuários autenticados com tokens de ID não revogados acessem os dados protegidos:

{
  "rules": {
    "users": {
      "$user_id": {
        ".read": "$user_id === auth.uid && auth.token.auth_time > (root.child('metadata').child(auth.uid).child('revokeTime').val() || 0)",
        ".write": "$user_id === auth.uid && auth.token.auth_time > (root.child('metadata').child(auth.uid).child('revokeTime').val() || 0)"
      }
    }
  }
}

Detectar revogação do token de ID no SDK

No servidor, implemente a seguinte lógica para revogar o token de atualização e validar o token de ID:

Quando o token de ID de um usuário precisar ser verificado, será necessário transmitir a sinalização booleana checkRevoked adicional para verifyIdToken. Se o token do usuário for revogado, o usuário precisará ser desconectado do cliente ou se autenticar novamente com as APIs de nova autenticação fornecidas pelos SDKs de cliente do Firebase Authentication.

Para inicializar o SDK Admin na sua plataforma, siga as instruções na página de configuração. Veja exemplos de recuperação do token de ID na seção verifyIdToken.

Node.js

// Verify the ID token while checking if the token is revoked by passing
// checkRevoked true.
let checkRevoked = true;
admin.auth().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.InvalidIdTokenError:
    # Token is invalid
    pass

Go

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.
    }
}

Responder à revogação do token no cliente

Caso o token seja revogado por meio do SDK Admin, o cliente será informado sobre a revogação e o usuário precisará se autenticar novamente ou ficará desconectado:

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.
    });
}

Segurança avançada: aplicar restrições de endereço IP

Um mecanismo de segurança comum para detectar o roubo do token é acompanhar as origens de endereço IP das solicitações. Por exemplo, se as solicitações sempre vierem do mesmo endereço IP (servidor que faz a chamada), poderão ser aplicadas sessões de endereço IP único. Ou revogue o token de um usuário se detectar que a localização geográfica do endereço IP dele foi alterada de repente ou se receber uma solicitação de origem suspeita.

Para realizar verificações de segurança com base no endereço IP, inspecione o token de ID de cada solicitação autenticada. Verifique se o endereço IP da solicitação corresponde aos endereços IP confiáveis anteriores ou está dentro de um intervalo confiável antes de permitir o acesso a dados restritos. Exemplo:

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!' })
      });
    }
  });
});