Как управлять доступом с помощью специальных утверждений и правил безопасности

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

Роли пользователей можно определить для следующих распространенных случаев:

  • Предоставление пользователю прав администратора для доступа к данным и ресурсам.
  • определять, к каким группам относится пользователь;
  • Предоставление многоуровневого доступа:
    • Разделение платных и бесплатных подписчиков.
    • Отличать модераторов от обычных пользователей.
    • Заявление преподавателя/учащегося и т. д.
  • Добавьте дополнительный идентификатор пользователя. Например, пользователь Firebase может быть сопоставлен с другим UID в другой системе.

Предположим, вы хотите ограничить доступ к узлу базы данных "adminContent". Это можно сделать с помощью поиска в базе данных по списку пользователей с правами администратора. Однако ту же цель можно достичь более эффективно, используя специальное утверждение пользователя с названием admin и следующее правило Realtime Database:

{
  "rules": {
    "adminContent": {
      ".read": "auth.token.admin === true",
      ".write": "auth.token.admin === true",
    }
  }
}

Пользовательские утверждения доступны через токены аутентификации пользователя. В примере выше доступ на чтение и запись к узлу adminContent будет предоставлен только пользователям, у которых в утверждении токена для параметра admin задано значение true. Поскольку утверждения уже содержатся в токене идентификатора, для проверки разрешений администратора не требуется дополнительная обработка или поиск. Кроме того, токен идентификатора – это надежный механизм для передачи этих специальных утверждений. При любом доступе с аутентификацией необходимо проверять токен идентификатора, прежде чем обрабатывать связанный с ним запрос.

В примерах кода и решениях, описанных на этой странице, используются как клиентские Firebase Auth API, так и серверные Auth API, предоставляемые Admin SDK.

Как задавать и проверять собственные утверждения о пользователе с помощью Admin SDK

Поскольку специальные утверждения могут содержать данные деликатного характера, их следует задавать только в привилегированной серверной среде с помощью Firebase Admin SDK.

Node.js

// Set admin privilege on the user corresponding to uid.

getAuth()
  .setCustomUserClaims(uid, { admin: true })
  .then(() => {
    // The new custom claims will propagate to the user's ID token the
    // next time a new one is issued.
  });

Java

// Set admin privilege on the user corresponding to uid.
Map<String, Object> claims = new HashMap<>();
claims.put("admin", true);
FirebaseAuth.getInstance().setCustomUserClaims(uid, claims);
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.

Python

# Set admin privilege on the user corresponding to uid.
auth.set_custom_user_claims(uid, {'admin': True})
# The new custom claims will propagate to the user's ID token the
# next time a new one is issued.

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

// Get an auth client from the firebase.App
client, err := app.Auth(ctx)
if err != nil {
	log.Fatalf("error getting Auth client: %v\n", err)
}

// Set admin privilege on the user corresponding to uid.
claims := map[string]interface{}{"admin": true}
err = client.SetCustomUserClaims(ctx, uid, claims)
if err != nil {
	log.Fatalf("error setting custom claims %v\n", err)
}
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.

C#

// Set admin privileges on the user corresponding to uid.
var claims = new Dictionary<string, object>()
{
    { "admin", true },
};
await FirebaseAuth.DefaultInstance.SetCustomUserClaimsAsync(uid, claims);
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.

Объект пользовательских утверждений не должен содержать зарезервированные имена ключей OIDC или Firebase. Размер полезной нагрузки не должен превышать 1000 байт. Специальные утверждения должны быть сериализованы в JSON. Поддерживаются строки, числа, логические значения, массивы, объекты и значения null. Неподдерживаемые типы, такие как Date, undefined, функции или другие значения, не относящиеся к JSON, вызывают ошибки.

Токен идентификатора, отправленный на серверную часть, может подтвердить личность пользователя и уровень доступа с помощью Admin SDK следующим образом:

Node.js

// Verify the ID token first.
getAuth()
  .verifyIdToken(idToken)
  .then((claims) => {
    if (claims.admin === true) {
      // Allow access to requested admin resource.
    }
  });

Java

// Verify the ID token first.
FirebaseToken decoded = FirebaseAuth.getInstance().verifyIdToken(idToken);
if (Boolean.TRUE.equals(decoded.getClaims().get("admin"))) {
  // Allow access to requested admin resource.
}

Python

# Verify the ID token first.
claims = auth.verify_id_token(id_token)
if claims['admin'] is True:
    # Allow access to requested admin resource.
    pass

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

// Verify the ID token first.
token, err := client.VerifyIDToken(ctx, idToken)
if err != nil {
	log.Fatal(err)
}

claims := token.Claims
if admin, ok := claims["admin"]; ok {
	if admin.(bool) {
		//Allow access to requested admin resource.
	}
}

C#

// Verify the ID token first.
FirebaseToken decoded = await FirebaseAuth.DefaultInstance.VerifyIdTokenAsync(idToken);
object isAdmin;
if (decoded.Claims.TryGetValue("admin", out isAdmin))
{
    if ((bool)isAdmin)
    {
        // Allow access to requested admin resource.
    }
}

Вы также можете проверить существующие специальные утверждения пользователя, которые доступны как свойство объекта пользователя:

Node.js

// Lookup the user associated with the specified uid.
getAuth()
  .getUser(uid)
  .then((userRecord) => {
    // The claims can be accessed on the user record.
    console.log(userRecord.customClaims['admin']);
  });

Java

// Lookup the user associated with the specified uid.
UserRecord user = FirebaseAuth.getInstance().getUser(uid);
System.out.println(user.getCustomClaims().get("admin"));

Python

# Lookup the user associated with the specified uid.
user = auth.get_user(uid)
# The claims can be accessed on the user record.
print(user.custom_claims.get('admin'))

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

// Lookup the user associated with the specified uid.
user, err := client.GetUser(ctx, uid)
if err != nil {
	log.Fatal(err)
}
// The claims can be accessed on the user record.
if admin, ok := user.CustomClaims["admin"]; ok {
	if admin.(bool) {
		log.Println(admin)
	}
}

C#

// Lookup the user associated with the specified uid.
UserRecord user = await FirebaseAuth.DefaultInstance.GetUserAsync(uid);
Console.WriteLine(user.CustomClaims["admin"]);

Чтобы удалить специальное утверждение пользователя, передайте в customClaims значение null.

Передача специальных утверждений клиенту

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

  • Пользователь входит в аккаунт или проходит повторную аутентификацию после изменения специальных утверждений. Выданный в результате токен идентификатора будет содержать актуальные утверждения.
  • Идентификатор сеанса существующего пользователя обновляется после того, как истекает срок действия старого токена.
  • Токен идентификатора принудительно обновляется при вызове метода currentUser.getIdToken(true).

Как получить доступ к специальным утверждениям на стороне клиента

Специальные утверждения можно получить только через токен идентификатора пользователя. Доступ к этим утверждениям может потребоваться, чтобы изменить интерфейс клиента в зависимости от роли или уровня доступа пользователя. Однако доступ к серверной части всегда должен предоставляться через токен идентификатора после его проверки и анализа утверждений. Пользовательские утверждения не следует отправлять непосредственно в серверную часть, поскольку им нельзя доверять за пределами токена.

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

JavaScript

import { getAuth } from "firebase/auth";
getAuth().currentUser?.getIdTokenResult()
  .then((idTokenResult) => {
     // Confirm the user is an Admin.
     if (!!idTokenResult.claims.admin) {
       // Show admin UI.
       showAdminUI();
     } else {
       // Show regular user UI.
       showRegularUI();
     }
  })
  .catch((error) => {
    console.log(error);
  });

Android

user.getIdToken(false).addOnSuccessListener(new OnSuccessListener<GetTokenResult>() {
  @Override
  public void onSuccess(GetTokenResult result) {
    boolean isAdmin = result.getClaims().get("admin");
    if (isAdmin) {
      // Show admin UI.
      showAdminUI();
    } else {
      // Show regular user UI.
      showRegularUI();
    }
  }
});

Swift

user.getIDTokenResult(completion: { (result, error) in
  guard let admin = result?.claims?["admin"] as? NSNumber else {
    // Show regular user UI.
    showRegularUI()
    return
  }
  if admin.boolValue {
    // Show admin UI.
    showAdminUI()
  } else {
    // Show regular user UI.
    showRegularUI()
  }
})

Objective-C

user.getIDTokenResultWithCompletion:^(FIRAuthTokenResult *result,
                                      NSError *error) {
  if (error != nil) {
    BOOL *admin = [result.claims[@"admin"] boolValue];
    if (admin) {
      // Show admin UI.
      [self showAdminUI];
    } else {
      // Show regular user UI.
      [self showRegularUI];
    }
  }
}];

Рекомендации по работе с заявками Content ID

Специальные утверждения используются только для контроля доступа. Они не предназначены для хранения дополнительных данных, таких как профиль и другие специальные данные. Хотя это может показаться удобным способом, мы настоятельно не рекомендуем его использовать, поскольку утверждения хранятся в токене идентификатора и могут вызвать проблемы с производительностью, так как все запросы с аутентификацией всегда содержат токен идентификатора Firebase, соответствующий вошедшему в аккаунт пользователю.

  • Используйте специальные утверждения только для хранения данных, необходимых для управления доступом пользователей. Все остальные данные следует хранить отдельно в базе данных реального времени или другом хранилище на стороне сервера.
  • Размер специальных утверждений ограничен. Если передать полезную нагрузку с утверждениями, размер которой превышает 1000 байт, возникнет ошибка.

Примеры и варианты использования

В приведенных ниже примерах показано, как использовать специальные утверждения в контексте определенных вариантов использования Firebase.

Как задавать роли с помощью Firebase Functions при создании пользователя

В этом примере специальные утверждения задаются для пользователя при создании с помощью Cloud Functions.

Специальные утверждения можно добавить с помощью Cloud Functions и сразу же распространить с помощью Realtime Database. Функция вызывается только при регистрации с помощью триггера onCreate. После того как специальные утверждения будут заданы, они будут распространяться на все существующие и будущие сеансы. При следующем входе пользователя с учетными данными пользователя токен будет содержать настраиваемые утверждения.

Реализация на стороне клиента (JavaScript)

import { GoogleAuthProvider, signInWithPopup, getAuth, onAuthStateChanged } from "firebase/auth";
import { getDatabase, onValue, ref } from "firebase/database";

const auth = getAuth();
const database = getDatabase();

const provider = new GoogleAuthProvider();

signInWithPopup(auth, provider).catch(error => {
  console.log(error);
});

let unsubscribeFn = null;
let metadataRef = null;
onAuthStateChanged(auth, user => {
  // Remove previous listener.
  if (unsubscribeFn) {
    unsubscribeFn();
  }
  // On user login add new listener.
  if (user) {
    // Check if refresh is required.
    metadataRef = ref(database, 'metadata/' + user.uid + '/refreshTime');
    // Subscribe new listener to changes on that node.
    unsubscribeFn = onValue(metadataRef, async (snapshot) => {
      // Force refresh to pick up the latest custom claims changes.
      // Note this is always triggered on first call. Further optimization could be
      // added to avoid the initial trigger when the token is issued and already contains
      // the latest claims.
      user.getIdToken(true);
    });
  }
});

Логика Cloud Functions

Добавлен новый узел базы данных (metadata/($uid)}, доступ к чтению и записи которого ограничен аутентифицированным пользователем.

const functions = require('firebase-functions');
const { initializeApp } = require('firebase-admin/app');
const { getAuth } = require('firebase-admin/auth');
const { getDatabase } = require('firebase-admin/database');

initializeApp();

// On sign up.
exports.processSignUp = functions.auth.user().onCreate(async (user) => {
  // Check if user meets role criteria.
  if (
    user.email &&
    user.email.endsWith('@admin.example.com') &&
    user.emailVerified
  ) {
    const customClaims = {
      admin: true,
      accessLevel: 9
    };

    try {
      // Set custom user claims on this newly created user.
      await getAuth().setCustomUserClaims(user.uid, customClaims);

      // Update real-time database to notify client to force refresh.
      const metadataRef = getDatabase().ref('metadata/' + user.uid);

      // Set the refresh time to the current UTC timestamp.
      // This will be captured on the client to force a token refresh.
      await  metadataRef.set({refreshTime: new Date().getTime()});
    } catch (error) {
      console.log(error);
    }
  }
});

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

{
  "rules": {
    "metadata": {
      "$user_id": {
        // Read access only granted to the authenticated user.
        ".read": "$user_id === auth.uid",
        // Write access only via Admin SDK.
        ".write": false
      }
    }
  }
}

Как задать роли с помощью HTTP-запроса

В приведенном ниже примере с помощью HTTP-запроса задаются специальные утверждения пользователя для нового пользователя, выполнившего вход.

Реализация на стороне клиента (JavaScript)

import { GoogleAuthProvider, signInWithPopup, getAuth, onAuthStateChanged } from "firebase/auth";
import { getDatabase, onValue, ref } from "firebase/database";

const auth = getAuth();
const database = getDatabase();

const provider = new GoogleAuthProvider();

signInWithPopup(auth, provider)
.then((result) => {
  // User is signed in. Get the ID token.
  return result.user.getIdToken();
})
.then((idToken) => {
  // Pass the ID token to the server.
  $.post(
    '/setCustomClaims',
    {
      idToken: idToken
    },
    (data, status) => {
      // This is not required. You could just wait until the token is expired
      // and it proactively refreshes.
      if (status == 'success' && data) {
        const json = JSON.parse(data);
        if (json && json.status == 'success') {
          // Force token refresh. The token claims will contain the additional claims.
          auth.currentUser.getIdToken(true);
        }
      }
    });
}).catch((error) => {
  console.log(error);
});

Реализация на стороне сервера (Admin SDK)

app.post('/setCustomClaims', async (req, res) => {
  // Get the ID token passed.
  const idToken = req.body.idToken;

  // Verify the ID token and decode its payload.
  const claims = await getAuth().verifyIdToken(idToken);

  // Verify user is eligible for additional privileges.
  if (
    typeof claims.email !== 'undefined' &&
    typeof claims.email_verified !== 'undefined' &&
    claims.email_verified &&
    claims.email.endsWith('@admin.example.com')
  ) {
    // Add custom claims for additional privileges.
    await getAuth().setCustomUserClaims(claims.sub, {
      admin: true
    });

    // Tell client to refresh token on user.
    res.end(JSON.stringify({
      status: 'success'
    }));
  } else {
    // Return nothing.
    res.end(JSON.stringify({ status: 'ineligible' }));
  }
});

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

Как задать роли с помощью серверного скрипта

Повторяющийся скрипт (не инициированный клиентом) может быть настроен на обновление специальных утверждений пользователя:

Node.js

getAuth()
  .getUserByEmail('user@admin.example.com')
  .then((user) => {
    // Confirm user is verified.
    if (user.emailVerified) {
      // Add custom claims for additional privileges.
      // This will be picked up by the user on token refresh or next sign in on new device.
      return getAuth().setCustomUserClaims(user.uid, {
        admin: true,
      });
    }
  })
  .catch((error) => {
    console.log(error);
  });

Java

UserRecord user = FirebaseAuth.getInstance()
    .getUserByEmail("user@admin.example.com");
// Confirm user is verified.
if (user.isEmailVerified()) {
  Map<String, Object> claims = new HashMap<>();
  claims.put("admin", true);
  FirebaseAuth.getInstance().setCustomUserClaims(user.getUid(), claims);
}

Python

user = auth.get_user_by_email('user@admin.example.com')
# Confirm user is verified
if user.email_verified:
    # Add custom claims for additional privileges.
    # This will be picked up by the user on token refresh or next sign in on new device.
    auth.set_custom_user_claims(user.uid, {
        'admin': True
    })

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

user, err := client.GetUserByEmail(ctx, "user@admin.example.com")
if err != nil {
	log.Fatal(err)
}
// Confirm user is verified
if user.EmailVerified {
	// Add custom claims for additional privileges.
	// This will be picked up by the user on token refresh or next sign in on new device.
	err := client.SetCustomUserClaims(ctx, user.UID, map[string]interface{}{"admin": true})
	if err != nil {
		log.Fatalf("error setting custom claims %v\n", err)
	}

}

C#

UserRecord user = await FirebaseAuth.DefaultInstance
    .GetUserByEmailAsync("user@admin.example.com");
// Confirm user is verified.
if (user.EmailVerified)
{
    var claims = new Dictionary<string, object>()
    {
        { "admin", true },
    };
    await FirebaseAuth.DefaultInstance.SetCustomUserClaimsAsync(user.Uid, claims);
}

Настраиваемые утверждения также можно изменять постепенно с помощью Admin SDK:

Node.js

getAuth()
  .getUserByEmail('user@admin.example.com')
  .then((user) => {
    // Add incremental custom claim without overwriting existing claims.
    const currentCustomClaims = user.customClaims;
    if (currentCustomClaims['admin']) {
      // Add level.
      currentCustomClaims['accessLevel'] = 10;
      // Add custom claims for additional privileges.
      return getAuth().setCustomUserClaims(user.uid, currentCustomClaims);
    }
  })
  .catch((error) => {
    console.log(error);
  });

Java

UserRecord user = FirebaseAuth.getInstance()
    .getUserByEmail("user@admin.example.com");
// Add incremental custom claim without overwriting the existing claims.
Map<String, Object> currentClaims = user.getCustomClaims();
if (Boolean.TRUE.equals(currentClaims.get("admin"))) {
  // Add level.
  currentClaims.put("level", 10);
  // Add custom claims for additional privileges.
  FirebaseAuth.getInstance().setCustomUserClaims(user.getUid(), currentClaims);
}

Python

user = auth.get_user_by_email('user@admin.example.com')
# Add incremental custom claim without overwriting existing claims.
current_custom_claims = user.custom_claims
if current_custom_claims.get('admin'):
    # Add level.
    current_custom_claims['accessLevel'] = 10
    # Add custom claims for additional privileges.
    auth.set_custom_user_claims(user.uid, current_custom_claims)

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

user, err := client.GetUserByEmail(ctx, "user@admin.example.com")
if err != nil {
	log.Fatal(err)
}
// Add incremental custom claim without overwriting existing claims.
currentCustomClaims := user.CustomClaims
if currentCustomClaims == nil {
	currentCustomClaims = map[string]interface{}{}
}

if _, found := currentCustomClaims["admin"]; found {
	// Add level.
	currentCustomClaims["accessLevel"] = 10
	// Add custom claims for additional privileges.
	err := client.SetCustomUserClaims(ctx, user.UID, currentCustomClaims)
	if err != nil {
		log.Fatalf("error setting custom claims %v\n", err)
	}

}

C#

UserRecord user = await FirebaseAuth.DefaultInstance
    .GetUserByEmailAsync("user@admin.example.com");
// Add incremental custom claims without overwriting the existing claims.
object isAdmin;
if (user.CustomClaims.TryGetValue("admin", out isAdmin) && (bool)isAdmin)
{
    var claims = user.CustomClaims.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
    // Add level.
    var level = 10;
    claims["level"] = level;
    // Add custom claims for additional privileges.
    await FirebaseAuth.DefaultInstance.SetCustomUserClaimsAsync(user.Uid, claims);
}