Rozszerz uwierzytelnianie Firebase o blokowanie funkcji w chmurze, Rozszerz uwierzytelnianie Firebase o blokowanie funkcji w chmurze


Funkcje blokujące umożliwiają wykonanie niestandardowego kodu, który modyfikuje wynik rejestracji użytkownika lub logowania się do aplikacji. Możesz na przykład uniemożliwić uwierzytelnienie użytkownika, jeśli nie spełnia on określonych kryteriów, lub zaktualizować informacje o użytkowniku przed zwróceniem ich do aplikacji klienckiej.

Zanim zaczniesz

Aby korzystać z funkcji blokowania, musisz uaktualnić swój projekt Firebase do Firebase Authentication with Identity Platform. Jeśli jeszcze nie dokonałeś aktualizacji, zrób to najpierw.

Zrozumienie funkcji blokujących

Możesz zarejestrować funkcje blokujące dla dwóch zdarzeń:

  • beforeCreate : Wyzwala, zanim nowy użytkownik zostanie zapisany w bazie danych Firebase Authentication i zanim token zostanie zwrócony do aplikacji klienckiej.

  • beforeSignIn : wyzwala po zweryfikowaniu poświadczeń użytkownika, ale zanim uwierzytelnianie Firebase zwróci token identyfikatora do aplikacji klienckiej. Jeśli Twoja aplikacja korzysta z uwierzytelniania wieloskładnikowego, funkcja uruchamia się, gdy użytkownik zweryfikuje swój drugi czynnik. Zauważ, że tworzenie nowego użytkownika wyzwala również beforeSignIn , oprócz beforeCreate .

Podczas korzystania z funkcji blokujących należy pamiętać o następujących kwestiach:

  • Twoja funkcja musi odpowiedzieć w ciągu 7 sekund. Po 7 sekundach uwierzytelnianie Firebase zwraca błąd, a operacja klienta kończy się niepowodzeniem.

  • Kody odpowiedzi HTTP inne niż 200 są przekazywane do aplikacji klienckich. Upewnij się, że kod klienta obsługuje wszelkie błędy, które funkcja może zwrócić.

  • Funkcje mają zastosowanie do wszystkich użytkowników w Twoim projekcie, w tym do użytkowników zawartych w dzierżawie . Uwierzytelnianie Firebase dostarcza informacji o użytkownikach Twojej funkcji, w tym o wszystkich dzierżawcach, do których należą, dzięki czemu możesz odpowiednio zareagować.

  • Połączenie innego dostawcy tożsamości z kontem powoduje ponowne wyzwolenie wszystkich zarejestrowanych funkcji beforeSignIn .

  • Uwierzytelnianie anonimowe i niestandardowe nie uruchamia funkcji blokowania.

Wdróż i zarejestruj funkcję blokującą

Aby wstawić niestandardowy kod do przepływów uwierzytelniania użytkownika, wdróż i zarejestruj funkcje blokujące. Po wdrożeniu i zarejestrowaniu funkcji blokujących kod niestandardowy musi zakończyć się pomyślnie, aby uwierzytelnianie i tworzenie użytkowników powiodło się.

Wdróż funkcję blokującą

Funkcję blokującą wdraża się w taki sam sposób, jak każdą inną. (szczegółowe informacje można znaleźć na stronie Rozpoczęcie pracy z Cloud Functions). W podsumowaniu:

  1. Napisz Cloud Functions, które obsługują zdarzenie beforeCreate , zdarzenie beforeSignIn lub oba.

    Na przykład, aby rozpocząć, możesz dodać następujące funkcje no-op do index.js :

    const functions = require('firebase-functions');
    
    exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
      // TODO
    });
    
    exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
      // TODO
    });
    

    W powyższych przykładach pominięto implementację niestandardowej logiki uwierzytelniania. Zapoznaj się z poniższymi sekcjami, aby dowiedzieć się, jak zaimplementować funkcje blokujące i Typowe scenariusze dla konkretnych przykładów.

  2. Wdróż swoje funkcje za pomocą Firebase CLI:

    firebase deploy --only functions
    

    Musisz ponownie wdrożyć swoje funkcje za każdym razem, gdy je aktualizujesz.

Zarejestruj funkcję blokującą

  1. Przejdź do strony Ustawienia uwierzytelniania Firebase w konsoli Firebase.

  2. Wybierz zakładkę Funkcje blokujące .

  3. Zarejestruj swoją funkcję blokowania, wybierając ją z menu rozwijanego w sekcji Przed utworzeniem konta (beforeCreate) lub Przed zalogowaniem (beforeSignIn) .

  4. Zapisz zmiany.

Uzyskiwanie informacji o użytkowniku i kontekście

Zdarzenia beforeSignIn i beforeCreate udostępniają obiekty User i EventContext , które zawierają informacje o logowaniu się użytkownika. Użyj tych wartości w swoim kodzie, aby określić, czy zezwolić na kontynuację operacji.

Aby zapoznać się z listą właściwości dostępnych w obiekcie User , zapoznaj się z dokumentacją interfejsu API UserRecord .

Obiekt EventContext zawiera następujące właściwości:

Nazwa Opis Przykład
locale Ustawienia regionalne aplikacji. Ustawienia regionalne można ustawić za pomocą zestawu SDK klienta lub przekazując nagłówek ustawień regionalnych w interfejsie API REST. fr lub sv-SE
ipAddress Adres IP urządzenia, z którego użytkownik końcowy się rejestruje lub loguje. 114.14.200.1
userAgent Agent użytkownika wyzwalający funkcję blokowania. Mozilla/5.0 (X11; Linux x86_64)
eventId Unikalny identyfikator wydarzenia. rWsyPtolplG2TBFoOkkgyg
eventType Typ zdarzenia. Zawiera informacje na temat nazwy zdarzenia, takiej jak beforeSignIn lub beforeCreate , oraz powiązanej metody logowania, takiej jak Google lub adres e-mail/hasło. providers/cloud.auth/eventTypes/user.beforeSignIn:password
authType Zawsze USER . USER
resource Projekt lub dzierżawa uwierzytelniania Firebase. projects/ project-id /tenants/ tenant-id
timestamp Czas wyzwolenia zdarzenia, sformatowany jako ciąg znaków RFC 3339 . Tue, 23 Jul 2019 21:10:57 GMT
additionalUserInfo Obiekt zawierający informacje o użytkowniku. AdditionalUserInfo
credential Obiekt zawierający informacje o poświadczeniach użytkownika. AuthCredential

Blokowanie rejestracji lub logowania

Aby zablokować próbę rejestracji lub logowania, zgłoś błąd HttpsError w swojej funkcji. Na przykład:

Node.js

throw new functions.auth.HttpsError('permission-denied');

Poniższa tabela zawiera listę błędów, które możesz zgłosić, wraz z ich domyślnym komunikatem o błędzie:

Nazwa Kod Wiadomość
invalid-argument 400 Klient podał nieprawidłowy argument.
failed-precondition 400 Żądanie nie może zostać wykonane w bieżącym stanie systemu.
out-of-range 400 Klient określił nieprawidłowy zakres.
unauthenticated 401 Brakujący, nieprawidłowy lub wygasły token OAuth.
permission-denied 403 Klient nie ma wystarczających uprawnień.
not-found 404 Nie znaleziono określonego zasobu.
aborted 409 Konflikt współbieżności, taki jak konflikt odczytu, modyfikacji i zapisu.
already-exists 409 Zasób, który klient próbował utworzyć, już istnieje.
resource-exhausted 429 Brak limitu zasobów lub osiągnięcie limitu szybkości.
cancelled 499 Żądanie anulowane przez klienta.
data-loss 500 Nieodwracalna utrata lub uszkodzenie danych.
unknown 500 Nieznany błąd serwera.
internal 500 Wewnętrzny błąd serwera.
not-implemented 501 Metoda API nie zaimplementowana przez serwer.
unavailable 503 Serwis niedostępny.
deadline-exceeded 504 Przekroczono termin składania wniosków.

Możesz także określić niestandardowy komunikat o błędzie:

Node.js

throw new functions.auth.HttpsError('permission-denied', 'Unauthorized request origin!');

Poniższy przykład pokazuje, jak zablokować użytkownikom, którzy nie należą do określonej domeny, możliwość rejestracji w Twojej aplikacji:

Node.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  // (If the user is authenticating within a tenant context, the tenant ID can be determined from
  // user.tenantId or from context.resource, e.g. 'projects/project-id/tenant/tenant-id-1')

  // Only users of a specific domain can sign up.
  if (user.email.indexOf('@acme.com') === -1) {
    throw new functions.auth.HttpsError('invalid-argument', `Unauthorized email "${user.email}"`);
  }
});

Niezależnie od tego, czy używasz komunikatu domyślnego, czy niestandardowego, Cloud Functions opakowuje błąd i zwraca go klientowi jako błąd wewnętrzny. Na przykład:

throw new functions.auth.HttpsError('invalid-argument', `Unauthorized email user@evil.com}`);

Twoja aplikacja powinna wychwycić błąd i odpowiednio go obsłużyć. Na przykład:

JavaScript

// Blocking functions can also be triggered in a multi-tenant context before user creation.
// firebase.auth().tenantId = 'tenant-id-1';
firebase.auth().createUserWithEmailAndPassword('johndoe@example.com', 'password')
  .then((result) => {
    result.user.getIdTokenResult()
  })
  .then((idTokenResult) => {
    console.log(idTokenResult.claim.admin);
  })
  .catch((error) => {
    if (error.code !== 'auth/internal-error' && error.message.indexOf('Cloud Function') !== -1) {
      // Display error.
    } else {
      // Registration succeeds.
    }
  });

Modyfikowanie użytkownika

Zamiast blokować próbę rejestracji lub logowania, możesz zezwolić na kontynuację operacji, ale zmodyfikować obiekt User , który jest zapisywany w bazie danych Firebase Authentication i zwracany do klienta.

Aby zmodyfikować użytkownika, zwróć obiekt z procedury obsługi zdarzeń zawierający pola do modyfikacji. Możesz modyfikować następujące pola:

  • displayName
  • disabled
  • emailVerified
  • photoUrl
  • customClaims
  • sessionClaims (tylko beforeSignIn )

Z wyjątkiem sessionClaims , wszystkie zmodyfikowane pola są zapisywane w bazie danych Firebase Authentication, co oznacza, że ​​są uwzględniane w tokenie odpowiedzi i utrzymują się między sesjami użytkownika.

Poniższy przykład pokazuje, jak ustawić domyślną wyświetlaną nazwę:

Node.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  return {
    // If no display name is provided, set it to "Guest".
    displayName: user.displayName || 'Guest';
  };
});

Jeśli zarejestrujesz procedurę obsługi zdarzeń zarówno dla beforeCreate , jak i beforeSignIn , pamiętaj, że beforeSignIn jest wykonywany po beforeCreate . Pola użytkownika zaktualizowane w beforeCreate są widoczne w beforeSignIn . Jeśli ustawisz pole inne niż sessionClaims w obu procedurach obsługi zdarzeń, wartość ustawiona w beforeSignIn zastępuje wartość ustawioną w beforeCreate . Tylko w przypadku sessionClaims są one propagowane do oświadczeń tokenów bieżącej sesji, ale nie są utrwalane ani przechowywane w bazie danych.

Na przykład, jeśli ustawione są jakiekolwiek sessionClaims , beforeSignIn zwróci je z wszelkimi roszczeniami beforeCreate i zostaną one scalone. Gdy zostaną scalone, jeśli klucz sessionClaims pasuje do klucza w customClaims , pasujące customClaims zostaną zastąpione w oświadczeniach tokenu przez klucz sessionClaims . Jednak nadpisany klucz customClaims będzie nadal zachowywany w bazie danych na potrzeby przyszłych żądań.

Obsługiwane poświadczenia i dane OAuth

Poświadczenia i dane OAuth można przekazywać funkcjom blokowania od różnych dostawców tożsamości. W poniższej tabeli przedstawiono, jakie poświadczenia i dane są obsługiwane przez każdego dostawcę tożsamości:

Dostawca tożsamości Token identyfikacyjny Token dostępu Data ważności Sekret Tokena Odśwież token Roszczenia dotyczące logowania
Google Tak Tak Tak NIE Tak NIE
Facebook NIE Tak Tak NIE NIE NIE
Świergot NIE Tak NIE Tak NIE NIE
GitHub NIE Tak NIE NIE NIE NIE
Microsoftu Tak Tak Tak NIE Tak NIE
Linkedin NIE Tak Tak NIE NIE NIE
Wieśniak Tak Tak Tak NIE Tak NIE
Jabłko Tak Tak Tak NIE Tak NIE
SAML NIE NIE NIE NIE NIE Tak
OIDC Tak Tak Tak NIE Tak Tak

Odśwież tokeny

Aby użyć tokena odświeżania w funkcji blokującej, musisz najpierw zaznaczyć pole wyboru na stronie Funkcje blokujące w konsoli Firebase.

Żadni dostawcy tożsamości nie zwracają tokenów odświeżania podczas bezpośredniego logowania przy użyciu poświadczeń OAuth, takich jak token identyfikatora lub token dostępu. W tej sytuacji to samo poświadczenie OAuth po stronie klienta zostanie przekazane do funkcji blokującej.

W poniższych sekcjach opisano poszczególne typy dostawców tożsamości oraz ich obsługiwane poświadczenia i dane.

Ogólni dostawcy OIDC

Gdy użytkownik zaloguje się przy użyciu ogólnego dostawcy OIDC, zostaną przekazane następujące poświadczenia:

  • Token identyfikatora : podany, jeśli wybrano przepływ id_token .
  • Token dostępu : dostarczany, jeśli wybrano przepływ kodu. Należy pamiętać, że przepływ kodu jest obecnie obsługiwany tylko przez interfejs API REST.
  • Odśwież token : podany, jeśli wybrano zakres offline_access .

Przykład:

const provider = new firebase.auth.OAuthProvider('oidc.my-provider');
provider.addScope('offline_access');
firebase.auth().signInWithPopup(provider);

Google

Gdy użytkownik zaloguje się w Google, zostaną przekazane następujące dane uwierzytelniające:

  • token identyfikacyjny
  • Token dostępu
  • Token odświeżania : dostarczany tylko wtedy, gdy wymagane są następujące parametry niestandardowe:
    • access_type=offline
    • prompt=consent , jeśli użytkownik wcześniej wyraził zgodę i nie zażądano nowego zakresu

Przykład:

const provider = new firebase.auth.GoogleAuthProvider();
provider.setCustomParameters({
  'access_type': 'offline',
  'prompt': 'consent'
});
firebase.auth().signInWithPopup(provider);

Dowiedz się więcej o tokenach odświeżania Google .

Facebook

Gdy użytkownik zaloguje się za pomocą Facebooka, zostaną przekazane następujące dane uwierzytelniające:

GitHub

Gdy użytkownik zaloguje się w usłudze GitHub, zostaną przekazane następujące dane uwierzytelniające:

  • Token dostępu : nie wygasa, chyba że zostanie odwołany.

Microsoftu

Gdy użytkownik zaloguje się w firmie Microsoft, zostaną przekazane następujące poświadczenia:

  • token identyfikacyjny
  • Token dostępu
  • Token odświeżania : przekazywany do funkcji blokującej, jeśli wybrano zakres offline_access .

Przykład:

const provider = new firebase.auth.OAuthProvider('microsoft.com');
provider.addScope('offline_access');
firebase.auth().signInWithPopup(provider);

Wieśniak

Gdy użytkownik loguje się za pomocą Yahoo, następujące poświadczenia zostaną przekazane bez żadnych niestandardowych parametrów ani zakresów:

  • token identyfikacyjny
  • Token dostępu
  • Odśwież token

Linkedin

Gdy użytkownik zaloguje się na LinkedIn, zostaną przekazane następujące dane uwierzytelniające:

  • Token dostępu

Jabłko

Gdy użytkownik zaloguje się w Apple, następujące poświadczenia zostaną przekazane bez żadnych niestandardowych parametrów ani zakresów:

  • token identyfikacyjny
  • Token dostępu
  • Odśwież token

Typowe scenariusze

Poniższe przykłady ilustrują niektóre typowe przypadki użycia funkcji blokujących:

Zezwalanie tylko na rejestrację z określonej domeny

Poniższy przykład pokazuje, jak uniemożliwić użytkownikom, którzy nie należą do domeny example.com , rejestrowanie się w Twojej aplikacji:

Node.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (!user.email || user.email.indexOf('@example.com') === -1) {
    throw new functions.auth.HttpsError(
      'invalid-argument', `Unauthorized email "${user.email}"`);
  }
});

Blokowanie rejestracji użytkowników z niezweryfikowanymi adresami e-mail

Poniższy przykład pokazuje, jak uniemożliwić użytkownikom z niezweryfikowanymi adresami e-mail rejestrację w Twojej aplikacji:

Node.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (user.email && !user.emailVerified) {
    throw new functions.auth.HttpsError(
      'invalid-argument', `Unverified email "${user.email}"`);
  }
});

Wymaganie weryfikacji adresu e-mail przy rejestracji

Poniższy przykład pokazuje, jak wymagać od użytkownika weryfikacji adresu e-mail po rejestracji:

Node.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  const locale = context.locale;
  if (user.email && !user.emailVerified) {
    // Send custom email verification on sign-up.
    return admin.auth().generateEmailVerificationLink(user.email).then((link) => {
      return sendCustomVerificationEmail(user.email, link, locale);
    });
  }
});

exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
 if (user.email && !user.emailVerified) {
   throw new functions.auth.HttpsError(
     'invalid-argument', `"${user.email}" needs to be verified before access is granted.`);
  }
});

Traktowanie niektórych adresów e-mail dostawców tożsamości jako zweryfikowanych

Poniższy przykład pokazuje, jak traktować wiadomości e-mail użytkowników od określonych dostawców tożsamości jako zweryfikowane:

Node.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (user.email && !user.emailVerified && context.eventType.indexOf(':facebook.com') !== -1) {
    return {
      emailVerified: true,
    };
  }
});

Blokowanie logowania z określonych adresów IP

Poniższy przykład blokuje logowanie z określonych zakresów adresów IP:

Node.js

exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
  if (isSuspiciousIpAddress(context.ipAddress)) {
    throw new functions.auth.HttpsError(
      'permission-denied', 'Unauthorized access!');
  }
});

Ustawianie oświadczeń niestandardowych i dotyczących sesji

Poniższy przykład pokazuje, jak ustawić oświadczenia niestandardowe i dotyczące sesji:

Node.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (context.credential &&
      context.credential.providerId === 'saml.my-provider-id') {
    return {
      // Employee ID does not change so save in persistent claims (stored in
      // Auth DB).
      customClaims: {
        eid: context.credential.claims.employeeid,
      },
      // Copy role and groups to token claims. These will not be persisted.
      sessionClaims: {
        role: context.credential.claims.role,
        groups: context.credential.claims.groups,
      }
    }
  }
});

Śledzenie adresów IP w celu monitorowania podejrzanej aktywności

Możesz zapobiec kradzieży tokena, śledząc adres IP, z którego loguje się użytkownik, i porównując go z adresem IP przy kolejnych żądaniach. Jeśli żądanie wydaje się podejrzane — na przykład adresy IP pochodzą z różnych regionów geograficznych — możesz poprosić użytkownika o ponowne zalogowanie.

  1. Użyj oświadczeń dotyczących sesji, aby śledzić adres IP, za pomocą którego użytkownik się loguje:

    Node.js

    exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
      return {
        sessionClaims: {
          signInIpAddress: context.ipAddress,
        },
      };
    });
    
  2. Gdy użytkownik próbuje uzyskać dostęp do zasobów wymagających uwierzytelnienia za pomocą uwierzytelniania Firebase, porównaj adres IP w żądaniu z adresem IP użytym do zalogowania się:

    Node.js

    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 request IP address
        const requestIpAddress = req.connection.remoteAddress;
        // Get sign-in IP address.
        const signInIpAddress = claims.signInIpAddress;
        // Check if the request IP address origin is suspicious relative to
        // the session 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
        // geographical change in a short period of time, then it will give
        // stronger signals of possible abuse.
        if (!isSuspiciousIpAddressChange(signInIpAddress, requestIpAddress)) {
          // Suspicious IP address change. Require re-authentication.
          // You can also revoke all user sessions by calling:
          // admin.auth().revokeRefreshTokens(claims.sub).
          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!' })
          });
        }
      });
    });
    

Przeglądanie zdjęć użytkowników

Poniższy przykład pokazuje, jak oczyścić zdjęcia profilowe użytkowników:

Node.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (user.photoURL) {
    return isPhotoAppropriate(user.photoURL)
      .then((status) => {
        if (!status) {
          // Sanitize inappropriate photos by replacing them with guest photos.
          // Users could also be blocked from sign-up, disabled, etc.
          return {
            photoUrl: PLACEHOLDER_GUEST_PHOTO_URL,
          };
        }
      });
});

Aby dowiedzieć się więcej o wykrywaniu i oczyszczaniu obrazów, zapoznaj się z dokumentacją Cloud Vision .

Uzyskiwanie dostępu do poświadczeń OAuth dostawcy tożsamości użytkownika

Poniższy przykład pokazuje, jak uzyskać token odświeżania dla użytkownika, który zalogował się w Google, i używać go do wywoływania interfejsów API Kalendarza Google. Token odświeżania jest przechowywany w celu uzyskania dostępu w trybie offline.

Node.js

const {OAuth2Client} = require('google-auth-library');
const {google} = require('googleapis');
// ...
// Initialize Google OAuth client.
const keys = require('./oauth2.keys.json');
const oAuth2Client = new OAuth2Client(
  keys.web.client_id,
  keys.web.client_secret
);

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (context.credential &&
      context.credential.providerId === 'google.com') {
    // Store the refresh token for later offline use.
    // These will only be returned if refresh tokens credentials are included
    // (enabled by Cloud console).
    return saveUserRefreshToken(
        user.uid,
        context.credential.refreshToken,
        'google.com'
      )
      .then(() => {
        // Blocking the function is not required. The function can resolve while
        // this operation continues to run in the background.
        return new Promise((resolve, reject) => {
          // For this operation to succeed, the appropriate OAuth scope should be requested
          // on sign in with Google, client-side. In this case:
          // https://www.googleapis.com/auth/calendar
          // You can check granted_scopes from within:
          // context.additionalUserInfo.profile.granted_scopes (space joined list of scopes).

          // Set access token/refresh token.
          oAuth2Client.setCredentials({
            access_token: context.credential.accessToken,
            refresh_token: context.credential.refreshToken,
          });
          const calendar = google.calendar('v3');
          // Setup Onboarding event on user's calendar.
          const event = {/** ... */};
          calendar.events.insert({
            auth: oauth2client,
            calendarId: 'primary',
            resource: event,
          }, (err, event) => {
            // Do not fail. This is a best effort approach.
            resolve();
          });
      });
    })
  }
});