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

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

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

SDK Firebase Admin предоставляет возможность отзывать токены обновления для указанного пользователя. Кроме того, доступен API для проверки отзыва токенов ID. Благодаря этим возможностям вы получаете больший контроль над пользовательскими сессиями. 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 .

Сохраните метку времени отзыва токена обновления. Это необходимо для отслеживания отзыва токенов ID с помощью 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 для вашей платформы следуйте инструкциям на странице настройки . Примеры получения ID-токена приведены в разделе 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!' })
      });
    }
  });
});