Catch up on everything we announced at this year's Firebase Summit. Learn more

Управление сеансами пользователей

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

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

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

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

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

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

Вот пример реализации, которая использует Admin SDK для отзыва токена обновления данного пользователя. Для инициализации администратора 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}`);
  });

Джава

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

Идти

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

Обнаружение отзыва токена ID

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

Обнаружение отзыва токена ID в правилах безопасности Firebase

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

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

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

Node.js

const metadataRef = getDatabase().ref('metadata/' + uid);
metadataRef.set({ revokeTime: utcRevocationTimeSecs }).then(() => {
  console.log('Database updated successfully.');
});

Джава

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

Чтобы принудительно выполнить эту проверку, настройте правило без доступа клиента на запись для хранения времени отзыва для каждого пользователя. Это может быть обновлено меткой времени в формате 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",
      }
    }
  }
}

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

{
  "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.

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

Когда ID фишка пользователя должны быть проверена, дополнительный checkRevoked логического флаг должен быть передан verifyIdToken . Если токен пользователя отозван, пользователь должен выйти из системы на клиенте или попросить его повторно пройти аутентификацию с помощью 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.
    }
  });

Джава

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