Zarządzanie sesjami użytkowników

Sesje uwierzytelniania Firebase trwają długo. Za każdym razem, gdy użytkownik się loguje, jego dane logowania są wysyłane do backendu uwierzytelniania Firebase i wymieniane na token identyfikatora Firebase (JWT) oraz token odświeżania. Tokeny identyfikatorów Firebase są krótkotrwałe i działają przez godzinę. Token odświeżania może służyć do pobierania nowych tokenów tożsamości. Tokeny odświeżania tracą ważność tylko wtedy, gdy:

  • Konto użytkownika zostało usunięte
  • Użytkownik jest wyłączony
  • Wykryto dużą zmianę na koncie użytkownika. Obejmuje to m.in. aktualizację hasła lub adresu e-mail.

Pakiet SDK Firebase Admin umożliwia unieważnianie tokenów odświeżania dla określonego użytkownika. Dodatkowo dostępny jest interfejs API do sprawdzania, czy token tożsamości został unieważniony. Te funkcje dają większą kontrolę nad sesjami użytkowników. Pakiet SDK umożliwia dodawanie ograniczeń, które zapobiegają używaniu sesji w podejrzanych okolicznościach, a także mechanizm odzyskiwania tokena przed potencjalną kradzieżą tokenów.

Unieważnij tokeny odświeżania

Możesz unieważnić istniejący token odświeżania użytkownika, gdy zgłasza on zgubione lub skradzione urządzenie. Podobnie, jeśli wykryjesz ogólną lukę w zabezpieczeniach lub podejrzewasz wyciek aktywnych tokenów na dużą skalę, możesz za pomocą interfejsu API listUsers wyszukać wszystkich użytkowników i unieważnić ich tokeny w określonym projekcie.

Reset hasła anuluje też dotychczasowe tokeny użytkownika, jednak w takim przypadku backend uwierzytelniania Firebase obsługuje je automatycznie. Po cofnięciu unieważnienia użytkownik zostanie wylogowany i poprosi o ponowne uwierzytelnienie.

Oto przykład implementacji, w której użyto pakietu Admin SDK do unieważnienia tokena odświeżania danego użytkownika. Aby zainicjować pakiet Admin SDK, postępuj zgodnie z instrukcjami na stronie konfiguracji.

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('Tokens revoked at: {0}'.format(revocation_second))

Go

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

Wykrywanie unieważnienia tokena tożsamości

Ponieważ tokeny identyfikatora Firebase są bezstanowymi tokenami JWT, można ustalić, czy token został unieważniony, wysyłając do backendu uwierzytelniania Firebase jego stan. Z tego powodu przeprowadzenie tego sprawdzenia na serwerze jest kosztowne i wymaga dodatkowego przesyłania danych w obie strony. Możesz uniknąć wysyłania tego żądania sieciowego, konfigurując reguły zabezpieczeń Firebase, które sprawdzają żądania unieważnienia, zamiast korzystać z pakietu Admin SDK.

Wykrywanie unieważnienia tokena identyfikatora w regułach zabezpieczeń Firebase

Aby wykryć, że token identyfikatora został unieważniony za pomocą reguł zabezpieczeń, musimy najpierw zapisać metadane dla poszczególnych użytkowników.

Aktualizowanie metadanych związanych z użytkownikiem w Bazie danych czasu rzeczywistego Firebase.

Zapisz sygnaturę czasową unieważnienia tokena odświeżania. Jest to potrzebne do śledzenia unieważniania tokena tożsamości za pomocą reguł zabezpieczeń Firebase. Pozwala to na sprawne kontrole w bazie danych. W poniższych przykładach kodu użyj identyfikatora UID i czasu unieważnienia uzyskanego w poprzedniej sekcji.

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

Dodaj kontrolę do reguł zabezpieczeń Firebase

Aby wymusić sprawdzanie, skonfiguruj regułę, która nie ma uprawnień do zapisu w przypadku klienta. Pozwoli to przechowywać czas odwołania przez poszczególnych użytkowników. Można go zaktualizować za pomocą sygnatury czasowej UTC ostatniego czasu unieważnienia, jak pokazano w poprzednich przykładach:

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

Wszystkie dane, które wymagają uwierzytelnionego dostępu, muszą mieć skonfigurowaną podaną niżej regułę. Ta logika zezwala na dostęp do chronionych danych tylko uwierzytelnionym użytkownikom z nieunieważnionymi tokenami tożsamości:

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

Wykrywaj unieważnienie tokena tożsamości w pakiecie SDK.

Zaimplementuj na serwerze tę logikę odświeżania i sprawdzania poprawności tokena identyfikatora:

Podczas weryfikowania tokena identyfikatora użytkownika do verifyIdToken musi zostać przekazana dodatkowa flaga logiczna checkRevoked. Jeśli token użytkownika zostanie unieważniony, należy go wylogować z klienta lub poprosić o ponowne uwierzytelnienie przy użyciu interfejsów API do ponownego uwierzytelniania dostarczonych przez pakiety SDK klienta uwierzytelniania Firebase.

Aby zainicjować pakiet Admin SDK dla swojej platformy, postępuj zgodnie z instrukcjami na stronie konfiguracji. Przykłady pobierania tokena identyfikatora znajdziesz w sekcji 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

Go

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

Odpowiadanie na unieważnienie tokena klienta

Jeśli token zostanie unieważniony za pomocą pakietu Admin SDK, klient zostanie poinformowany o tym unieważnieniu, a użytkownik będzie musiał ponownie uwierzytelnić się lub zostanie wylogowany:

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

Zabezpieczenia zaawansowane: wymuszanie ograniczeń adresów IP

Typowym mechanizmem wykrywania kradzieży tokenów jest śledzenie źródeł adresów IP żądań. Jeśli na przykład żądania są zawsze wysyłane z tego samego adresu IP (serwer, który wywołał połączenie), można wymuszać sesje z jednym adresem IP. Możesz też unieważnić token użytkownika, jeśli stwierdzisz, że jego adres IP nagle zmieniła geolokalizację lub otrzymasz żądanie z podejrzanego źródła.

Aby przeprowadzić kontrole zabezpieczeń na podstawie adresu IP, w przypadku każdego uwierzytelnionego żądania sprawdź token identyfikatora i sprawdź, czy adres IP żądania pasuje do poprzednich zaufanych adresów IP lub znajduje się w zaufanym zakresie, zanim zezwolisz na dostęp do danych objętych ograniczeniami. Przykład:

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