Сеансы Firebase Authentication являются долгосрочными. Каждый раз, когда пользователь входит в систему, его учётные данные отправляются в бэкенд Firebase Authentication и обмениваются на токен Firebase ID (JWT) и токен обновления. Токены Firebase ID недолговечны и действуют в течение часа; токен обновления можно использовать для получения новых токенов ID. Токены обновления истекают только в одном из следующих случаев:
- Пользователь удален.
- Пользователь отключен
- Обнаружено существенное изменение учётной записи пользователя. Это включает такие события, как обновление пароля или адреса электронной почты.
Firebase Admin SDK предоставляет возможность отзыва токенов обновления для указанного пользователя. Кроме того, доступен 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}`);
});
Ява
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);
Питон
# 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)
С#
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 для проверки.
Обнаружение отзыва токена ID в 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.');
});
Ява
DatabaseReference ref = FirebaseDatabase.getInstance().getReference("metadata/" + uid);
Map<String, Object> userData = new HashMap<>();
userData.put("revokeTime", revocationSecond);
ref.setValueAsync(userData);
Питон
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.
На вашем сервере реализуйте следующую логику для отзыва токена обновления и проверки идентификатора токена:
При проверке токена идентификатора пользователя необходимо передать дополнительный логический флаг 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.
}
}
Питон
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)
С#
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!' })
});
}
});
});