Как управлять сеансами пользователей

Сеансы Firebase Authentication являются долгосрочными. При каждом входе пользователя его учетные данные отправляются в серверную часть Firebase Authentication и обмениваются на токен Firebase ID (JWT) и токен обновления. Токены Firebase ID действуют в течение часа, а токен обновления можно использовать для получения новых токенов ID. Срок действия токена обновления истекает только в следующих случаях:

  • Пользователь удален
  • Пользователь отключен
  • Обнаружено существенное изменение в аккаунте пользователя. например об изменении пароля или адреса электронной почты.

Firebase Admin SDK позволяет отзывать токены обновления для указанного пользователя. Кроме того, доступен API для проверки отзыва токена идентификатора. Эти возможности позволяют лучше контролировать сеансы пользователей. SDK позволяет добавлять ограничения, чтобы предотвратить использование сеансов в подозрительных обстоятельствах, а также механизм восстановления после потенциальной кражи токена.

Отменить токены обновления

Вы можете отозвать существующий токен обновления пользователя, если он сообщит о потере или краже устройства. Аналогично, если вы обнаружите общую уязвимость или заподозрите масштабную утечку активных токенов, вы можете использовать API listUsers, чтобы найти всех пользователей и отозвать их токены для указанного проекта.

При сбросе пароля существующие токены пользователя также отзываются, но в этом случае отзыв автоматически обрабатывается серверной частью Firebase Authentication. После отзыва пользователь выйдет из системы и ему будет предложено повторно пройти аутентификацию.

Ниже приведен пример реализации, в которой Admin SDK используется для отзыва токена обновления определенного пользователя. Чтобы инициализировать Admin SDK, следуйте инструкциям на странице настройки.

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

Проложить маршрут

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

Обнаружение отзыва токена идентификации

Поскольку токены Firebase ID – это JWT без сохранения состояния, определить, что токен был отозван, можно только запросив статус токена из бэкенда Firebase Authentication. Поэтому выполнение этой проверки на сервере – дорогостоящая операция, требующая дополнительного сетевого обмена данными. Чтобы избежать этого сетевого запроса, настройте Firebase Security Rules, которые проверяют отзыв, вместо того чтобы использовать Admin SDK для выполнения проверки.

Как обнаружить отзыв токена идентификатора в Firebase Security Rules

Чтобы обнаруживать отзыв токена идентификатора с помощью правил безопасности, сначала необходимо сохранить некоторые метаданные, относящиеся к пользователю.

Обновление метаданных пользователя в Firebase Realtime Database.

Сохраните временную метку отзыва токена обновления. Это необходимо, чтобы отслеживать отзыв токена идентификатора через Firebase Security Rules. Это позволяет эффективно выполнять проверки в базе данных. В примерах кода ниже используйте uid и время отзыва, полученные в предыдущем разделе.

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

Добавить проверку в Firebase Security Rules

Чтобы принудительно выполнить эту проверку, настройте правило без доступа клиента на запись для хранения времени отзыва для каждого пользователя. Его можно обновить, указав временную метку UTC последнего отзыва, как показано в предыдущих примерах:

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

Для любых данных, требующих аутентифицированного доступа, должно быть настроено следующее правило. Эта логика позволяет получить доступ к защищенным данным только аутентифицированным пользователям с неотмененными токенами идентификации:

{
  "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()
        )",
      }
    }
  }
}

Обнаружение отзыва токена идентификатора в SDK.

Реализуйте на сервере следующую логику для отзыва токена обновления и проверки токена идентификации:

При проверке токена идентификации пользователя в verifyIdToken необходимо передать дополнительный логический флаг checkRevoked. Если токен пользователя отозван, пользователь должен быть выведен из системы на клиенте или ему должно быть предложено повторно пройти аутентификацию с помощью API повторной аутентификации, предоставляемых клиентскими SDK Firebase Authentication.

Чтобы инициализировать Admin SDK для своей платформы, следуйте инструкциям на странице настройки. Примеры получения токена идентификатора приведены в разделе 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

Проложить маршрут

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

Как реагировать на отзыв токена на клиенте

Если токен отозван через Admin SDK, клиент получает уведомление об этом, а пользователю предлагается повторно пройти аутентификацию или выйти из системы.

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

Расширенные настройки безопасности: принудительное применение ограничений по IP-адресам

Распространенный механизм безопасности для обнаружения кражи токенов – отслеживание IP-адресов, с которых поступают запросы. Например, если запросы всегда поступают с одного и того же IP-адреса (сервера, выполняющего вызов), можно принудительно использовать сеансы с одним IP-адресом. Вы также можете отозвать токен пользователя, если обнаружите, что его IP-адрес внезапно изменил геолокацию или вы получили запрос из подозрительного источника.

Чтобы выполнять проверки безопасности на основе IP-адреса, для каждого аутентифицированного запроса проверяйте токен идентификатора и убедитесь, что IP-адрес запроса совпадает с предыдущими доверенными IP-адресами или находится в доверенном диапазоне, прежде чем разрешать доступ к ограниченным данным. Пример:

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