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

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

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

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

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

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

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

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

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