Как создать специальные токены

Firebase позволяет полностью контролировать аутентификацию пользователей и устройств с помощью защищенных веб-токенов JSON (JWT). Вы генерируете эти токены на своем сервере, передаете их обратно на клиентское устройство, а затем используете их для аутентификации с помощью метода signInWithCustomToken().

Для этого необходимо создать конечную точку сервера, которая принимает учетные данные для входа, например имя пользователя и пароль, и, если учетные данные действительны, возвращает пользовательский JWT. Затем клиентское устройство может использовать специальный JWT, возвращенный вашим сервером, для аутентификации в Firebase (iOS+, Android, веб). После аутентификации эти идентификационные данные будут использоваться для доступа к другим сервисам Firebase, таким как Firebase Realtime Database и Cloud Storage. Кроме того, содержимое JWT будет доступно в объекте auth в Realtime Database Security Rules и в объекте request.auth в Cloud Storage Security Rules.

Вы можете создать собственный токен с помощью Firebase Admin SDK или использовать стороннюю библиотеку JWT, если ваш сервер написан на языке, который Firebase не поддерживает.

Подготовка

Пользовательские токены – это подписанные JWT, в которых закрытый ключ, используемый для подписи, принадлежит сервисному аккаунту Google. Указать сервисный аккаунт Google, который должен использоваться Firebase Admin SDK для подписания пользовательских токенов, можно несколькими способами:

  • Использование JSON-файла сервисного аккаунта. Этот метод можно использовать в любой среде, но он требует упаковки JSON-файла сервисного аккаунта вместе с кодом. Примите меры, чтобы JSON-файл сервисного аккаунта не попал к третьим лицам.
  • Обнаружение сервисного аккаунта с помощью Admin SDK. Этот метод можно использовать в средах, управляемых Google, таких как Google Cloud Functions и App Engine. Возможно, вам потребуется настроить дополнительные разрешения в консоли Google Cloud.
  • Использование идентификатора сервисного аккаунта. При использовании в среде, управляемой Google, этот метод будет подписывать токены с помощью ключа указанного сервисного аккаунта. Однако он использует удаленный веб-сервис, и вам может потребоваться настроить дополнительные разрешения для этого сервисного аккаунта в консоли Google Cloud.

Как использовать JSON-файл сервисного аккаунта

JSON-файлы сервисных аккаунтов содержат всю информацию, относящуюся к сервисным аккаунтам (включая закрытый ключ RSA). Их можно скачать из консоли Firebase. Подробные сведения о том, как инициализировать Admin SDK с помощью JSON-файла сервисного аккаунта, можно найти в инструкциях по настройке Admin SDK.

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

Как разрешить Admin SDK обнаруживать сервисный аккаунт

Если ваш код развернут в среде, управляемой Google, Admin SDK может попытаться автоматически найти способ подписи пользовательских токенов:

  • Если ваш код развернут в стандартной среде App Engine для Java, Python или Go, Admin SDK может использовать сервис идентификации приложений, доступный в этой среде, для подписания пользовательских токенов. Сервис App Identity подписывает данные с помощью сервисного аккаунта, предоставленного для вашего приложения Google App Engine.

  • Если ваш код развернут в другой управляемой среде (например, Google Cloud Functions, Google Compute Engine), Firebase Admin SDK может автоматически обнаружить строку идентификатора сервисного аккаунта на локальном сервере метаданных. Затем обнаруженный идентификатор сервисного аккаунта используется вместе с сервисом IAM для удаленной подписи токенов.

Чтобы использовать эти методы подписи, инициализируйте SDK с учетными данными Google по умолчанию и не указывайте строку идентификатора сервисного аккаунта:

Node.js

initializeApp();

Java

FirebaseApp.initializeApp();

Python

default_app = firebase_admin.initialize_app()

Проложить маршрут

app, err := firebase.NewApp(context.Background(), nil)
if err != nil {
	log.Fatalf("error initializing app: %v\n", err)
}

C#

FirebaseApp.Create();

Чтобы протестировать тот же код локально, скачайте JSON-файл сервисного аккаунта и настройте переменную среды GOOGLE_APPLICATION_CREDENTIALS так, чтобы она указывала на него.

Если Firebase Admin SDK должен обнаружить строку идентификатора сервисного аккаунта, он делает это, когда ваш код впервые создает пользовательский токен. Результат кешируется и используется для последующих операций подписания токенов. Автоматически обнаруженный идентификатор сервисного аккаунта обычно является одним из сервисных аккаунтов по умолчанию, предоставляемых Google Cloud:

Как и в случае с явно указанными идентификаторами сервисных аккаунтов, для автоматического обнаружения идентификаторов сервисных аккаунтов требуется разрешение iam.serviceAccounts.signBlob, чтобы создание пользовательских токенов работало. Чтобы предоставить сервисным аккаунтам по умолчанию необходимые разрешения, возможно, потребуется использовать раздел IAM и администрирование в консоли Google Cloud. Подробная информация приведена в разделе устранения неполадок ниже.

Использование идентификатора сервисного аккаунта

Чтобы обеспечить согласованность между различными частями приложения, вы можете указать идентификатор сервисного аккаунта, ключи которого будут использоваться для подписи токенов при работе в среде, управляемой Google. Это позволяет упростить правила IAM и повысить их безопасность, а также избежать необходимости включать JSON-файл сервисного аккаунта в код.

Идентификатор сервисного аккаунта можно найти в консоли Google Cloud или в поле client_email скачанного JSON-файла сервисного аккаунта. Идентификаторы сервисных аккаунтов – это адреса электронной почты в следующем формате: <client-id>@<project-id>.iam.gserviceaccount.com. Они уникальным образом идентифицируют сервисные аккаунты в проектах Firebase и Google Cloud.

Чтобы создать пользовательские токены с помощью отдельного идентификатора сервисного аккаунта, инициализируйте SDK, как показано ниже:

Node.js

initializeApp({
  serviceAccountId: 'my-client-id@my-project-id.iam.gserviceaccount.com',
});

Java

FirebaseOptions options = FirebaseOptions.builder()
    .setCredentials(GoogleCredentials.getApplicationDefault())
    .setServiceAccountId("my-client-id@my-project-id.iam.gserviceaccount.com")
    .build();
FirebaseApp.initializeApp(options);

Python

options = {
    'serviceAccountId': 'my-client-id@my-project-id.iam.gserviceaccount.com',
}
firebase_admin.initialize_app(options=options)

Проложить маршрут

conf := &firebase.Config{
	ServiceAccountID: "my-client-id@my-project-id.iam.gserviceaccount.com",
}
app, err := firebase.NewApp(context.Background(), conf)
if err != nil {
	log.Fatalf("error initializing app: %v\n", err)
}

C#

FirebaseApp.Create(new AppOptions()
{
    Credential = GoogleCredential.GetApplicationDefault(),
    ServiceAccountId = "my-client-id@my-project-id.iam.gserviceaccount.com",
});

Идентификаторы сервисных аккаунтов не являются конфиденциальной информацией, поэтому их раскрытие не имеет значения. Однако для подписания пользовательских токенов с помощью указанного сервисного аккаунта Firebase Admin SDK должен вызвать удаленный сервис. Кроме того, необходимо убедиться, что у сервисного аккаунта, который Admin SDK использует для вызова (обычно это {project-name}@appspot.gserviceaccount.com), есть разрешение iam.serviceAccounts.signBlob. Подробная информация приведена в разделе устранения неполадок ниже.

Создание пользовательских токенов с помощью Firebase Admin SDK

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

Node.js

const uid = 'some-uid';

getAuth()
  .createCustomToken(uid)
  .then((customToken) => {
    // Send token back to client
  })
  .catch((error) => {
    console.log('Error creating custom token:', error);
  });

Java

String uid = "some-uid";

String customToken = FirebaseAuth.getInstance().createCustomToken(uid);
// Send token back to client

Python

uid = 'some-uid'

custom_token = auth.create_custom_token(uid)

Проложить маршрут

client, err := app.Auth(context.Background())
if err != nil {
	log.Fatalf("error getting Auth client: %v\n", err)
}

token, err := client.CustomToken(ctx, "some-uid")
if err != nil {
	log.Fatalf("error minting custom token: %v\n", err)
}

log.Printf("Got custom token: %v\n", token)

C#

var uid = "some-uid";

string customToken = await FirebaseAuth.DefaultInstance.CreateCustomTokenAsync(uid);
// Send token back to client

Вы также можете указать дополнительные утверждения, которые будут включены в специальный токен. Например, в примере ниже в специальный токен добавлено поле premiumAccount, которое будет доступно в объектах auth / request.auth в правилах безопасности:

Node.js

const userId = 'some-uid';
const additionalClaims = {
  premiumAccount: true,
};

getAuth()
  .createCustomToken(userId, additionalClaims)
  .then((customToken) => {
    // Send token back to client
  })
  .catch((error) => {
    console.log('Error creating custom token:', error);
  });

Java

String uid = "some-uid";
Map<String, Object> additionalClaims = new HashMap<String, Object>();
additionalClaims.put("premiumAccount", true);

String customToken = FirebaseAuth.getInstance()
    .createCustomToken(uid, additionalClaims);
// Send token back to client

Python

uid = 'some-uid'
additional_claims = {
    'premiumAccount': True
}

custom_token = auth.create_custom_token(uid, additional_claims)

Проложить маршрут

client, err := app.Auth(context.Background())
if err != nil {
	log.Fatalf("error getting Auth client: %v\n", err)
}

claims := map[string]interface{}{
	"premiumAccount": true,
}

token, err := client.CustomTokenWithClaims(ctx, "some-uid", claims)
if err != nil {
	log.Fatalf("error minting custom token: %v\n", err)
}

log.Printf("Got custom token: %v\n", token)

C#

var uid = "some-uid";
var additionalClaims = new Dictionary<string, object>()
{
    { "premiumAccount", true },
};

string customToken = await FirebaseAuth.DefaultInstance
    .CreateCustomTokenAsync(uid, additionalClaims);
// Send token back to client

Зарезервированные названия пользовательских токенов

Вход с помощью специальных токенов на клиентах

После создания специального токена его нужно отправить в клиентское приложение. Клиентское приложение выполняет аутентификацию с помощью специального токена, вызывая signInWithCustomToken():

iOS+

Objective-C
[[FIRAuth auth] signInWithCustomToken:customToken
                           completion:^(FIRAuthDataResult * _Nullable authResult,
                                        NSError * _Nullable error) {
  // ...
}];
Swift
Auth.auth().signIn(withCustomToken: customToken ?? "") { user, error in
  // ...
}

Android

mAuth.signInWithCustomToken(mCustomToken)
        .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
            @Override
            public void onComplete(@NonNull Task<AuthResult> task) {
                if (task.isSuccessful()) {
                    // Sign in success, update UI with the signed-in user's information
                    Log.d(TAG, "signInWithCustomToken:success");
                    FirebaseUser user = mAuth.getCurrentUser();
                    updateUI(user);
                } else {
                    // If sign in fails, display a message to the user.
                    Log.w(TAG, "signInWithCustomToken:failure", task.getException());
                    Toast.makeText(CustomAuthActivity.this, "Authentication failed.",
                            Toast.LENGTH_SHORT).show();
                    updateUI(null);
                }
            }
        });

Unity

auth.SignInWithCustomTokenAsync(custom_token).ContinueWith(task => {
  if (task.IsCanceled) {
    Debug.LogError("SignInWithCustomTokenAsync was canceled.");
    return;
  }
  if (task.IsFaulted) {
    Debug.LogError("SignInWithCustomTokenAsync encountered an error: " + task.Exception);
    return;
  }

  Firebase.Auth.AuthResult result = task.Result;
  Debug.LogFormat("User signed in successfully: {0} ({1})",
      result.User.DisplayName, result.User.UserId);
});

C++

firebase::Future<firebase::auth::AuthResult> result =
    auth->SignInWithCustomToken(custom_token);

Web

firebase.auth().signInWithCustomToken(token)
  .then((userCredential) => {
    // Signed in
    var user = userCredential.user;
    // ...
  })
  .catch((error) => {
    var errorCode = error.code;
    var errorMessage = error.message;
    // ...
  });

Web

import { getAuth, signInWithCustomToken } from "firebase/auth";

const auth = getAuth();
signInWithCustomToken(auth, token)
  .then((userCredential) => {
    // Signed in
    const user = userCredential.user;
    // ...
  })
  .catch((error) => {
    const errorCode = error.code;
    const errorMessage = error.message;
    // ...
  });

Если аутентификация пройдет успешно, пользователь войдет в клиентское приложение с аккаунтом, указанным в параметре uid, который включен в специальный токен. Если аккаунт не существовал, он будет создан.

Как и в случае с другими способами входа (например, signInWithEmailAndPassword() и signInWithCredential()), объект auth в Realtime Database Security Rules и объект request.auth в Cloud Storage Security Rules будут заполнены значением uid пользователя. В этом случае uid будет тем, который вы указали при создании пользовательского токена.

Правила базы данных

{
  "rules": {
    "adminContent": {
      ".read": "auth.uid === 'some-uid'"
    }
  }
}

Правила хранения

service firebase.storage {
  match /b/<your-firebase-storage-bucket>/o {
    match /adminContent/{filename} {
      allow read, write: if request.auth != null && request.auth.uid == "some-uid";
    }
  }
}

Если специальный токен содержит дополнительные утверждения, на них можно ссылаться из объекта auth.token (Firebase Realtime Database) или request.auth.token (Cloud Storage) в правилах:

Правила базы данных

{
  "rules": {
    "premiumContent": {
      ".read": "auth.token.premiumAccount === true"
    }
  }
}

Правила хранения

service firebase.storage {
  match /b/<your-firebase-storage-bucket>/o {
    match /premiumContent/{filename} {
      allow read, write: if request.auth.token.premiumAccount == true;
    }
  }
}

Как создать специальные токены с помощью сторонней библиотеки JWT

Если ваш бэкенд написан на языке, для которого нет официального Firebase Admin SDK, вы все равно можете создавать собственные токены вручную. Сначала найдите стороннюю библиотеку JWT для своего языка. Затем с помощью этой библиотеки создайте JWT, который включает следующие утверждения:

Специальные утверждения токенов
alg Алгоритм "RS256"
iss Издатель Адрес электронной почты сервисного аккаунта вашего проекта
sub Субъект Адрес электронной почты сервисного аккаунта вашего проекта.
aud Аудитория "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit"
iat Время выпуска Текущее время в секундах, прошедших с начала эпохи UNIX
exp Срок действия Время в секундах, прошедшее с начала эпохи UNIX, когда истекает срок действия токена. Оно может быть максимум на 3600 секунд позже, чем iat.
Примечание. Это значение определяет только время, когда истекает срок действия самого специального токена. Однако после входа пользователя с помощью signInWithCustomToken() он останется в системе на устройстве, пока сеанс не будет аннулирован или пользователь не выйдет из системы.
uid Уникальный идентификатор пользователя, выполнившего вход, должен быть строкой длиной от 1 до 128 символов включительно. Чем короче uid, тем выше эффективность.
claims (необязательно) Необязательные настраиваемые утверждения, которые можно включить в переменные auth / request.auth правил безопасности.

Ниже приведены примеры создания пользовательских токенов на языках, которые не поддерживаются Firebase Admin SDK.

PHP

Использование php-jwt:

// Requires: composer require firebase/php-jwt
use Firebase\JWT\JWT;

// Get your service account's email address and private key from the JSON key file
$service_account_email = "abc-123@a-b-c-123.iam.gserviceaccount.com";
$private_key = "-----BEGIN PRIVATE KEY-----...";

function create_custom_token($uid, $is_premium_account) {
  global $service_account_email, $private_key;

  $now_seconds = time();
  $payload = array(
    "iss" => $service_account_email,
    "sub" => $service_account_email,
    "aud" => "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
    "iat" => $now_seconds,
    "exp" => $now_seconds+(60*60),  // Maximum expiration time is one hour
    "uid" => $uid,
    "claims" => array(
      "premium_account" => $is_premium_account
    )
  );
  return JWT::encode($payload, $private_key, "RS256");
}

Ruby

Использование ruby-jwt:

require "jwt"

# Get your service account's email address and private key from the JSON key file
$service_account_email = "service-account@my-project-abc123.iam.gserviceaccount.com"
$private_key = OpenSSL::PKey::RSA.new "-----BEGIN PRIVATE KEY-----\n..."

def create_custom_token(uid, is_premium_account)
  now_seconds = Time.now.to_i
  payload = {:iss => $service_account_email,
             :sub => $service_account_email,
             :aud => "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
             :iat => now_seconds,
             :exp => now_seconds+(60*60), # Maximum expiration time is one hour
             :uid => uid,
             :claims => {:premium_account => is_premium_account}}
  JWT.encode payload, $private_key, "RS256"
end

После создания специального токена отправьте его в клиентское приложение, чтобы использовать для аутентификации в Firebase. Примеры кода приведены выше.

Устранение неполадок

В этом разделе описаны распространенные проблемы, с которыми могут столкнуться разработчики при создании собственных токенов, и способы их решения.

IAM API не включен

Если вы указываете идентификатор сервисного аккаунта для подписи токенов, может возникнуть ошибка, похожая на следующую:

Identity and Access Management (IAM) API has not been used in project
1234567890 before or it is disabled. Enable it by visiting
https://console.developers.google.com/apis/api/iam.googleapis.com/overview?project=1234567890
then retry. If you enabled this API recently, wait a few minutes for the action
to propagate to our systems and retry.

Firebase Admin SDK использует IAM API для подписи токенов. Эта ошибка означает, что IAM API в настоящее время не включен для вашего проекта Firebase. Откройте ссылку из сообщения об ошибке в браузере и нажмите кнопку "Включить API", чтобы включить его для своего проекта.

У сервисного аккаунта нет необходимых разрешений

Если у сервисного аккаунта, в котором запущен Firebase Admin SDK, нет разрешения iam.serviceAccounts.signBlob, вы можете получить сообщение об ошибке, подобное следующему:

Permission iam.serviceAccounts.signBlob is required to perform this operation
on service account projects/-/serviceAccounts/{your-service-account-id}.

Чтобы устранить эту проблему, назначьте роль IAM Создатель токенов сервисных аккаунтов нужному сервисному аккаунту. Сервисный аккаунт по умолчанию зависит от среды и версии Cloud Functions:

  • Cloud Functions (первое поколение). Используется сервисный аккаунт App Engine по умолчанию.
  • Cloud Functions (второе поколение). Используется сервисный аккаунт Compute Engine по умолчанию.
  1. В консоли Google Cloud перейдите в раздел IAM и администрирование.

  2. Нажмите на значок редактирования рядом с нужным сервисным аккаунтом.

  3. Нажмите Добавить роль.

  4. Введите "Service Account Token Creator" в фильтр поиска и выберите его из результатов.

  5. Нажмите Сохранить, чтобы подтвердить назначение роли.

Подробную информацию об этом процессе можно найти в документации по IAM. Также вы можете узнать, как обновлять роли с помощью инструментов командной строки gcloud.

Не удалось определить сервисный аккаунт.

Если вы видите сообщение об ошибке, похожее на приведенное ниже, значит Firebase Admin SDK не был инициализирован должным образом.

Failed to determine service account ID. Initialize the SDK with service account
credentials or specify a service account ID with iam.serviceAccounts.signBlob
permission.

Если вы полагаетесь на SDK для автоматического обнаружения идентификатора сервисного аккаунта, убедитесь, что код развернут в управляемой среде Google с сервером метаданных. В противном случае при инициализации SDK необходимо указать JSON-файл или идентификатор сервисного аккаунта.